_config = Dispatcher::config(); } /** * Usermeta key for storing dismissed license notices. * * @since 2.9.1 * * @var string */ const NOTICE_DISMISSED_META_KEY = 'w3tc_license_notice_dismissed'; /** * Time in seconds after which a dismissed notice can reappear if conditions persist. * Set to 6 days (automatic license check interval is 5 days). * * @since 2.9.1 * * @var int */ const NOTICE_DISMISSAL_RESET_TIME = 518400; /** * Cache duration for the billing URL transient (19 minutes). * * The HLT (Hosted Login Token) in the billing URL is only valid for 19 minutes, * so we cache for 19 minutes to ensure users always get a valid token with some buffer. * * @since 2.9.1 * * @var int */ const BILLING_URL_CACHE_DURATION = 1140; /** * Registers hooks for the plugin's admin functionality. * * Adds actions and filters for admin initialization, AJAX, UI updates, and admin bar menu. * * @return void */ public function run() { add_action( 'admin_init', array( $this, 'admin_init' ) ); add_action( 'w3tc_config_ui_save-w3tc_general', array( $this, 'possible_state_change' ), 2, 10 ); add_action( 'w3tc_message_action_licensing_upgrade', array( $this, 'w3tc_message_action_licensing_upgrade' ) ); add_filter( 'w3tc_admin_bar_menu', array( $this, 'w3tc_admin_bar_menu' ) ); // AJAX handler for dismissing license notices. add_action( 'wp_ajax_w3tc_dismiss_license_notice', array( $this, 'ajax_dismiss_license_notice' ) ); // AJAX handler for rechecking license status. add_action( 'wp_ajax_w3tc_recheck_license', array( $this, 'ajax_recheck_license' ) ); // Register scripts for license notice dismissal. add_action( 'admin_enqueue_scripts', array( $this, 'register_notice_scripts' ) ); } /** * Adds licensing menu items to the admin bar. * * @param array $menu_items Existing admin bar menu items. * * @return array Modified admin bar menu items. */ public function w3tc_admin_bar_menu( $menu_items ) { if ( ! Util_Environment::is_w3tc_pro( $this->_config ) ) { $menu_items['00020.licensing'] = array( 'id' => 'w3tc_overlay_upgrade', 'parent' => 'w3tc', 'title' => wp_kses( sprintf( // translators: 1 opening HTML span tag, 2 closing HTML span tag. __( '%1$sUpgrade Performance%2$s', 'w3-total-cache' ), '', '' ), array( 'span' => array( 'style' => array(), ), ) ), 'href' => wp_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_message_action=licensing_upgrade' ), 'w3tc' ), ); } if ( defined( 'W3TC_DEBUG' ) && W3TC_DEBUG ) { $menu_items['90040.licensing'] = array( 'id' => 'w3tc_debug_overlay_upgrade', 'parent' => 'w3tc_debug_overlays', 'title' => esc_html__( 'Upgrade', 'w3-total-cache' ), 'href' => wp_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_message_action=licensing_upgrade' ), 'w3tc' ), ); } return $menu_items; } /** * Handles the licensing upgrade action. * * Adds a hook to modify the admin head for licensing upgrades. * * @return void */ public function w3tc_message_action_licensing_upgrade() { add_action( 'admin_head', array( $this, 'admin_head_licensing_upgrade' ) ); } /** * Outputs JavaScript for the licensing upgrade page. * * @return void */ public function admin_head_licensing_upgrade() { ?> get_string( 'plugin.license_key' ); $new_key_set = ! empty( $new_key ); $old_key = $old_config->get_string( 'plugin.license_key' ); $old_key_set = ! empty( $old_key ); // Clear billing URL transient if license key is changing. if ( $old_key_set && $old_key !== $new_key ) { $old_transient_key = 'w3tc_billing_url_' . md5( $old_key ); delete_transient( $old_transient_key ); } switch ( true ) { // No new key or old key. Do nothing. case ( ! $new_key_set && ! $old_key_set ): return; // Current key set but new is blank, deactivating old. case ( ! $new_key_set && $old_key_set ): $deactivate_result = Licensing_Core::deactivate_license( $old_key ); $changed = true; break; // Current key is blank but new is not, activating new. case ( $new_key_set && ! $old_key_set ): $activate_result = Licensing_Core::activate_license( $new_key, W3TC_VERSION ); $changed = true; if ( $activate_result ) { $config->set( 'common.track_usage', true ); } break; // Current key is set and new different key provided. Deactivating old and activating new. case ( $new_key_set && $old_key_set && $new_key !== $old_key ): $deactivate_result = Licensing_Core::deactivate_license( $old_key ); $activate_result = Licensing_Core::activate_license( $new_key, W3TC_VERSION ); $changed = true; break; } if ( $changed ) { $state = Dispatcher::config_state(); $state->set( 'license.next_check', 0 ); // If license key was removed, immediately clear the status. if ( ! $new_key_set && $old_key_set ) { $state->set( 'license.status', 'no_key' ); $state->set( 'license.paypal_billing_update_required', false ); // Clear dismissed notices for billing update since license is removed. $this->clear_dismissed_notice_for_all_users( 'paypal-billing-update-required' ); try { $config->set( 'plugin.type', '' ); $config->save(); } catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // missing exception handle? } } // Clear billing URL transient for the new key to force fresh fetch. if ( $new_key_set ) { $new_transient_key = 'w3tc_billing_url_' . md5( $new_key ); delete_transient( $new_transient_key ); } // Process activation result to update paypal_billing_update_required immediately. if ( isset( $activate_result ) && $activate_result ) { // Update license status from activation result. if ( isset( $activate_result->license_status ) ) { $state->set( 'license.status', $activate_result->license_status ); } // Update paypal_billing_update_required if present in activation result. if ( isset( $activate_result->paypal_billing_update_required ) ) { $paypal_billing_update_required = filter_var( $activate_result->paypal_billing_update_required, FILTER_VALIDATE_BOOLEAN ); $state->set( 'license.paypal_billing_update_required', $paypal_billing_update_required ); } } $state->save(); delete_transient( 'w3tc_imageservice_limited' ); $messages = array(); // If the old key was deactivated, add a message. if ( isset( $deactivate_result ) && ! empty( $deactivate_result->license_status ) ) { $status = $deactivate_result->license_status; switch ( true ) { case ( strpos( $status, 'inactive.expired.' ) === 0 ): $messages[] = array( 'message' => sprintf( // Translators: 1 Product name. __( 'Your previous %1$s Pro license key is expired and will remain registered to this domain.', 'w3-total-cache' ), W3TC_POWERED_BY ), 'type' => 'error', ); break; case ( strpos( $status, 'inactive.not_present' ) === 0 ): $messages[] = array( 'message' => sprintf( // Translators: 1 Product name. __( 'Your previous %1$s Pro license key was not found and cannot be deactivated.', 'w3-total-cache' ), W3TC_POWERED_BY ), 'type' => 'info', ); break; case ( strpos( $status, 'inactive' ) === 0 ): $messages[] = array( 'message' => sprintf( // Translators: 1 Product name. __( 'Your previous %1$s Pro license key has been deactivated.', 'w3-total-cache' ), W3TC_POWERED_BY ), 'type' => 'info', ); break; case ( strpos( $status, 'invalid' ) === 0 ): $messages[] = array( 'message' => sprintf( // translators: 1 Product name, 2 HTML anchor open tag, 3 HTML anchor close tag. __( 'Your previous %1$s Pro license key is invalid and cannot be deactivated. Please %2$scontact support%3$s for assistance.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ), 'type' => 'error', ); break; } } // Handle new activation status. if ( isset( $activate_result ) ) { $status = $activate_result->license_status; switch ( true ) { case ( strpos( $status, 'active' ) === 0 ): $messages[] = array( 'message' => sprintf( // Translators: 1 Product name. __( 'The %1$s Pro license key you provided is valid and has been applied.', 'w3-total-cache' ), W3TC_POWERED_BY ), 'type' => 'info', ); break; } } // Store messages for processing. update_option( 'license_update_messages', $messages ); // Only force license check if we didn't just activate a license. // If we activated, we already have the status from the activation result. if ( $new_key_set && ( ! isset( $activate_result ) || ! $activate_result ) ) { // Force immediate license check to get latest status including billing requirements. $this->maybe_update_license_status(); } } } /** * Initializes admin-specific features and hooks. * * Adds admin notices, UI filters, and license status checks. * * @return void */ public function admin_init() { $capability = apply_filters( 'w3tc_capability_admin_notices', 'manage_options' ); $this->maybe_update_license_status(); if ( current_user_can( $capability ) ) { if ( is_admin() ) { /** * Only admin can see W3TC notices and errors */ if ( ! Util_Environment::is_wpmu() ) { add_action( 'admin_notices', array( $this, 'admin_notices' ), 1, 1 ); } add_action( 'network_admin_notices', array( $this, 'admin_notices' ), 1, 1 ); if ( Util_Admin::is_w3tc_admin_page() ) { add_filter( 'w3tc_notes', array( $this, 'w3tc_notes' ) ); } else { // Enqueue lightbox assets on non-W3TC pages if license notices may be shown. $this->maybe_enqueue_lightbox_assets(); } } } } /** * Enqueues lightbox assets if license-related notices may need them. * * This is called on non-W3TC admin pages where the lightbox isn't already loaded. * Only enqueues if there's a license status that would display a notice. * * @since 2.9.1 * * @return void */ private function maybe_enqueue_lightbox_assets() { $state = Dispatcher::config_state(); $license_status = $state->get_string( 'license.status' ); $license_key = $this->get_license_key(); // Check if we have a license notice to display. $license_message = $this->get_license_notice( $license_status, $license_key ); if ( $license_message ) { $sanitized_status = $this->sanitize_status_for_id( $license_status ); // Only enqueue if the notice isn't dismissed. if ( ! $this->is_notice_dismissed( 'license-status-' . $sanitized_status ) ) { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_lightbox_assets' ) ); } } } /** * Enqueues lightbox JavaScript and CSS assets. * * @since 2.9.1 * * @return void */ public function enqueue_lightbox_assets() { wp_enqueue_script( 'w3tc-lightbox', plugins_url( 'pub/js/lightbox.js', W3TC_FILE ), array( 'jquery' ), W3TC_VERSION, false ); wp_enqueue_style( 'w3tc-lightbox', plugins_url( 'pub/css/lightbox.css', W3TC_FILE ), array(), W3TC_VERSION ); } /** * Checks if a status starts with a specific prefix. * * @param string $s The status string. * @param string $starts_with The prefix to check against. * * @return bool True if the status starts with the prefix, false otherwise. */ private function _status_is( $s, $starts_with ) { $s .= '.'; $starts_with .= '.'; return substr( $s, 0, strlen( $starts_with ) ) === $starts_with; } /** * Sanitizes a license status string for use in HTML IDs. * * Ensures the status only contains valid characters for HTML IDs * (lowercase letters, numbers, hyphens, underscores) and replaces * dots with hyphens for readability. * * @since 2.9.1 * * @param string $status The license status to sanitize. * * @return string The sanitized status safe for use in HTML IDs. */ private function sanitize_status_for_id( $status ) { // Replace dots with hyphens for readability (e.g., "active.dunning" -> "active-dunning"). $status = str_replace( '.', '-', $status ); // Use WordPress sanitize_html_class which only allows a-z, A-Z, 0-9, _, -. return sanitize_html_class( $status, 'unknown' ); } /** * Displays admin notices related to licensing. * * @return void */ public function admin_notices() { $state = Dispatcher::config_state(); $license_status = $state->get_string( 'license.status' ); $license_key = $this->get_license_key(); $has_notices = false; $paypal_billing_update_required = $state->get_boolean( 'license.paypal_billing_update_required' ); $is_dismissed = $this->is_notice_dismissed( 'paypal-billing-update-required' ); // Check for PayPal billing update requirement (shows on all admin pages). // Only show if there's a license key and the status is not 'no_key'. if ( $paypal_billing_update_required && ! $is_dismissed && ! empty( $license_key ) && 'no_key' !== $license_status ) { $billing_url = $this->get_billing_url( $license_key ); if ( $billing_url ) { $billing_message = sprintf( // Translators: 1 Product name, 2 opening HTML a tag to billing portal, 3 closing HTML a tag, 4 opening HTML a tag for recheck, 5 closing HTML a tag. __( 'Your %1$s Pro subscription requires a PayPal billing agreement update. Please %2$sUpdate your billing agreement%3$s to ensure uninterrupted service. Already updated? %4$sRecheck your license status%5$s or dismiss this notice.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '', '', '' ); } else { $billing_message = sprintf( // Translators: 1 Product name, 2 opening HTML a tag for recheck, 3 closing HTML a tag. __( 'Your %1$s Pro subscription requires a PayPal billing agreement update. Please contact support to ensure uninterrupted service. Already updated? %2$sRecheck your license status%3$s or dismiss this notice.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ); } Util_Ui::error_box( '

' . $billing_message . '

', 'w3tc-paypal-billing-update-required', true ); $has_notices = true; } $license_message = $this->get_license_notice( $license_status, $license_key ); if ( $license_message ) { // Sanitize status for use in HTML ID (only allows a-z, 0-9, -, _). $sanitized_status = $this->sanitize_status_for_id( $license_status ); if ( ! $this->is_notice_dismissed( 'license-status-' . $sanitized_status ) ) { Util_Ui::error_box( '

' . $license_message . '

', 'w3tc-license-status-' . $sanitized_status, true ); $has_notices = true; } } $license_update_messages = get_option( 'license_update_messages' ); if ( $license_update_messages ) { foreach ( $license_update_messages as $message_data ) { if ( 'error' === $message_data['type'] ) { Util_Ui::error_box( '

' . $message_data['message'] . '

', 'w3tc-license-update-message', true ); } elseif ( 'info' === $message_data['type'] ) { Util_Ui::e_notification_box( '

' . $message_data['message'] . '

', 'w3tc-license-update-message', true ); } } delete_option( 'license_update_messages' ); $has_notices = true; } // Enqueue dismissal script if there are notices. if ( $has_notices ) { wp_enqueue_script( 'w3tc-license-notice-dismiss' ); } } /** * Registers the license notice dismissal script. * * Called on admin_enqueue_scripts to register the script early, * which can then be enqueued later when notices are displayed. * * @since 2.9.1 * * @return void */ public function register_notice_scripts() { wp_register_script( 'w3tc-license-notice-dismiss', false, // No external file, inline script only. array( 'jquery' ), W3TC_VERSION, true // Load in footer. ); // Add the inline script. $nonce = wp_create_nonce( 'w3tc' ); $rechecking_text = __( 'Rechecking...', 'w3-total-cache' ); $script = " jQuery(function($) { // Use event delegation with a namespace to prevent duplicate handlers. $(document).off('click.w3tcLicenseNotice').on('click.w3tcLicenseNotice', '.notice.is-dismissible[id^=\"w3tc-\"] .notice-dismiss', function() { var \$notice = $(this).closest('.notice'); var noticeId = \$notice.attr('id'); if (noticeId && noticeId.indexOf('w3tc-') === 0) { // Remove the 'w3tc-' prefix for storage. var cleanId = noticeId.replace('w3tc-', ''); $.post(ajaxurl, { action: 'w3tc_dismiss_license_notice', notice_id: cleanId, _wpnonce: '" . esc_js( $nonce ) . "' }); } }); // Handle recheck license link. $(document).off('click.w3tcRecheckLicense').on('click.w3tcRecheckLicense', '.w3tc-recheck-license', function(e) { e.preventDefault(); var \$link = $(this); \$link.text('" . esc_js( $rechecking_text ) . "').css('pointer-events', 'none'); $.post(ajaxurl, { action: 'w3tc_recheck_license', _wpnonce: '" . esc_js( $nonce ) . "' }).always(function() { location.reload(); }); }); }); "; wp_add_inline_script( 'w3tc-license-notice-dismiss', $script ); } /** * Generates a license notice based on the provided license status and key. * * @since 2.9.1 * * @param string $status The current status of the license (e.g., 'active', 'expired'). * @param string $license_key The license key associated with the plugin. * * @return string The formatted license notice message. */ private function get_license_notice( $status, $license_key ) { switch ( true ) { case $this->_status_is( $status, 'active.dunning' ): $billing_url = $this->get_billing_url( $license_key ); if ( $billing_url ) { return sprintf( // Translators: 1 Product name, 2 opening HTML a tag to billing portal, 3 closing HTML a tag, 4 opening HTML a tag for recheck, 5 closing HTML a tag. __( 'Your %1$s Pro subscription payment is past due. Please update your %2$sBilling Information%3$s to prevent service interruption. Already updated? %4$sRecheck your license status%5$s.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '', '', '' ); } return sprintf( // Translators: 1 Product name, 2 opening HTML a tag for recheck, 3 closing HTML a tag. __( 'Your %1$s Pro subscription payment is past due. Please update your billing information or contact us to prevent service interruption. Already updated? %2$sRecheck your license status%3$s.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ); case $this->_status_is( $status, 'inactive.expired' ): return sprintf( // Translators: 1 Product name, 2 HTML input to renew subscription. __( 'Your %1$s Pro license key has expired. %2$s to continue using the Pro features', 'w3-total-cache' ), W3TC_POWERED_BY, '' ); case $this->_status_is( $status, 'inactive.by_rooturi' ) || $this->_status_is( $status, 'inactive.by_rooturi.activations_limit_not_reached' ): // Only show this notice if there's actually a license key. if ( empty( $license_key ) ) { return ''; } $reset_url = Util_Ui::url( array( 'page' => 'w3tc_general', 'w3tc_licensing_reset_rooturi' => 'y', ) ); return sprintf( // Translators: 1 Product name, 2 opening HTML a tag to reset license URIs, 3 closing HTML a tag. __( 'Your %1$s license key is not active for this site. You can switch your license to this website following %2$sthis link%3$s', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ); case $this->_status_is( $status, 'inactive.by_rooturi.activations_limit_reached' ): return sprintf( // Translators: 1 Product name. __( 'Your %1$s license key is not active and cannot be activated due to the license activation limit being reached.', 'w3-total-cache' ), W3TC_POWERED_BY ); case $this->_status_is( $status, 'inactive' ): return sprintf( // Translators: 1 Product name. __( 'The %1$s license key is not active.', 'w3-total-cache' ), W3TC_POWERED_BY ); case $this->_status_is( $status, 'invalid' ): $url = is_network_admin() ? network_admin_url( 'admin.php?page=w3tc_general#licensing' ) : admin_url( 'admin.php?page=w3tc_general#licensing' ); return sprintf( // Translators: 1 Product name, 2 opening HTML a tag to license setting, 3 closing HTML a tag. __( 'Your current %1$s Pro license key is not valid. %2$sPlease confirm it%3$s.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ); case ( 'no_key' === $status || $this->_status_is( $status, 'active' ) || $this->_status_is( $status, 'free' ) ): return ''; default: return sprintf( // translators: 1: Product name, 2: HTML anchor open tag, 3: HTML anchor close tag. __( 'The %1$s license key cannot be verified. Please %2$scontact support%3$s for assistance.', 'w3-total-cache' ), W3TC_POWERED_BY, '', '' ); } } /** * Generates the billing URL for a given license key. * * Uses transient caching to avoid making HTTP requests on every page load. * The URL is cached for 1 hour to balance freshness with performance. * * @since 2.9.1 * * @param string $license_key The license key used to generate the billing URL. * * @return string The billing URL associated with the provided license key, or * an empty string if the request fails or the response is invalid. */ private function get_billing_url( $license_key ) { if ( empty( $license_key ) ) { return ''; } // Generate a unique transient key based on the license key. $transient_key = 'w3tc_billing_url_' . md5( $license_key ); // Check for cached URL. $cached_url = get_transient( $transient_key ); if ( false !== $cached_url ) { // Return cached value (empty string is a valid cached "no URL" result). return $cached_url; } $billing_url = $this->fetch_billing_url_from_api( $license_key ); // Cache the result (including empty string for failed requests to avoid repeated failures). set_transient( $transient_key, $billing_url, self::BILLING_URL_CACHE_DURATION ); return $billing_url; } /** * Fetches the billing URL from the API. * * @since 2.9.1 * * @param string $license_key The license key used to generate the billing URL. * * @return string The billing URL or empty string on failure. */ private function fetch_billing_url_from_api( $license_key ) { $api_params = array( 'edd_action' => 'get_recurly_hlt_link', 'license' => $license_key, 'license_key' => $license_key, ); $response = wp_remote_get( add_query_arg( $api_params, W3TC_LICENSE_API_URL ), array( 'timeout' => 15, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { return ''; } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { return ''; } $body = wp_remote_retrieve_body( $response ); return filter_var( $body, FILTER_VALIDATE_URL ) ? esc_url_raw( $body ) : ''; } /** * Modifies the notes displayed in the W3TC UI. * * @param array $notes Existing notes to display. * * @return array Modified notes with licensing terms. */ public function w3tc_notes( $notes ) { $terms = ''; $state_master = Dispatcher::config_state_master(); if ( Util_Environment::is_pro_constant( $this->_config ) ) { $terms = 'accept'; } elseif ( ! Util_Environment::is_w3tc_pro( $this->_config ) ) { $terms = $state_master->get_string( 'license.community_terms' ); $buttons = sprintf( '

%s %s', Util_Ui::button_link( __( 'Accept', 'w3-total-cache' ), Util_Ui::url( array( 'w3tc_licensing_terms_accept' => 'y' ) ) ), Util_Ui::button_link( __( 'Decline', 'w3-total-cache' ), Util_Ui::url( array( 'w3tc_licensing_terms_decline' => 'y' ) ) ) ); } else { $state = Dispatcher::config_state(); $terms = $state->get_string( 'license.terms' ); $return_url = self_admin_url( Util_Ui::url( array( 'w3tc_licensing_terms_refresh' => 'y' ) ) ); $buttons = sprintf( '
', W3TC_TERMS_ACCEPT_URL ) . Util_Ui::r_hidden( 'return_url', 'return_url', $return_url ) . Util_Ui::r_hidden( 'license_key', 'license_key', $this->get_license_key() ) . Util_Ui::r_hidden( 'home_url', 'home_url', home_url() ) . ' ' . '' . '
'; } if ( 'accept' !== $terms && 'decline' !== $terms && 'postpone' !== $terms ) { if ( $state_master->get_integer( 'common.install' ) < 1542029724 ) { /* installed before 2018-11-12 */ $notes['licensing_terms'] = sprintf( // translators: 1 opening HTML a tag to W3TC Terms page, 2 closing HTML a tag. esc_html__( 'Our terms of use and privacy policies have been updated. Please %1$sreview%2$s and accept them.', 'w3-total-cache' ), '', '' ) . $buttons; } else { $notes['licensing_terms'] = sprintf( // translators: 1: Product name, 2: HTML break tag, 3: Anchor/link open tag, 4: Anchor/link close tag. esc_html__( 'By allowing us to collect data about how %1$s is used, we can improve our features and experience for everyone. This data will not include any personally identifiable information.%2$sFeel free to review our %3$sterms of use and privacy policy%4$s.', 'w3-total-cache' ), W3TC_POWERED_BY, '
', '', '' ) . $buttons; } } return $notes; } /** * Updates the license status if needed. * * Performs a license check and updates the configuration state accordingly. * * @return string The updated license status. */ private function maybe_update_license_status() { $state = Dispatcher::config_state(); if ( time() < $state->get_integer( 'license.next_check' ) ) { return; } $check_timeout = 3600 * 24 * 5; $status = ''; $terms = ''; $paypal_billing_update_required = false; $license_key = $this->get_license_key(); $old_plugin_type = $this->_config->get_string( 'plugin.type' ); $old_status = $state->get_string( 'license.status' ); $old_paypal_billing_update_required = $state->get_boolean( 'license.paypal_billing_update_required' ); $plugin_type = ''; if ( ! empty( $license_key ) || defined( 'W3TC_LICENSE_CHECK' ) ) { $license = Licensing_Core::check_license( $license_key, W3TC_VERSION ); if ( $license ) { $status = $license->license_status; $terms = $license->license_terms; if ( $this->_status_is( $status, 'active' ) ) { $plugin_type = 'pro'; } elseif ( $this->_status_is( $status, 'inactive.by_rooturi' ) && Util_Environment::is_w3tc_pro_dev() ) { $status = 'valid'; $plugin_type = 'pro_dev'; } // Check for PayPal billing update requirement. if ( isset( $license->paypal_billing_update_required ) ) { $paypal_billing_update_required = filter_var( $license->paypal_billing_update_required, FILTER_VALIDATE_BOOLEAN ); } } $this->_config->set( 'plugin.type', $plugin_type ); } else { $status = 'no_key'; } if ( 'no_key' === $status ) { // Do nothing. } elseif ( $this->_status_is( $status, 'invalid' ) ) { // Do nothing. } elseif ( $this->_status_is( $status, 'inactive' ) ) { // Do nothing. } elseif ( $this->_status_is( $status, 'active' ) ) { // Do nothing. } else { $check_timeout = 60; } // Clear dismissed notices when conditions change. if ( $old_paypal_billing_update_required && ! $paypal_billing_update_required ) { // PayPal billing update is no longer required, clear the dismissal for all users. $this->clear_dismissed_notice_for_all_users( 'paypal-billing-update-required' ); } if ( $old_status !== $status && ! empty( $old_status ) && ! empty( $status ) ) { // License status changed, clear the old status dismissal for all users. $this->clear_dismissed_notice_for_all_users( 'license-status-' . $this->sanitize_status_for_id( $old_status ) ); } $state->set( 'license.status', $status ); $state->set( 'license.next_check', time() + $check_timeout ); $state->set( 'license.terms', $terms ); $state->set( 'license.paypal_billing_update_required', $paypal_billing_update_required ); $state->save(); if ( $old_plugin_type !== $plugin_type ) { try { $this->_config->set( 'plugin.type', $plugin_type ); $this->_config->save(); } catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // missing exception handle? } } return $status; } /** * Retrieves the license key for the plugin. * * @return string The license key. */ public function get_license_key() { $license_key = $this->_config->get_string( 'plugin.license_key', '' ); if ( '' === $license_key ) { $license_key = ini_get( 'w3tc.license_key' ); } return $license_key; } /** * AJAX handler for dismissing license notices. * * Saves the dismissal timestamp in usermeta for persistent per-user dismissal. * * @since 2.9.1 * * @return void */ public function ajax_dismiss_license_notice() { check_ajax_referer( 'w3tc', '_wpnonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } $notice_id = isset( $_POST['notice_id'] ) ? sanitize_key( $_POST['notice_id'] ) : ''; if ( empty( $notice_id ) ) { wp_send_json_error( array( 'message' => 'Invalid notice ID' ) ); } $user_id = get_current_user_id(); $dismissed_notices = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true ); if ( ! is_array( $dismissed_notices ) ) { $dismissed_notices = array(); } $dismissed_notices[ $notice_id ] = time(); update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices ); wp_send_json_success( array( 'message' => 'Notice dismissed' ) ); } /** * AJAX handler for rechecking license status. * * Forces an immediate license check and clears cached billing URL. * * @since 2.9.1 * * @return void */ public function ajax_recheck_license() { check_ajax_referer( 'w3tc', '_wpnonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } // Force next check to happen immediately. $state = Dispatcher::config_state(); $state->set( 'license.next_check', 0 ); $state->save(); // Clear cached billing URL so a fresh one is fetched. $license_key = $this->get_license_key(); $transient_key = 'w3tc_billing_url_' . md5( $license_key ); delete_transient( $transient_key ); // Perform the license check now. $this->maybe_update_license_status(); wp_send_json_success( array( 'message' => 'License status rechecked' ) ); } /** * Checks if a specific license notice has been dismissed by the current user. * * Returns true if the notice was dismissed and the reset time has not elapsed. * If the reset time has elapsed and the condition still persists, the dismissal * is cleared and the notice will show again. * * @since 2.9.1 * * @param string $notice_id The unique identifier for the notice. * * @return bool True if the notice is dismissed and should not be shown. */ private function is_notice_dismissed( $notice_id ) { $user_id = get_current_user_id(); $dismissed_notices = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true ); if ( ! is_array( $dismissed_notices ) || ! isset( $dismissed_notices[ $notice_id ] ) ) { return false; } $dismissed_time = (int) $dismissed_notices[ $notice_id ]; $time_elapsed = time() - $dismissed_time; // If enough time has passed, clear the dismissal so the notice can show again. if ( $time_elapsed > self::NOTICE_DISMISSAL_RESET_TIME ) { unset( $dismissed_notices[ $notice_id ] ); update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices ); return false; } return true; } /** * Clears a specific dismissed notice for all users. * * This should be called when the condition that triggered the notice is resolved. * Uses a targeted query to only retrieve users who have the specific notice dismissed, * rather than all users with any dismissed notices. * * @since 2.9.1 * * @param string $notice_id The unique identifier for the notice to clear. * * @return void */ private function clear_dismissed_notice_for_all_users( $notice_id ) { global $wpdb; /* * Two-step approach for performance: * 1. Use LIKE query to narrow down candidate users (faster than loading all users). * Search for '"notice_id"' (with quotes) to match serialized array key format * and reduce false positives from partial matches. * 2. Validate with isset() to confirm exact key match, handling any edge cases. */ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $user_ids = $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value LIKE %s", self::NOTICE_DISMISSED_META_KEY, '%"' . $wpdb->esc_like( $notice_id ) . '"%' ) ); foreach ( $user_ids as $user_id ) { $dismissed_notices = get_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, true ); // Verify exact key match to handle any edge cases from LIKE query. if ( is_array( $dismissed_notices ) && isset( $dismissed_notices[ $notice_id ] ) ) { unset( $dismissed_notices[ $notice_id ] ); if ( empty( $dismissed_notices ) ) { delete_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY ); } else { update_user_meta( $user_id, self::NOTICE_DISMISSED_META_KEY, $dismissed_notices ); } } } } }