client = &$client; $this->option_key = $this->client->getHookName( 'manage_license' ); $this->data_key = $this->client->getHookName( 'license' ); $this->schedule_hook = $this->client->getHookName( 'license_check_event' ); $this->redirect_on_activation = $redirect_on_activation; // Load the license. $this->get_license(); } public function set_page_url( $url ): SE_License_SDK_License { $this->page_url = esc_url_raw( $url ); return $this; } public function get_page_url(): string { if ( null !== $this->page_url ) { return $this->page_url; } return admin_url( 'admin.php?page=' . $this->menu_args['menu_slug'] ); } public function set_header_message( string $message = null ): SE_License_SDK_License { $this->header_message = $message; return $this; } public function set_header_content( string $message = null ): SE_License_SDK_License { $this->header_content = $message; return $this; } public function set_manage_license_url( string $url = null ): SE_License_SDK_License { $this->manage_license_url = $url; return $this; } /** * The store/account dashboard URL where the customer manages/downloads their * product (set from the `store_dashboard_url` init arg), or null. * * @return ?string */ public function get_manage_license_url(): ?string { return $this->manage_license_url ?: null; } public function use_custom_style(): SE_License_SDK_License { $this->use_custom_style = true; return $this; } private function updating_license( bool $status ): void { // Set initial flag. $this->updating_license = $status; if ( $status ) { set_transient( $this->option_key . '_is_updating_license', 'yes', 20 ); } else { delete_transient( $this->option_key . '_is_updating_license' ); } // Method Chain. } private function is_updating_license(): bool { return $this->updating_license || 'yes' === get_transient( $this->option_key . '_is_updating_license' ); } /** * Initialize License. * * @return void */ public function init() { add_action( 'init', [ $this, 'handle_license_page_form' ] ); if ( null === $this->menu_args ) { $this->set_menu_args(); } // Run hook to check license status daily. add_action( $this->schedule_hook, [ $this, 'check_license_status' ] ); $this->userCapability = $this->menu_args['capability']; $this->is_license_page = isset( $_GET['page'] ) && $_GET['page'] === $this->menu_args['menu_slug']; // phpcs:ignore if ( $this->client->isPlugin() ) { add_action( 'plugin_action_links_' . $this->client->getBasename(), [ $this, 'plugin_action_links' ] ); } add_action( 'admin_notices', [ $this, '__admin_notices' ] ); if ( $this->client->is_network_activated() ) { add_action( 'network_admin_notices', [ $this, '__admin_notices' ] ); } add_action( 'admin_init', [ $this, 'maybe_dismiss_renewal_notice' ] ); // Activation/Deactivation hooks. $this->activation_deactivation(); // Background re-verification of a license whose signature couldn't be checked. add_action( $this->client->getHookName( 'license_recheck' ), [ $this, 'check_license_status' ] ); // Check the validity and save the state. (check after cron scheduled) $this->is_valid(); // Self-heal: an active license must have its daily check scheduled. // Runs after is_valid() so an old-format signature (which included the // event's timestamp) is migrated before the event is re-created. if ( ! empty( $this->license['license'] ) && 'active' === ( $this->license['status'] ?? '' ) && ! wp_next_scheduled( $this->schedule_hook ) ) { $this->schedule_license_check(); } // Set Did Init Flag $this->did_init = true; } /** * Expose the License Key. * * @return string */ public function get_key(): string { $this->get_license(); return $this->license['license'] ?? ''; } /** * Display Admin Notices. * * @return void */ public function __admin_notices() { if ( ! current_user_can( $this->userCapability ) ) { return; } if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL === true ) { $host = wp_parse_url( $this->client->getLicenseserver(), PHP_URL_HOST ); if ( ! defined( 'WP_ACCESSIBLE_HOSTS' ) || ( defined( 'WP_ACCESSIBLE_HOSTS' ) && false === stristr( WP_ACCESSIBLE_HOSTS, $host ) ) ) { ?>

' . esc_html__( 'Warning!', 'storeengine-sdk' ) . '', esc_html( $this->client->getPackageName() ), '' . esc_html( $host ) . '', 'WP_ACCESSIBLE_HOSTS' ); ?>

