Skip to content
Open
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
57 changes: 52 additions & 5 deletions src/Common/Integrations/Harbor/PUE.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ protected function do_register(): void {
add_filter( 'tec_common_pue_pre_validate_key', [ $this, 'filter_tec_common_pue_pre_validate_key' ], 10, 3 );
add_filter( 'tribe_settings_save_field_value', [ $this, 'prevent_storing_unified_license_key' ], 10, 2 );
add_filter( 'tribe_license_fields', [ $this, 'readonly_and_disable_harbor_managed_license_fields' ], 30 );
add_filter( 'stellarwp/uplink/tec/license_field_html', [ $this, 'filter_stellarwp_uplink_tec_license_field_html' ], 10, 2 );
}

/**
Expand All @@ -62,6 +63,7 @@ public function unregister(): void {
remove_filter( 'tec_common_pue_pre_validate_key', [ $this, 'filter_tec_common_pue_pre_validate_key' ] );
remove_filter( 'tribe_settings_save_field_value', [ $this, 'prevent_storing_unified_license_key' ] );
remove_filter( 'tribe_license_fields', [ $this, 'readonly_and_disable_harbor_managed_license_fields' ], 30 );
remove_filter( 'stellarwp/uplink/tec/license_field_html', [ $this, 'filter_stellarwp_uplink_tec_license_field_html' ] );
}

/**
Expand Down Expand Up @@ -105,6 +107,55 @@ public function readonly_and_disable_harbor_managed_license_fields( array $field
return $fields;
}

/**
* Disable Harbor-managed Uplink license inputs (Seating, Event Tickets Plus).
*
* Those fields are rendered as HTML, not as PUE `license_key` fields, so the
* tribe_license_fields attribute lock above cannot reach the text input.
*
* @since TBD
*
* @param string $html Field HTML.
* @param string $slug Plugin slug.
*
* @return string
*/
public function filter_stellarwp_uplink_tec_license_field_html( string $html, string $slug ): string {
if ( ! $this->harbor->is_license_field_managed_by_harbor( $slug ) ) {
return $html;
}

return $this->disable_uplink_license_input( $html );
}

/**
* Add readonly and disabled attributes to the Uplink license text input.
*
* @since TBD
*
* @param string $html Field HTML.
*
* @return string
*/
private function disable_uplink_license_input( string $html ): string {
if ( '' === $html || ! str_contains( $html, 'stellarwp-uplink__settings-field' ) ) {
return $html;
}

$processor = new \WP_HTML_Tag_Processor( $html );

while ( $processor->next_tag( 'input' ) ) {
if ( ! $processor->has_class( 'stellarwp-uplink__settings-field' ) ) {
continue;
}

$processor->set_attribute( 'readonly', 'readonly' );
$processor->set_attribute( 'disabled', 'disabled' );
}

return $processor->get_updated_html();
}

/**
* Short-circuit legacy PUE key validation for Harbor unified licenses.
*
Expand Down Expand Up @@ -134,11 +185,7 @@ public function filter_tec_common_pue_pre_validate_key( ?array $response, string
if ( $this->harbor->is_license_field_managed_by_harbor( $slug ) ) {
return [
'status' => 1,
'message' => sprintf(
/* translators: URL to the Liquid Web License Manager */
__( 'Licensed via <a href="%s" target="_blank">Unified License Manager</a>', 'tribe-common' ),
esc_url( lw_harbor_get_license_page_url() ),
),
'message' => $this->harbor->get_harbor_managed_license_message(),
];
}

