Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions docs/guides/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ See [Section 2](#2-bundling-a-license-key). Bundling a key is done entirely thro
| Function | Signature | Purpose |
| ---------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `lw_harbor_is_product_license_active` | `(string $slug): bool` | Check if a specific product slug has an active license. |
| `lw_harbor_is_capability_license_available` | `(string $slug): bool` | Check if the unified key includes a capability, regardless of domain activation. |
| `lw_harbor_is_capability_license_active` | `(string $slug): bool` | Check if a capability is valid and activated on the current domain. |
| `lw_harbor_has_unified_license_key` | `(): bool` | Check if a unified key is stored locally (no remote call). |
| `lw_harbor_get_unified_license_key` | `(): ?string` | Retrieve the stored unified license key. |
| `lw_harbor_is_feature_enabled` | `(string $slug): bool` | Check if a feature is currently active locally on this site. |
Expand Down
28 changes: 28 additions & 0 deletions src/Harbor/API/Functions/Global_Function_Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,34 @@ static function ( string $product ): bool {
}
);

\_lw_harbor_global_function_registry(
'lw_harbor_is_capability_license_available',
$version,
static function ( string $capability_slug ): bool {
try {
return Config::get_container()->get( License_Repository::class )->has_capability( $capability_slug );
} catch ( Throwable $e ) {
self::debug_log_throwable( $e, 'Error checking capability license' );

return false;
}
}
);

\_lw_harbor_global_function_registry(
'lw_harbor_is_capability_license_active',
$version,
static function ( string $capability_slug ): bool {
try {
return Config::get_container()->get( License_Repository::class )->is_capability_active( $capability_slug );
} catch ( Throwable $e ) {
self::debug_log_throwable( $e, 'Error checking active capability license' );

return false;
}
}
);

\_lw_harbor_global_function_registry(
'lw_harbor_is_feature_enabled',
$version,
Expand Down
96 changes: 96 additions & 0 deletions src/Harbor/Licensing/Repositories/License_Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace LiquidWeb\Harbor\Licensing\Repositories;

use LiquidWeb\Harbor\Licensing\Enums\Validation_Status;
use LiquidWeb\Harbor\Licensing\Product_Collection;
use LiquidWeb\Harbor\Licensing\Validation_State;
use LiquidWeb\Harbor\Utils\Sanitize;
Expand Down Expand Up @@ -458,6 +459,70 @@ public function is_product_valid( string $slug ): bool {
return false;
}

/**
* Whether the unified license includes a capability, regardless of domain activation.
*
* Returns true when at least one cached product entry lists the capability and
* the entry is not in a status that indicates the license does not cover it
* (e.g. expired, no entitlement).
*
* @since TBD
*
* @param string $capability_slug The capability slug to check.
*
* @return bool
*/
public function has_capability( string $capability_slug ): bool {
$products = $this->get_products();

if ( ! $products instanceof Product_Collection ) {
return false;
}

foreach ( $products as $entry ) {
if (
$this->is_capability_covered_by_license( $entry->get_validation_status() )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we used is_valid() here, we'd get false for entries like:

  • not_activated
  • activation_required
  • out_of_activations
  • tier_selection_required

Those are not valid, but they are covered by the license. That's why I had to create is_capability_covered_by_license function.

&& in_array( $capability_slug, $entry->get_capabilities(), true )
) {
return true;
}
}

return false;
}

/**
* Whether a capability is active on the current domain under the unified license.
*
* Returns true when at least one cached product entry is valid, activated on
* this domain, and lists the capability.
*
* @since TBD
*
* @param string $capability_slug The capability slug to check.
*
* @return bool
*/
public function is_capability_active( string $capability_slug ): bool {
$products = $this->get_products();

if ( ! $products instanceof Product_Collection ) {
return false;
}

foreach ( $products as $entry ) {
if (
$entry->is_valid()
&& $entry->get_activated_here()
&& in_array( $capability_slug, $entry->get_capabilities(), true )
) {
return true;
}
}

return false;
}

/**
* Whether a product has a valid, active license.
*
Expand Down Expand Up @@ -559,6 +624,37 @@ private function is_in_grace_period( string $slug ): bool {
return $current_time <= $last_active + $this->get_grace_period_in_seconds();
}

/**
* Whether a validation status indicates the license covers the product.
*
* Excludes statuses where the unified key does not entitle the product
* (expired, suspended, no entitlement, etc.). Includes not_activated and
* similar states where the product is on the license but not active here.
*
* @since TBD
*
* @param string|null $status A Validation_Status constant value, or null.
*
* @return bool
*/
private function is_capability_covered_by_license( ?string $status ): bool {
if ( $status === null ) {
return false;
}

$not_covered = [
Validation_Status::EXPIRED,
Validation_Status::SUSPENDED,
Validation_Status::CANCELLED,
Validation_Status::LICENSE_SUSPENDED,
Validation_Status::LICENSE_BANNED,
Validation_Status::NO_ENTITLEMENT,
Validation_Status::INVALID_KEY,
];

return ! in_array( $status, $not_covered, true );
}

/**
* Get the cached WP_Error from the most recent failed validation attempt
* for the given license key if it is still within the supplied TTL.
Expand Down
41 changes: 41 additions & 0 deletions src/Harbor/global-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,47 @@ function lw_harbor_refresh_catalog(): bool {
}
}

if ( ! function_exists( 'lw_harbor_is_capability_license_available' ) ) {
/**
* Whether the unified license includes a capability, regardless of domain activation.
*
* Does not make any remote API calls — only checks the locally cached
* product catalog.
*
* @since TBD
*
* @param string $capability_slug The capability slug to check.
*
* @return bool
*/
function lw_harbor_is_capability_license_available( string $capability_slug ): bool {
$callback = _lw_harbor_global_function_registry( 'lw_harbor_is_capability_license_available' );

return $callback ? (bool) $callback( $capability_slug ) : false;
}
}