inactive_license_notice(); $this->renewal_notice(); if ( ! empty( $this->error ) ) { ?>
error ) ); ?>
success ) ) { ?>
success ) ); ?>
is_license_page && ! $this->is_valid() && ! $this->is_updating_license() ) { ?>

' . esc_attr( $this->client->getPackageName() ) . '', '' . esc_html__( 'Click here', 'storeengine-sdk' ) . '', '' . esc_attr( $this->client->getPackageName() ) . '' ); ?>

client->getHookName( 'renewal_notice_days' ), 14 ) ); } /** * Transient key snoozing the renewal nag for this product. */ protected function renewal_snooze_key(): string { return 'se_sdk_renew_snooze_' . md5( $this->client->getSlug() ); } /** * Nag the admin when an active license is expiring soon or has expired, with * a renewal link. Skipped on the license page itself (they manage it there), * for unlimited/no-expiry licenses, and while snoozed. Shown site-wide so a * lapse is noticed before Pro features quietly stop. * * @return void */ public function renewal_notice() { if ( $this->is_license_page || $this->client->isFree() ) { return; } $license = $this->get_license(); $expires = ! empty( $license['expires'] ) ? (int) $license['expires'] : 0; // No expiry to warn about (lifetime/unlimited or unknown). if ( ! $expires || ! empty( $license['unlimited'] ) ) { return; } // Only nag for a license the user has actually engaged with (has a key). if ( empty( $license['license'] ) ) { return; } $now = current_time( 'timestamp', true ); $days_left = (int) floor( ( $expires - $now ) / DAY_IN_SECONDS ); $expired = $expires <= $now; // Not in the warning window yet. if ( ! $expired && $days_left > $this->renewal_notice_window() ) { return; } // Snoozed for this exact expiry value? if ( (string) get_transient( $this->renewal_snooze_key() ) === (string) $expires ) { return; } $renew_url = $this->manage_license_url ?: $this->client->get_purchase_url(); $dismiss = wp_nonce_url( add_query_arg( 'se_sdk_dismiss_renewal', rawurlencode( $this->client->getSlug() ) ), 'se_sdk_dismiss_renewal_' . $this->client->getSlug() ); if ( $expired ) { $message = sprintf( /* translators: %s: product name. */ esc_html__( 'Your %s license has expired. Renew it to keep receiving automatic updates and support.', 'storeengine-sdk' ), '' . esc_html( $this->client->getPackageName() ) . '' ); $class = 'notice-error'; } else { $message = sprintf( /* translators: 1: product name, 2: human time diff e.g. "6 days". */ esc_html__( 'Your %1$s license expires in %2$s. Renew now to avoid losing automatic updates and support.', 'storeengine-sdk' ), '' . esc_html( $this->client->getPackageName() ) . '', '' . esc_html( human_time_diff( $now, $expires ) ) . '' ); $class = 'notice-warning'; } ?>

   

client->getSlug() ) { return; } if ( ! current_user_can( $this->userCapability ?: 'manage_options' ) ) { return; } check_admin_referer( 'se_sdk_dismiss_renewal_' . $this->client->getSlug() ); $expires = (int) ( $this->get_license()['expires'] ?? 0 ); // Snooze for this expiry value for a week; re-nags after that or when the // expiry changes (e.g. a partial renewal). set_transient( $this->renewal_snooze_key(), (string) $expires, WEEK_IN_SECONDS ); wp_safe_redirect( remove_query_arg( [ 'se_sdk_dismiss_renewal', '_wpnonce' ] ) ); exit; } /** * Setup plugin action link to the license page. * * @param array $links plugin action links. * * @return array */ public function plugin_action_links( array $links ): array { $age_url = $this->get_page_url(); if ( ! empty( $age_url ) ) { $label = $this->is_valid() ? __( 'Manage License', 'storeengine-sdk' ) : __( 'Activate License', 'storeengine-sdk' ); array_unshift( $links, '' . esc_html( $label ) . '' ); } return $links; } /** * Check license. * * @return array */ public function check(): array { return $this->request( 'status', $this->license ); } /** * Get Plugin data. * * @return array { * Plugin Information * @type bool $success API response status * @type string $api_call_execution_time API Man (Rest Response) Execution Time * @type array $data { * Plugin Data ( API Man.) * @type array $package { * @type int $product_id API Man Product ID * } * @type array $info { * @type string $id Plugin Id * @type string $name Plugin Name * @type string $author Author Name * @type string $author_profile Author Profile URL * @type string $slug Plugin Slug * @type string $plugin Plugin main file path * @type string $new_version New Version String * @type string $url Plugin URL * @type string $package Plugin update download URL * @type string $icons Plugin Icons * @type string $banners Plugin Banners * @type string $banner_rtl RTL Version of Plugin Banners * @type string $upgrade_notice Upgrade Notice * @type string $requires Minimum WordPress Version * @type string $requires_php Minimum PHP Version * @type string $tested Tested upto WordPress Version * @type array $compatibility Compatibility information (API Man sends string) * @type array $contributors Plugin Contributors List (if available) * @type array $ratings Plugin Rating (if available) * @type float $num_ratings Plugin Rating (if available) * @type string $last_updated Last updated Date * @type string $homepage Plugin Home Page URL * @type array $sections { * Plugin Description Sections * @type string $description Plugin Description * @type string $changelog Change LOG * } * @type mixed $author_block_count * @type mixed $author_block_rating * } * } * } * * @deprecated kept for documentation. */ public function get_information(): array { return $this->request( 'information', $this->license ); } /** * Active a license. * * @param array $license license data. * * @return array */ public function activate( #[\SensitiveParameter] array $license ): array { return $this->request( 'activate', $license ); } /** * Deactivate current license. * * @return array */ public function deactivate(): array { return $this->request( 'deactivate', $this->license ); } /** * Send common request. * * @param string $action request action. * @param array $license license data. * * @return array */ protected function request( string $action, #[\SensitiveParameter] array $license = [] ): array { $actions = [ 'activate' => 'activate-license', 'deactivate' => 'deactivate-license', 'status' => 'check-license', 'information' => 'package-info', 'update' => 'check-update', ]; if ( ! in_array( $action, array_keys( $actions ) ) ) { return [ 'success' => false, 'error' => __( 'Invalid Request Action.', 'storeengine-sdk' ), ]; } // parse license data $license = wp_parse_args( $license, $this->get_license() ); // validate license data. if ( ! $this->validate_license_data( $license ) ) { return [ 'success' => false, 'error' => __( 'Invalid/Empty License Data.', 'storeengine-sdk' ), ]; } return $this->client->request( [ 'body' => array_merge( $license, $this->client->get_admin_info() ), 'route' => $actions[ $action ] ] ); } public function set_menu_args( $args = [] ): SE_License_SDK_License { $this->menu_args = wp_parse_args( $args, [ 'type' => 'submenu', // Can be: menu, options, submenu. 'menu_title' => $this->client->getPackageName(), 'page_title' => sprintf( /* translators: 1. Theme/Plugin Name. */ esc_html__( '%s License Management', 'storeengine-sdk' ), esc_html( $this->client->getPackageName() ) ), 'capability' => $this->client->is_network_activated() ? 'manage_network_options' : 'manage_options', 'menu_slug' => 'manage-' . $this->client->getSlug() . '-license', 'icon_url' => 'dashicons-admin-network', 'position' => null, // Themes have no Settings context by convention — put their // license screen under Appearance; plugins under Settings. 'parent_slug' => $this->client->isTheme() ? 'themes.php' : 'options-general.php', ] ); return $this; } /** * Add settings page for license. * * @param array $args settings for rendering the menu. * * @return $this */ public function add_settings_page(): SE_License_SDK_License { if ( $this->did_init ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: 1. Class Method. */ __( '%s Should be called before License::init()', 'storeengine-sdk' ), '' . __METHOD__ . '' ), '1.0.0' ); return $this; } if ( ! in_array( $this->menu_args['type'], [ 'menu', 'options', 'submenu' ], true ) ) { $this->menu_args['type'] = $this->menu_args['parent_slug'] ? 'submenu' : 'menu'; } if ( 'submenu' === $this->menu_args['type'] && ! $this->menu_args['parent_slug'] ) { $this->menu_args['type'] = 'options'; } if ( 'menu' === $this->menu_args['type'] && $this->menu_args['parent_slug'] ) { $this->menu_args['type'] = 'submenu'; } // Network-activated products manage their (network-wide) license from the // Network Admin; everything else from the site admin. $menu_hook = $this->client->is_network_activated() ? 'network_admin_menu' : 'admin_menu'; add_action( $menu_hook, [ $this, 'register_admin_menu' ], 999 ); return $this; } /** * Admin Menu hook. * * @return void */ public function register_admin_menu() { switch ( $this->menu_args['type'] ) { case 'submenu': $this->add_submenu_page(); break; case 'options': $this->add_options_page(); break; case 'menu': default: $this->add_menu_page(); break; } } /** * License menu output. * * @return void */ public function render_menu_page() { $mount_id = 'se-sdk-license-app-' . sanitize_html_class( $this->client->getSlug() ); $this->enqueue_license_app( $mount_id ); ?>