Expand Down
228 changes: 214 additions & 14 deletions src/Common/Libraries/Harbor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
use TEC\Common\Integrations\Harbor\EventAggregator;
use TEC\Common\Integrations\Harbor\PUE;
use TEC\Common\Integrations\Harbor\PUE_Resolver;
use TEC\Common\StellarWP\Uplink\API\Validation_Response;
use TEC\Common\StellarWP\Uplink\API\V3\Auth\Contracts\Auth_Url;
use TEC\Common\StellarWP\Uplink\Resources\Plugin as Uplink_Plugin;
use TEC\Common\StellarWP\Uplink\Resources\Resource as Uplink_Resource;
use TEC\Common\Integrations\Uplink\Auth_URL_Decorator;
use Tribe__Dependency as Dependency;
use Tribe__Main as Common;
use function TEC\Common\StellarWP\Uplink\get_plugins;
use function TEC\Common\StellarWP\Uplink\get_resource;
use function lw_harbor_has_unified_license_key;
use function lw_harbor_get_unified_license_key;
use function lw_harbor_is_feature_enabled;
Expand All @@ -24,6 +28,22 @@
/**
* Controller for setting up the Harbor library.
*
* Unified license keys (LWSW-) are handled on two independent validation pipelines.
* This controller is always registered, even when Harbor does not fully load
* (no premium plugin). When Harbor is loaded, Integrations\Harbor\PUE takes over
* the PUE pipeline; this class still owns the Uplink pipeline.
*
* 1. PUE - Tribe__PUE__Checker::validate_key()
* Products such as Events Calendar Pro, Filter Bar, Promoter, Community.
* AJAX action: pue-validate-key_{slug}.
* Filter: tec_common_pue_pre_validate_key (before the remote request).
* This class only handles the Harbor-not-loaded case.
*
* 2. Uplink - StellarWP Uplink Client::validate_license()
* Products: Seating, Event Tickets Plus.
* AJAX action: pue-validate-key-uplink-tec.
* Filter: stellarwp/uplink/tec/client_validate_license (after the remote request).
*
* @since 6.11.0
*
* @package TEC\Common\Libraries\Harbor
Expand Down Expand Up @@ -90,10 +110,16 @@ public function do_register(): void {

add_filter( 'lw-harbor/legacy_licenses', [ $this,'register_legacy_licenses' ] );
add_filter( 'lw_harbor/premium_plugin_exists', [ $this, 'register_premium_plugin_exists' ] );
// Runs even when Harbor does not fully load (no premium plugin), so unified keys
// pasted into free-plugin PUE fields still get a clear guidance message.

// PUE pipeline: Tribe__PUE__Checker. Runs even when Harbor does not fully load
// so a unified key pasted into a free-plugin PUE field still gets a guidance message.
// When Harbor is loaded this callback bails; Integrations\Harbor\PUE owns the filter then.
add_filter( 'tec_common_pue_pre_validate_key', [ $this, 'filter_tec_common_pue_pre_validate_key' ], 10, 3 );

// Uplink pipeline: Client::validate_license() for Seating / Event Tickets Plus.
// Those products never enter Tribe__PUE__Checker. This class always owns this filter.
add_filter( 'stellarwp/uplink/tec/client_validate_license', [ $this, 'filter_stellarwp_uplink_tec_client_validate_license' ], 10, 2 );

Harbor_Provider::init();

// Uplink is being initialized in init with prio 8 - so we want to decorate it with our own decorator later.
Expand All @@ -118,6 +144,7 @@ public function unregister(): void {
remove_filter( 'lw-harbor/legacy_licenses', [ $this,'register_legacy_licenses' ] );
remove_filter( 'lw_harbor/premium_plugin_exists', [ $this, 'register_premium_plugin_exists' ] );
remove_filter( 'tec_common_pue_pre_validate_key', [ $this, 'filter_tec_common_pue_pre_validate_key' ] );
remove_filter( 'stellarwp/uplink/tec/client_validate_license', [ $this, 'filter_stellarwp_uplink_tec_client_validate_license' ] );
remove_action( 'init', [ $this, 'decorate_uplinks_auth_url' ] );
}

Expand All @@ -133,11 +160,13 @@ public function decorate_uplinks_auth_url(): void {
}

/**
* Reject unified license keys when Harbor is not loaded.
* PUE pipeline: reject unified license keys when Harbor is not loaded.
*
* Hook: `tec_common_pue_pre_validate_key` (before Tribe__PUE__Checker hits the remote API).
*
* Without a premium plugin, Harbor never fires `lw_harbor/loaded` and the PUE
* Harbor integration is not registered — so this callback is the only guard that
* stops a unified key from being sent through legacy PUE validation.
* Without a premium plugin, Harbor never fires `lw_harbor/loaded` and
* Integrations\Harbor\PUE is not registered — so this callback only
* prevents a unified key from being sent through legacy PUE validation.
*
* @since TBD
*
Expand All @@ -163,17 +192,173 @@ public function filter_tec_common_pue_pre_validate_key( ?array $response, string

return [
'status' => 0,
'message' => sprintf(
/* translators: %s: My account page link. */
__(
'This is a unified license key. To activate it, install The Events Calendar Pro or Event Tickets Pro, then add your license in the Unified License Manager. You can download the plugin from <a href="%s" target="_blank">your account</a>.',
'tribe-common'
),
esc_url( $this->get_portal_url() )
),
'message' => $this->get_unified_license_key_requires_premium_message(),
];
}