if ( ! function_exists( 'lw_harbor_is_capability_license_active' ) ) {
/**
* Whether a capability is active on the current domain under the unified license.
*
* Returns true when the capability is covered by the unified key and the
* product is valid and activated on this domain. Does not make any remote
* API calls — only checks the locally cached product catalog.
*
* @since TBD
*
* @param string $capability_slug The capability slug to check.
*
* @return bool
*/
function lw_harbor_is_capability_license_active( string $capability_slug ): bool {
$callback = _lw_harbor_global_function_registry( 'lw_harbor_is_capability_license_active' );

return $callback ? (bool) $callback( $capability_slug ) : false;
}
}

if ( ! function_exists( 'lw_harbor_display_legacy_license_page_notice' ) ) {
/**
* Displays an informational admin notice on legacy plugin license pages.
Expand Down
165 changes: 165 additions & 0 deletions tests/wpunit/API/Functions/GlobalFunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,171 @@ public function test_is_product_license_active_returns_false_for_unknown_product
$this->assertFalse( lw_harbor_is_product_license_active( 'learndash' ) );
}

// -------------------------------------------------------------------------
// lw_harbor_is_capability_license_available()
// -------------------------------------------------------------------------

public function test_is_capability_license_available_returns_false_without_cached_products(): void {
$this->assertFalse( lw_harbor_is_capability_license_available( 'give' ) );
}

public function test_is_capability_license_available_returns_true_for_valid_capability(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'valid',
'activated_here' => true,
'capabilities' => [ 'give', 'give-recurring' ],
],
]
);

$this->assertTrue( lw_harbor_is_capability_license_available( 'give-recurring' ) );
}

public function test_is_capability_license_available_returns_true_when_not_activated_on_domain(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'not_activated',
'activated_here' => false,
'capabilities' => [ 'give', 'give-recurring' ],
],
]
);

$this->assertTrue( lw_harbor_is_capability_license_available( 'give-recurring' ) );
}

public function test_is_capability_license_available_returns_false_for_expired_product(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2020-12-31 23:59:59',
'validation_status' => 'expired',
'activated_here' => false,
'capabilities' => [ 'give', 'give-recurring' ],
],
]
);

$this->assertFalse( lw_harbor_is_capability_license_available( 'give-recurring' ) );
}

public function test_is_capability_license_available_returns_false_for_unknown_capability(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'valid',
'activated_here' => true,
'capabilities' => [ 'give' ],
],
]
);

$this->assertFalse( lw_harbor_is_capability_license_available( 'give-recurring' ) );
}

// -------------------------------------------------------------------------
// lw_harbor_is_capability_license_active()
// -------------------------------------------------------------------------

public function test_is_capability_license_active_returns_false_without_cached_products(): void {
$this->assertFalse( lw_harbor_is_capability_license_active( 'give' ) );
}

public function test_is_capability_license_active_returns_true_for_valid_activated_capability(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'valid',
'activated_here' => true,
'capabilities' => [ 'give', 'give-recurring' ],
],
]
);

$this->assertTrue( lw_harbor_is_capability_license_active( 'give-recurring' ) );
}

public function test_is_capability_license_active_returns_false_when_not_activated_on_domain(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'not_activated',
'activated_here' => false,
'capabilities' => [ 'give', 'give-recurring' ],
],
]
);

$this->assertFalse( lw_harbor_is_capability_license_active( 'give-recurring' ) );
}

public function test_is_capability_license_active_returns_false_for_unknown_capability(): void {
$this->seed_capability_products(
[
[
'product_slug' => 'give',
'tier' => 'pro',
'status' => 'active',
'expires' => '2030-12-31 23:59:59',
'validation_status' => 'valid',
'activated_here' => true,
'capabilities' => [ 'give' ],
],
]
);

$this->assertFalse( lw_harbor_is_capability_license_active( 'give-recurring' ) );
}

/**
* @param array<int, array<string, mixed>> $products
*/
private function seed_capability_products( array $products ): void {
$entries = array_map(
static function ( array $product ): Product_Entry {
return Product_Entry::from_array( $product );
},
$products
);

$collection = Product_Collection::from_array( $entries );

update_option(
License_Repository::PRODUCTS_STATE_OPTION_NAME,
[
'collection' => $collection->to_array(),
'last_success_at' => null,
'last_error' => null,
]
);
}

// -------------------------------------------------------------------------
// lw_harbor_is_feature_enabled() / lw_harbor_is_feature_available()
// -------------------------------------------------------------------------
Expand Down
Loading
Loading