_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( '