client->getPackageName() ) ); ?>


client->getHookName( 'disable_react_panel' ), false ); if ( ! $react_disabled && $this->client->maybe_init_restapi() ) : ?>
render_license_page(); endif; ?>
client->getSlug() ); wp_enqueue_style( $handle, SE_License_SDK::sdk_url( 'static/license-app.css' ), [], $this->client->getVersion() ); wp_enqueue_script( $handle, SE_License_SDK::sdk_url( 'static/license-app.js' ), [ 'wp-element', 'wp-i18n' ], $this->client->getVersion(), true ); // Wire JS __()/sprintf strings to translations shipped in the SDK's // languages/ folder (storeengine-sdk-{locale}-{handle-md5}.json). if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( $handle, 'storeengine-sdk', SE_License_SDK::sdk_path( 'languages' ) ); } $is_free = $this->client->isFree(); // Surface only the bits the React app actually needs — never echo // raw license data; the JS calls /license/status itself. $config = [ 'mountId' => $mount_id, 'slug' => $this->client->getSlug(), 'packageName' => $this->client->getPackageName(), 'isFree' => $is_free, 'restUrl' => trailingslashit( rest_url( 'storeengine-sdk/v1/' . $this->client->getSlug() ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'initialLicense' => $is_free ? null : $this->get_public_data(), 'storeDashboardUrl' => $this->manage_license_url ?: null, 'purchaseUrl' => $this->client->get_purchase_url() ?: null, // Manual-install fallback shown when an automatic update fails: where // to download the package (account dashboard) and where to upload it. 'packageType' => $this->client->getType(), 'uploadUrl' => $this->client->isPlugin() ? self_admin_url( 'plugin-install.php?tab=upload' ) : self_admin_url( 'theme-install.php?upload' ), ]; // Per-instance global so multiple SDK consumers on the same page // (e.g. plugin + addon) don't clobber each other's config. $var_name = 'seSdkLicenseAppConfig_' . preg_replace( '/[^A-Za-z0-9_]/', '_', $this->client->getSlug() ); wp_add_inline_script( $handle, sprintf( 'window.%s = %s;', $var_name, wp_json_encode( $config ) ), 'before' ); } public function render_license_page() { $this->licenses_style(); $status = isset( $this->license['status'] ) && 'active' === $this->license['status'] ? 'active' : 'inactive'; $action = 'active' === $status ? 'deactivate' : 'activate'; $submit_label = 'activate' === $action ? __( 'Activate License', 'storeengine-sdk' ) : __( 'Deactivate License', 'storeengine-sdk' ); $status_label = 'active' === $status ? __( 'Active', 'storeengine-sdk' ) : __( 'Inactive', 'storeengine-sdk' ); $isUnlimited = (bool) ( $this->license['unlimited'] ?? false ); $remaining = absint( $this->license['remaining'] ?? 0 ); if ( ! $this->header_message ) { $this->header_message = sprintf( /* translators: %s: Plugin Name */ esc_html__( 'Active %s license to get professional support and automatic update from your WordPress dashboard.', 'storeengine-sdk' ), '' . esc_html( $this->client->getPackageName() ) . '' ); } if ( ! $this->header_content ) { $this->header_content = sprintf( /* translators: %s: Plugin Name */ esc_html__( 'Activate %s to unlock automatic updates, priority support, and all tools to optimize your WordPress store.', 'storeengine-sdk' ), '' . esc_html( $this->client->getPackageName() ) . '' ); } do_action( $this->client->getHookName( 'before_license_section' ), $action ); include __DIR__ . '/../views/license-form.php'; do_action( $this->client->getHookName( 'after_license_section' ) ); } /** * License form submit. * * @return void */ public function handle_license_page_form() { $check_key = $this->client->getSlug() . '-check-license'; if ( isset( $_GET[ $check_key ] ) && wp_verify_nonce( sanitize_text_field( $_GET[ $check_key ] ), $this->client->getSlug() ) ) { $this->client->interactive( [ $this, 'check_license_status' ] ); wp_safe_redirect( $this->get_page_url() . '#' . $this->client->getSlug() . '-license-form' ); die(); } $action = sanitize_text_field( wp_unslash( $_POST[ $this->data_key ]['_action'] ?? '' ) ); if ( $action ) { check_admin_referer( $this->data_key ); switch ( $action ) { case 'activate': $this->activate_client_license( array_map( 'sanitize_text_field', $_POST[ $this->data_key ] ) ); do_action( $this->client->getHookName( 'license-activate' ) ); break; case 'deactivate': $this->deactivate_client_license(); do_action( $this->client->getHookName( 'license-deactivate' ) ); break; default: break; } } } /** * Check license status on schedule. * Check and update license status on db. * * @return void */ public function check_license_status() { $license = $this->get_license(); if ( empty( $license['license'] ) || 'inactive' === $license['status'] ) { $this->clear_license_check_schedule(); return; } $this->updating_license( true ); $response = $this->check(); // --- Authoritative server verdict ------------------------------------ // The server answered (even "inactive" comes back as success=true with // status=inactive). Apply it verbatim and clear any grace bookkeeping. if ( isset( $response['success'] ) && $response['success'] ) { $was_active = 'active' === ( $license['status'] ?? '' ); $license = wp_parse_args( $response['data'], $license ); $this->set_grace_state( [ 'last_verified_at' => time() ] ); $this->set_license( $license ); // Lifecycle hook: license transitioned active → inactive/expired. if ( $was_active && 'active' !== ( $license['status'] ?? '' ) ) { $this->emit_license_event( 'license_deactivated', $license ); } $this->updating_license( false ); return; } // --- Server unreachable → grace period -------------------------------- // A transport-level failure (DNS/timeout/TLS/blocked request/5xx) is NOT // a verdict. Keep the last-known-good license active until the grace // window since the last successful verification has elapsed. Prevents a // brief outage from deactivating a paying customer's product. if ( ! empty( $response['transport_error'] ) ) { $grace = $this->client->getLicenseGracePeriod(); $state = $this->get_grace_state(); if ( empty( $state['grace_started_at'] ) ) { $state['grace_started_at'] = time(); } $state['last_error'] = $response['error'] ?? ''; $anchor = ! empty( $state['last_verified_at'] ) ? (int) $state['last_verified_at'] : (int) $state['grace_started_at']; $expired = ( 0 === $grace ) || ( ( time() - $anchor ) > $grace ); if ( $expired ) { // Grace exhausted — fall through to the deactivation path. $this->set_grace_state( [] ); $this->error = $response['error'] ?: __( 'Could not verify your license and the grace period has ended.', 'storeengine-sdk' ); $this->deactivate_local_license( $license ); $this->emit_license_event( 'license_grace_expired', $license ); } else { // Still within grace — keep the product working, re-sign so the // license stays valid, and record when grace started. $state['in_grace'] = true; $this->set_grace_state( $state ); $this->set_license( $license ); $this->emit_license_event( 'license_check_deferred', $license ); } $this->updating_license( false ); return; } // --- Definitive rejection -------------------------------------------- // The server responded with a genuine business error (e.g. license // revoked). Deactivate locally but keep the key so a later renewal can // reactivate it. $this->error = $response['error'] ?: __( 'Unknown error occurred.', 'storeengine-sdk' ); $this->set_grace_state( [] ); $this->deactivate_local_license( $license ); $this->emit_license_event( 'license_deactivated', $license ); $this->updating_license( false ); } /** * Mark the stored license inactive locally without touching the key, so a * later successful check (e.g. after renewal) can reactivate it. * * @param array $license Current license array. */ protected function deactivate_local_license( array $license ) { // `license` (the key) and `expires` are deliberately preserved: this // method is the "server said no" / "grace ran out" path, not a user // asking to unlink the site, and the caller's contract is that a later // renewal can reactivate without the user digging out their key again. // // This used to pass the overrides below as wp_parse_args()' FIRST // argument, i.e. as `$args` rather than `$defaults`, so they won every // key — including `'license' => ''` and `'expires' => ''`. The stored key // was destroyed on every expiry, revocation and grace timeout, which is // the exact opposite of what both this docblock and the call sites say. $license = array_merge( $license, [ 'status' => 'inactive', 'device_id' => $this->client->get_device_id(), 'slug' => $this->client->getSlug(), 'product_id' => $this->client->getProductId(), 'remaining' => 0, 'activations' => 0, 'limit' => 0, 'unlimited' => false, ] ); $this->set_license( $license ); } /** * Grace-period bookkeeping (stored separately from license_data so it never * fights parse_license_data's whitelist). * * @return array{last_verified_at?:int, grace_started_at?:int, last_error?:string, in_grace?:bool} */ public function get_grace_state(): array { $state = $this->client->get_option( 'license_verify_state', [] ); return is_array( $state ) ? $state : []; } /** * Persist grace-period bookkeeping. Passing [] clears it. * * @param array $state New state (merged onto the existing one; [] resets). */ public function set_grace_state( array $state ) { if ( empty( $state ) ) { $this->client->set_option( 'license_verify_state', [] ); return; } $this->client->set_option( 'license_verify_state', wp_parse_args( $state, $this->get_grace_state() ) ); } /** * Whether the license is currently being honoured under the offline grace * period (server unreachable, last-known-good still active). */ public function is_in_grace(): bool { $state = $this->get_grace_state(); return ! empty( $state['in_grace'] ); } /** * Fire a documented license lifecycle event on this product's hook * namespace so consuming plugins/themes can react. * * @param string $event Event slug (e.g. 'license_activated'). * @param array $license License data at the time of the event. */ protected function emit_license_event( string $event, array $license = [] ) { /** * Fires on a license lifecycle transition for this product. * * Hook name: "{product-hook-prefix}_{$event}". Events: * - license_activated License successfully activated. * - license_deactivated License deactivated (by user or server verdict). * - license_grace_expired Offline grace ended; license failed closed. * - license_check_deferred Scheduled check skipped (server unreachable, within grace). * * @param array $license The license data. * @param SE_License_SDK_License $this The license handler. */ $this->client->do_action( $event, $license, $this ); } /** * Check this is a valid license. * * @return bool */ public function is_valid(): bool { if ( null !== $this->is_valid_license ) { return $this->is_valid_license; } // Load the license if already not loaded. $license = $this->get_license(); if ( isset( $license['license'], $license['device_id'], $license['product_id'], $license['status'] ) && 'active' === $license['status'] ) { $this->is_valid_license = $this->validate_license_signature(); if ( ! $this->is_valid_license ) { $this->maybe_schedule_signature_recheck(); } } else { $this->is_valid_license = false; } return $this->is_valid_license; } /** * Validate license data for request. * * @param array $license license data. * * @return bool */ public function validate_license_data( #[\SensitiveParameter] array $license = [] ): bool { $license = $this->parse_license_data( $license ); return ( ! empty( $license['license'] ) && ! empty( $license['device_id'] ) && // ! empty( $license['activation_id'] ) && ! empty( $license['slug'] ) && ! empty( $license['product_id'] ) ); } /** * Styles for licenses page. * * @return void */ private function licenses_style() { ?> license['updated_at'] ) { return; } $time_diff = $this->license['updated_at'] - current_time( 'timestamp', true ); if ( absint( $time_diff ) < WEEK_IN_SECONDS ) { // translators: placeholder is human time diff (e.g. "3 weeks") printf( __( '%s ago', 'storeengine-sdk' ), human_time_diff( current_time( 'timestamp', true ), $this->license['updated_at'] ) ); return; } echo esc_html( get_date_from_gmt( '@' . $this->license['updated_at'], 'Y-m-d h:i A (P)' ) ); } protected function render_license_expire_datetime() { if ( 'active' !== $this->license['status'] ) { $expires = __( 'N/A', 'storeengine-sdk' ); $expiring = true; } elseif ( empty( $this->license['expires'] ) ) { $expires = __( 'N/A', 'storeengine-sdk' ); $expiring = false; } else { $time_diff = $this->license['expires'] - current_time( 'timestamp', true ); if ( $time_diff > 0 && $time_diff < WEEK_IN_SECONDS ) { // translators: placeholder is human time diff (e.g. "3 weeks") $expires = sprintf( __( 'In %s', 'storeengine-sdk' ), human_time_diff( current_time( 'timestamp', true ), $this->license['expires'] ) ); $expiring = true; } elseif ( $time_diff < 0 && absint( $time_diff ) < WEEK_IN_SECONDS ) { // translators: placeholder is human time diff (e.g. "3 weeks") $expires = sprintf( __( '%s ago', 'storeengine-sdk' ), human_time_diff( current_time( 'timestamp', true ), $this->license['expires'] ) ); $expiring = true; } else { $expires = get_date_from_gmt( '@' . $this->license['expires'], 'Y-m-d h:i A (P)' ); $expiring = false; } } ?>