/**
* Uplink pipeline: handle unified license keys validated through StellarWP Uplink.
*
* Hook: `stellarwp/uplink/tec/client_validate_license` (after Client::validate_license()).
*
* Seating and Event Tickets Plus use `pue-validate-key-uplink-tec` and never
* enter Tribe__PUE__Checker::validate_key().
*
* Only the Licenses UI AJAX is rewritten. Uplink also uses this method for
* plugin update checks, which must keep the catalog/Herald payload.
*
* @since TBD
*
* @param Validation_Response $results License validation results.
* @param array $args License validation arguments.
*
* @return Validation_Response
*/
public function filter_stellarwp_uplink_tec_client_validate_license( $results, array $args ) {
if ( ! $results instanceof Validation_Response ) {
return $results;
}

// Uplink also calls validate_license() during plugin update checks.
// Only rewrite the Licenses UI AJAX so catalog/Herald update payloads stay intact.
if ( ! $this->is_uplink_license_field_validation_request() ) {
return $results;
}

$plugin = $args['plugin'] ?? '';
if ( ! is_string( $plugin ) || '' === $plugin ) {
return $results;
}

$key = $args['key'] ?? '';
$key = is_string( $key ) ? $key : '';

if ( $this->is_license_field_managed_by_harbor( $plugin ) ) {
return $this->make_uplink_validation_response(
$key,
$plugin,
true,
$this->get_harbor_managed_license_message()
);
}

if ( ! $this->is_unified_license_key( $key ) ) {
return $results;
}

$message = did_action( 'lw_harbor/loaded' )
? $this->get_unified_license_key_entry_error_message()
: $this->get_unified_license_key_requires_premium_message();

return $this->make_uplink_validation_response( $key, $plugin, false, $message );
}

/**
* Uplink pipeline: whether the current request is license-field AJAX validation.
*
* Distinguishes Licenses UI (`pue-validate-key-uplink-*`) from plugin update checks,
* which share Client::validate_license() and must not be rewritten.
*
* @since TBD
*
* @return bool
*/
private function is_uplink_license_field_validation_request(): bool {
$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

return str_starts_with( $action, 'pue-validate-key-uplink-' );
}

/**
* Uplink pipeline: build a synthetic validation response for the Licenses UI.
*
* Uses the real Uplink resource when it is registered so a valid result is
* not persisted as a "new" per-product key. Falls back to a stub resource
* for message rendering when the collection is unavailable.
*
* @since TBD
*
* @param string $key License key being validated.
* @param string $plugin Uplink plugin slug.
* @param bool $is_valid Whether the synthetic result is valid.
* @param string $message Message shown in the license field (may include HTML).
*
* @return Validation_Response
*/
private function make_uplink_validation_response( string $key, string $plugin, bool $is_valid, string $message ): Validation_Response {
$resource = $this->get_uplink_resource_for_validation( $plugin );

if ( $is_valid ) {
$stored_key = $resource->get_license_key();
if ( is_string( $stored_key ) && '' !== $stored_key ) {
$key = $stored_key;
}
}

$payload = [
'plugin' => $plugin,
'slug' => $plugin,
];

if ( $is_valid ) {
$payload['api_message'] = $message;
} else {
$payload['api_invalid'] = 1;
$payload['api_inline_invalid_message'] = $message;
}

return new Validation_Response( $key, 'local', (object) $payload, $resource );
}

/**
* Uplink pipeline: get the resource used to render a synthetic validation message.
*
* @since TBD
*
* @param string $plugin Uplink plugin slug.
*
* @return Uplink_Resource
*/
private function get_uplink_resource_for_validation( string $plugin ): Uplink_Resource {
try {
if ( function_exists( '\TEC\Common\StellarWP\Uplink\get_resource' ) ) {
$resource = get_resource( $plugin );
if ( $resource instanceof Uplink_Resource ) {
return $resource;
}
}
} catch ( \Throwable $exception ) {
// Uplink collection is not ready; a stub is enough for message rendering.
unset( $exception );
}

return new Uplink_Plugin(
$plugin,
$plugin,
'0.0.0',
$plugin . '.php',
\stdClass::class
);
}

/**
* Guidance shown when a unified key is entered but Harbor cannot load yet.
*
* @since TBD
*
* @return string
*/
public function get_unified_license_key_requires_premium_message(): string {
return sprintf(
/* translators: %s: My account page link. */
__(
'This is a unified license key. To activate it, install The Events Calendar Pro or Event Tickets Plus, then add your license in the Unified License Manager. You can download the plugin from <a href="%s" target="_blank">your account</a>.',
'tribe-common'
),
esc_url( $this->get_portal_url() )
);
}

/**
* Get the premium plugin existence callbacks.
*
Expand Down Expand Up @@ -376,6 +561,21 @@ public function get_unified_license_key_entry_error_message(): string {
);
}

/**
* Success message shown on Harbor-managed per-product license fields.
*
* @since TBD
*
* @return string
*/
public function get_harbor_managed_license_message(): string {
return sprintf(
/* translators: URL to the Liquid Web License Manager */
__( 'Licensed via <a href="%s" target="_blank">Unified License Manager</a>', 'tribe-common' ),
esc_url( lw_harbor_get_license_page_url() )
);
}

/**
* Get the unified license key if the feature is enabled.
*
Expand Down
Loading
Loading