updatepulse-server/inc/api/class-license-api.php
Alexandre Froger cbc129ce72 * Fix issue [Add License Button missing from Licenses tab](https://github.com/Anyape/updatepulse-server/issues/9)
* Fix `Webhook_API::fire_webhook` method to handle base64-encoded compressed payloads
* Fix missing `upserv_deactivate_license_payload` hook in `License_API` class
* Fix issue [Reactivation blocked after reinstalling plugin or website](https://github.com/Anyape/updatepulse-server/issues/8)
2026-03-01 11:13:31 +08:00

1751 lines
46 KiB
PHP

<?php
namespace Anyape\UpdatePulse\Server\API;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
use WP_Error;
use Exception;
use Anyape\UpdatePulse\Server\Server\License\License_Server;
use Anyape\Utils\Utils;
/**
* License API
*
* @since 1.0.0
*/
class License_API {
/**
* Is doing API request
*
* @var boolean|null
* @since 1.0.0
*/
protected static $doing_api_request = null;
/**
* Instance
*
* @var License_API|null
* @since 1.0.0
*/
protected static $instance;
/**
* Config
*
* @var array|null
* @since 1.0.0
*/
protected static $config;
/**
* License server
*
* @var License_Server
* @since 1.0.0
*/
protected $license_server;
/**
* HTTP response code
*
* @var int|null
* @since 1.0.0
*/
protected $http_response_code = null;
/**
* API key ID
*
* @var string|null
* @since 1.0.0
*/
protected $api_key_id;
/**
* API access
*
* @var array|null
* @since 1.0.0
*/
protected $api_access;
/**
* Constructor
*
* @param boolean $init_hooks
* @param boolean $local_request
* @since 1.0.0
*/
public function __construct( $init_hooks = false, $local_request = true ) {
if ( upserv_get_option( 'use_licenses' ) ) {
if ( $local_request ) {
$this->init_server();
}
if ( $init_hooks ) {
add_action( 'init', array( $this, 'add_endpoints' ), -99, 0 );
add_action( 'parse_request', array( $this, 'parse_request' ), -99, 0 );
add_action( 'upserv_pre_activate_license', array( $this, 'upserv_bypass_did_edit_license_action' ), 10, 0 );
add_action( 'upserv_did_activate_license', array( $this, 'upserv_did_license_action' ), 20, 2 );
add_action( 'upserv_pre_deactivate_license', array( $this, 'upserv_bypass_did_edit_license_action' ), 10, 0 );
add_action( 'upserv_did_deactivate_license', array( $this, 'upserv_did_license_action' ), 20, 2 );
add_action( 'upserv_did_add_license', array( $this, 'upserv_did_license_action' ), 20, 2 );
add_action( 'upserv_did_edit_license', array( $this, 'upserv_did_license_action' ), 20, 3 );
add_action( 'upserv_did_delete_license', array( $this, 'upserv_did_license_action' ), 20, 2 );
add_filter( 'query_vars', array( $this, 'query_vars' ), -99, 1 );
add_filter( 'upserv_handle_update_request_params', array( $this, 'upserv_handle_update_request_params' ), 0, 1 );
add_filter( 'upserv_api_license_actions', array( $this, 'upserv_api_license_actions' ), 0, 1 );
add_filter( 'upserv_api_webhook_events', array( $this, 'upserv_api_webhook_events' ), 0, 1 );
add_filter( 'upserv_nonce_api_payload', array( $this, 'upserv_nonce_api_payload' ), 0, 1 );
}
}
}
/*******************************************************************
* Public methods
*******************************************************************/
// API action --------------------------------------------------
/**
* Browse licenses
*
* @param string $query
* @return object Result of the browse operation
* @since 1.0.0
*/
public function browse( $query ) {
$payload = json_decode( wp_unslash( $query ), true );
switch ( json_last_error() ) {
case JSON_ERROR_NONE:
if ( ! empty( $payload['criteria'] ) ) {
foreach ( $payload['criteria'] as $index => $criteria ) {
if ( 'id' === $criteria['field'] ) {
unset( $payload['criteria'][ $index ] );
}
}
}
$result = $this->license_server->browse_licenses( $payload );
if (
is_array( $result ) &&
! empty( $result ) &&
$this->api_access &&
$this->api_key_id &&
! in_array( 'other', $this->api_access, true )
) {
foreach ( $result as $index => $license ) {
if (
! isset( $license->data, $license->data['api_owner'] ) ||
$license->data['api_owner'] !== $this->api_key_id
) {
unset( $result[ $index ] );
}
}
}
break;
case JSON_ERROR_DEPTH:
$result = 'JSON parse error - Maximum stack depth exceeded.';
break;
case JSON_ERROR_STATE_MISMATCH:
$result = 'JSON parse error - Underflow or the modes mismatch.';
break;
case JSON_ERROR_CTRL_CHAR:
$result = 'JSON parse error - Unexpected control character found.';
break;
case JSON_ERROR_SYNTAX:
$result = 'JSON parse error - Syntax error, malformed JSON.';
break;
case JSON_ERROR_UTF8:
$result = 'JSON parse error - Malformed UTF-8 characters, possibly incorrectly encoded.';
break;
default:
$result = 'JSON parse error - Unknown error.';
break;
}
if ( $result instanceof WP_Error ) {
$this->http_response_code = 400;
$result = array(
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
);
} elseif ( ! is_array( $result ) ) {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_json',
'message' => $result,
);
} elseif ( empty( $result ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'licenses_not_found',
'message' => __( 'Licenses not found.', 'updatepulse-server' ),
);
} else {
$result['count'] = count( $result );
}
return (object) $result;
}
/**
* Read license
*
* @param array $license_data
* @return object Result of the read operation
* @since 1.0.0
*/
public function read( $license_data ) {
$result = wp_cache_get(
'upserv_license_' . md5( wp_json_encode( $license_data ) ),
'updatepulse-server',
false,
$found
);
if ( ! $found ) {
$result = $this->license_server->read_license( $license_data );
wp_cache_set(
'upserv_license_' . md5( wp_json_encode( $license_data ) ),
$result,
'updatepulse-server'
);
}
if ( ! is_object( $result ) ) {
if ( isset( $result['license_not_found'] ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
} else {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
}
} elseif ( ! isset( $result->license_key ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
}
return (object) $result;
}
/**
* Edit license
*
* @param array $license_data
* @return object Result of the edit operation
* @since 1.0.0
*/
public function edit( $license_data ) {
if ( upserv_is_doing_api_request() ) {
if ( ! $this->api_access || ! $this->api_key_id || ! in_array( 'other', $this->api_access, true ) ) {
$original = $this->read( $license_data );
if ( isset( $original->data, $original->data['api_owner'] ) ) {
$license_data['data']['api_owner'] = $original->data['api_owner'];
} else {
unset( $license_data['data']['api_owner'] );
}
}
}
$result = $this->license_server->edit_license( $license_data );
if ( ! is_object( $result ) ) {
if ( ! is_array( $result ) ) {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array( __( 'Unknown error.', 'updatepulse-server' ) ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
} elseif ( isset( $result['license_not_found'] ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
} elseif ( ! empty( $result ) ) {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array_values( $result ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
} else {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array( __( 'Unknown error.', 'updatepulse-server' ) ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
}
} elseif ( ! isset( $result->license_key ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
}
return (object) $result;
}
/**
* Add license
*
* @param array $license_data
* @return object Result of the add operation
* @since 1.0.0
*/
public function add( $license_data ) {
if ( $this->api_key_id ) {
$license_data['data']['api_owner'] = $this->api_key_id;
}
$result = $this->license_server->add_license( $license_data );
if ( is_object( $result ) ) {
$result->result = 'success';
$result->message = 'License successfully created';
$result->key = $result->license_key;
} elseif ( ! is_array( $result ) ) {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array( __( 'Unknown error.', 'updatepulse-server' ) ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
} elseif ( ! empty( $result ) ) {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array_values( $result ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
} else {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'errors' => array( __( 'Unknown error.', 'updatepulse-server' ) ),
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
}
return (object) $result;
}
/**
* Delete license
*
* @param array $license_data
* @return object Result of the delete operation
* @since 1.0.0
*/
public function delete( $license_data ) {
$result = $this->license_server->delete_license( $license_data );
if ( ! is_object( $result ) ) {
if ( isset( $license['license_not_found'] ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
} else {
$this->http_response_code = 400;
$result = array(
'code' => 'invalid_license_data',
'message' => __( 'Invalid license data.', 'updatepulse-server' ),
);
}
} elseif ( ! isset( $result->license_key ) ) {
$this->http_response_code = 404;
$result = array(
'code' => 'license_not_found',
'message' => __( 'License not found.', 'updatepulse-server' ),
);
}
return (object) $result;
}
/**
* Check license
*
* @param array $license_data
* @return object Result of the check operation
* @since 1.0.0
*/
public function check( $license_data ) {
/**
* Filter the license data payload before checking a license.
*
* @param array $license_data The license data payload.
* @since 1.0.0
*/
$license_data = apply_filters( 'upserv_check_license_dirty_payload', $license_data );
$license = $this->license_server->read_license( $license_data );
$raw_result = array();
if ( is_object( $license ) ) {
$raw_result = clone $license;
$this->sanitize_license_result( $license );
} else {
$raw_result = $license;
$result = null;
}
/**
* Filter the result of the license check operation.
*
* @param object|null $license The license object or null if not found.
* @param array $license_data The license data payload.
* @since 1.0.0
*/
$result = apply_filters( 'upserv_check_license_result', $license, $license_data );
/**
* Fired after checking a license.
*
* @param mixed $raw_result The raw result of the license check.
* @since 1.0.0
*/
do_action( 'upserv_did_check_license', $raw_result );
if ( ! is_object( $result ) ) {
$this->handle_invalid_license( $result, $license_data );
}
return (object) $result;
}
/**
* Activate license
*
* @param array $license_data
* @return object Result of the activate operation
* @since 1.0.0
*/
public function activate( $license_data ) {
/**
* Filter the license data payload before activating a license.
*
* @param array $license_data The license data payload.
* @since 1.0.0
*/
$license_data = apply_filters( 'upserv_activate_license_dirty_payload', $license_data );
$this->normalize_allowed_domains( $license_data );
$request_slug = isset( $license_data['package_slug'] ) ? $license_data['package_slug'] : false;
$license = $this->license_server->read_license( $license_data );
$domain = $this->extract_domain_from_license_data( $license_data );
/**
* Fired before activating a license.
*
* @param object $license The license object.
* @since 1.0.0
*/
do_action( 'upserv_pre_activate_license', $license );
if ( $this->is_valid_license_for_state_transition( $license, $request_slug, $domain ) ) {
$result = $this->handle_license_activation( $license, $domain );
} else {
$result = $this->handle_invalid_license( $license, $license_data );
}
$raw_result = isset( $result['raw_result'] ) ? $result['raw_result'] : $result;
$result = isset( $result['result'] ) ? $result['result'] : $result;
/**
* Filter the result of the license activation operation.
*
* @param object $result The result of the license activation.
* @param array $license_data The license data payload.
* @param object $license The license object.
* @since 1.0.0
*/
$result = apply_filters( 'upserv_activate_license_result', $result, $license_data, $license );
/**
* Fired after activating a license.
*
* @param mixed $raw_result The raw result of the license activation.
* @param array $license_data The license data payload.
* @since 1.0.0
*/
do_action( 'upserv_did_activate_license', $raw_result, $license_data );
return (object) $result;
}
/**
* Deactivate license
*
* @param array $license_data
* @return object Result of the deactivate operation
* @since 1.0.0
*/
public function deactivate( $license_data ) {
/**
* Filter the license data payload before deactivating a license.
*
* @param array $license_data The license data payload.
* @since 1.0.0
*/
$license_data = apply_filters( 'upserv_deactivate_license_dirty_payload', $license_data );
$this->normalize_allowed_domains( $license_data );
$request_slug = isset( $license_data['package_slug'] ) ? $license_data['package_slug'] : false;
$license = $this->license_server->read_license( $license_data );
$domain = $this->extract_domain_from_license_data( $license_data );
/**
* Fired before deactivating a license.
*
* @param object $license The license object.
* @since 1.0.0
*/
do_action( 'upserv_pre_deactivate_license', $license );
if ( $this->is_valid_license_for_state_transition( $license, $request_slug, $domain ) ) {
$result = $this->handle_license_deactivation( $license, $domain );
} else {
$result = $this->handle_invalid_license( $license, $license_data );
}
$raw_result = isset( $result['raw_result'] ) ? $result['raw_result'] : $result;
$result = isset( $result['result'] ) ? $result['result'] : $result;
/**
* Filter the result of the license deactivation operation.
*
* @param object $result The result of the license deactivation.
* @param array $license_data The license data payload.
* @param object $license The license object.
* @since 1.0.0
*/
$result = apply_filters( 'upserv_deactivate_license_result', $result, $license_data, $license );
/**
* Fired after deactivating a license.
*
* @param mixed $raw_result The raw result of the license deactivation.
* @param array $license_data The license data payload.
* @since 1.0.0
*/
do_action( 'upserv_did_deactivate_license', $raw_result, $license_data );
return (object) $result;
}
// WordPress hooks ---------------------------------------------
/**
* Add endpoints
*
* @since 1.0.0
*/
public function add_endpoints() {
add_rewrite_rule(
'^updatepulse-server-license-api/*$',
'index.php?$matches[1]&__upserv_license_api=1&',
'top'
);
add_rewrite_rule(
'^updatepulse-server-license-api$',
'index.php?&__upserv_license_api=1&',
'top'
);
}
/**
* Parse request
*
* @since 1.0.0
*/
public function parse_request() {
global $wp;
if ( isset( $wp->query_vars['__upserv_license_api'] ) ) {
$this->handle_api_request();
exit;
}
}
/**
* Query vars filter
*
* @param array $query_vars
* @return array The filtered query vars
* @since 1.0.0
*/
public function query_vars( $query_vars ) {
$query_vars = array_merge(
$query_vars,
array(
'__upserv_license_api',
'action',
'api',
'api_token',
'api_credentials',
'browse_query',
'license_key',
'license_signature',
),
array_keys( License_Server::$license_definition )
);
return $query_vars;
}
/**
* Filter update request params
*
* @param array $params
* @return array The filtered params
* @since 1.0.0
*/
public function upserv_handle_update_request_params( $params ) {
global $wp;
$vars = $wp->query_vars;
$params['license_key'] = isset( $vars['license_key'] ) ?
trim( $vars['license_key'] ) :
null;
$params['license_signature'] = isset( $vars['license_signature'] ) ?
trim( $vars['license_signature'] ) :
null;
return $params;
}
/**
* Filter license actions
*
* @param array $actions
* @return array The filtered actions
* @since 1.0.0
*/
public function upserv_api_license_actions( $actions ) {
$actions['browse'] = __( 'Browse multiple license records', 'updatepulse-server' );
$actions['read'] = __( 'Get single license records', 'updatepulse-server' );
$actions['edit'] = __( 'Update license records', 'updatepulse-server' );
$actions['add'] = __( 'Create license records', 'updatepulse-server' );
$actions['delete'] = __( 'Delete license records', 'updatepulse-server' );
return $actions;
}
/**
* Filter webhook events
*
* @param array $webhook_events
* @return array The filtered webhook events
* @since 1.0.0
*/
public function upserv_api_webhook_events( $webhook_events ) {
if ( isset( $webhook_events['license'], $webhook_events['license']['events'] ) ) {
$webhook_events['license']['events']['license_activate'] = __( 'License activated', 'updatepulse-server' );
$webhook_events['license']['events']['license_deactivate'] = __( 'License deactivated', 'updatepulse-server' );
$webhook_events['license']['events']['license_add'] = __( 'License added', 'updatepulse-server' );
$webhook_events['license']['events']['license_edit'] = __( 'License edited', 'updatepulse-server' );
$webhook_events['license']['events']['license_delete'] = __( 'License deleted', 'updatepulse-server' );
}
return $webhook_events;
}
/**
* Bypass license action
*
* @since 1.0.0
*/
public function upserv_bypass_did_edit_license_action() {
remove_action( 'upserv_did_edit_license', array( $this, 'upserv_did_license_action' ), 20 );
}
/**
* License action
*
* @param object $result
* @param array $payload
* @param object $original
* @since 1.0.0
*/
public function upserv_did_license_action( $result, $payload, $original = null ) {
$format = '';
$event = 'license_' . str_replace(
array( 'upserv_did_', '_license' ),
array( '', '' ),
current_action()
);
if ( ! is_object( $result ) ) {
$description = sprintf(
// translators: %s is operation slug
esc_html__( 'An error occured for License operation `%s` on UpdatePulse Server.', 'updatepulse-server' ),
$event
);
$content = array(
'error' => true,
'result' => $result,
'payload' => $payload,
);
} else {
$content = null !== $original ?
array(
'new' => $result,
'original' => $original,
) :
$result;
switch ( $event ) {
case 'license_edit':
// translators: %s is the license key
$format = esc_html__( 'The license `%s` has been edited on UpdatePulse Server', 'updatepulse-server' );
break;
case 'license_add':
// translators: %s is the license key
$format = esc_html__( 'The license `%s` has been added on UpdatePulse Server', 'updatepulse-server' );
break;
case 'license_delete':
// translators: %s is the license key
$format = esc_html__( 'The license `%s` has been deleted on UpdatePulse Server', 'updatepulse-server' );
break;
case 'license_activate':
// translators: %s is the license key
$format = esc_html__( 'The license `%s` has been activated on UpdatePulse Server', 'updatepulse-server' );
break;
case 'license_deactivate':
// translators: %s is the license key
$format = esc_html__( 'The license `%s` has been deactivated on UpdatePulse Server', 'updatepulse-server' );
break;
default:
return;
}
$description = sprintf( $format, $result->license_key );
}
$payload = array(
'event' => $event,
'description' => $description,
'content' => $content,
);
add_filter( 'upserv_webhook_fire', array( $this, 'upserv_webhook_fire' ), 10, 4 );
upserv_schedule_webhook( $payload, 'license' );
remove_filter( 'upserv_webhook_fire', array( $this, 'upserv_webhook_fire' ), 10 );
}
/**
* Webhook fire filter
*
* @param boolean $fire
* @param array $payload
* @param string $url
* @param array $info
* @return boolean The filtered fire value
* @since 1.0.0
*/
public function upserv_webhook_fire( $fire, $payload, $url, $info ) {
if ( ! isset( $info['licenseAPIKey'] ) || empty( $info['licenseAPIKey'] ) ) {
return $fire;
}
$owner = false;
if (
is_array( $payload['content'] ) &&
isset( $payload['content']['new'] ) &&
isset( $payload['content']['new']->data['api_owner'] )
) {
$owner = $payload['content']['new']->data['api_owner'];
} elseif (
is_object( $payload['content'] ) &&
isset( $payload['content']->data['api_owner'] )
) {
$owner = $payload['content']->data['api_owner'];
}
$config = self::get_config();
$api_access = false;
foreach ( $config['private_api_auth_keys'] as $id => $values ) {
if (
$id === $info['licenseAPIKey'] &&
isset( $values['access'] ) &&
is_array( $values['access'] )
) {
$api_access = $values['access'];
break;
}
}
if ( $api_access && in_array( 'other', $api_access, true ) ) {
$fire = true;
} elseif ( $api_access ) {
$action = str_replace( 'license_', '', $payload['event'] );
if (
in_array( 'all', $api_access, true ) ||
in_array( 'read', $api_access, true ) ||
in_array( 'browse', $api_access, true ) ||
(
in_array( $action, array( 'edit', 'add', 'delete' ), true ) &&
in_array( $action, $api_access, true )
)
) {
$fire = $owner === $info['licenseAPIKey'];
} else {
$fire = false;
}
} else {
$fire = $owner === $info['licenseAPIKey'];
}
return $fire;
}
/**
* Fetch nonce filter
*
* @param string $nonce
* @param string $true_nonce
* @param int $expiry
* @param array $data
* @return string|null The filterd nonce or null if invalid
* @since 1.0.0
*/
public function upserv_fetch_nonce_private( $nonce, $true_nonce, $expiry, $data ) {
$config = self::get_config();
$valid = false;
if (
! empty( $config['private_api_auth_keys'] ) &&
isset( $data['license_api'], $data['license_api']['id'], $data['license_api']['access'] )
) {
global $wp;
$action = $wp->query_vars['action'];
foreach ( $config['private_api_auth_keys'] as $id => $values ) {
if (
$id === $data['license_api']['id'] &&
isset( $values['access'] ) &&
is_array( $values['access'] ) &&
(
in_array( 'all', $values['access'], true ) ||
in_array( $action, $values['access'], true )
)
) {
$this->api_key_id = $id;
$this->api_access = $values['access'];
$valid = true;
break;
}
}
}
if ( ! $valid ) {
$nonce = null;
}
return $nonce;
}
/**
* Nonce API payload filter
*
* @param array $payload
* @return array The filtered payload
* @since 1.0.0
*/
public function upserv_nonce_api_payload( $payload ) {
global $wp;
if ( ! isset( $wp->query_vars['api'] ) || 'license' !== $wp->query_vars['api'] ) {
return $payload;
}
$key_id = false;
$credentials = array();
$config = self::get_config();
if ( ! empty( $_SERVER['HTTP_X_UPDATEPULSE_API_CREDENTIALS'] ) ) {
$credentials = explode(
'|',
sanitize_text_field(
wp_unslash( $_SERVER['HTTP_X_UPDATEPULSE_API_CREDENTIALS'] )
)
);
} elseif (
isset( $wp->query_vars['api_credentials'], $wp->query_vars['api'] ) &&
is_string( $wp->query_vars['api_credentials'] ) &&
! empty( $wp->query_vars['api_credentials'] )
) {
$credentials = explode( '|', $wp->query_vars['api_credentials'] );
}
if ( 2 === count( $credentials ) ) {
$key_id = end( $credentials );
}
if ( $key_id && isset( $config['private_api_auth_keys'][ $key_id ] ) ) {
$values = $config['private_api_auth_keys'][ $key_id ];
$payload['data']['license_api'] = array(
'id' => $key_id,
'access' => isset( $values['access'] ) ? $values['access'] : array(),
);
}
$payload['expiry_length'] = HOUR_IN_SECONDS / 2;
return $payload;
}
// Misc. -------------------------------------------------------
/**
* Is doing API request
*
* @return boolean True if doing API request, false otherwise
* @since 1.0.0
*/
public static function is_doing_api_request() {
if ( null === self::$doing_api_request ) {
self::$doing_api_request = Utils::is_url_subpath_match( '/^updatepulse-server-license-api$/' );
}
return self::$doing_api_request;
}
/**
* Get config
*
* @return array The config
* @since 1.0.0
*/
public static function get_config() {
if ( ! self::$config ) {
$config = array(
'private_api_auth_keys' => upserv_get_option( 'api/licenses/private_api_keys' ),
'ip_whitelist' => upserv_get_option( 'api/licenses/private_api_ip_whitelist' ),
);
self::$config = $config;
}
/**
* Filter the License API configuration.
*
* @param array $config The License API configuration.
* @since 1.0.0
*/
return apply_filters( 'upserv_license_api_config', self::$config );
}
/**
* Get instance
*
* @return License_API The instance
* @since 1.0.0
*/
public static function get_instance() {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Is package require license
*
* @param int $package_id
* @return boolean True if package requires license, false otherwise
* @since 1.0.0
*/
public static function is_package_require_license( $package_id ) {
$require_license = wp_cache_get( 'upserv_package_require_license_' . $package_id, 'updatepulse-server', false, $found );
if ( ! $found ) {
$package_info = upserv_get_package_info( $package_id, false );
$require_license = (
is_array( $package_info ) &&
isset( $package_info['require_license'] ) &&
$package_info['require_license']
);
wp_cache_set( 'upserv_package_require_license_' . $package_id, $require_license, 'updatepulse-server' );
}
return $require_license;
}
/*******************************************************************
* Protected methods
*******************************************************************/
/**
* Sanitize license result
*
* @param object $result - by reference
* @return void
* @since 1.0.0
*/
protected function sanitize_license_result( &$result ) {
$num_allowed_domains = (
isset( $result->allowed_domains ) &&
is_array( $result->allowed_domains )
) ? count( $result->allowed_domains ) : 0;
$result->num_allowed_domains = $num_allowed_domains;
$properties_to_unset = array(
'allowed_domains',
'hmac_key',
'crypto_key',
'data',
'owner_name',
'email',
'company_name',
'txn_id',
);
foreach ( $properties_to_unset as $property ) {
unset( $result->$property );
}
}
/**
* Prepare error response
*
* @param string $code
* @param string $message
* @param array $data
* @return array The response
* @since 1.0.0
*/
protected function prepare_error_response( $code, $message, $data = array() ) {
return array(
'code' => $code,
'message' => $message,
'data' => $data,
);
}
/**
* Normalize allowed domains
*
* @param array $license_data - by reference
* @return void
* @since 1.0.0
*/
protected function normalize_allowed_domains( &$license_data ) {
if ( isset( $license_data['allowed_domains'] ) && ! is_array( $license_data['allowed_domains'] ) ) {
$license_data['allowed_domains'] = array( $license_data['allowed_domains'] );
}
}
/**
* Extract domain from license data
*
* @param array $license_data
* @return string|false The first domain found or false if not found
* @since 1.0.0
*/
protected function extract_domain_from_license_data( $license_data ) {
if (
isset( $license_data['allowed_domains'] ) &&
is_array( $license_data['allowed_domains'] ) &&
1 === count( $license_data['allowed_domains'] )
) {
return $license_data['allowed_domains'][0];
}
return false;
}
/**
* Is valid license for state transition
*
* @param object $license
* @param string $request_slug
* @param string $domain
* @return boolean True if valid, false otherwise
* @since 1.0.0
*/
protected function is_valid_license_for_state_transition( $license, $request_slug, $domain ) {
return (
is_object( $license ) &&
$domain &&
isset( $license->package_slug ) &&
$request_slug === $license->package_slug
);
}
/**
* Handle license activation
*
* @param object $license
* @param string $domain
* @return array|null The result or null if not found
* @since 1.0.0
*/
protected function handle_license_activation( $license, $domain ) {
if ( in_array( $license->status, array( 'expired', 'blocked', 'on-hold' ), true ) ) {
$this->http_response_code = 403;
return $this->prepare_illegal_status_response( $license );
} elseif ( 'activated' === $license->status && in_array( $domain, $license->allowed_domains, true ) ) {
$this->http_response_code = 409;
return $this->process_already_activated( $license, $domain );
} elseif (
( count( $license->allowed_domains ) + 1 ) > abs( intval( $license->max_allowed_domains ) )
) {
$this->http_response_code = 422;
return $this->prepare_max_domains_response( $license );
}
return $this->process_license_activation( $license, $domain );
}
/**
* Prepare illegal status response
*
* @param object $license
* @return array The response
* @since 1.0.0
*/
protected function prepare_illegal_status_response( $license ) {
$response = array(
'code' => 'illegal_license_status',
'message' => __( 'The license cannot be activated due to its current status.', 'updatepulse-server' ),
'data' => array(
'status' => $license->status,
),
);
if ( 'expired' === $license->status ) {
$response['data']['date_expiry'] = $license->date_expiry;
}
return $response;
}
/**
* Prepare max domains response
*
* @param object $license
* @return array The response
* @since 1.0.0
*/
protected function prepare_max_domains_response( $license ) {
return array(
'code' => 'max_domains_reached',
'message' => __( 'The license has reached the maximum allowed activations for domains.', 'updatepulse-server' ),
'data' => array(
'max_allowed_domains' => $license->max_allowed_domains,
),
);
}
/**
* Process already activated license
*
* @param object $license
* @param string $domain
* @return array The response
* @since 1.0.0
*/
protected function process_already_activated( $license, $domain ) {
$data = isset( $license->data ) ? $license->data : array();
$op = 'read';
if ( ! isset( $data['next_deactivate'] ) || time() > $data['next_deactivate'] ) {
/**
* Filter the timestamp for the next allowed deactivation after activation.
*
* @param int $timestamp The timestamp for the next allowed deactivation.
* @param object $license The license object.
* @since 1.0.0
*/
$data['next_deactivate'] = apply_filters( 'upserv_activate_license_next_deactivate', time(), $license );
$op = 'edit';
}
$payload = array(
'license_key' => $license->license_key,
'status' => 'activated',
'allowed_domains' => array_unique( array_merge( array( $domain ), $license->allowed_domains ) ),
'data' => $data,
);
try {
if ( 'edit' === $op ) {
$result = $this->license_server->edit_license(
/**
* Filter the payload for license activation.
*
* @param array $payload The license activation payload.
* @since 1.0.0
*/
apply_filters( 'upserv_activate_license_payload', $payload )
);
} else {
$result = $this->license_server->read_license( $payload );
}
} catch ( Exception $e ) {
return array( $e->getMessage() );
}
if ( is_object( $result ) ) {
$result->license_signature = $this->license_server->generate_license_signature( $license, $domain );
$result->next_deactivate = $data['next_deactivate'];
$this->sanitize_license_result( $result );
} else {
$result = null;
}
return array(
'code' => 'license_already_activated',
'message' => __( 'The license is already activated for the specified domain.', 'updatepulse-server' ),
'data' => array(
'domain' => $domain,
'license' => $result,
),
);
}
/**
* Process license activation
*
* @param object $license
* @param string $domain
* @return array|null The result or null if not found
* @since 1.0.0
*/
protected function process_license_activation( $license, $domain ) {
$data = isset( $license->data ) ? $license->data : array();
if ( ! isset( $data['next_deactivate'] ) || time() > $data['next_deactivate'] ) {
/**
* Filter the timestamp for the next allowed deactivation after activation.
*
* @param int $timestamp The timestamp for the next allowed deactivation.
* @param object $license The license object.
* @since 1.0.0
*/
$data['next_deactivate'] = apply_filters( 'upserv_activate_license_next_deactivate', time(), $license );
}
$payload = array(
'license_key' => $license->license_key,
'status' => 'activated',
'allowed_domains' => array_unique( array_merge( array( $domain ), $license->allowed_domains ) ),
'data' => $data,
);
try {
$result = $this->license_server->edit_license(
/**
* Filter the payload for license activation.
*
* @param array $payload The license activation payload.
* @since 1.0.0
*/
apply_filters( 'upserv_activate_license_payload', $payload )
);
} catch ( Exception $e ) {
return array( $e->getMessage() );
}
if ( is_object( $result ) ) {
$result->license_signature = $this->license_server->generate_license_signature( $license, $domain );
$raw_result = clone $result;
$result->next_deactivate = $data['next_deactivate'];
$this->sanitize_license_result( $result );
return array(
'result' => $result,
'raw_result' => $raw_result,
);
}
return null;
}
/**
* Handle license deactivation
*
* @param object $license
* @param string $domain
* @return array|null The result or null if not found
* @since 1.0.0
*/
protected function handle_license_deactivation( $license, $domain ) {
if ( in_array( $license->status, array( 'expired', 'blocked', 'on-hold' ), true ) ) {
$this->http_response_code = 403;
return $this->prepare_illegal_status_response( $license );
} elseif ( 'deactivated' === $license->status || ! in_array( $domain, $license->allowed_domains, true ) ) {
$this->http_response_code = 409;
return $this->process_already_deactivated( $license, $domain );
} elseif (
isset( $license->data, $license->data['next_deactivate'] ) &&
$license->data['next_deactivate'] > time()
) {
$this->http_response_code = 403;
return $this->prepare_too_early_deactivation_response( $license );
}
return $this->process_license_deactivation( $license, $domain );
}
/**
* Process already deactivated
*
* @param object $license
* @param string $domain
* @return array The response
* @since 1.0.11
*/
protected function process_already_deactivated( $license, $domain ) {
$payload = array( 'license_key' => $license->license_key );
try {
$result = $this->license_server->read_license( $payload );
} catch ( Exception $e ) {
return array( $e->getMessage() );
}
if ( is_object( $result ) ) {
$this->sanitize_license_result( $result );
} else {
$result = null;
}
return array(
'code' => 'license_already_deactivated',
'message' => __( 'The license is already deactivated for the specified domain.', 'updatepulse-server' ),
'data' => array(
'domain' => $domain,
'license' => $result,
),
);
}
/**
* Prepare too early deactivation response
*
* @param object $license
* @return array The response
* @since 1.0.0
*/
protected function prepare_too_early_deactivation_response( $license ) {
return array(
'code' => 'too_early_deactivation',
'message' => __( 'The license cannot be deactivated before the specified date.', 'updatepulse-server' ),
'data' => array(
'next_deactivate' => $license->data['next_deactivate'],
),
);
}
/**
* Process license deactivation
*
* @param object $license
* @param string $domain
* @return array|null The result or null if not found
* @since 1.0.0
*/
protected function process_license_deactivation( $license, $domain ) {
$data = isset( $license->data ) ? $license->data : array();
/**
* Filter the timestamp for the next allowed deactivation.
*
* @param int $timestamp The timestamp for the next allowed deactivation.
* @param object $license The license object.
* @since 1.0.0
*/
$data['next_deactivate'] = apply_filters(
'upserv_deactivate_license_next_deactivate',
(bool) ( constant( 'WP_DEBUG' ) ) ? time() + ( MINUTE_IN_SECONDS / 4 ) : time() + MONTH_IN_SECONDS,
$license
);
$allowed_domains = array_diff( $license->allowed_domains, array( $domain ) );
$payload = array(
'license_key' => $license->license_key,
'status' => empty( $allowed_domains ) ? 'deactivated' : $license->status,
'allowed_domains' => $allowed_domains,
'data' => $data,
);
try {
$result = $this->license_server->edit_license(
/**
* Filter the payload for license deactivation.
*
* @param array $payload The license deactivation payload.
* @since 1.0.0
*/
apply_filters( 'upserv_deactivate_license_payload', $payload )
);
} catch ( Exception $e ) {
return array( $e->getMessage() );
}
if ( is_object( $result ) ) {
$result->license_signature = $this->license_server->generate_license_signature( $license, $domain );
$raw_result = clone $result;
$result->next_deactivate = $data['next_deactivate'];
$this->sanitize_license_result( $result );
return array(
'result' => $result,
'raw_result' => $raw_result,
);
}
return null;
}
/**
* Handle invalid license
*
* @param array $license
* @param array $license_data
* @return array The response
* @since 1.0.0
*/
protected function handle_invalid_license( $license, $license_data ) {
if ( is_array( $license ) && isset( $license['license_not_found'] ) ) {
$this->http_response_code = 400;
return $this->prepare_error_response(
'invalid_license_key',
__( 'The provided license key is invalid.', 'updatepulse-server' ),
array( 'license_key' => $license_data['license_key'] ? $license_data['license_key'] : false )
);
}
if ( ! is_array( $license ) || empty( $license ) || ! isset( $license[0] ) ) {
$license = array( __( 'Unknown error.', 'updatepulse-server' ) );
}
$this->http_response_code = 500;
return $this->prepare_error_response(
'unexpected_error',
__( 'This is an unexpected error. Please contact support.', 'updatepulse-server' ),
array( 'error' => $license )
);
}
/**
* Authorize private API access
*
* @param string $action
* @param array $payload
* @return boolean True if authorized, false otherwise
* @since 1.0.0
*/
protected function authorize_private( $action, $payload ) {
$token = false;
$is_auth = false;
if ( ! empty( $_SERVER['HTTP_X_UPDATEPULSE_TOKEN'] ) ) {
$token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_UPDATEPULSE_TOKEN'] ) );
} else {
global $wp;
if (
isset( $wp->query_vars['api_token'] ) &&
is_string( $wp->query_vars['api_token'] ) &&
! empty( $wp->query_vars['api_token'] )
) {
$token = $wp->query_vars['api_token'];
}
}
add_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_private' ), 10, 4 );
$is_auth = upserv_validate_nonce( $token );
remove_filter( 'upserv_fetch_nonce', array( $this, 'upserv_fetch_nonce_private' ), 10 );
if ( $this->api_key_id && $this->api_access ) {
if ( 'browse' === $action || 'add' === $action ) {
$is_auth = $is_auth && (
in_array( 'all', $this->api_access, true ) ||
in_array( $action, $this->api_access, true )
);
} elseif ( isset( $payload['license_key'] ) ) {
$license = $this->license_server->read_license( $payload );
$is_auth = $is_auth && (
! is_object( $license ) ||
(
is_object( $license ) &&
empty( get_object_vars( $license ) )
) ||
(
is_object( $license ) &&
(
in_array( 'all', $this->api_access, true ) ||
in_array( $action, $this->api_access, true )
) &&
(
(
isset( $license->data['api_owner'] ) &&
$this->api_key_id === $license->data['api_owner']
) ||
in_array( 'other', $this->api_access, true )
)
)
);
}
}
return $is_auth;
}
/**
* Is API public
*
* @param string $method
* @return boolean True if public, false otherwise
* @since 1.0.0
*/
protected function is_api_public( $method ) {
/**
* Filter the list of public License API actions.
*
* @param array $public_api_actions List of public License API actions.
* @since 1.0.0
*/
$public_api = apply_filters(
'upserv_license_public_api_actions',
array(
'check',
'activate',
'deactivate',
)
);
$is_api_public = in_array( $method, $public_api, true );
return $is_api_public;
}
/**
* Handle API request
*
* @since 1.0.0
*/
protected function handle_api_request() {
global $wp;
$method = isset( $wp->query_vars['action'] ) ? $wp->query_vars['action'] : false;
if (
sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'action' ) ) ) &&
! $this->is_api_public( $method )
) {
$this->http_response_code = 405;
$response = array(
'code' => 'method_not_allowed',
'message' => __( 'Unauthorized GET method.', 'updatepulse-server' ),
);
} else {
$malformed_request = false;
if ( ! isset( $wp->query_vars['action'] ) ) {
$malformed_request = true;
} elseif ( 'browse' === $wp->query_vars['action'] ) {
if ( isset( $wp->query_vars['browse_query'] ) ) {
$payload = $wp->query_vars['browse_query'];
} else {
$malformed_request = true;
}
} else {
$payload = $wp->query_vars;
unset( $payload['id'] );
}
if ( ! $malformed_request ) {
$this->init_server();
/**
* Filter whether the License API request is authorized.
*
* @param bool $authorized Whether the License API request is authorized.
* @param string $method The method of the request.
* @param array $payload The payload of the request.
* @since 1.0.0
*/
$authorized = apply_filters(
'upserv_license_api_request_authorized',
(
$this->is_api_public( $method ) ||
(
$this->authorize_ip() &&
$this->authorize_private( $method, $payload )
)
),
$method,
$payload
);
if ( $authorized ) {
/**
* Fired before the License API request is processed.
*
* @param string $method The License API action.
* @param array $payload The payload of the request.
* @since 1.0.0
*/
do_action( 'upserv_license_api_request', $method, $payload );
if ( method_exists( $this, $method ) ) {
$response = $this->$method( $payload );
if ( is_object( $response ) && ! empty( get_object_vars( $response ) ) ) {
$response->time_elapsed = Utils::get_time_elapsed();
}
} else {
$this->http_response_code = 400;
$response = array(
'code' => 'action_not_found',
'message' => __( 'License API action not found.', 'updatepulse-server' ),
);
}
} else {
$this->http_response_code = 403;
$response = array(
'code' => 'unauthorized',
'message' => __( 'Unauthorized access.', 'updatepulse-server' ),
);
}
} else {
$this->http_response_code = 400;
$response = array(
'code' => 'malformed_request',
'message' => __( 'Malformed request.', 'updatepulse-server' ),
);
}
}
wp_send_json( $response, $this->http_response_code, Utils::JSON_OPTIONS );
}
/**
* Authorize IP
*
* @return boolean True if authorized, false otherwise
* @since 1.0.0
*/
protected function authorize_ip() {
$result = false;
$config = self::get_config();
if ( is_array( $config['ip_whitelist'] ) && ! empty( $config['ip_whitelist'] ) ) {
foreach ( $config['ip_whitelist'] as $range ) {
if ( Utils::cidr_match( Utils::get_remote_ip(), $range ) ) {
$result = true;
break;
}
}
} else {
$result = true;
}
return $result;
}
/**
* Init server
*
* @since 1.0.0
*/
protected function init_server() {
/**
* Filter the License Server instance.
*
* @param License_Server $license_server The license server instance.
* @since 1.0.0
*/
$this->license_server = apply_filters( 'upserv_license_server', new License_Server() );
}
}