header_icon_url = $url ? esc_url_raw( $url ) : null; return $this; } public function remove_header(): SE_License_SDK_License { $this->remove_header = true; return $this; } /** * Card header. * * @return void */ public function get_error() { return $this->error; } public function get_error_code(): string { return $this->error_code; } public function get_error_data(): array { return $this->error_data; } public function get_success() { return $this->success; } /** * Activate client license. * * @param array $postData post data. * * @return void */ public function activate_client_license( array $postData ) { $this->updating_license( true ); // Reset structured error state for this attempt. $this->error_code = ''; $this->error_data = []; $license = $this->get_license(); // Re-activating after an expiry/revocation: deactivate_local_license() // keeps the key on file precisely so the user can renew upstream and // click Activate without re-typing it. Fall back to the stored key when // the caller sends none. if ( empty( $postData['license_key'] ) && ! empty( $license['license'] ) ) { $postData['license_key'] = $license['license']; } if ( empty( $postData['license_key'] ) ) { $this->error = __( 'The license key field is required.', 'storeengine-sdk' ); $this->updating_license( false ); return; } $updateKey = $this->validate_license_data( $license ) && $postData['license_key'] !== $license['license']; // Check if it's a change request. if ( $updateKey ) { // Deactivate Previous. $deactivate = $this->deactivate(); // deactivate first. if ( ! $deactivate['success'] ) { $check = $this->check(); // Check api status. if ( $check['success'] && isset( $check['data']['activated'] ) && $check['data']['activated'] ) { if ( $deactivate['error'] ) { $this->error = $deactivate['error']; } else { $this->error = __( 'Unknown error occurred.', 'storeengine-sdk' ); } $this->updating_license( false ); return; } } } // Set new license info. $license['license'] = $postData['license_key']; $license['device_id'] = $this->client->get_device_id(); // Seats the user chose to release during a limit-reached takeover // (Freemius-style). The server frees these before re-checking the // activation limit so this site can take a seat. if ( ! empty( $postData['deactivate_activations'] ) && is_array( $postData['deactivate_activations'] ) ) { $license['deactivate_activations'] = array_values( array_filter( array_map( 'absint', $postData['deactivate_activations'] ) ) ); } // Activate The License. $response = $this->activate( $license ); if ( ! $response['success'] ) { if ( $response['error'] ) { $this->error = $response['error']; } else { $this->error = __( 'Unknown error occurred.', 'storeengine-sdk' ); } // Capture the machine-readable code + payload (e.g. the active-site // list when the activation limit is reached) for the REST layer. $this->error_code = $response['code'] ?? ''; $this->error_data = ( isset( $response['data'] ) && is_array( $response['data'] ) ) ? $response['data'] : []; } else { if ( ! $updateKey ) { $this->success = __( 'License activated successfully.', 'storeengine-sdk' ); } else { $this->success = __( 'License updated successfully.', 'storeengine-sdk' ); } // Fresh authoritative verdict — anchor the grace period here and // clear any stale offline state from a previous key. $this->set_grace_state( [ 'last_verified_at' => time() ] ); } // Don't reset the key. // keep it, so if the user renew subscription update the status and reactivate the plugin. // Schedule before saving so the signature payload uses the same next-run timestamp as validation. $this->schedule_license_check(); $new_license = wp_parse_args( $response['data'], $license ); // Update license status. $this->set_license( $new_license ); if ( $response['success'] && 'active' === ( $new_license['status'] ?? '' ) ) { $this->emit_license_event( 'license_activated', $new_license ); } $this->updating_license( false ); } /** * deactivate client license. * * @return void */ public function deactivate_client_license() { if ( ! isset( $this->license['license'] ) || empty( $this->license['license'] ) ) { $this->error = __( 'License key not found.', 'storeengine-sdk' ); } else { $response = $this->deactivate(); if ( ! $response['success'] ) { // check api status. $check = $this->check(); if ( $check['success'] && isset( $check['data']['activated'] ) && $check['data']['activated'] ) { if ( $response['error'] ) { $this->error = $response['error']; } else { $this->error = __( 'Unknown error occurred.', 'storeengine-sdk' ); } } } } $this->clear_license_check_schedule(); $this->set_grace_state( [] ); $deactivated_license = $this->get_license(); // Reset license data. $this->set_license(); $this->emit_license_event( 'license_deactivated', $deactivated_license ); $this->success = __( 'License deactivated successfully.', 'storeengine-sdk' ); } /** * Add license menu page. * * @return void */ private function add_menu_page() { add_menu_page( $this->menu_args['page_title'], $this->menu_args['menu_title'], $this->menu_args['capability'], $this->menu_args['menu_slug'], [ $this, 'render_menu_page' ], $this->menu_args['icon_url'], $this->menu_args['position'] ); } /** * Add submenu page. * * @return void */ private function add_submenu_page() { add_submenu_page( $this->menu_args['parent_slug'], $this->menu_args['page_title'], $this->menu_args['menu_title'], $this->menu_args['capability'], $this->menu_args['menu_slug'], [ $this, 'render_menu_page' ] ); } /** * Add submenu page. * * @return void */ private function add_options_page() { add_options_page( $this->menu_args['page_title'], $this->menu_args['menu_title'], $this->menu_args['capability'], $this->menu_args['menu_slug'], [ $this, 'render_menu_page' ] ); } /** * Schedule daily license checker event. * * @return void */ public function schedule_license_check() { if ( empty( $this->license['license'] ) && ! $this->get_key() ) { return; } if ( ! wp_next_scheduled( $this->schedule_hook ) ) { // Spread over the first hour so sites activated together (or healed // together after an update) don't all check at the same minute. wp_schedule_event( time() + SE_License_SDK_Client::jitter( 30 * MINUTE_IN_SECONDS, 0.95 ), 'daily', $this->schedule_hook ); } } /** * Clear any scheduled hook. * * @return void */ public function clear_license_check_schedule() { wp_clear_scheduled_hook( $this->schedule_hook ); } /** * Register Activation And Deactivation Hooks. * * @return void */ private function activation_deactivation() { switch ( $this->client->getType() ) { case 'plugin': register_activation_hook( $this->client->getBasename(), [ $this, 'schedule_license_check' ] ); register_deactivation_hook( $this->client->getBasename(), [ $this, 'project_deactivation' ] ); add_action( 'activated_plugin', [ $this, 'redirect_to_license_page' ], 999, 2 ); break; case 'theme': add_action( 'switch_theme', [ $this, 'project_deactivation' ] ); add_action( 'after_switch_theme', [ $this, 'schedule_license_check' ] ); add_action( 'after_switch_theme', [ $this, 'redirect_to_license_page' ], 999, 2 ); break; } } /** * Project Deactivation Callback. * * @return void */ public function project_deactivation() { // Intentionally NOT calling deactivate_client_license() here. // // WordPress plugin deactivation is often temporary — a user // disables a plugin to debug, swap themes, test a conflict, etc. // Surrendering the license seat on every deactivation forces the // user to re-paste their key on reactivation, which is bad UX and // also creates spurious "deactivated → reactivated" noise in the // vendor's activation audit log. // // We match the behaviour of ACF Pro / EDD / Yoast Premium: the // license stays active across WP plugin deactivate/reactivate. // The seat is only released when the user explicitly clicks // "Deactivate License" in the SDK panel, or when they truly // uninstall the plugin (via wp-admin Plugins → Delete, which // runs the consumer's own uninstall.php — the SDK can't hook // that path because it's bundled inside the host plugin). $this->clear_license_check_schedule(); // Drop the cached update-info transient so a future reactivation // starts with a fresh check. if ( method_exists( $this->client, 'updater' ) ) { $this->client->updater()->delete_cached_version_info(); } } /** * Redirect to the license activation page after plugin/theme is activated. * * @TODO make option for the plugin/theme (which is using this lib) can alter this method with their custom function. * * @param string $param1 Plugin: base file|Theme: old theme name. * @param bool|WP_Theme $param2 Plugin: network wide activation status|Theme: WP_Theme instance of the old theme. * * @return void */ public function redirect_to_license_page( $param1, $param2 = null ) { /** @noinspection PhpUndefinedClassInspection */ if ( ! $this->redirect_on_activation || class_exists( \WP_CLI::class, false ) ) { return; } $canRedirect = false; if ( 'plugin' == $this->client->getType() ) { $canRedirect = ( $param1 == $this->client->getBasename() ); } if ( 'theme' == $this->client->getType() ) { $canRedirect = ( ! get_option( 'theme_switched_via_customizer' ) ); } if ( $canRedirect ) { wp_safe_redirect( $this->get_page_url( false ) ); die(); } } /** * Form action URL. */ private function formActionUrl(): void { echo esc_url( $this->get_page_url() ); } /** * Get input license key. * * @param array $license license data. * @param string $action * * @return string */ private function get_input_license_value( #[\SensitiveParameter] array $license, string $action ): string { //phpcs:disable if ( 'deactivate' !== $action ) { if ( ! empty( $_REQUEST[ $this->data_key ]['license_key'] ) ) { return sanitize_text_field( $_REQUEST[ $this->data_key ]['license_key'] ); } return ''; } // phpcs:enabled if ( empty( $license['license'] ) ) { return ''; } return $this->mask_string( $license['license'] ); } public function mask_string( #[\SensitiveParameter] string $string, ?int $size = null ): string { $length = strlen( $string ); if ( ! $size ) { $size = (int) max( 8, $length / 8 ); $size = (int) ( $length < $size ? $length / 10 : $size ); } $mid = (int) ( $size * 2 ); $mid = $length > $mid ? $length - $mid : $mid; $masked = substr( $string, 0, $size ); $masked .= str_repeat( '•', $mid ); $masked .= substr( $string, - 1 * $size ); return $masked; } /** * Signature payload (v2): the product slug and the license data. * * Until 1.6.0 the payload also contained wp_next_scheduled() of the daily * check event, so anything that removed or moved that cron event (cron * cleanup plugins, migrations, a manual "run now") silently invalidated a * perfectly good license. The salted HMAC over the license data is what * stops the option from being edited by hand; the timestamp added nothing * to that. * * @return string */ private function license_signature_payload(): string { return 'v2||' . $this->client->getSlug() . '||' . implode( '||', array_values( $this->license ) ); } /** * Pre-1.6.0 payload, kept only to migrate existing signatures. * * @return string */ private function legacy_license_signature_payload(): string { return $this->client->getSlug() . '||' . wp_next_scheduled( $this->schedule_hook ) . '||' . implode( '||', array_values( $this->license ) ); } private function sign( string $payload ): string { return hash_hmac( 'sha256', $payload, $this->hash( $payload ) ); } private function update_license_signature() { if ( $this->license && is_array( $this->license ) ) { $this->client->set_option( 'license_signature', $this->sign( $this->license_signature_payload() ) ); } } private function get_license_signature() { return $this->client->get_option( 'license_signature', false ); } private function validate_license_signature(): bool { $license_signature = $this->get_license_signature(); if ( ! $license_signature || ! is_string( $license_signature ) ) { return false; } if ( hash_equals( $this->sign( $this->license_signature_payload() ), $license_signature ) ) { return true; } // Signed by an older SDK: accept once and re-sign in the new format. if ( hash_equals( $this->sign( $this->legacy_license_signature_payload() ), $license_signature ) ) { $this->update_license_signature(); return true; } return false; } /** * Recover licenses whose old-format signature can't be verified any more * because the daily check event was removed: ask the server in the * background instead of showing the product as unlicensed until the user * re-activates. The server's answer decides, so this can't be used to * unlock anything. * * @return void */ private function maybe_schedule_signature_recheck() { $license = $this->get_license(); if ( empty( $license['license'] ) || 'active' !== ( $license['status'] ?? '' ) ) { return; } $hook = $this->client->getHookName( 'license_recheck' ); if ( ! has_action( $hook, [ $this, 'check_license_status' ] ) ) { add_action( $hook, [ $this, 'check_license_status' ] ); } if ( ! wp_installing() && ! wp_next_scheduled( $hook ) && ! get_site_transient( $hook . '_tried' ) ) { // Once a day at most, in case the server keeps rejecting it. set_site_transient( $hook . '_tried', 1, DAY_IN_SECONDS ); wp_schedule_single_event( time(), $hook ); } } /** * Update License Data. * Call this method without license data will deactivate the license (set empty data). * * @param array $license { * Optional. License Data. * * @type string $key The License Key. * @type string $status Activation Status. * @type int $remaining Remaining Activation. * @type int $activation_limit Number of activation allowed for the license key. * @type int $expires Number of day remaining before the license expires. * } */ private function set_license( #[\SensitiveParameter] array $license = [] ) { // Parse & sanitize. $this->license = $this->parse_license_data( $license ); $this->license['updated_at'] = current_time( 'timestamp', 1 ); // Update in db. $this->client->set_option( 'license_data', $this->license ); // Update license signature. $this->update_license_signature(); } /** * Get Plugin/Theme License. * * @return array { * Optional. License Data. * @type string $key The License Key. * @type string $status Activation Status. * @type int $remaining Remaining Activation. * @type int $activation_limit Number of activation allowed for the license key. * @type int $expires Number of day remaining before the license expires. * } */ public function get_license(): array { if ( null !== $this->license ) { return $this->license; } $this->license = $this->client->get_option( 'license_data', false ); // Initialize blank inactive license data. if ( false === $this->license || ! is_array( $this->license ) ) { $this->set_license(); } $this->license = $this->parse_license_data( $this->license ); return $this->license; } public function get_public_data(): array { $data = $this->get_license(); if ( $data['license'] ) { $data['license'] = $this->mask_string( $data['license'] ); } if ( $data['updated_at'] ) { $data['updated_at'] = gmdate( 'Y-m-d H:i:s', $data['updated_at'] ); } if ( $data['expires'] ) { $data['expires'] = gmdate( 'Y-m-d H:i:s', $data['expires'] ); } return $data; } /** * Parse License data. * * @param array $data license data. * * @return array */ private function parse_license_data( #[\SensitiveParameter] array $data = [] ): array { $defaults = [ 'license' => '', // License key. 'status' => 'inactive', // Current status. 'activation_id' => 0, 'device_id' => $this->client->get_device_id(), // Instance unique id. 'slug' => $this->client->getSlug(), 'product_id' => $this->client->getProductId(), 'remaining' => 0, // Remaining activation. 'activations' => 0, // Total activation. 'limit' => 0, //Activation limit. 'unlimited' => false, // Is unlimited activation. 'expires' => '', // Expires set this to a unix timestamp [GMT]. 'updated_at' => current_time( 'timestamp', 1 ), // Expires set this to a unix timestamp [GMT]. ]; // Parse. $data = wp_parse_args( $data, $defaults ); $license = []; $data['updated_at'] = $data['updated_at'] && ! is_numeric( $data['updated_at'] ) ? strtotime( $data['updated_at'] ) : absint( $data['updated_at'] ); // Sanitize data. $license['license'] = sanitize_text_field( $data['license'] ); $license['status'] = strtolower( $data['status'] ) === 'active' ? 'active' : 'inactive'; $license['activation_id'] = absint( $data['activation_id'] ); $license['device_id'] = sanitize_text_field( $data['device_id'] ); $license['slug'] = sanitize_text_field( $data['slug'] ); $license['product_id'] = absint( $data['product_id'] ); // Product id can be string too. $license['remaining'] = absint( $data['remaining'] ); $license['activations'] = absint( $data['activations'] ); $license['limit'] = absint( $data['limit'] ); $license['unlimited'] = (bool) $data['unlimited']; $license['expires'] = $data['expires'] && ! is_numeric( $data['expires'] ) ? strtotime( $data['expires'] ) : absint( $data['expires'] ); $license['updated_at'] = $data['updated_at']; return $license; } /** * Gets a form of `wp_hash()` specific to the plugin using license service. * * We cannot use `wp_hash()` because it is defined in `pluggable.php` which is not loaded until after plugins are loaded, * which is too late to verify the recovery mode cookie. * * This tries to use the `AUTH` salts first, but if they aren't valid specific salts will be generated and stored. * * @param string $data Data to hash. * * @return string The hashed $data, or false on failure. * * @see wp_hash() * @noinspection PhpUndefinedConstantInspection */ private function hash( string $data ): string { if ( ! function_exists( 'wp_generate_password' ) ) { require_once ABSPATH . WPINC . '/pluggable.php'; } return wp_hash( $data, 'auth', 'sha256' ); } public function migrate_from_freemius( string $freemius_license ) { $this->updating_license( true ); $license = $this->get_license(); $license['license'] = $freemius_license; $license['device_id'] = $this->client->get_device_id(); $license['freemius_license'] = true; // Activate The License. $response = $this->activate( $license ); if ( ! $response['success'] ) { if ( $response['error'] ) { return new WP_Error( 'error-activating-freemius-license-', $response['error'], $response ); } else { return new WP_Error( 'error-activating-freemius-license-', __( 'Unknown error occurred.', 'storeengine-sdk' ), $response ); } } // Schedule before saving so the signature payload uses the same next-run timestamp as validation. $this->schedule_license_check(); // Update license status. $this->set_license( wp_parse_args( $response['data'], $license ) ); $this->updating_license( false ); $this->client->set_option( 'migrated_from_freemius', current_time( 'timestamp', 1 ) ); return true; } final public function __clone() { trigger_error( 'Singleton. No cloning allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } /** * Wakeup. */ final public function __wakeup() { trigger_error( 'Singleton. No serialization allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } } // End of file SE_License_SDK_License.php.