diff --git a/README.md b/README.md index a4d867fc..4f01a124 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ add_action( 'plugins_loaded', function() { */ Config::set_token_auth_prefix( 'my_origin' ); + // Optionally, change the default auth token caching. + Config::set_auth_cache_expiration( WEEK_IN_SECONDS ); + + // Or, disable it completely. + Config::set_auth_cache_expiration( -1 ); + Uplink::init(); }, 0 ); ``` diff --git a/docs/features.md b/docs/features.md index a7966d34..7e8e35d7 100644 --- a/docs/features.md +++ b/docs/features.md @@ -89,6 +89,8 @@ On the _Plugins_ page in the WP Dashboard, any plugin that has an update availab ### Multisite Licenses +TODO: Notes about stored license file? + Out of the box, Uplink treats all sub-sites as their _own independent site_, storing license keys and authorization tokens in the options table of that sub-site. @@ -97,15 +99,15 @@ multisite network across multiple situations. > 💡 To operate at the network level, your plugin **must be network activated** and the proper configuration options enabled. -| Install Type | Network Activated? | Clause | Uplink Config Option (default is false) | License & Token Storage Location | -|----------------------------|--------------------|--------|-----------------------------------------------------|----------------------------------| -| Standard | – | – | – | Site level | -| Multisite (subfolders) | Yes | AND | `Config::set_network_subfolder_license(true)` | Network level | -| Multisite (subfolders) | No | OR | `Config::set_network_subfolder_license(false)` | Site Level | -| Multisite (subdomains) | Yes | AND | `Config::set_network_subdomain_license(true)` | Network level | -| Multisite (subdomains) | No | OR | `Config::set_network_subdomain_license(false)` | Site Level | -| Multisite (domain mapping) | Yes | AND | `Config::set_network_domain_mapping_license(true)` | Network level | -| Multisite (domain mapping) | No | OR | `Config::set_network_domain_mapping_license(false)` | Site Level | +| Install Type | Network Activated? | Clause | Uplink Config Option (default is false) | License & Token Storage Location | +|----------------------------|--------------------|--------|------------------------------------------------------------------------|----------------------------------| +| Standard | – | – | – | Site level | +| Multisite (subfolders) | Yes | AND | `Config::allow_site_level_licenses_for_subfolder_multisite(true)` | Network level | +| Multisite (subfolders) | No | OR | `Config::allow_site_level_licenses_for_subfolder_multisite(false)` | Site Level | +| Multisite (subdomains) | Yes | AND | `Config::allow_site_level_licenses_for_subdomain_multisite(true)` | Network level | +| Multisite (subdomains) | No | OR | `Config::allow_site_level_licenses_for_subdomain_multisite(false)` | Site Level | +| Multisite (domain mapping) | Yes | AND | `Config::allow_site_level_licenses_for_mapped_domain_multisite(true)` | Network level | +| Multisite (domain mapping) | No | OR | `Config::allow_site_level_licenses_for_mapped_domain_multisite(false)` | Site Level | #### Examples @@ -118,13 +120,13 @@ use StellarWP\Uplink\Uplink; // ...other config above // Allow a single network license for multisite subfolders. -Config::set_network_subfolder_license( true ); +Config::allow_site_level_licenses_for_subfolder_multisite( true ); // Allow a single network license for multisite using subdomains. -Config::set_network_subdomain_license( true ); +Config::allow_site_level_licenses_for_subdomain_multisite( true ); // Allow a single network license for custom domains/mapped domains. -Config::set_network_domain_mapping_license( true ); +Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); Uplink::init(); ``` @@ -140,17 +142,17 @@ add_action( 'init', static function(): void { // Replace with a call to your own plugin's config/option to check whether it should be managed in the network. $network_enabled = true; - add_filter( 'stellarwp/uplink/my-plugin-slug/allows_network_subfolder_license', + add_filter( 'stellarwp/uplink/my-plugin-slug/supports_site_level_licenses_for_subfolder_multisite', static function () use ( $network_enabled ): bool { return $network_enabled; }, 10 ); - add_filter( 'stellarwp/uplink/my-plugin-slug/allows_network_subdomain_license', + add_filter( 'stellarwp/uplink/my-plugin-slug/supports_site_level_licenses_for_subdomain_multisite', static function () use ( $network_enabled ): bool { return $network_enabled; }, 10 ); - add_filter( 'stellarwp/uplink/my-plugin-slug/allows_network_domain_mapping_license', + add_filter( 'stellarwp/uplink/my-plugin-slug/supports_site_level_licenses_for_mapped_domain_multisite', static function () use ( $network_enabled ): bool { return $network_enabled; }, 10 ); @@ -185,7 +187,4 @@ the correct location. // Gets the local URL to disconnect a token (delete it locally). \StellarWP\Uplink\get_disconnect_url( 'my-plugin-slug' ); - -// Checks if the current site (e.g. the sub-site on multisite) would support multisite licensing. -\StellarWP\Uplink\allows_multisite_license( 'my-plugin-slug' ); ``` diff --git a/src/Uplink/API/Client.php b/src/Uplink/API/Client.php index fe178aa7..f1dd6af9 100644 --- a/src/Uplink/API/Client.php +++ b/src/Uplink/API/Client.php @@ -18,6 +18,7 @@ * @property-read ContainerInterface $container Container instance. */ class Client { + /** * API base endpoint. * @@ -187,16 +188,15 @@ protected function request( $method, $endpoint, $args ) { * * @since 1.0.0 * - * @param Resource $resource Resource to validate. - * @param string|null $key License key. - * @param string $validation_type Validation type (local or network). - * @param bool $force Force the validation. + * @param Resource $resource Resource to validate. + * @param string|null $key License key. + * @param bool $force Force the validation. * * @throws \RuntimeException * - * @return mixed + * @return Validation_Response|mixed */ - public function validate_license( Resource $resource, ?string $key = null, string $validation_type = 'local', bool $force = false ) { + public function validate_license( Resource $resource, ?string $key = null, bool $force = false ) { /** @var Data */ $site_data = $this->container->get( Data::class ); $args = $resource->get_validation_args(); @@ -235,7 +235,7 @@ public function validate_license( Resource $resource, ?string $key = null, strin $results = null; } - $results = new Validation_Response( $key, $validation_type, $results, $resource ); + $results = new Validation_Response( $key, $results, $resource ); /** * Filter the license validation results. diff --git a/src/Uplink/API/V3/Auth/Contracts/Token_Authorizer.php b/src/Uplink/API/V3/Auth/Contracts/Token_Authorizer.php new file mode 100644 index 00000000..7de31571 --- /dev/null +++ b/src/Uplink/API/V3/Auth/Contracts/Token_Authorizer.php @@ -0,0 +1,21 @@ +authorizer = $authorizer; + $this->expiration = $expiration; + } + + /** + * Check if a license is authorized and cache successful responses. + * + * @see Config::set_auth_cache_expiration() + * @see is_authorized() + * @see Token_Authorizer + * + * @param string $license The license key. + * @param string $token The stored token. + * @param string $domain The user's domain. + * + * @return bool + */ + public function is_authorized( string $license, string $token, string $domain ): bool { + $transient = $this->build_transient( [ $license, $token, $domain ] ); + $is_authorized = get_transient( $transient ); + + if ( $is_authorized === true ) { + return true; + } + + $is_authorized = $this->authorizer->is_authorized( $license, $token, $domain ); + + // Only cache successful responses. + if ( $is_authorized ) { + set_transient( $transient, true, $this->expiration ); + } + + return $is_authorized; + } + + /** + * Build a transient key. + * + * @param array ...$args + * + * @return string + */ + public function build_transient( array ...$args ): string { + return self::TRANSIENT_PREFIX . hash( 'sha256', json_encode( $args ) ); + } + +} diff --git a/src/Uplink/API/V3/Provider.php b/src/Uplink/API/V3/Provider.php index ad2bb834..2d4795e0 100644 --- a/src/Uplink/API/V3/Provider.php +++ b/src/Uplink/API/V3/Provider.php @@ -4,6 +4,8 @@ use StellarWP\Uplink\API\V3\Auth\Auth_Url_Cache_Decorator; use StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url; +use StellarWP\Uplink\API\V3\Auth\Contracts\Token_Authorizer; +use StellarWP\Uplink\API\V3\Auth\Token_Authorizer_Cache_Decorator; use StellarWP\Uplink\API\V3\Contracts\Client_V3; use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; @@ -54,6 +56,36 @@ public function register() { return new Client( $api_root, $base_url, $request_args, new WP_Http() ); } ); + + $this->register_token_authorizer(); + } + + /** + * Based on the developer's configuration, determine if we will enable Token Authorization caching. + * + * @return void + */ + private function register_token_authorizer(): void { + $expiration = Config::get_auth_cache_expiration(); + + if ( $expiration >= 0 ) { + $this->container->bind( + Token_Authorizer::class, + static function ( $c ) use ( $expiration ): Token_Authorizer { + return new Token_Authorizer_Cache_Decorator( + $c->get( Auth\Token_Authorizer::class ), + $expiration + ); + } + ); + + return; + } + + $this->container->bind( + Token_Authorizer::class, + Auth\Token_Authorizer::class + ); } } diff --git a/src/Uplink/API/Validation_Response.php b/src/Uplink/API/Validation_Response.php index 4fb60cc4..92a48d8d 100644 --- a/src/Uplink/API/Validation_Response.php +++ b/src/Uplink/API/Validation_Response.php @@ -98,15 +98,6 @@ class Validation_Response { */ protected $result = 'success'; - /** - * Validation type. - * - * @since 1.0.0 - * - * @var string - */ - protected $validation_type; - /** * Version from validation response. * @@ -121,16 +112,14 @@ class Validation_Response { * * @since 1.0.0 * - * @param string|null $key License key. - * @param string $validation_type Validation type (local or network). - * @param stdClass|null $response Validation response. - * @param Resource $resource Resource instance. + * @param string|null $key License key. + * @param stdClass|null $response Validation response. + * @param Resource $resource Resource instance. */ - public function __construct( ?string $key, string $validation_type, ?stdClass $response, Resource $resource ) { - $this->key = $key ?: ''; - $this->validation_type = 'network' === $validation_type ? 'network' : 'local'; - $this->response = $response; - $this->resource = $resource; + public function __construct( ?string $key, ?stdClass $response, Resource $resource ) { + $this->key = (string) $key; + $this->response = $response; + $this->resource = $resource; if ( isset( $this->response->results ) ) { $this->response = is_array( $this->response->results ) ? reset( $this->response->results ) : $this->response->results; @@ -406,7 +395,7 @@ public function set_is_valid( bool $is_valid ): void { * @return void */ private function parse(): void { - $this->current_key = $this->resource->get_license_key( $this->validation_type ); + $this->current_key = $this->resource->get_license_key(); $this->expiration = $this->response->expiration ?? __( 'unknown date', '%TEXTDOMAIN%' ); if ( ! empty( $this->response->api_inline_invalid_message ) ) { diff --git a/src/Uplink/Admin/Ajax.php b/src/Uplink/Admin/Ajax.php index 8f30ab25..842c4c21 100644 --- a/src/Uplink/Admin/Ajax.php +++ b/src/Uplink/Admin/Ajax.php @@ -2,7 +2,6 @@ namespace StellarWP\Uplink\Admin; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Resources\Collection; use StellarWP\Uplink\Utils; @@ -18,26 +17,18 @@ class Ajax { */ protected $field; - /** - * @var License_Manager - */ - protected $license_manager; - /** * Constructor. * - * @param Collection $resources The plugin/services collection. - * @param License_Field $field The license field. - * @param License_Manager $license_manager The license manager. + * @param Collection $resources The plugin/services collection. + * @param License_Field $field The license field. */ public function __construct( Collection $resources, - License_Field $field, - License_Manager $license_manager + License_Field $field ) { - $this->resources = $resources; - $this->field = $field; - $this->license_manager = $license_manager; + $this->resources = $resources; + $this->field = $field; } /** @@ -70,9 +61,8 @@ public function validate_license(): void { ] ); } - $network_validate = $this->license_manager->allows_multisite_license( $plugin ); - $results = $plugin->validate_license( $submission['key'], $network_validate ); - $message = $network_validate ? $results->get_network_message()->get() : $results->get_message()->get(); + $results = $plugin->validate_license( $submission['key'] ); + $message = $plugin->uses_network_licensing() ? $results->get_network_message()->get() : $results->get_message()->get(); wp_send_json( [ 'status' => absint( $results->is_valid() ), diff --git a/src/Uplink/Admin/License_Field.php b/src/Uplink/Admin/License_Field.php index 6b8b0d70..1f4a1ed2 100644 --- a/src/Uplink/Admin/License_Field.php +++ b/src/Uplink/Admin/License_Field.php @@ -2,13 +2,15 @@ namespace StellarWP\Uplink\Admin; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Config; +use StellarWP\Uplink\License\Storage\Local_Storage; use StellarWP\Uplink\Resources\Plugin; use StellarWP\Uplink\Resources\Resource; use StellarWP\Uplink\Resources\Service; use StellarWP\Uplink\Uplink; +use function StellarWP\Uplink\get_license_key; + class License_Field extends Field { public const LICENSE_FIELD_ID = 'stellarwp_uplink_license'; @@ -18,13 +20,6 @@ class License_Field extends Field { */ protected $path = '/admin-views/fields/settings.php'; - /** - * The license manager. - * - * @var License_Manager - */ - private $license_manager; - /** * The script and style handle when registering assets for this field. * @@ -34,14 +29,9 @@ class License_Field extends Field { /** * Constructor. - * - * @param License_Manager $license_manager - * - * @throws \RuntimeException */ - public function __construct( License_Manager $license_manager ) { - $this->license_manager = $license_manager; - $this->handle = sprintf( 'stellarwp-uplink-license-admin-%s', Config::get_hook_prefix() ); + public function __construct() { + $this->handle = sprintf( 'stellarwp-uplink-license-admin-%s', Config::get_hook_prefix() ); } /** @@ -56,10 +46,14 @@ public static function get_section_name( $plugin ) : string { /** * @since 1.0.0 * + * @throws \RuntimeException + * * @return void */ public function register_settings(): void { foreach ( $this->get_resources() as $resource ) { + $id = Local_Storage::option_name( $resource ); + add_settings_section( self::get_section_name( $resource ), '', @@ -69,23 +63,21 @@ public function register_settings(): void { register_setting( $this->get_group_name( sanitize_title( $resource->get_slug() ) ), - $resource->get_license_object()->get_key_option_name() + $id ); - $network = $this->license_manager->allows_multisite_license( $resource ); - add_settings_field( - $resource->get_license_object()->get_key_option_name(), + $id, __( 'License Key', '%TEXTDOMAIN%' ), [ $this, 'field_html' ], $this->get_group_name( sanitize_title( $resource->get_slug() ) ), self::get_section_name( $resource ), [ - 'id' => $resource->get_license_object()->get_key_option_name(), - 'label_for' => $resource->get_license_object()->get_key_option_name(), + 'id' => $id, + 'label_for' => $id, 'type' => 'text', 'path' => $resource->get_path(), - 'value' => $resource->get_license_key( $network ? 'network' : 'local' ), + 'value' => get_license_key( $resource->get_slug() ), 'placeholder' => __( 'License Number', '%TEXTDOMAIN%' ), 'html' => $this->get_field_html( $resource ), 'html_classes' => 'stellarwp-uplink-license-key-field', diff --git a/src/Uplink/Auth/Admin/Connect_Controller.php b/src/Uplink/Auth/Admin/Connect_Controller.php index 4df6bd3d..dc8fe166 100644 --- a/src/Uplink/Auth/Admin/Connect_Controller.php +++ b/src/Uplink/Auth/Admin/Connect_Controller.php @@ -3,12 +3,11 @@ namespace StellarWP\Uplink\Auth\Admin; use StellarWP\Uplink\Auth\Authorizer; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Auth\Nonce; use StellarWP\Uplink\Auth\Token\Connector; use StellarWP\Uplink\Auth\Token\Exceptions\InvalidTokenException; -use StellarWP\Uplink\Notice\Notice_Handler; use StellarWP\Uplink\Notice\Notice; +use StellarWP\Uplink\Notice\Notice_Handler; use StellarWP\Uplink\Resources\Collection; /** @@ -37,11 +36,6 @@ final class Connect_Controller { */ private $collection; - /** - * @var License_Manager - */ - private $license_manager; - /** * @var Authorizer */ @@ -51,14 +45,12 @@ public function __construct( Connector $connector, Notice_Handler $notice, Collection $collection, - License_Manager $license_manager, Authorizer $authorizer ) { - $this->connector = $connector; - $this->notice = $notice; - $this->collection = $collection; - $this->license_manager = $license_manager; - $this->authorizer = $authorizer; + $this->connector = $connector; + $this->notice = $notice; + $this->collection = $collection; + $this->authorizer = $authorizer; } /** @@ -136,8 +128,7 @@ public function maybe_store_token_data(): void { // Store or override an existing license. if ( $license ) { - $network = $this->license_manager->allows_multisite_license( $plugin ); - $response = $plugin->validate_license( $license, $network ); + $response = $plugin->validate_license( $license ); if ( ! $response->is_valid() ) { $this->notice->add( new Notice( Notice::ERROR, @@ -148,7 +139,7 @@ public function maybe_store_token_data(): void { return; } - if ( ! $plugin->set_license_key( $license, $network ? 'network' : 'local' ) ) { + if ( ! $plugin->set_license_key( $license ) ) { $this->notice->add( new Notice( Notice::ERROR, __( 'Error storing license key.', '%TEXTDOMAIN%' ), true diff --git a/src/Uplink/Auth/Provider.php b/src/Uplink/Auth/Provider.php index dc333866..a412ee62 100644 --- a/src/Uplink/Auth/Provider.php +++ b/src/Uplink/Auth/Provider.php @@ -4,16 +4,10 @@ use StellarWP\Uplink\Auth\Admin\Connect_Controller; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; -use StellarWP\Uplink\Auth\License\License_Manager; -use StellarWP\Uplink\Auth\License\Pipeline\Processors\Multisite_Domain_Mapping; -use StellarWP\Uplink\Auth\License\Pipeline\Processors\Multisite_Main_Site; -use StellarWP\Uplink\Auth\License\Pipeline\Processors\Multisite_Subdomain; -use StellarWP\Uplink\Auth\License\Pipeline\Processors\Multisite_Subfolder; use StellarWP\Uplink\Auth\Token\Managers\Network_Token_Manager; use StellarWP\Uplink\Auth\Token\Managers\Token_Manager; use StellarWP\Uplink\Config; use StellarWP\Uplink\Contracts\Abstract_Provider; -use StellarWP\Uplink\Pipeline\Pipeline; final class Provider extends Abstract_Provider { @@ -40,7 +34,6 @@ static function ( $c ) { ); $this->register_nonce(); - $this->register_license_manager(); $this->register_auth_disconnect(); $this->register_auth_connect(); } @@ -66,28 +59,6 @@ private function register_nonce(): void { $this->container->singleton( Nonce::class, new Nonce( $expiration ) ); } - /** - * Register the license manager and its pipeline to detect different - * mulitsite licenses. - * - * @return void - */ - private function register_license_manager(): void { - $pipeline = ( new Pipeline( $this->container ) )->through( [ - Multisite_Main_Site::class, - Multisite_Subfolder::class, - Multisite_Subdomain::class, - Multisite_Domain_Mapping::class, - ] ); - - $this->container->singleton( - License_Manager::class, - static function () use ( $pipeline ) { - return new License_Manager( $pipeline ); - } - ); - } - /** * Register auth disconnection definitions and hooks. * diff --git a/src/Uplink/Auth/Token/Token_Factory.php b/src/Uplink/Auth/Token/Token_Factory.php index 4843dc92..3a7828ba 100644 --- a/src/Uplink/Auth/Token/Token_Factory.php +++ b/src/Uplink/Auth/Token/Token_Factory.php @@ -3,30 +3,22 @@ namespace StellarWP\Uplink\Auth\Token; use StellarWP\ContainerContract\ContainerInterface; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Auth\Token\Contracts\Token_Manager; use StellarWP\Uplink\Auth\Token\Managers\Network_Token_Manager; use StellarWP\Uplink\Resources\Resource; final class Token_Factory { - /** - * @var License_Manager - */ - private $license_manager; - /** * @var ContainerInterface */ private $container; /** - * @param License_Manager $license_manager * @param ContainerInterface $container */ - public function __construct( License_Manager $license_manager, ContainerInterface $container ) { - $this->license_manager = $license_manager; - $this->container = $container; + public function __construct( ContainerInterface $container ) { + $this->container = $container; } /** @@ -37,7 +29,7 @@ public function __construct( License_Manager $license_manager, ContainerInterfac * @return Token_Manager */ public function make( Resource $resource ): Token_Manager { - $network_license = $this->license_manager->allows_multisite_license( $resource ); + $network_license = $resource->uses_network_licensing(); return $this->container->get( $network_license ? Network_Token_Manager::class : Managers\Token_Manager::class ); } diff --git a/src/Uplink/Components/Controller.php b/src/Uplink/Components/Controller.php index e5538e05..bd7bd839 100644 --- a/src/Uplink/Components/Controller.php +++ b/src/Uplink/Components/Controller.php @@ -48,4 +48,29 @@ protected function classes( array $classes ): string { return implode( ' ', $classes ); } + /** + * Format data attributes, escaped and ready for output in the view. + * + * @param array $attributes An array of attributes indexed by their name. + * + * @return string + */ + protected function data_attr( array $attributes ): string { + if ( ! $attributes ) { + return ''; + } + + $formatted = []; + + foreach ( $attributes as $name => $value ) { + $formatted[] = sprintf( + 'data-%s="%s"', + esc_html( sanitize_html_class( $name ) ), + esc_attr( $value ) + ); + } + + return implode( ' ', $formatted ); + } + } diff --git a/src/Uplink/Components/Settings/Description_Controller.php b/src/Uplink/Components/Settings/Description_Controller.php new file mode 100644 index 00000000..1884e0a8 --- /dev/null +++ b/src/Uplink/Components/Settings/Description_Controller.php @@ -0,0 +1,34 @@ +view->render( self::VIEW, [ + 'description' => $description, + ] ); + } + +} diff --git a/src/Uplink/Components/Settings/Fields/Checkbox_Controller.php b/src/Uplink/Components/Settings/Fields/Checkbox_Controller.php new file mode 100644 index 00000000..0dc2d695 --- /dev/null +++ b/src/Uplink/Components/Settings/Fields/Checkbox_Controller.php @@ -0,0 +1,42 @@ +, classes?: string[]} $args + * + * @throws FileNotFoundException + */ + public function render( array $args = [] ): void { + $id = $args['id'] ?? ''; + $label = $args['label'] ?? ''; + $description = $args['description'] ?? ''; + $default = $args['default'] ?? false; + $classes = $args['classes'] ?? [ $id ]; + + echo $this->view->render( self::VIEW, [ + 'id' => $id, + 'label' => $label, + 'description' => $description, + 'data_attr' => $this->data_attr( $args['data_attr'] ?? [] ), + 'classes' => $this->classes( $classes ), + 'value' => $this->get_value( $id, $default ), + ] ); + } + +} diff --git a/src/Uplink/Components/Settings/Traits/Get_Value_Trait.php b/src/Uplink/Components/Settings/Traits/Get_Value_Trait.php new file mode 100644 index 00000000..342bf07a --- /dev/null +++ b/src/Uplink/Components/Settings/Traits/Get_Value_Trait.php @@ -0,0 +1,20 @@ +singleton( self::TOKEN_OPTION_NAME, null ); @@ -195,6 +227,27 @@ public static function set_token_auth_prefix( string $prefix ): void { self::get_container()->singleton( self::TOKEN_OPTION_NAME, $key ); } + /** + * Set the token authorization expiration. + * + * @param int $seconds The time seconds the cache will exist for. + * -1 = disabled, 0 = no expiration. + * + * @return void + */ + public static function set_auth_cache_expiration( int $seconds ): void { + static::$auth_cache_expiration = $seconds; + } + + /** + * Get the token authorization expiration. + * + * @return int + */ + public static function get_auth_cache_expiration(): int { + return static::$auth_cache_expiration; + } + /** * Allow or disallow multisite subfolder licenses at the network level. * @@ -202,58 +255,58 @@ public static function set_token_auth_prefix( string $prefix ): void { * * @return void */ - public static function set_network_subfolder_license( bool $allowed ): void { - self::$network_subfolder_license = $allowed; + public static function allow_site_level_licenses_for_subfolder_multisite( bool $allowed ): void { + static::$supports_site_level_licenses_for_subfolder_multisite = $allowed; } /** - * Whether your plugin allows multisite network subfolder licenses. + * Allow or disallow multisite subdomain licenses at the network level. * - * @throws RuntimeException + * @param bool $allowed * - * @return bool + * @return void */ - public static function allows_network_subfolder_license(): bool { - return (bool) apply_filters( - 'stellarwp/uplink/' . Config::get_hook_prefix() . '/allows_network_subfolder_license', - self::$network_subfolder_license - ); + public static function allow_site_level_licenses_for_subdomain_multisite( bool $allowed ): void { + static::$supports_site_level_licenses_for_subdomain_multisite = $allowed; } /** - * Allow or disallow multisite subdomain licenses at the network level. + * Allow or disallow multisite domain mapping licenses at the network level. * * @param bool $allowed * * @return void */ - public static function set_network_subdomain_license( bool $allowed ): void { - self::$network_subdomain_license = $allowed; + public static function allow_site_level_licenses_for_mapped_domain_multisite( bool $allowed ): void { + static::$supports_site_level_licenses_for_mapped_domain_multisite = $allowed; } /** - * Whether your plugin allows multisite network subdomain licenses. + * Whether your plugin allows multisite network subfolder licenses. * * @throws RuntimeException * * @return bool */ - public static function allows_network_subdomain_license(): bool { + public static function supports_site_level_licenses_for_subfolder_multisite(): bool { return (bool) apply_filters( - 'stellarwp/uplink/' . Config::get_hook_prefix() . '/allows_network_subdomain_license', - self::$network_subdomain_license + 'stellarwp/uplink/' . Config::get_hook_prefix() . '/supports_site_level_licenses_for_subfolder_multisite', + static::$supports_site_level_licenses_for_subfolder_multisite ); } /** - * Allow or disallow multisite domain mapping licenses at the network level. + * Whether your plugin allows multisite network subdomain licenses. * - * @param bool $allowed + * @throws RuntimeException * - * @return void + * @return bool */ - public static function set_network_domain_mapping_license( bool $allowed ): void { - self::$network_domain_mapping_license = $allowed; + public static function supports_site_level_licenses_for_subdomain_multisite(): bool { + return (bool) apply_filters( + 'stellarwp/uplink/' . Config::get_hook_prefix() . '/supports_site_level_licenses_for_subdomain_multisite', + static::$supports_site_level_licenses_for_subdomain_multisite + ); } /** @@ -263,28 +316,52 @@ public static function set_network_domain_mapping_license( bool $allowed ): void * * @return bool */ - public static function allows_network_domain_mapping_license(): bool { + public static function supports_site_level_licenses_for_mapped_domain_multisite(): bool { return (bool) apply_filters( - 'stellarwp/uplink/' . Config::get_hook_prefix() . '/allows_network_domain_mapping_license', - self::$network_domain_mapping_license + 'stellarwp/uplink/' . Config::get_hook_prefix() . '/supports_site_level_licenses_for_mapped_domain_multisite', + static::$supports_site_level_licenses_for_mapped_domain_multisite ); } /** - * Check if any of the network license options are enabled. + * Check if any of the network licencing options are enabled. * * @throws RuntimeException * * @return bool */ - public static function allows_network_licenses(): bool { + public static function supports_network_licenses(): bool { $config = [ - self::allows_network_subfolder_license(), - self::allows_network_subdomain_license(), - self::allows_network_domain_mapping_license(), + static::supports_site_level_licenses_for_subfolder_multisite(), + static::supports_site_level_licenses_for_subdomain_multisite(), + static::supports_site_level_licenses_for_mapped_domain_multisite(), ]; return in_array( true, $config, true ); } + /** + * Enables a checkbox in the License Field so that you can use a local license key in place of + * the network key. + * + * @param bool $allowed + * + * @return void + */ + public static function allow_site_level_override_for_multisite_license( bool $allowed ): void { + static::$supports_site_level_override_for_multisite_license = $allowed; + } + + /** + * If this instance allows site level license key overrides. + * + * @return bool + */ + public static function supports_site_level_override_for_multisite_license(): bool { + return (bool) apply_filters( + 'stellarwp/uplink/' . Config::get_hook_prefix() . '/supports_site_level_override_for_multisite_license', + static::$supports_site_level_override_for_multisite_license + ); + } + } diff --git a/src/Uplink/Auth/License/License_Manager.php b/src/Uplink/License/Manager/License_Handler.php similarity index 51% rename from src/Uplink/Auth/License/License_Manager.php rename to src/Uplink/License/Manager/License_Handler.php index 5e9acb63..0b43ea4f 100644 --- a/src/Uplink/Auth/License/License_Manager.php +++ b/src/Uplink/License/Manager/License_Handler.php @@ -1,12 +1,12 @@ get_slug(); - $cache = $this->cache[ $key ] ?? null; + public function current_site_allows_network_licensing( Resource $resource ): bool { + $key = $resource->get_slug(); - if ( $cache !== null ) { - return $cache; + if ( $this->cache_enabled ) { + $cache = $this->cache[ $key ] ?? null; + + if ( $cache !== null ) { + return $cache; + } } // We're on single site or, the plugin isn't network activated. @@ -59,4 +68,11 @@ public function allows_multisite_license( Resource $resource ): bool { return $this->cache[ $key ] = $this->pipeline->send( false )->thenReturn(); } + /** + * Disable memoization cache, useful for automated tests. + */ + public function disable_cache(): void { + $this->cache_enabled = false; + } + } diff --git a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Domain_Mapping.php b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Domain_Mapping.php similarity index 74% rename from src/Uplink/Auth/License/Pipeline/Processors/Multisite_Domain_Mapping.php rename to src/Uplink/License/Manager/Pipeline/Processors/Multisite_Domain_Mapping.php index 2740c47c..5509f8f0 100644 --- a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Domain_Mapping.php +++ b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Domain_Mapping.php @@ -1,10 +1,10 @@ is_unique_domain() ) { - return Config::allows_network_domain_mapping_license(); + return Config::supports_site_level_licenses_for_mapped_domain_multisite(); } } catch ( Throwable $e ) { return false; diff --git a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Main_Site.php b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Main_Site.php similarity index 80% rename from src/Uplink/Auth/License/Pipeline/Processors/Multisite_Main_Site.php rename to src/Uplink/License/Manager/Pipeline/Processors/Multisite_Main_Site.php index d15d750a..21ba67d0 100644 --- a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Main_Site.php +++ b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Main_Site.php @@ -1,6 +1,6 @@ is_subdomain() ) { - return Config::allows_network_subdomain_license(); + return Config::supports_site_level_licenses_for_subdomain_multisite(); } } catch ( Throwable $e ) { return false; diff --git a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Subfolder.php b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Subfolder.php similarity index 73% rename from src/Uplink/Auth/License/Pipeline/Processors/Multisite_Subfolder.php rename to src/Uplink/License/Manager/Pipeline/Processors/Multisite_Subfolder.php index 3f8e4f31..178270b0 100644 --- a/src/Uplink/Auth/License/Pipeline/Processors/Multisite_Subfolder.php +++ b/src/Uplink/License/Manager/Pipeline/Processors/Multisite_Subfolder.php @@ -1,10 +1,10 @@ is_subfolder_install() ) { - return Config::allows_network_subfolder_license(); + return Config::supports_site_level_licenses_for_subfolder_multisite(); } return $next( $is_multisite_license ); diff --git a/src/Uplink/Auth/License/Pipeline/Traits/Multisite_Trait.php b/src/Uplink/License/Manager/Pipeline/Traits/Multisite_Trait.php similarity index 96% rename from src/Uplink/Auth/License/Pipeline/Traits/Multisite_Trait.php rename to src/Uplink/License/Manager/Pipeline/Traits/Multisite_Trait.php index 8c9586d1..621194dc 100644 --- a/src/Uplink/Auth/License/Pipeline/Traits/Multisite_Trait.php +++ b/src/Uplink/License/Manager/Pipeline/Traits/Multisite_Trait.php @@ -1,6 +1,6 @@ register_license_handler(); + } + + /** + * Register the license handler and its pipeline to detect different + * multisite licenses. + * + * @return void + */ + private function register_license_handler(): void { + $pipeline = ( new Pipeline( $this->container ) )->through( [ + Multisite_Network_Admin::class, + Multisite_Main_Site::class, + Multisite_Subfolder::class, + Multisite_Subdomain::class, + Multisite_Domain_Mapping::class, + ] ); + + $this->container->singleton( + License_Handler::class, + static function () use ( $pipeline ) { + return new License_Handler( $pipeline ); + } + ); + } + +} diff --git a/src/Uplink/License/Storage/Contracts/Storage.php b/src/Uplink/License/Storage/Contracts/Storage.php new file mode 100644 index 00000000..c13f03ee --- /dev/null +++ b/src/Uplink/License/Storage/Contracts/Storage.php @@ -0,0 +1,43 @@ +get_license_class(); + + if ( empty( $license_class ) ) { + return null; + } + + $key = null; + + if ( defined( $license_class . '::KEY' ) ) { + $key = $license_class::KEY; + } elseif ( defined( $license_class . '::DATA' ) ) { + $key = $license_class::DATA; + } + + return $key; + } + + /** + * @inheritDoc + * + * @throws RuntimeException + */ + public function delete( Resource $resource ): bool { + throw new RuntimeException( 'You cannot delete a license using file based storage' ); + } + +} diff --git a/src/Uplink/License/Storage/Local_Storage.php b/src/Uplink/License/Storage/Local_Storage.php new file mode 100644 index 00000000..3f871e9d --- /dev/null +++ b/src/Uplink/License/Storage/Local_Storage.php @@ -0,0 +1,44 @@ +get( $resource ) ) { + return true; + } + + return update_option( self::option_name( $resource ), $license_key ); + } + + /** + * @inheritDoc + */ + public function get( Resource $resource ): ?string { + return get_option( self::option_name( $resource ), null ); + } + + /** + * @inheritDoc + */ + public function delete( Resource $resource ): bool { + return delete_option( self::option_name( $resource ) ); + } + +} diff --git a/src/Uplink/License/Storage/Network_Storage.php b/src/Uplink/License/Storage/Network_Storage.php new file mode 100644 index 00000000..f2bc4c13 --- /dev/null +++ b/src/Uplink/License/Storage/Network_Storage.php @@ -0,0 +1,67 @@ +is_multisite( $resource ) ) { + return false; + } + + $license_key = Sanitize::key( $license_key ); + + // WordPress would otherwise return false if the items match. + if ( $license_key === $this->get( $resource ) ) { + return true; + } + + return update_site_option( self::option_name( $resource ), $license_key ); + } + + /** + * @inheritDoc + */ + public function get( Resource $resource ): ?string { + if ( ! $this->is_multisite( $resource ) ) { + return null; + } + + return get_site_option( self::option_name( $resource ), null ); + } + + /** + * @inheritDoc + */ + public function delete( Resource $resource ): bool { + if ( ! $this->is_multisite( $resource ) ) { + return false; + } + + return delete_site_option( self::option_name( $resource ) ); + } + + /** + * Determine if we can even store or fetch license keys. + * + * @param Resource $resource + * + * @return bool + */ + private function is_multisite( Resource $resource ): bool { + return is_multisite() && $resource->is_network_activated(); + } + +} diff --git a/src/Uplink/License/Storage/Storage_Factory.php b/src/Uplink/License/Storage/Storage_Factory.php new file mode 100644 index 00000000..86300643 --- /dev/null +++ b/src/Uplink/License/Storage/Storage_Factory.php @@ -0,0 +1,40 @@ +container = $container; + } + + /** + * Make a storage instance that either stores in site_options or the local options tables. + * + * @param Resource $resource + * + * @return Storage + */ + public function make( Resource $resource ): Storage { + $class = $resource->uses_network_licensing() ? Network_Storage::class : Local_Storage::class; + + return $this->container->get( $class ); + } + +} diff --git a/src/Uplink/License/Storage/Storage_Handler.php b/src/Uplink/License/Storage/Storage_Handler.php new file mode 100644 index 00000000..c2f85153 --- /dev/null +++ b/src/Uplink/License/Storage/Storage_Handler.php @@ -0,0 +1,107 @@ +factory = $factory; + $this->file = $file; + } + + /** + * Whether the key was the original file based key. + * + * @return bool + */ + public function is_original(): bool { + return $this->is_original_key; + } + + /** + * Store a license key in either site_options or the site's options table. + * + * @param Resource $resource + * @param string $license_key + * + * @return bool + */ + public function store( Resource $resource, string $license_key ): bool { + return $this->factory->make( $resource )->store( $resource, $license_key ); + } + + /** + * Get a license key from either site_options or the site's options table. + * + * @param Resource $resource + * + * @return string|null + */ + public function get( Resource $resource ): ?string { + $license_key = $this->factory->make( $resource )->get( $resource ); + + // Fallback to the original file based storage key. + if ( ! $license_key ) { + $this->is_original_key = true; + $license_key = $this->get_from_file( $resource ); + } + + return $license_key; + } + + /** + * Get a license key from the packaged class that came with the plugin (if provided). + * + * @param Resource $resource + * + * @return string|null + */ + public function get_from_file( Resource $resource ): ?string { + return $this->file->get( $resource ); + } + + /** + * Delete a license key from either site_options or the site's options table. + * + * @param Resource $resource + * + * @return bool + */ + public function delete( Resource $resource ): bool { + return $this->factory->make( $resource )->delete( $resource ); + } + +} diff --git a/src/Uplink/License/Storage/Traits/Option_Name_Trait.php b/src/Uplink/License/Storage/Traits/Option_Name_Trait.php new file mode 100644 index 00000000..255f2bbf --- /dev/null +++ b/src/Uplink/License/Storage/Traits/Option_Name_Trait.php @@ -0,0 +1,25 @@ +get_slug(); + } + +} diff --git a/src/Uplink/Resources/License.php b/src/Uplink/Resources/License.php index b2d760ff..8cac2279 100644 --- a/src/Uplink/Resources/License.php +++ b/src/Uplink/Resources/License.php @@ -3,12 +3,13 @@ namespace StellarWP\Uplink\Resources; use StellarWP\ContainerContract\ContainerInterface; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Config; +use StellarWP\Uplink\License\Storage\Storage_Handler; use StellarWP\Uplink\Site\Data; use StellarWP\Uplink\Utils; class License { + /** * How often to check for updates (in hours). * @@ -60,15 +61,6 @@ class License { */ protected $key_origin_code; - /** - * Option prefix for the key. - * - * @since 1.0.0 - * - * @var string - */ - public static $key_option_prefix = 'stellarwp_uplink_license_key_'; - /** * Option prefix for the key status. * @@ -87,66 +79,73 @@ class License { */ protected $resource; + /** + * The storage handler. + * + * @var Storage_Handler + */ + protected $storage; + /** * Constructor. * * @since 1.0.0 * - * @param Resource $resource The resource instance. - * @param ContainerInterface|null $container Container instance. + * @param Resource $resource The resource instance. + * @param ContainerInterface|null $container Container instance. + * + * @throws \RuntimeException */ public function __construct( Resource $resource, $container = null ) { $this->resource = $resource; $this->container = $container ?: Config::get_container(); + $this->storage = $this->container->get( Storage_Handler::class ); } /** - * Deletes the key in site options. + * Sets the key in site options. + * + * @TODO we should fire an action here to validate the key. * * @since 1.0.0 * - * @param string $type Type of key (network, local). + * @param string $key License key. * * @return bool */ - public function delete_key( string $type = 'local' ): bool { - if ( 'network' === $type && is_multisite() ) { - return delete_network_option( 0, $this->get_key_option_name() ); - } + public function set_key( string $key ): bool { + $key = Utils\Sanitize::key( $key ); + + $this->key = $key; - return delete_option( $this->get_key_option_name() ); + return $this->storage->store( $this->resource, $key ); } /** - * Get the license key. + * Deletes the license key from the appropriate storage location. + * + * @TODO we should fire an action here to validate the key. * * @since 1.0.0 * - * @param string $type The type of key to get (any, network, local, default). + * @return bool + */ + public function delete_key(): bool { + return $this->storage->delete( $this->resource ); + } + + /** + * Get the license key. + * + * @since 1.0.0 * * @return string */ - public function get_key( $type = 'any' ) { - if ( empty( $this->key ) && ( 'any' === $type || 'network' === $type ) ) { - $this->key = $this->get_key_from_network_option(); + public function get_key(): string { + if ( empty( $this->key ) ) { + $this->key = $this->storage->get( $this->resource ); - if ( ! empty( $this->key ) ) { - $this->key_origin = 'network_option'; - } - } - - if ( empty( $this->key ) && ( 'any' === $type || 'local' === $type ) ) { - $this->key = $this->get_key_from_option(); - - if ( ! empty( $this->key ) ) { - $this->key_origin = 'site_option'; - } - } - - if ( empty( $this->key ) && ( 'any' === $type || 'default' === $type ) ) { - $this->key = $this->get_key_from_license_file(); - - if ( ! empty( $this->key ) ) { + if ( $this->storage->is_original() ) { $this->key_origin = 'file'; } } @@ -160,74 +159,18 @@ public function get_key( $type = 'any' ) { */ $key = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix(). '/license_get_key', $this->key ); - return $key ?: ''; + return (string) $key; } /** - * Get the license key from a class that holds the license key. - * - * @since 1.0.0 - * - * @return string|null - */ - protected function get_key_from_license_file() { - $license_class = $this->resource->get_license_class(); - $key = null; - - if ( empty( $license_class ) ) { - return null; - } - - if ( defined( $license_class . '::KEY' ) ) { - $key = $license_class::KEY; - } elseif ( defined( $license_class . '::DATA' ) ) { - $key = $license_class::DATA; - } - - return $key; - } - - /** - * Get the license key from a network option. - * - * @since 1.0.0 - * - * @return string|null - */ - protected function get_key_from_network_option() { - if ( ! is_multisite() ) { - return null; - } - - if ( ! $this->resource->is_network_activated() ) { - return null; - } - - /** @var string|null */ - return get_network_option( 0, $this->get_key_option_name(), null ); - } - - /** - * Get the license key from an option. - * - * @since 1.0.0 - * - * @return string|null - */ - protected function get_key_from_option() { - /** @var string|null */ - return get_option( $this->get_key_option_name(), null ); - } - - /** - * Get the option name for the license key. + * Get the license key from a class that holds the license key (aka the original key). * * @since 1.0.0 * * @return string */ - public function get_key_option_name(): string { - return static::$key_option_prefix . $this->resource->get_slug(); + public function get_default_key(): ?string { + return (string) $this->storage->get_from_file( $this->resource ); } /** @@ -243,7 +186,7 @@ public function get_key_origin_code(): string { } $key = $this->get_key(); - $default_key = $this->get_key( 'default' ); + $default_key = $this->get_default_key(); if ( $key === $default_key ) { $this->key_origin_code = 'o'; @@ -256,33 +199,6 @@ public function get_key_origin_code(): string { return $this->key_origin_code; } - /** - * Get the license key status from an option. - * - * @since 1.0.0 - * - * @return string|null - */ - protected function get_key_status(): ?string { - $network = $this->container->get( License_Manager::class )->allows_multisite_license( $this->resource ); - $func = 'get_option'; - - if ( $network ) { - $func = 'get_site_option'; - } - - /** @var string|null */ - $status = $func( $this->get_key_status_option_name(), 'invalid' ); - $key = $this->get_key( $network ? 'network' : 'local' ); - - if ( null === $status && $key ) { - $this->resource->validate_license( $key, $network ); - $status = $func( $this->get_key_status_option_name(), 'invalid' ); - } - - return $status; - } - /** * Get the option name for the license key status. * @@ -300,25 +216,28 @@ public function get_key_status_option_name(): string { /** * Whether the plugin is network activated and licensed or not. * + * @TODO remove this once override logic is complete. + * * @since 1.0.0 * * @return bool */ - public function is_network_licensed() { - $is_network_licensed = false; - - if ( ! is_network_admin() && $this->resource->is_network_activated() ) { - $network_key = $this->get_key( 'network' ); - $local_key = $this->get_key( 'local' ); - - // Check whether the network is licensed and NOT overridden by local license - if ( $network_key && ( empty( $local_key ) || $local_key === $network_key ) ) { - $is_network_licensed = true; - } - } - - return $is_network_licensed; - } +// public function is_network_licensed() { +// $is_network_licensed = false; +// +// if ( ! is_network_admin() && $this->resource->is_network_activated() ) { +// $network_key = $this->get_key( 'network' ); +// $local_key = $this->get_key( 'local' ); +// +// // Check whether the network is licensed and NOT overridden by local license +// // TODO: Need to account for this in the new system +// if ( $network_key && ( empty( $local_key ) || $local_key === $network_key ) ) { +// $is_network_licensed = true; +// } +// } +// +// return $is_network_licensed; +// } /** * Whether the plugin is validly licensed or not. @@ -327,7 +246,7 @@ public function is_network_licensed() { * * @return bool */ - public function is_valid() { + public function is_valid(): bool { return 'valid' === $this->get_key_status(); } @@ -349,41 +268,42 @@ public function is_expired(): bool { * * @return bool */ - public function is_validation_expired() { + public function is_validation_expired(): bool { $option_expiration = get_option( $this->get_key_status_option_name() . '_timeout', null ); return is_null( $option_expiration ) || ( time() > $option_expiration ); } /** - * Sets the key in site options. + * Get the license key status from an option. * * @since 1.0.0 * - * @param string $key License key. - * @param string $type Type of key (network, local). - * - * @return bool + * @return string|null */ - public function set_key( string $key, string $type = 'local' ): bool { - $key = Utils\Sanitize::key( $key ); + protected function get_key_status(): ?string { + $network = $this->resource->uses_network_licensing(); + $func = 'get_option'; - $this->key = $key; + if ( $network ) { + $func = 'get_site_option'; + } - if ( 'network' === $type && is_multisite() && $this->resource->is_network_activated() ) { - // WordPress would otherwise return false if the keys already match. - if ( $this->get_key_from_network_option() === $key ) { - return true; - } + /** @var string|null $status */ + $status = $func( $this->get_key_status_option_name(), null ); + $key = $this->get_key(); - return update_network_option( 0, $this->get_key_option_name(), $key ); - } + // If no status has been set, run the update again. + if ( null === $status && $key ) { + $this->resource->validate_license( $key ); + $status = $func( $this->get_key_status_option_name(), null ); - // WordPress would otherwise return false if the keys already match. - if ( $this->get_key_from_option() === $key ) { - return true; + // If it still failed, default to invalid. + if ( null === $status ) { + $status = 'invalid'; + } } - return update_option( $this->get_key_option_name(), $key ); + return $status; } /** @@ -391,13 +311,13 @@ public function set_key( string $key, string $type = 'local' ): bool { * * @since TBD * - * @param int $valid 0 for invalid, 1 or 2 for valid. + * @param int|bool|string $valid 0 for invalid, 1 or 2 for valid. * * @return void */ public function set_key_status( $valid ): void { $status = Utils\Checks::is_truthy( $valid ) ? 'valid' : 'invalid'; - $network = $this->container->get( License_Manager::class )->allows_multisite_license( $this->resource ); + $network = $this->resource->uses_network_licensing(); $timeout = $this->check_period * HOUR_IN_SECONDS; $func = 'update_option'; @@ -417,6 +337,6 @@ public function set_key_status( $valid ): void { * @return string */ public function __toString(): string { - return $this->get_key() ?: ''; + return $this->get_key(); } } diff --git a/src/Uplink/Resources/Resource.php b/src/Uplink/Resources/Resource.php index b3289fda..bc050573 100644 --- a/src/Uplink/Resources/Resource.php +++ b/src/Uplink/Resources/Resource.php @@ -7,6 +7,7 @@ use StellarWP\Uplink\API; use StellarWP\Uplink\Config; use StellarWP\Uplink\Exceptions; +use StellarWP\Uplink\License\Manager\License_Handler; use StellarWP\Uplink\Site\Data; use StellarWP\Uplink\Utils; @@ -23,6 +24,7 @@ * @property-read string $path The resource path. */ abstract class Resource { + /** * Resource class. * @@ -135,17 +137,24 @@ public function __construct( $slug, $name, $version, $path, $class, string $lice $this->container = Config::get_container(); } + /** + * Whether the current site, in the current configuration is using network licensing. + * + * @return bool + */ + public function uses_network_licensing(): bool { + return $this->container->get( License_Handler::class )->current_site_allows_network_licensing( $this ); + } + /** * Deletes the resource license key. * * @since 1.0.0 * - * @param string $type The type of key to get (any, network, local, default). - * * @return bool */ - public function delete_license_key( $type = 'local' ): bool { - return $this->get_license_object()->delete_key( $type ); + public function delete_license_key(): bool { + return $this->get_license_object()->delete_key(); } /** @@ -166,7 +175,7 @@ public function get_class() { * * @return array */ - public function get_download_args() { + public function get_download_args(): array { $args = []; /** @var Data */ @@ -186,8 +195,8 @@ public function get_download_args() { $args['wp_version'] = $stats['versions']['wp']; // the following is for install key inclusion (will apply later with PUE addons.) - $args['key'] = Utils\Sanitize::key( $this->get_license_object()->get_key() ?: '' ); - $args['dk'] = Utils\Sanitize::key( $this->get_license_object()->get_key( 'default' ) ?: '' ); + $args['key'] = Utils\Sanitize::key( $this->get_license_object()->get_key() ); + $args['dk'] = Utils\Sanitize::key( $this->get_license_object()->get_default_key() ); $args['o'] = sanitize_text_field( $this->get_license_object()->get_key_origin_code() ); return $args; @@ -227,12 +236,10 @@ public function get_license_class() { * * @since 1.0.0 * - * @param string $type The type of key to get (any, network, local, default). - * * @return string */ - public function get_license_key( $type = 'any' ): string { - return $this->get_license_object()->get_key( $type ); + public function get_license_key(): string { + return $this->get_license_object()->get_key(); } /** @@ -308,11 +315,11 @@ public function get_type() { * * @return array */ - public function get_validation_args() { + public function get_validation_args(): array { $args = []; - $args['key'] = Utils\Sanitize::key( $this->get_license_object()->get_key() ?: '' ); - $args['default_key'] = Utils\Sanitize::key( $this->get_license_object()->get_key( 'default' ) ?: '' ); + $args['key'] = Utils\Sanitize::key( $this->get_license_object()->get_key() ); + $args['default_key'] = Utils\Sanitize::key( $this->get_license_object()->get_default_key() ); $args['license_origin'] = sanitize_text_field( $this->get_license_object()->get_key_origin_code() ); $args['plugin'] = sanitize_text_field( $this->get_slug() ); $args['version'] = sanitize_text_field( $this->get_installed_version() ?: '' ); @@ -372,17 +379,6 @@ public function is_network_activated() { return is_plugin_active_for_network( $this->get_path() ); } - /** - * Whether the plugin is network activated and licensed or not. - * - * @since 1.0.0 - * - * @return bool - */ - public function is_network_licensed(): bool { - return $this->get_license_object()->is_network_licensed(); - } - /** * Register a resource and add it to the collection. * @@ -454,12 +450,11 @@ public static function register_resource( $resource_class, $slug, $name, $versio * @since 1.0.0 * * @param string $key License key. - * @param string $type The type of key to get (any, network, local, default). * * @return bool */ - public function set_license_key( $key, $type = 'local' ): bool { - return $this->get_license_object()->set_key( $key, $type ); + public function set_license_key( string $key ): bool { + return $this->get_license_object()->set_key( $key ); } /** @@ -489,30 +484,30 @@ public function should_validate(): bool { /** * Validates the resource's license key. * + * @TODO add an action here so this can fire when the key is deleted or modified. + * * @since 1.0.0 * * @param string|null $key License key. - * @param bool $do_network_validate Validate the key as a network key. * * @return API\Validation_Response */ - public function validate_license( ?string $key = null, bool $do_network_validate = false ) { - /** @var API\Client */ - $api = $this->container->get( API\Client::class ); - - $validation_type = $do_network_validate ? 'network' : 'local'; - + public function validate_license( ?string $key = null ): API\Validation_Response { if ( empty( $key ) ) { - $key = $this->get_license_key( $validation_type ); + $key = $this->get_license_key(); } if ( empty( $key ) ) { - $results = new API\Validation_Response( null, $validation_type, new stdClass(), $this ); + $results = new API\Validation_Response( null, new stdClass(), $this ); $results->set_is_valid( false ); + return $results; } - $results = $api->validate_license( $this, $key, $validation_type ); + /** @var API\Client $api */ + $api = $this->container->get( API\Client::class ); + + $results = $api->validate_license( $this, $key ); $results_key = $results->get_key(); $result_type = $results->get_result(); $has_replacement_key = $results->has_replacement_key(); @@ -521,7 +516,7 @@ public function validate_license( ?string $key = null, bool $do_network_validate $result_type === 'new' || $has_replacement_key ) { - $this->get_license_object()->set_key( $results_key, $validation_type ); + $this->set_license_key( $results_key ); } $this->get_license_object()->set_key_status( $results->is_valid() ); diff --git a/src/Uplink/Site/Data.php b/src/Uplink/Site/Data.php index 0a02f68e..1dc357c2 100644 --- a/src/Uplink/Site/Data.php +++ b/src/Uplink/Site/Data.php @@ -106,13 +106,13 @@ public function get_db_version(): string { * @return string */ public function get_domain( bool $original = false ): string { - $cache_key = 'stellarwp_uplink_domain'; - $domain = $this->container->has( $cache_key ) ? $this->container->get( $cache_key ) : null; - $allows_network_subfolder_license = Config::allows_network_subfolder_license(); + $cache_key = 'stellarwp_uplink_domain'; + $domain = $this->container->has( $cache_key ) ? $this->container->get( $cache_key ) : null; + $supports_ms_subfolders = Config::supports_site_level_licenses_for_subfolder_multisite(); if ( is_multisite() ) { // Ensure subsite also contains its path and don't cache. - if ( ! $allows_network_subfolder_license ) { + if ( ! $supports_ms_subfolders ) { $domain = $this->get_domain_multisite_subsite_subfolder(); } elseif ( $domain === null ) { $domain = $this->get_domain_multisite_option(); @@ -126,7 +126,7 @@ public function get_domain( bool $original = false ): string { * This prevents the main site from refreshing the token on the Licensing server * when multisite is off or network licenses aren't enabled. */ - if ( ! $original && Config::allows_network_licenses() ) { + if ( ! $original && Config::supports_network_licenses() ) { $domain .= '/' . hash( 'crc32c', $domain ); } } elseif ( $domain === null ) { @@ -140,9 +140,9 @@ public function get_domain( bool $original = false ): string { * @since 1.0.0 * * @param string $domain Domain. - * @param bool $allows_network_subfolder_license Whether network subfolder licenses are allowed. + * @param bool $supports_ms_subfolders Whether network subfolder licenses are allowed. */ - $domain = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix(). '/get_domain', $domain, $allows_network_subfolder_license ); + $domain = apply_filters( 'stellarwp/uplink/' . Config::get_hook_prefix(). '/get_domain', $domain, $supports_ms_subfolders ); return sanitize_text_field( $domain ); } diff --git a/src/Uplink/Uplink.php b/src/Uplink/Uplink.php index 0fe5b493..76c31085 100644 --- a/src/Uplink/Uplink.php +++ b/src/Uplink/Uplink.php @@ -14,6 +14,8 @@ class Uplink { * * @since 1.0.0 * + * @throws RuntimeException + * * @return void */ public static function init(): void { @@ -34,6 +36,7 @@ public static function init(): void { $container->singleton( Site\Data::class, Site\Data::class ); $container->singleton( Notice\Provider::class, Notice\Provider::class ); $container->singleton( Admin\Provider::class, Admin\Provider::class ); + $container->singleton( License\Provider::class, License\Provider::class ); $container->singleton( Auth\Provider::class, Auth\Provider::class ); if ( static::is_enabled() ) { @@ -41,10 +44,8 @@ public static function init(): void { $container->get( API\V3\Provider::class )->register(); $container->get( Notice\Provider::class )->register(); $container->get( Admin\Provider::class )->register(); - - if ( $container->has( Config::TOKEN_OPTION_NAME ) ) { - $container->get( Auth\Provider::class )->register(); - } + $container->get( License\Provider::class )->register(); + $container->get( Auth\Provider::class )->register(); } require_once __DIR__ . '/functions.php'; diff --git a/src/Uplink/functions.php b/src/Uplink/functions.php index f364653d..54570717 100644 --- a/src/Uplink/functions.php +++ b/src/Uplink/functions.php @@ -2,17 +2,20 @@ namespace StellarWP\Uplink; +use StellarWP\Uplink\API\V3\Auth\Contracts\Token_Authorizer; use StellarWP\ContainerContract\ContainerInterface; use StellarWP\Uplink\Admin\License_Field; use StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url; -use StellarWP\Uplink\API\V3\Auth\Token_Authorizer; use StellarWP\Uplink\API\Validation_Response; use StellarWP\Uplink\Auth\Admin\Disconnect_Controller; use StellarWP\Uplink\Auth\Auth_Url_Builder; use StellarWP\Uplink\Auth\Authorizer; -use StellarWP\Uplink\Auth\License\License_Manager; use StellarWP\Uplink\Auth\Token\Token_Factory; use StellarWP\Uplink\Components\Admin\Authorize_Button_Controller; +use StellarWP\Uplink\Components\Controller; +use StellarWP\Uplink\License\Storage\File_Storage; +use StellarWP\Uplink\License\Storage\Network_Storage; +use StellarWP\Uplink\License\Storage\Local_Storage; use StellarWP\Uplink\Resources\Collection; use StellarWP\Uplink\Resources\Plugin; use StellarWP\Uplink\Resources\Resource; @@ -73,7 +76,9 @@ function get_authorization_token( string $slug ): ?string { } /** - * Manually check if a license is authorized. + * Check if a license is authorized. + * + * @note This response may be cached. * * @param string $license The license key. * @param string $token The stored token. @@ -83,9 +88,9 @@ function get_authorization_token( string $slug ): ?string { */ function is_authorized( string $license, string $token, string $domain ): bool { try { - return get_container() - ->get( Token_Authorizer::class ) - ->is_authorized( $license, $token, $domain ); + return Config::get_container() + ->get( Token_Authorizer::class ) + ->is_authorized( $license, $token, $domain ); } catch ( Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( "An Authorization error occurred: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); @@ -147,7 +152,7 @@ function is_user_authorized(): bool { function build_auth_url( string $slug, string $domain = '' ): string { try { return get_container()->get( Auth_Url_Builder::class ) - ->build( $slug, $domain ); + ->build( $slug, $domain ); } catch ( Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( "Error building auth URL: {$e->getMessage()} {$e->getFile()}:{$e->getLine()} {$e->getTraceAsString()}" ); @@ -171,75 +176,157 @@ function get_resource( string $slug ) { } /** - * Compares the Uplink configuration to the current site this function is called on, - * e.g. a sub-site to determine if the product supports multisite licenses. + * A multisite aware license validation check. + * + * @param string $slug The plugin/service slug to validate against. + * @param string $license An optional license key, otherwise we'll fetch it automatically. + * + * @throws \RuntimeException * - * Not to be confused with Config::allows_network_licenses(). + * @return Validation_Response|null + */ +function validate_license( string $slug, string $license = '' ): ?Validation_Response { + $resource = get_resource( $slug ); + + if ( ! $resource ) { + return null; + } + + return $resource->validate_license( $license ); +} + +/** + * A multisite license aware way to get a resource's license key automatically + * from the network, local site level or the file helper class which + * respects the configured license key strategy. * - * @param string|Resource|Plugin|Service $slug_or_resource The product/service slug or a Resource object. + * @param string $slug The plugin/service slug. * * @throws \RuntimeException * - * @return bool + * @return string|null */ -function allows_multisite_license( $slug_or_resource ): bool { - $resource = $slug_or_resource instanceof Resource ? $slug_or_resource : get_resource( $slug_or_resource ); +function get_license_key( string $slug ): ?string { + $resource = get_resource( $slug ); if ( ! $resource ) { - return false; + return null; } - return get_container()->get( License_Manager::class )->allows_multisite_license( $resource ); + return $resource->get_license_key(); } /** - * A multisite aware license validation check. + * Get the underlying Network storage object to manipulate license keys directly. + * Prefer get_license_key() were possible, but if you need to manipulate the + * license keys without respecting Uplink Configuration/multisite, use this. * - * @param string $slug The plugin/service slug to validate against. - * @param string $license An optional license key, otherwise we'll fetch it automatically. + * @throws \RuntimeException + * + * @return Network_Storage + */ +function get_license_network_storage(): Network_Storage { + return get_container()->get( Network_Storage::class ); +} + +/** + * Get the underlying Single Site storage object to manipulate license keys directly. + * Prefer get_license_key() were possible, but if you need to manipulate the license + * keys without respecting Uplink Configuration/multisite, use this. * * @throws \RuntimeException * - * @return Validation_Response|null + * @return Local_Storage */ -function validate_license( string $slug, string $license = '' ): ?Validation_Response { +function get_license_local_storage(): Local_Storage { + return get_container()->get( Local_Storage::class ); +} + +/** + * Get the raw license key from the network, ignoring any Uplink/multisite configuration. + * + * @note You should avoid this function unless you're doing something REALLY custom. + * + * @param string $slug The plugin/service slug. + * + * @throws \RuntimeException + * + * @return string|null + */ +function get_raw_network_license_key( string $slug ): ?string { $resource = get_resource( $slug ); if ( ! $resource ) { return null; } - $license = $license ?: get_license_key( $slug ); - $network = allows_multisite_license( $resource ); + return get_license_network_storage()->get( $resource ); +} - return $resource->validate_license( $license, $network ); +/** + * Get the raw license key from the current single site, ignoring any Uplink/multisite + * configuration. + * + * @note You should avoid this function unless you're doing something REALLY custom. + * + * @param string $slug The plugin/service slug. + * + * @throws \RuntimeException + * + * @return string|null + */ +function get_raw_local_license_key( string $slug ): ?string { + $resource = get_resource( $slug ); + + if ( ! $resource ) { + return null; + } + + return get_license_local_storage()->get( $resource ); } /** - * A multisite license aware way to get a resource's license key automatically - * from the network or local site level. + * Get the embedded license key provided by the plugin's helper class. * * @param string $slug The plugin/service slug. * * @throws \RuntimeException * - * @return string + * @return string|null */ -function get_license_key( string $slug ): string { +function get_default_license_key( string $slug ): ?string { $resource = get_resource( $slug ); if ( ! $resource ) { - return ''; + return null; } - $network = allows_multisite_license( $resource ); + return get_container()->get( File_Storage::class )->get( $resource ); +} + +/** + * Check if the current license key is the default key. + * + * @param string $slug The plugin/service slug. + * + * @throws \RuntimeException + * + * @return bool + */ +function is_default_license_key( string $slug ): bool { + $key = get_license_key( $slug ); + $file_key = get_default_license_key( $slug ); + + if ( $key === null && $file_key === null ) { + return false; + } - return $resource->get_license_key( $network ? 'network' : 'local' ); + return $key === $file_key; } /** * A multisite license aware way to set a resource's license key automatically - * from the network or local site level. + * from the network or local site level. * * @param string $slug The plugin/service slug. * @param string $license The license key to store. @@ -255,15 +342,35 @@ function set_license_key( string $slug, string $license ): bool { return false; } - $network = allows_multisite_license( $resource ); - $result = $resource->set_license_key( $license, $network ? 'network' : 'local' ); + $result = $resource->set_license_key( $license ); // Force update the key status. - $resource->validate_license( $license, $network ); + // TODO: we'll automate this via set_license_key + $resource->validate_license( $license ); return $result; } +/** + * A multisite license aware way to delete a resource's license key automatically + * from the network or local site level which respects the configured license key strategy. + * + * @param string $slug The plugin/service slug. + * + * @throws \RuntimeException + * + * @return bool + */ +function delete_license_key( string $slug ): bool { + $resource = get_resource( $slug ); + + if ( ! $resource ) { + return false; + } + + return $resource->delete_license_key(); +} + /** * Get the current site's license domain without any hash suffix. * @@ -321,3 +428,20 @@ function get_license_field(): License_Field { function get_auth_url( string $slug ): string { return get_container()->get( Auth_Url::class )->get( $slug ); } + +/** + * Renders a component. + * + * @param class-string|string $controller The controller's class name. + * @param array $args The arguments to pass to the controller's render method. + * + * @throws \RuntimeException + * + * @return void + */ +function render_component( string $controller, array $args ): void { + /** @var Controller $component */ + $component = get_container()->get( $controller ); + + $component->render( $args ); +} diff --git a/src/views/settings/description.php b/src/views/settings/description.php new file mode 100644 index 00000000..8cf70519 --- /dev/null +++ b/src/views/settings/description.php @@ -0,0 +1,9 @@ + +

diff --git a/src/views/settings/fields/checkbox.php b/src/views/settings/fields/checkbox.php new file mode 100644 index 00000000..8bb2243f --- /dev/null +++ b/src/views/settings/fields/checkbox.php @@ -0,0 +1,35 @@ + +
> + + + + +
diff --git a/src/views/settings/fields/index.php b/src/views/settings/fields/index.php new file mode 100644 index 00000000..9170000d --- /dev/null +++ b/src/views/settings/fields/index.php @@ -0,0 +1 @@ +container = Config::get_container(); - - $this->network_subfolder_license = Config::allows_network_subfolder_license(); - $this->network_subdomain_license = Config::allows_network_subdomain_license(); - $this->network_domain_mapping_license = Config::allows_network_domain_mapping_license(); } protected function tearDown(): void { - // Reset back to default config, in case any tests changed them. - Config::set_network_subfolder_license( $this->network_subfolder_license ); - Config::set_network_subdomain_license( $this->network_subdomain_license ); - Config::set_network_domain_mapping_license( $this->network_domain_mapping_license ); + Config::reset(); parent::tearDown(); } /** - * @param string $path The path to the plugin file, e.g. my-plugin/my-plugin.php - * @param bool $network_wide Whether this should happen network wide. + * Fake that WordPress has a plugin activated by manually inserting the records. + * + * @param string $path The path to the plugin file, e.g. my-plugin/my-plugin.php + * @param bool $network_wide Whether this should happen network wide. * * @return void */ @@ -83,12 +57,14 @@ protected function mock_activate_plugin( string $path, bool $network_wide = fals $current[ $path ] = time(); update_site_option( 'active_sitewide_plugins', $current ); - } else { - update_option( - 'active_plugins', - array_merge(get_option('active_plugins', []), [$path]) - ); + + return; } + + update_option( + 'active_plugins', + array_merge( get_option( 'active_plugins', [] ), [ $path ] ) + ); } } diff --git a/tests/muwpunit/Replacement_Key_Test.php b/tests/muwpunit/Replacement_Key_Test.php index e9d1dc6e..1e0b7f3d 100644 --- a/tests/muwpunit/Replacement_Key_Test.php +++ b/tests/muwpunit/Replacement_Key_Test.php @@ -57,9 +57,8 @@ public function set_plugin_active_for_network(): void { * @test */ public function should_not_update_license_key_if_replacement_key_not_provided(): void { - // Ensure there is no key set. - $this->resource->delete_license_key(); $validated_key = md5( microtime() ); + $this->assertEmpty( $this->resource->get_license_key() ); $body = $this->service_mock->get_validate_key_success_body(); $mock_response = $this->service_mock->make_response( 200, $body, 'application/json' ); $this->service_mock->will_reply_to_request( 'POST', '/plugins/v2/license/validate', $mock_response ); @@ -76,16 +75,16 @@ public function should_not_update_license_key_if_replacement_key_not_provided(): * @test */ public function should_not_update_license_key_if_replacement_key_is_empty(): void { - // Ensure there is no key set. - $this->resource->delete_license_key(); $validated_key = md5( microtime() ); + $this->assertEmpty( $this->resource->get_license_key() ); + $body = $this->service_mock->get_validate_key_success_body(); // Add an empty replacement key to the response body. $body['results'][0]['replacement_key'] = ''; $mock_response = $this->service_mock->make_response( 200, $body, 'application/json' ); $this->service_mock->will_reply_to_request( 'POST', '/plugins/v2/license/validate', $mock_response ); - $result = $this->resource->validate_license( $validated_key ); + $this->resource->validate_license( $validated_key ); $this->assertEquals( $validated_key, $this->resource->get_license_key() ); } @@ -97,8 +96,7 @@ public function should_not_update_license_key_if_replacement_key_is_empty(): voi */ public function should_update_license_key_if_replacement_key_provided_and_key_not_previously_set(): void { $validated_key = md5( microtime() ); - // Ensure there is no key set. - $this->resource->delete_license_key(); + $this->assertEmpty( $this->resource->get_license_key() ); // Set the response mock to provide a replacement key. $replacement_key = '2222222222222222222222222222222222222222'; $body = $this->service_mock->get_validate_key_success_body(); @@ -107,7 +105,7 @@ public function should_update_license_key_if_replacement_key_provided_and_key_no $mock_response = $this->service_mock->make_response( 200, $body, 'application/json' ); $this->service_mock->will_reply_to_request( 'POST', '/plugins/v2/license/validate', $mock_response ); - $result = $this->resource->validate_license( $validated_key ); + $this->resource->validate_license( $validated_key ); $this->assertEquals( $replacement_key, $this->resource->get_license_key() ); } @@ -141,9 +139,7 @@ public function should_update_license_key_if_replacement_key_provided_and_key_pr */ public function should_set_network_key_to_validated_key_when_not_previously_set_and_replacement_not_provided(): void { $validated_key = md5( microtime() ); - // Ensure there is no license key locally or network wide. - $this->resource->delete_license_key(); - $this->resource->delete_license_key( 'network' ); + $this->assertEmpty( $this->resource->get_license_key() ); $body = $this->service_mock->get_validate_key_success_body(); $mock_response = $this->service_mock->make_response( 200, $body, 'application/json' ); $this->service_mock->will_reply_to_request( 'POST', '/plugins/v2/license/validate', $mock_response ); @@ -158,9 +154,7 @@ public function should_set_network_key_to_validated_key_when_not_previously_set_ */ public function should_set_network_key_to_validated_key_when_not_previously_set_and_replacement_key_empty(): void { $validated_key = md5( microtime() ); - // Ensure there is no license key locally or network wide. - $this->resource->delete_license_key(); - $this->resource->delete_license_key( 'network' ); + $this->assertEmpty( $this->resource->get_license_key() ); $body = $this->service_mock->get_validate_key_success_body(); // Add a replacement key to the response body. $body['results'][0]['replacement_key'] = ''; @@ -177,9 +171,7 @@ public function should_set_network_key_to_validated_key_when_not_previously_set_ */ public function should_set_network_key_to_provided_replacement_key_when_not_previously_set(): void { $validated_key = md5( microtime() ); - // Ensure there is no license key locally or network wide. - $this->resource->delete_license_key(); - $this->resource->delete_license_key( 'network' ); + $this->assertEmpty( $this->resource->get_license_key() ); $body = $this->service_mock->get_validate_key_success_body(); // Add a replacement key to the response body. $replacement_key = '2222222222222222222222222222222222222222'; @@ -200,9 +192,8 @@ public function should_set_network_key_to_provided_replacement_key_when_not_prev */ public function should_set_previously_set_network_key_to_replacement_key_if_provided() { $validated_key = md5( microtime() ); - // Ensure there is no license key locally or network wide. - $this->resource->delete_license_key(); - $this->resource->delete_license_key( 'network' ); + $this->assertEmpty( $this->resource->get_license_key() ); + $body = $this->service_mock->get_validate_key_success_body(); // Add a replacement key to the response body. $replacement_key = '2222222222222222222222222222222222222222'; diff --git a/tests/wpunit/API/V3/AuthorizerCacheTest.php b/tests/wpunit/API/V3/AuthorizerCacheTest.php new file mode 100644 index 00000000..71a0726a --- /dev/null +++ b/tests/wpunit/API/V3/AuthorizerCacheTest.php @@ -0,0 +1,65 @@ +assertInstanceOf( + Token_Authorizer_Cache_Decorator::class, + $this->container->get( Token_Authorizer::class ) + ); + } + + public function test_it_caches_a_valid_token_response(): void { + $authorizer_mock = $this->makeEmpty( \StellarWP\Uplink\API\V3\Auth\Token_Authorizer::class, [ + 'is_authorized' => static function (): bool { + return true; + }, + ] ); + + $this->container->bind( \StellarWP\Uplink\API\V3\Auth\Token_Authorizer::class, $authorizer_mock ); + + $decorator = $this->container->get( Token_Authorizer::class ); + $transient = $decorator->build_transient( [ '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ] ); + + // No cache should exist. + $this->assertFalse( get_transient( $transient ) ); + + $authorized = $decorator->is_authorized( '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ); + + $this->assertTrue( $authorized ); + + // Cache should now be present. + $this->assertTrue( get_transient( $transient ) ); + $this->assertTrue( $decorator->is_authorized( '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ) ); + } + + public function test_it_does_not_cache_an_invalid_token_response(): void { + $authorizer_mock = $this->makeEmpty( \StellarWP\Uplink\API\V3\Auth\Token_Authorizer::class, [ + 'is_authorized' => static function (): bool { + return false; + }, + ] ); + + $this->container->bind( \StellarWP\Uplink\API\V3\Auth\Token_Authorizer::class, $authorizer_mock ); + + $decorator = $this->container->get( Token_Authorizer::class ); + $transient = $decorator->build_transient( [ '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ] ); + + // No cache should exist. + $this->assertFalse( get_transient( $transient ) ); + + $authorized = $decorator->is_authorized( '1234', 'dc2c98d9-9ff8-4409-bfd2-a3cce5b5c840', 'test.com' ); + + $this->assertFalse( $authorized ); + + // Cache should still be empty, unfortunately the default is "false" for transients, so this isn't the best test. + $this->assertFalse( get_transient( $transient ) ); + } + +} diff --git a/tests/wpunit/API/V3/AuthorizerTest.php b/tests/wpunit/API/V3/AuthorizerTest.php index a0053fd4..49a0490d 100644 --- a/tests/wpunit/API/V3/AuthorizerTest.php +++ b/tests/wpunit/API/V3/AuthorizerTest.php @@ -4,10 +4,28 @@ use StellarWP\Uplink\API\V3\Auth\Token_Authorizer; use StellarWP\Uplink\API\V3\Contracts\Client_V3; +use StellarWP\Uplink\Config; use StellarWP\Uplink\Tests\UplinkTestCase; +use StellarWP\Uplink\Uplink; final class AuthorizerTest extends UplinkTestCase { + protected function setUp(): void { + parent::setUp(); + + // Disable auth caching. + Config::set_auth_cache_expiration( -1 ); + + Uplink::init(); + } + + public function test_it_binds_the_correct_instance_when_auth_cache_is_disabled(): void { + $this->assertInstanceOf( + Token_Authorizer::class, + $this->container->get( \StellarWP\Uplink\API\V3\Auth\Contracts\Token_Authorizer::class ) + ); + } + public function test_it_authorizes_a_valid_token(): void { $clientMock = $this->makeEmpty( Client_V3::class, [ 'get' => static function () { diff --git a/tests/wpunit/API/Validation_ResponseTest.php b/tests/wpunit/API/Validation_ResponseTest.php index dc732476..f2535657 100644 --- a/tests/wpunit/API/Validation_ResponseTest.php +++ b/tests/wpunit/API/Validation_ResponseTest.php @@ -35,7 +35,7 @@ public function get_dummy_api_invalid_response(): \stdClass { public function test_it_should_provide_valid_update_details(): void { - $result = new Validation_Response( 'aaa11', 'local', $this->get_dummy_valid_response(), $this->resource ); + $result = new Validation_Response( 'aaa11', $this->get_dummy_valid_response(), $this->resource ); $update = $result->get_update_details(); $this->assertEquals( '', $update->id ); @@ -46,7 +46,7 @@ public function test_it_should_provide_valid_update_details(): void { } public function test_it_should_provide_api_error_details_with_corresponding_message() { - $result = new Validation_Response( 'aaa11', 'local', $this->get_dummy_api_invalid_response(), $this->resource ); + $result = new Validation_Response( 'aaa11', $this->get_dummy_api_invalid_response(), $this->resource ); $update = $result->get_update_details(); $this->assertEquals( '1.0.10', $update->new_version ); diff --git a/tests/wpunit/Admin/AjaxText.php b/tests/wpunit/Admin/AjaxText.php deleted file mode 100644 index 14e4ecf2..00000000 --- a/tests/wpunit/Admin/AjaxText.php +++ /dev/null @@ -1,48 +0,0 @@ - 0, - 'message' => __( 'Invalid request: nonce field is expired. Please try again.', '%TEXTDOMAIN%' ) - ]; - - $this->assertSame( - json_encode( $invalid_response ), - $handler->validate_license(), - 'Should return invalid request message if nonce or key is missing is empty' - ); - $_POST['_wpnonce'] = wp_create_nonce( License_Field::get_group_name() ); - $_POST['key'] = 'sample'; - $_POST['plugin'] = 'sample/index.php'; - - $this->assertSame( json_encode( [ - 'status' => 0, - ] ), $handler->validate_license(), 'Should return 0 status since endpoint is unreachable' ); - } - -} diff --git a/tests/wpunit/Auth/Admin/ConnectControllerTest.php b/tests/wpunit/Auth/Admin/ConnectControllerTest.php index 6250dedf..7ddd5ba6 100644 --- a/tests/wpunit/Auth/Admin/ConnectControllerTest.php +++ b/tests/wpunit/Auth/Admin/ConnectControllerTest.php @@ -105,7 +105,7 @@ public function test_it_sets_additional_license_key(): void { $response->api_upgrade = 0; $response->api_expired = 0; - return new Validation_Response( '123456', is_multisite() ? 'network' : 'local', $response, $plugin ); + return new Validation_Response( '123456', $response, $plugin ); }, ] ); @@ -139,7 +139,7 @@ public function test_it_sets_additional_license_key(): void { do_action( 'admin_init' ); $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $plugin->get_license_key( is_multisite() ? 'network' : 'local' ), $license ); + $this->assertSame( $plugin->get_license_key(), $license ); } public function test_it_does_not_store_with_an_invalid_nonce(): void { @@ -261,7 +261,7 @@ public function test_it_stores_token_but_not_license_without_a_license(): void { do_action( 'admin_init' ); $this->assertSame( $token, $token_manager->get() ); - $this->assertEmpty( $plugin->get_license_key( is_multisite() ? 'network' : 'local' ) ); + $this->assertEmpty( $plugin->get_license_key() ); } public function test_it_stores_token_but_not_license_without_a_valid_license(): void { @@ -275,7 +275,7 @@ public function test_it_stores_token_but_not_license_without_a_valid_license(): $response->api_upgrade = 0; $response->api_expired = 1; // makes validation fail. - return new Validation_Response( '123456', is_multisite() ? 'network' : 'local', $response, $plugin ); + return new Validation_Response( '123456', $response, $plugin ); }, ] ); @@ -308,7 +308,7 @@ public function test_it_stores_token_but_not_license_without_a_valid_license(): do_action( 'admin_init' ); $this->assertSame( $token, $token_manager->get() ); - $this->assertEmpty( $plugin->get_license_key( is_multisite() ? 'network' : 'local' ) ); + $this->assertEmpty( $plugin->get_license_key() ); } /** @@ -325,7 +325,7 @@ public function test_it_sets_token_and_additional_license_key_on_multisite_netwo $response->api_upgrade = 0; $response->api_expired = 0; - return new Validation_Response( '123456', 'network', $response, $plugin ); + return new Validation_Response( '123456', $response, $plugin ); }, ] ); @@ -341,7 +341,7 @@ public function test_it_sets_token_and_additional_license_key_on_multisite_netwo // Mock our sample plugin is network activated, otherwise license key check fails. $this->mock_activate_plugin( 'uplink/index.php', true ); - $this->assertEmpty( $plugin->get_license_key( 'network' ) ); + $this->assertEmpty( $plugin->get_license_key() ); $token_manager = $this->token_manager_factory->make( $plugin ); @@ -368,7 +368,7 @@ public function test_it_sets_token_and_additional_license_key_on_multisite_netwo do_action( 'admin_init' ); $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $plugin->get_license_key( 'network' ), $license ); + $this->assertSame( $plugin->get_license_key(), $license ); } /** @@ -385,7 +385,7 @@ public function test_it_stores_token_data_on_subfolder_subsite(): void { $response->api_upgrade = 0; $response->api_expired = 0; - return new Validation_Response( '123456', 'network', $response, $plugin ); + return new Validation_Response( '123456', $response, $plugin ); }, ] ); @@ -404,7 +404,7 @@ public function test_it_stores_token_data_on_subfolder_subsite(): void { // Mock our sample plugin is network activated, otherwise license key check fails. $this->mock_activate_plugin( 'uplink/index.php', true ); - $this->assertEmpty( $plugin->get_license_key( 'network' ) ); + $this->assertEmpty( $plugin->get_license_key() ); $token_manager = $this->token_manager_factory->make( $plugin ); @@ -431,7 +431,7 @@ public function test_it_stores_token_data_on_subfolder_subsite(): void { do_action( 'admin_init' ); $this->assertSame( $token, $token_manager->get() ); - $this->assertSame( $plugin->get_license_key( 'network' ), $license ); + $this->assertSame( $plugin->get_license_key(), $license ); } } diff --git a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php index c0db1dbc..139fcf18 100644 --- a/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/CustomDomainMultisiteTokenMangerTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); - Config::set_network_domain_mapping_license( true ); + Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); // Run init again to reload the Token/Provider. Uplink::init(); diff --git a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php index 9f7b17f5..1212f650 100644 --- a/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubDomainMultisiteTokenMangerTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); - Config::set_network_subdomain_license( true ); + Config::allow_site_level_licenses_for_subdomain_multisite( true ); // Run init again to reload the Token/Provider. Uplink::init(); diff --git a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php index 2e8878b2..db3f6459 100644 --- a/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php +++ b/tests/wpunit/Auth/Token/SubfolderMultisiteTokenMangerTest.php @@ -24,7 +24,7 @@ protected function setUp(): void { parent::setUp(); Config::set_token_auth_prefix( 'kadence_' ); - Config::set_network_subfolder_license( true ); + Config::allow_site_level_licenses_for_subfolder_multisite( true ); // Run init again to reload the Token/Provider. Uplink::init(); diff --git a/tests/wpunit/ConfigTest.php b/tests/wpunit/ConfigTest.php index 7654870d..b61e40ed 100644 --- a/tests/wpunit/ConfigTest.php +++ b/tests/wpunit/ConfigTest.php @@ -54,22 +54,30 @@ public function test_it_throws_exception_with_long_prefix(): void { Config::set_token_auth_prefix( 'fluffy_unicorn_rainbow_sunshine_happy_smile_peace_joy_love_puppy_harmony_giggles_dreams_celebrate_fantastic_wonderful_whimsical_serendipity_butterfly_magic_sparkle_sweetness_trust_' ); } + public function test_it_gets_and_sets_auth_token_cache_expiration(): void { + $this->assertSame( Config::DEFAULT_AUTH_CACHE, Config::get_auth_cache_expiration() ); + + Config::set_auth_cache_expiration( DAY_IN_SECONDS ); + + $this->assertSame( DAY_IN_SECONDS, Config::get_auth_cache_expiration() ); + } + public function test_it_detects_allowed_network_licenses_subfolder(): void { - $this->assertFalse( Config::allows_network_licenses() ); - Config::set_network_subfolder_license( true ); - $this->assertTrue( Config::allows_network_licenses() ); + $this->assertFalse( Config::supports_network_licenses() ); + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + $this->assertTrue( Config::supports_network_licenses() ); } public function test_it_detects_allowed_network_licenses_subdomain(): void { - $this->assertFalse( Config::allows_network_licenses() ); - Config::set_network_subdomain_license( true ); - $this->assertTrue( Config::allows_network_licenses() ); + $this->assertFalse( Config::supports_network_licenses() ); + Config::allow_site_level_licenses_for_subdomain_multisite( true ); + $this->assertTrue( Config::supports_network_licenses() ); } public function test_it_detects_allowed_network_licenses_domain_mapping(): void { - $this->assertFalse( Config::allows_network_licenses() ); - Config::set_network_domain_mapping_license( true ); - $this->assertTrue( Config::allows_network_licenses() ); + $this->assertFalse( Config::supports_network_licenses() ); + Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); + $this->assertTrue( Config::supports_network_licenses() ); } } diff --git a/tests/wpunit/Resources/License/LicenseKeyMultisiteNetworkAdminTest.php b/tests/wpunit/Resources/License/LicenseKeyMultisiteNetworkAdminTest.php new file mode 100644 index 00000000..a178aa3b --- /dev/null +++ b/tests/wpunit/Resources/License/LicenseKeyMultisiteNetworkAdminTest.php @@ -0,0 +1,182 @@ +resource = Register::plugin( + $this->slug, + 'Lib Sample', + '1.0.10', + 'uplink/index.php', + Sample_Plugin::class + ); + + // Mock our sample plugin is network activated, otherwise license key check fails. + $this->mock_activate_plugin( 'uplink/index.php', true ); + + // Allow all multisite modes. + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); + Config::allow_site_level_licenses_for_subdomain_multisite( true ); + + $this->container->get( License_Handler::class )->disable_cache(); + + $this->single_storage = $this->container->get( Local_Storage::class ); + $this->network_storage = $this->container->get( Network_Storage::class ); + + // Mock we're in the network dashboard, so is_network_admin() returns true. + $screen = WP_Screen::get( 'dashboard-network' ); + $GLOBALS['current_screen'] = $screen; + + $this->assertTrue( $screen->in_admin( 'network' ) ); + } + + protected function tearDown(): void { + $GLOBALS['current_screen'] = null; + + parent::tearDown(); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->network_storage->store( $this->resource, 'network-key' ); + + $this->assertSame( 'network-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_fallback_file_license_key(): void { + $this->assertTrue( is_multisite() ); + $slug = 'sample-isolated-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // No network key stored. + $this->assertEmpty( $this->network_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_with_existing_local_key(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->network_storage->store( $this->resource, 'network-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->assertSame( 'network-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_when_all_multisite_types(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $sites = [ + [ + 'domain' => 'wordpress.test', + 'path' => '/sub1', + 'name' => 'Test Subsite 1', + ], + [ + 'domain' => 'temp.wordpress.test', + 'path' => '/', + 'name' => 'Test Subdomain Subsite', + ], + [ + 'domain' => 'wordpress.custom', + 'path' => '/', + 'name' => 'Test Custom Domain Subsite', + ], + ]; + + $this->network_storage->store( $this->resource, 'network-key' ); + $this->assertSame( 'network-key', $this->network_storage->get( $this->resource ) ); + + $main_blog_id = get_current_blog_id(); + + foreach ( $sites as $site ) { + $id = wpmu_create_blog( $site['domain'], $site['path'], $site['name'], 1 ); + $this->assertNotInstanceOf( WP_Error::class, $id ); + $this->assertGreaterThan( 1, $id ); + + switch_to_blog( $id ); + + $this->assertEmpty( $this->single_storage->get( $this->resource ) ); + $this->single_storage->store( $this->resource, 'local-key' ); + + switch_to_blog( $main_blog_id ); + + $this->assertSame( 'network-key', $this->resource->get_license_key() ); + } + } + +} diff --git a/tests/wpunit/Resources/License/LicenseKeyMultisiteNoConfigTest.php b/tests/wpunit/Resources/License/LicenseKeyMultisiteNoConfigTest.php new file mode 100644 index 00000000..0a6bd10c --- /dev/null +++ b/tests/wpunit/Resources/License/LicenseKeyMultisiteNoConfigTest.php @@ -0,0 +1,188 @@ +resource = Register::plugin( + $this->slug, + 'Lib Sample', + '1.0.10', + 'uplink/index.php', + Sample_Plugin::class + ); + + // Mock our sample plugin is network activated, otherwise license key check fails. + $this->mock_activate_plugin( 'uplink/index.php', true ); + + $this->container->get( License_Handler::class )->disable_cache(); + + $this->single_storage = $this->container->get( Local_Storage::class ); + $this->network_storage = $this->container->get( Network_Storage::class ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_license_key_on_main_site(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->single_storage->store( $this->resource, 'local-key' ); + + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_fallback_file_license_key_on_main_site(): void { + $this->assertTrue( is_multisite() ); + $slug = 'sample-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // No local key stored. + $this->assertEmpty( $this->single_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_local_license_key_with_existing_network_key(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + + $this->network_storage->store( $this->resource, 'network-key' ); + $this->assertSame( 'network-key', $this->network_storage->get( $this->resource ) ); + + // Local key returned. + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_local_license_key_with_no_network_key(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + // No network key stored. + $this->assertEmpty( $this->network_storage->get( $this->resource ) ); + + // Local key returned. + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_file_license_key_no_local_or_network_key(): void { + $this->assertTrue( is_multisite() ); + + $slug = 'sample-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + // No local key stored. + $this->assertEmpty( $this->single_storage->get( $resource ) ); + + // No network key stored. + $this->assertEmpty( $this->network_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + +} diff --git a/tests/wpunit/Resources/License/LicenseKeyMultisiteWithConfigTest.php b/tests/wpunit/Resources/License/LicenseKeyMultisiteWithConfigTest.php new file mode 100644 index 00000000..36e4a297 --- /dev/null +++ b/tests/wpunit/Resources/License/LicenseKeyMultisiteWithConfigTest.php @@ -0,0 +1,409 @@ +resource = Register::plugin( + $this->slug, + 'Lib Sample', + '1.0.10', + 'uplink/index.php', + Sample_Plugin::class + ); + + // Mock our sample plugin is network activated, otherwise license key check fails. + $this->mock_activate_plugin( 'uplink/index.php', true ); + + $this->container->get( License_Handler::class )->disable_cache(); + + $this->single_storage = $this->container->get( Local_Storage::class ); + $this->network_storage = $this->container->get( Network_Storage::class ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_license_key_on_main_site(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->single_storage->store( $this->resource, 'local-key' ); + + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_fallback_file_license_key_on_main_site(): void { + $this->assertTrue( is_multisite() ); + $slug = 'sample-isolated-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // No local key stored. + $this->assertEmpty( $this->single_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_license_key_while_network_activated(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->network_storage->store( $this->resource, 'network-key' ); + $this->assertSame( 'network-key', $this->network_storage->get( $this->resource ) ); + + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_license_key_with_no_multisite_configuration(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->network_storage->store( $this->resource, 'network-key' ); + + // Local key returned. + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_single_site_license_key_with_all_multisite_types(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + $sites = [ + [ + 'domain' => 'wordpress.test', + 'path' => '/sub1', + 'name' => 'Test Subsite 1', + ], + [ + 'domain' => 'temp.wordpress.test', + 'path' => '/', + 'name' => 'Test Subdomain Subsite', + ], + [ + 'domain' => 'wordpress.custom', + 'path' => '/', + 'name' => 'Test Custom Domain Subsite', + ], + ]; + + $this->network_storage->store( $this->resource, 'network-key' ); + $this->assertSame( 'network-key', $this->network_storage->get( $this->resource ) ); + + foreach ( $sites as $site ) { + $id = wpmu_create_blog( $site['domain'], $site['path'], $site['name'], 1 ); + $this->assertNotInstanceOf( WP_Error::class, $id ); + $this->assertGreaterThan( 1, $id ); + + switch_to_blog( $id ); + + $this->assertEmpty( $this->single_storage->get( $this->resource ) ); + $this->single_storage->store( $this->resource, 'local-key' ); + + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + } + + /** + * @env multisite + */ + public function test_it_gets_fallback_file_license_key_with_no_multisite_configuration(): void { + $this->assertTrue( is_multisite() ); + + $slug = 'sample-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + // No local key. + $this->assertEmpty( $this->single_storage->get( $resource ) ); + + // Store network key, but it should never be fetched in this scenario. + $this->network_storage->store( $resource, 'network-key' ); + $this->assertSame( 'network-key', $this->network_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_with_subfolders_configured(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->network_storage->store( $this->resource, 'network-key-subfolder' ); + + $this->assertSame( 'network-key-subfolder', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_fallback_file_license_key_with_subfolders_configured(): void { + $this->assertTrue( is_multisite() ); + + $slug = 'sample-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.test', '/sub1', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + // Store a local key, but it should never be fetched in this scenario. + $this->single_storage->store( $resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $resource ) ); + + // No network key stored. + $this->assertEmpty( $this->network_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_with_subdomains_configured(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Only subdomains of the main site are licensed. + Config::allow_site_level_licenses_for_subdomain_multisite( true ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'temp.wordpress.test', '/', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->network_storage->store( $this->resource, 'network-key-subdomain' ); + + $this->assertSame( 'network-key-subdomain', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_with_domain_mapping_configured(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Only custom subsite domains are licensed. + Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); + + // Create a subsite. + $sub_site_id = wpmu_create_blog( 'wordpress.custom', '/', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->network_storage->store( $this->resource, 'network-key-domain' ); + + $this->assertSame( 'network-key-domain', $this->resource->get_license_key() ); + } + + /** + * @env multisite + */ + public function test_it_gets_network_license_key_with_all_multisite_types_enabled(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + Config::allow_site_level_licenses_for_subdomain_multisite( true ); + Config::allow_site_level_licenses_for_mapped_domain_multisite( true ); + + $sites = [ + [ + 'domain' => 'wordpress.test', + 'path' => '/sub1', + 'name' => 'Test Subsite 1', + ], + [ + 'domain' => 'temp.wordpress.test', + 'path' => '/', + 'name' => 'Test Subdomain Subsite', + ], + [ + 'domain' => 'wordpress.custom', + 'path' => '/', + 'name' => 'Test Custom Domain Subsite', + ], + ]; + + $this->network_storage->store( $this->resource, 'network-key' ); + + foreach ( $sites as $site ) { + $id = wpmu_create_blog( $site['domain'], $site['path'], $site['name'], 1 ); + $this->assertNotInstanceOf( WP_Error::class, $id ); + $this->assertGreaterThan( 1, $id ); + + switch_to_blog( $id ); + + $this->assertEmpty( $this->single_storage->get( $this->resource ) ); + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->assertSame( 'network-key', $this->resource->get_license_key() ); + } + } + + /** + * We allow only subfolder network licensing, but we check the license on a subsite with a custom domain. + * + * @env multisite + */ + public function test_it_gets_local_license_key_when_from_a_multisite_type_that_is_not_enabled(): void { + $this->assertTrue( is_multisite() ); + $this->assertEmpty( $this->resource->get_license_key() ); + + // Only subfolders are network licensed. + Config::allow_site_level_licenses_for_subfolder_multisite( true ); + + // Create a subsite with a custom domain. + $sub_site_id = wpmu_create_blog( 'wordpress.custom', '/', 'Test Subsite', 1 ); + $this->assertNotInstanceOf( WP_Error::class, $sub_site_id ); + $this->assertGreaterThan( 1, $sub_site_id ); + + switch_to_blog( $sub_site_id ); + + $this->single_storage->store( $this->resource, 'local-key' ); + $this->assertSame( 'local-key', $this->single_storage->get( $this->resource ) ); + + $this->network_storage->store( $this->resource, 'network-key-domain' ); + + // Local key returned. + $this->assertSame( 'local-key', $this->resource->get_license_key() ); + } + +} diff --git a/tests/wpunit/Resources/License/LicenseKeySingleSiteFetcherTest.php b/tests/wpunit/Resources/License/LicenseKeySingleSiteFetcherTest.php new file mode 100644 index 00000000..bb0bc220 --- /dev/null +++ b/tests/wpunit/Resources/License/LicenseKeySingleSiteFetcherTest.php @@ -0,0 +1,82 @@ +resource = Register::plugin( + $this->slug, + 'Lib Sample', + '1.0.10', + 'uplink/index.php', + Sample_Plugin::class + ); + + $this->container->get( License_Handler::class )->disable_cache(); + + $this->single_storage = $this->container->get( Local_Storage::class ); + } + + public function test_it_gets_single_site_license_key(): void { + $this->assertEmpty( $this->resource->get_license_key() ); + + $this->single_storage->store( $this->resource, 'abcdef' ); + + $this->assertSame( 'abcdef', $this->resource->get_license_key() ); + } + + public function test_it_gets_single_site_fallback_file_license_key(): void { + $slug = 'sample-with-license'; + + // Register the sample plugin as a developer would in their plugin. + $resource = Register::plugin( + $slug, + 'Lib Sample With License', + '1.2.0', + 'uplink/index.php', + Sample_Plugin::class, + Sample_Plugin_Helper::class + ); + + // No local key stored. + $this->assertEmpty( $this->single_storage->get( $resource ) ); + + // File based key returned. + $this->assertSame( 'file-based-license-key', $resource->get_license_key() ); + } + +} diff --git a/tests/wpunit/Site/DataTest.php b/tests/wpunit/Site/DataTest.php index 3a38ec3f..ea8c12a5 100644 --- a/tests/wpunit/Site/DataTest.php +++ b/tests/wpunit/Site/DataTest.php @@ -75,10 +75,10 @@ public function test_it_gets_single_site_domain(): void { } /** - * If Config::allows_network_subfolder_license() is not enabled, subsites in subfolder + * If Config::supports_site_level_licenses_for_subfolder_multisite() is not enabled, subsites in subfolder * mode must be unique, so include their subfolder path in the domain. * - * @see Config::allows_network_subfolder_license() + * @see Config::supports_site_level_licenses_for_subfolder_multisite() * * @env multisite */ @@ -116,7 +116,7 @@ public function test_it_should_get_multisite_subsite_domain_with_path(): void { * @env multisite */ public function test_it_returns_main_site_url_when_network_subfolders_are_allowed(): void { - Config::set_network_subfolder_license( true ); + Config::allow_site_level_licenses_for_subfolder_multisite( true ); Uplink\Uplink::init(); $this->assertTrue( is_multisite() ); @@ -172,7 +172,7 @@ public function test_it_gets_domain_with_custom_subsite_domain(): void { * @env multisite */ public function test_it_gets_main_domain_with_custom_subsite_domain_and_network_subfolders_enabled(): void { - Config::set_network_subfolder_license( true ); + Config::allow_site_level_licenses_for_subfolder_multisite( true ); Uplink\Uplink::init(); $this->assertTrue( is_multisite() ); @@ -217,7 +217,7 @@ public function test_it_gets_domain_with_subdomain(): void { * @env multisite */ public function test_it_gets_main_domain_with_subdomain_and_network_subfolders_enabled(): void { - Config::set_network_subfolder_license( true ); + Config::allow_site_level_licenses_for_subfolder_multisite( true ); Uplink\Uplink::init(); $this->assertTrue( is_multisite() );