Last active
July 13, 2026 14:36
-
-
Save otakupahp/6c0d70b40a4f2390dda0f3c2a0dfd024 to your computer and use it in GitHub Desktop.
Broker file for otakupahp/sdk-license-manager-for-woocommerce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Standalone broker/proxy for SDK License Manager for WooCommerce 2.0.0. | |
| * | |
| * Copy this file to a private PHP hosting/server that you control, configure the | |
| * constants below, and point the SDK broker URL to this file or to a rewrite that | |
| * routes to this file. | |
| * | |
| * Supported SDK logical endpoints: | |
| * - GET /licenses/{license_key} | |
| * - GET /licenses/activate/{license_key} | |
| * - GET /licenses/deactivate/{license_key} | |
| * | |
| * The broker forwards those endpoints to: | |
| * {LMFWC_SITE_URL}/wp-json/lmfwc/v2/{logical_endpoint} | |
| * and appends the real consumer credentials server-side. | |
| */ | |
| /** Real WordPress site URL where License Manager for WooCommerce is installed. */ | |
| const LMFWC_SITE_URL = 'https://licenses.example.com/wp-json/lmfwc/v2/'; | |
| /** Real License Manager for WooCommerce REST API consumer key. */ | |
| const LMFWC_CONSUMER_KEY = 'ck_replace_with_real_consumer_key'; | |
| /** Real License Manager for WooCommerce REST API consumer secret. */ | |
| const LMFWC_CONSUMER_SECRET = 'cs_replace_with_real_consumer_secret'; | |
| /** | |
| * Optional allow-list of product IDs enforced by the broker after validation or activation. | |
| * Leave empty to skip broker-side product checks and let the SDK check productId client-side. | |
| * | |
| * @var array<int, int|string> | |
| */ | |
| const LMFWC_ALLOWED_PRODUCT_IDS = array(); | |
| /** | |
| * Optional public token to reduce casual abuse. | |
| * | |
| * SDK 2.0.0 appends logical endpoints directly to the configured broker URL and does not | |
| * append custom query parameters or headers. Leave this empty unless your web server/rewrite | |
| * layer injects the configured query parameter before PHP receives the request, or unless a | |
| * future SDK/client wrapper explicitly sends it. | |
| */ | |
| const BROKER_PUBLIC_TOKEN = ''; | |
| /** Query parameter name checked when BROKER_PUBLIC_TOKEN is not empty. */ | |
| const BROKER_PUBLIC_TOKEN_QUERY_PARAM = 'broker_token'; | |
| /** Upstream request timeout in seconds. */ | |
| const BROKER_TIMEOUT_SECONDS = 5; | |
| /** Maximum accepted logical endpoint length. */ | |
| const BROKER_MAX_ENDPOINT_LENGTH = 512; | |
| /** | |
| * Send a JSON response and terminate execution. | |
| * | |
| * @param int $status_code HTTP status code. | |
| * @param array<string, mixed> $payload JSON payload. | |
| */ | |
| function broker_json_response( int $status_code, array $payload ): void { | |
| http_response_code( $status_code ); | |
| header( 'Content-Type: application/json; charset=UTF-8' ); | |
| header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); | |
| header( 'X-Content-Type-Options: nosniff' ); | |
| echo json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); | |
| exit; | |
| } | |
| /** | |
| * Send a safe JSON error response and terminate execution. | |
| * | |
| * @param int $status_code HTTP status code. | |
| * @param string $message Safe public error message. | |
| */ | |
| function broker_error_response( int $status_code, string $message ): void { | |
| broker_json_response( | |
| $status_code, | |
| array( | |
| 'success' => false, | |
| 'message' => $message, | |
| 'data' => array( | |
| 'status' => $status_code, | |
| ), | |
| ) | |
| ); | |
| } | |
| /** | |
| * Extract the SDK logical endpoint from the current request. | |
| * | |
| * @return string | |
| */ | |
| function broker_get_logical_endpoint(): string { | |
| $path = ''; | |
| if ( isset( $_SERVER['PATH_INFO'] ) && is_string( $_SERVER['PATH_INFO'] ) ) { | |
| $path = $_SERVER['PATH_INFO']; | |
| } elseif ( isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ) { | |
| $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); | |
| $script_name = isset( $_SERVER['SCRIPT_NAME'] ) && is_string( $_SERVER['SCRIPT_NAME'] ) ? $_SERVER['SCRIPT_NAME'] : ''; | |
| $script_base = '' !== $script_name ? dirname( $script_name ) : ''; | |
| if ( is_string( $request_path ) ) { | |
| if ( '' !== $script_name && str_starts_with( $request_path, $script_name ) ) { | |
| $path = substr( $request_path, strlen( $script_name ) ); | |
| } elseif ( '' !== $script_base && '.' !== $script_base && str_starts_with( $request_path, rtrim( $script_base, '/' ) . '/' ) ) { | |
| $path = substr( $request_path, strlen( rtrim( $script_base, '/' ) . '/' ) ); | |
| } else { | |
| $path = $request_path; | |
| } | |
| } | |
| } | |
| $path = trim( $path, '/' ); | |
| if ( str_starts_with( $path, 'wp-json/lmfwc/v2/' ) ) { | |
| $path = substr( $path, strlen( 'wp-json/lmfwc/v2/' ) ); | |
| } elseif ( str_starts_with( $path, 'lmfwc/v2/' ) ) { | |
| $path = substr( $path, strlen( 'lmfwc/v2/' ) ); | |
| } elseif ( str_starts_with( $path, 'v2/' ) ) { | |
| $path = substr( $path, strlen( 'v2/' ) ); | |
| } | |
| return trim( $path, '/' ); | |
| } | |
| /** | |
| * Validate and normalize a logical SDK endpoint. | |
| * | |
| * @param string $endpoint Raw logical endpoint. | |
| * @return string | |
| */ | |
| function broker_validate_endpoint( string $endpoint ): string { | |
| if ( '' === $endpoint || strlen( $endpoint ) > BROKER_MAX_ENDPOINT_LENGTH ) { | |
| broker_error_response( 404, 'Unsupported license endpoint.' ); | |
| } | |
| if ( | |
| str_contains( $endpoint, '..' ) || | |
| str_contains( $endpoint, '\\' ) || | |
| str_contains( $endpoint, '://' ) || | |
| str_starts_with( $endpoint, '/' ) | |
| ) { | |
| broker_error_response( 400, 'Invalid license endpoint.' ); | |
| } | |
| $license_key_pattern = '[A-Za-z0-9._~!$&\'()*+,;=:@%-]+'; | |
| $patterns = array( | |
| '#^licenses/' . $license_key_pattern . '$#', | |
| '#^licenses/activate/' . $license_key_pattern . '$#', | |
| '#^licenses/deactivate/' . $license_key_pattern . '$#', | |
| ); | |
| foreach ( $patterns as $pattern ) { | |
| if ( 1 === preg_match( $pattern, $endpoint ) ) { | |
| return $endpoint; | |
| } | |
| } | |
| broker_error_response( 404, 'Unsupported license endpoint.' ); | |
| return ''; | |
| } | |
| /** | |
| * Build the fixed upstream LMFWC URL for an allowed logical endpoint. | |
| * | |
| * @param string $endpoint Validated logical endpoint. | |
| * @return string | |
| */ | |
| function broker_build_upstream_url( string $endpoint ): string { | |
| $site_url = rtrim( LMFWC_SITE_URL, '/' ); | |
| $parts = parse_url( $site_url ); | |
| if ( | |
| ! is_array( $parts ) || | |
| empty( $parts['scheme'] ) || | |
| empty( $parts['host'] ) || | |
| ! in_array( strtolower( (string) $parts['scheme'] ), array( 'http', 'https' ), true ) | |
| ) { | |
| broker_error_response( 500, 'Broker configuration error.' ); | |
| } | |
| $query = http_build_query( | |
| array( | |
| 'consumer_key' => LMFWC_CONSUMER_KEY, | |
| 'consumer_secret' => LMFWC_CONSUMER_SECRET, | |
| ), | |
| '', | |
| '&', | |
| PHP_QUERY_RFC3986 | |
| ); | |
| return $site_url . '/wp-json/lmfwc/v2/' . $endpoint . '?' . $query; | |
| } | |
| /** | |
| * Perform an upstream GET request using cURL when available, or PHP streams otherwise. | |
| * | |
| * @param string $url Fixed upstream URL. | |
| * @return array{status: int, body: string} | |
| */ | |
| function broker_upstream_get( string $url ): array { | |
| if ( function_exists( 'curl_init' ) ) { | |
| $curl = curl_init( $url ); | |
| if ( false === $curl ) { | |
| broker_error_response( 500, 'Unable to initialize upstream request.' ); | |
| } | |
| curl_setopt_array( | |
| $curl, | |
| array( | |
| CURLOPT_RETURNTRANSFER => true, | |
| CURLOPT_HEADER => false, | |
| CURLOPT_CONNECTTIMEOUT => BROKER_TIMEOUT_SECONDS, | |
| CURLOPT_TIMEOUT => BROKER_TIMEOUT_SECONDS, | |
| CURLOPT_HTTPHEADER => array( | |
| 'Accept: application/json', | |
| 'Content-Type: application/json; charset=UTF-8', | |
| ), | |
| ) | |
| ); | |
| $body = curl_exec( $curl ); | |
| $status = (int) curl_getinfo( $curl, CURLINFO_RESPONSE_CODE ); | |
| $error = curl_errno( $curl ); | |
| curl_close( $curl ); | |
| if ( false === $body || 0 !== $error ) { | |
| broker_error_response( 500, 'License server request failed.' ); | |
| } | |
| return array( | |
| 'status' => $status > 0 ? $status : 500, | |
| 'body' => (string) $body, | |
| ); | |
| } | |
| $context = stream_context_create( | |
| array( | |
| 'http' => array( | |
| 'method' => 'GET', | |
| 'timeout' => BROKER_TIMEOUT_SECONDS, | |
| 'ignore_errors' => true, | |
| 'header' => "Accept: application/json\r\nContent-Type: application/json; charset=UTF-8\r\n", | |
| ), | |
| ) | |
| ); | |
| $body = file_get_contents( $url, false, $context ); | |
| if ( false === $body ) { | |
| broker_error_response( 500, 'License server request failed.' ); | |
| } | |
| $status = 500; | |
| $headers = function_exists( 'http_get_last_response_headers' ) ? http_get_last_response_headers() : array(); | |
| $headers = is_array( $headers ) ? $headers : array(); | |
| foreach ( $headers as $header ) { | |
| if ( is_string( $header ) && 1 === preg_match( '#^HTTP/\S+\s+(\d{3})#', $header, $matches ) ) { | |
| $status = (int) $matches[1]; | |
| break; | |
| } | |
| } | |
| return array( | |
| 'status' => $status, | |
| 'body' => (string) $body, | |
| ); | |
| } | |
| /** | |
| * Enforce optional broker-side product allow-list on successful LMFWC responses. | |
| * | |
| * @param array<string, mixed> $payload Upstream JSON payload. | |
| * @return array<string, mixed> | |
| */ | |
| function broker_apply_product_allow_list( array $payload ): array { | |
| $allowed_product_ids = array_map( 'strval', LMFWC_ALLOWED_PRODUCT_IDS ); | |
| if ( empty( $allowed_product_ids ) || empty( $payload['success'] ) || ! isset( $payload['data'] ) || ! is_array( $payload['data'] ) ) { | |
| return $payload; | |
| } | |
| $product_id = isset( $payload['data']['productId'] ) && is_scalar( $payload['data']['productId'] ) ? (string) $payload['data']['productId'] : ''; | |
| if ( '' !== $product_id && in_array( $product_id, $allowed_product_ids, true ) ) { | |
| return $payload; | |
| } | |
| return array( | |
| 'success' => false, | |
| 'message' => 'The license is not allowed for this product.', | |
| 'data' => array( | |
| 'status' => 403, | |
| ), | |
| ); | |
| } | |
| if ( 'GET' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) { | |
| broker_error_response( 405, 'Method not allowed.' ); | |
| } | |
| if ( '' !== BROKER_PUBLIC_TOKEN ) { | |
| $token = isset( $_GET[ BROKER_PUBLIC_TOKEN_QUERY_PARAM ] ) && is_scalar( $_GET[ BROKER_PUBLIC_TOKEN_QUERY_PARAM ] ) ? (string) $_GET[ BROKER_PUBLIC_TOKEN_QUERY_PARAM ] : ''; | |
| if ( ! hash_equals( BROKER_PUBLIC_TOKEN, $token ) ) { | |
| broker_error_response( 403, 'Broker token rejected.' ); | |
| } | |
| } | |
| if ( '' === LMFWC_CONSUMER_KEY || '' === LMFWC_CONSUMER_SECRET ) { | |
| broker_error_response( 500, 'Broker configuration error.' ); | |
| } | |
| $endpoint = broker_validate_endpoint( broker_get_logical_endpoint() ); | |
| $url = broker_build_upstream_url( $endpoint ); | |
| $result = broker_upstream_get( $url ); | |
| $payload = json_decode( $result['body'], true ); | |
| if ( ! is_array( $payload ) ) { | |
| broker_error_response( 500, 'License server returned an invalid response.' ); | |
| } | |
| $status_code = in_array( $result['status'], array( 200, 201 ), true ) ? $result['status'] : ( $result['status'] > 0 ? $result['status'] : 500 ); | |
| $payload = broker_apply_product_allow_list( $payload ); | |
| if ( isset( $payload['data'] ) && is_array( $payload['data'] ) && isset( $payload['data']['status'] ) ) { | |
| $payload_status = (int) $payload['data']['status']; | |
| if ( $payload_status >= 400 && $payload_status <= 599 ) { | |
| $status_code = $payload_status; | |
| } | |
| } | |
| if ( 403 === (int) ( $payload['data']['status'] ?? 0 ) && false === (bool) ( $payload['success'] ?? false ) ) { | |
| $status_code = 403; | |
| } | |
| broker_json_response( $status_code, $payload ); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a broker file sample that could be used by the "Broker mode" of https://github.com/otakupahp/sdk-license-manager-for-woocommerce