Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: multisite license storage #52

Draft
wants to merge 73 commits into
base: bugfix/multisite-token-logic
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
415c8dd
Allow license manager to be registered regardless of token configurat…
defunctl Dec 12, 2023
5d3e04b
Add license key strategy config options
defunctl Dec 12, 2023
c3514d9
Add license storage, strategies factory and provider.
defunctl Dec 12, 2023
f2df0d0
Register license provider, always try to register auth provider.
defunctl Dec 12, 2023
2e88b61
Add LicenseKeyFactoryTest.php
defunctl Dec 12, 2023
49db664
Remove useless property
defunctl Dec 12, 2023
dc81a0e
Update test name
defunctl Dec 12, 2023
4d588ab
Add LicenseKeyFetcherTest.php
defunctl Dec 12, 2023
48e60a9
Update get_license_key function to use license fetcher
defunctl Dec 12, 2023
5d8149f
Return null if in invalid resource
defunctl Dec 12, 2023
abd7542
Add test
defunctl Dec 12, 2023
b12cf5c
Add global license key file test
defunctl Dec 12, 2023
6bdd9a0
Add file based key storage tests
defunctl Dec 12, 2023
9b36037
Separate license key fetcher tests
defunctl Dec 12, 2023
993f6e7
Test all multisite strategies enabled at once.
defunctl Dec 12, 2023
feed11b
Add single site license check with all multisite types at once
defunctl Dec 12, 2023
9695050
Allow license manager caching to be disabled
defunctl Dec 12, 2023
89ff0cb
Clean up factory test
defunctl Dec 12, 2023
8c8b9aa
Separate out global/isolated and single/multisite tests
defunctl Dec 12, 2023
4a37e11
Add filter to key fetcher
defunctl Dec 12, 2023
0b2ba7a
add get_default_license_key and is_default_license_key functions
defunctl Dec 12, 2023
e980889
Add get_raw_single_site_license_key and get_raw_network_license_key f…
defunctl Dec 12, 2023
df859ed
Add get_license_network_storage and get_license_single_site_storage f…
defunctl Dec 12, 2023
bdb4486
formatting
defunctl Dec 12, 2023
79024d5
Add delete_license_key function
defunctl Dec 12, 2023
8f921a7
Make storage option name static, move into trait
defunctl Dec 12, 2023
750912e
Use storage option name and get_license_key function for license field
defunctl Dec 12, 2023
0658c2c
Fix return time for autocompletion
defunctl Dec 12, 2023
eed2773
Move License_Manager under License namespace + update providers
defunctl Dec 12, 2023
3782909
Add allow_site_level_override_for_multisite_license config
defunctl Dec 14, 2023
65bb055
Rename multisite config names/filters
defunctl Dec 14, 2023
e1df461
Add checkbox and description components and render_component function
defunctl Dec 14, 2023
3b878d0
Add data attributes and classes to checkbox
defunctl Dec 14, 2023
c9722c8
Add Get_Value_Trait, make the checkbox value fetching network aware. …
defunctl Dec 15, 2023
b07c786
First phase of storage refactor
defunctl Dec 20, 2023
f743b0e
Rename to License_Handler, improve method names and comments
defunctl Dec 20, 2023
5ba7396
First pass at replacing the underlying storage for License/Resource
defunctl Dec 20, 2023
5c5998f
Wire up uses_network_licensing method
defunctl Dec 20, 2023
0fbd48d
Replace license handler from functions
defunctl Dec 20, 2023
73f9334
Refactor license validation no longer require context
defunctl Dec 20, 2023
9627041
Rename provider method
defunctl Dec 20, 2023
f63fc43
Remove license handler from license resource
defunctl Dec 20, 2023
b907112
Remove license key fetcher/pipeline/strategies
defunctl Dec 20, 2023
6eda509
Remove strategy config, bring back and update license key tests
defunctl Dec 20, 2023
9e4825e
Update comment
defunctl Dec 20, 2023
5ed44d4
Update comment
defunctl Dec 20, 2023
31621ef
Add network admin logic for when we have a proper form + tests
defunctl Dec 20, 2023
fa21000
Remove test that ever ran, will add something later.
defunctl Dec 20, 2023
c44fa9c
Fix Ajax logic
defunctl Dec 20, 2023
604eee9
Remove license handler dep from connect controller
defunctl Dec 20, 2023
0d71c50
Add another todo, comment out code for phpstan
defunctl Dec 20, 2023
a10a41e
Remove network licensed proxy from Resource, fully comment out in lic…
defunctl Dec 20, 2023
da9f940
Remove unneeded license key strategy contracts
defunctl Dec 20, 2023
03fbfa3
Add some more TODOs
defunctl Dec 20, 2023
c6ffae0
Clean up function names, add notes
defunctl Dec 20, 2023
22369e7
Move related methods together
defunctl Dec 21, 2023
da10a67
cast key to string
defunctl Dec 21, 2023
b6940eb
Fix get_key_status() logic that never ran
defunctl Dec 21, 2023
a47aac5
Use local method to set key
defunctl Dec 21, 2023
fc91f49
Update docblock, although I think the comment is wrong and it's just …
defunctl Dec 21, 2023
9d80156
Move api container fetching below empty check
defunctl Dec 21, 2023
c882c78
Group key methods together
defunctl Dec 21, 2023
1f01bd8
Fix HTTP mocks not working after base url was renamed.
defunctl Jan 3, 2024
a519978
Reset config after each test
defunctl Jan 3, 2024
a223aa6
Add token auth caching config + docs
defunctl Jan 3, 2024
c4c52b5
Add token authorizer contract, decorator and tests
defunctl Jan 3, 2024
8bb2d1c
Update function to fetch interface
defunctl Jan 3, 2024
9fbcf02
Fix bad return type
defunctl Jan 3, 2024
bff5b58
Use auth default constant in test
defunctl Jan 3, 2024
9b8a74e
Merge branch 'feature/authorization-caching' into feature/multisite-l…
defunctl Jan 4, 2024
366318f
Use late static bindings
defunctl Jan 4, 2024
3132afa
Add a default state for multisite config options, and properly reset …
defunctl Jan 4, 2024
dc3e890
Update base testcase to fix merge issue and use config::reset()
defunctl Jan 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
```
Expand Down
35 changes: 17 additions & 18 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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();
```
Expand All @@ -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 );
Expand Down Expand Up @@ -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' );
```
14 changes: 7 additions & 7 deletions src/Uplink/API/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @property-read ContainerInterface $container Container instance.
*/
class Client {

/**
* API base endpoint.
*
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions src/Uplink/API/V3/Auth/Contracts/Token_Authorizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare( strict_types=1 );

namespace StellarWP\Uplink\API\V3\Auth\Contracts;

interface Token_Authorizer {

/**
* Check if a license is authorized.
*
* @see is_authorized()
* @see \StellarWP\Uplink\API\V3\Auth\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;

}
2 changes: 1 addition & 1 deletion src/Uplink/API/V3/Auth/Token_Authorizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* Manages authorization.
*/
final class Token_Authorizer {
class Token_Authorizer implements Contracts\Token_Authorizer {

use With_Debugging;

Expand Down
86 changes: 86 additions & 0 deletions src/Uplink/API/V3/Auth/Token_Authorizer_Cache_Decorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare( strict_types=1 );

namespace StellarWP\Uplink\API\V3\Auth;

use StellarWP\Uplink\API\V3\Provider;
use StellarWP\Uplink\Config;

/**
* Token Authorizer Cache Decorator.
*
* @see Config::set_auth_cache_expiration() to enable.
*/
final class Token_Authorizer_Cache_Decorator implements Contracts\Token_Authorizer {

public const TRANSIENT_PREFIX = 'stellarwp_auth_';

/**
* @var Token_Authorizer
*/
private $authorizer;

/**
* The cache expiration in seconds.
*
* @var int
*/
private $expiration;

/**
* @see Config::set_auth_cache_expiration()
* @see Provider::register_token_authorizer()
*
* @param Token_Authorizer $authorizer The original authorizer.
* @param int $expiration The expiration time in seconds.
*/
public function __construct(
Token_Authorizer $authorizer,
int $expiration = 21600
) {
$this->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<int, string> ...$args
*
* @return string
*/
public function build_transient( array ...$args ): string {
return self::TRANSIENT_PREFIX . hash( 'sha256', json_encode( $args ) );
}

}
32 changes: 32 additions & 0 deletions src/Uplink/API/V3/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}

}
27 changes: 8 additions & 19 deletions src/Uplink/API/Validation_Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand Down Expand Up @@ -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 ) ) {
Expand Down
24 changes: 7 additions & 17 deletions src/Uplink/Admin/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace StellarWP\Uplink\Admin;

use StellarWP\Uplink\Auth\License\License_Manager;
use StellarWP\Uplink\Resources\Collection;
use StellarWP\Uplink\Utils;

Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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() ),
Expand Down
Loading
Loading