文件操作 - Connect.php
返回文件管理
返回主菜单
删除本文件
文件: /home/www/public/wp-content/plugins/popup-maker/classes/Services/Connect.php
编辑文件内容
<?php /** * Connect service. * * @package PopupMaker * @copyright Copyright (c) 2024, Code Atlantic LLC */ namespace PopupMaker\Services; use PopupMaker\Base\Service; use function PopupMaker\plugin; defined( 'ABSPATH' ) || exit; /** * Connection management. * * NOTE: For wordpress.org admins: This is not called in the free, hosted version. This is only used if: * - The user explicitly entered a license key. * - This then opens a window to our site allowing the user to authorize one-time oauth-style connection & installation of pro. * * @package PopupMaker */ class Connect extends Service { const API_URL = 'https://upgrade.wppopupmaker.com/'; const TOKEN_OPTION_NAME = 'popup_maker_connect_token'; const NONCE_OPTION_NAME = 'popup_maker_connect_nonce'; const ERROR_REFERRER = 1; const ERROR_AUTHENTICATION = 2; const ERROR_USER_AGENT = 3; const ERROR_SIGNATURE = 4; const ERROR_NONCE = 5; const ERROR_WEBHOOK_ARGS = 6; /** * Check if debug mode is enabled. * * @return bool */ public function debug_mode_enabled() { return defined( '\WP_DEBUG' ) && \WP_DEBUG; } /** * Generate a new authorizatin token. * * @return string */ public function generate_token() { $token = hash( 'sha512', (string) wp_rand() ); \set_site_transient( self::TOKEN_OPTION_NAME, $token, HOUR_IN_SECONDS ); return $token; } /** * Get the current token. * * @return string|false */ public function get_access_token() { return \get_site_transient( self::TOKEN_OPTION_NAME ); } /** * Get the current nonce. * * @param string $token Token. * * @return string|false */ public function get_nonce_name( $token ) { return self::NONCE_OPTION_NAME . '_' . $token; } /** * Log a message to the debug log if enabled. * * Here to prevent constant conditional checks for the debug mode. * * @param string $message Message. * @param string $type Type. * * @return void */ public function debug_log( $message, $type = 'INFO' ) { if ( $this->debug_mode_enabled() ) { plugin( 'logging' )->log( "Plugin\Connect.$type: $message" ); } } /** * Get header Authorization * * @return null|string */ public function get_request_authorization_header() { $headers = null; if ( isset( $_SERVER['Authorization'] ) ) { $headers = trim( sanitize_text_field( wp_unslash( $_SERVER['Authorization'] ) ) ); } elseif ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) { // Nginx or fast CGI. $headers = trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) ) ); } elseif ( function_exists( 'apache_request_headers' ) ) { $request_headers = apache_request_headers(); // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization). $request_headers = array_combine( array_map( 'ucwords', array_keys( $request_headers ) ), array_values( $request_headers ) ); if ( isset( $request_headers['Authorization'] ) ) { $headers = trim( $request_headers['Authorization'] ); } } // Fallback: Some servers strip Authorization headers, check for custom headers if ( ! $headers ) { $fallback_headers = [ 'HTTP_X_AUTH_TOKEN' => 'X-Auth-Token', 'HTTP_X_AUTH_TOKEN_UC' => 'X-AUTH-TOKEN (uppercase)', // This won't match, but keeping for clarity 'HTTP_HTTP_X_AUTH_TOKEN' => 'HTTP_X_AUTH_TOKEN (server format)', ]; foreach ( $fallback_headers as $server_key => $header_name ) { if ( isset( $_SERVER[ $server_key ] ) ) { $token = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) ); $headers = 'Bearer ' . $token; // Using fallback header. break; } } } return $headers; } /** * Get access token from header. * * @return string|null */ public function get_request_token() { $headers = $this->get_request_authorization_header(); // HEADER: Get the access token from the header. if ( ! empty( $headers ) ) { if ( preg_match( '/Bearer\s(\S+)/', $headers, $matches ) ) { return trim( $matches[1], ', ' ); } } return null; } /** * Get nonce from header. * * @return string|null */ public function get_request_nonce() { $headers = $this->get_request_authorization_header(); // HEADER: Get the nonce from the header. if ( ! empty( $headers ) ) { if ( preg_match( '/Nonce\s(\S+)/', $headers, $matches ) ) { return trim( $matches[1], ', ' ); } } return null; } /** * Get the webhook signature header. * * @return string */ private function get_request_signature() { if ( isset( $_SERVER['HTTP_X_CONTENTCONTROL_SIGNATURE'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_CONTENTCONTROL_SIGNATURE'] ) ); } if ( isset( $_SERVER['HTTP_X_POPUPMAKER_SIGNATURE'] ) ) { return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_POPUPMAKER_SIGNATURE'] ) ); } return ''; } /** * Get request data used to verify the webhook signature. * * @param \WP_REST_Request|null $request Optional REST request object. * @return array<string,mixed> */ private function get_signature_request_data( $request = null ) { if ( $request instanceof \WP_REST_Request ) { $json_params = $request->get_json_params(); if ( is_array( $json_params ) && ! empty( $json_params ) ) { return $json_params; } $body_params = $request->get_body_params(); if ( is_array( $body_params ) && ! empty( $body_params ) ) { return $body_params; } } $request_data = json_decode( file_get_contents( 'php://input' ), true ); if ( is_array( $request_data ) && ! empty( $request_data ) ) { return $request_data; } // phpcs:ignore WordPress.Security.NonceVerification.Missing return ! empty( $_POST ) ? $_POST : []; } /** * Throw a connection validation exception for REST API handlers. * * @param int $error_no Error number. * @param string $message Error message. * @return void * @throws \Exception If validation fails. */ private function throw_connection_error( $error_no, $message ) { throw new \Exception( esc_html( $message ), (int) $error_no ); } /** * Normalize the sending domain header into a host. * * @param string $referer Sending domain or URL. * @return string */ private function normalize_sending_domain( $referer ) { $referer_host = wp_parse_url( $referer, PHP_URL_HOST ); if ( ! $referer_host && preg_match( '/^[a-z0-9.-]+$/i', $referer ) ) { $referer_host = $referer; } return is_string( $referer_host ) ? strtolower( $referer_host ) : ''; } /** * Get allowed sending hosts. * * @return string[] */ private function get_allowed_sending_hosts() { $allowed_hosts = [ 'wppopupmaker.com', 'upgrade.wppopupmaker.com', ]; /** * Filter the allowed hosts for signed Popup Maker install webhooks. * * @param string[] $allowed_hosts Allowed host names. */ $allowed_hosts = apply_filters( 'popup_maker/connect/allowed_sending_hosts', $allowed_hosts ); return array_values( array_filter( array_map( 'strtolower', (array) $allowed_hosts ) ) ); } /** * Assert the user agent is a Popup Maker upgrader. * * @return void * @throws \Exception If user agent validation fails. */ private function assert_user_agent() { $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( empty( $user_agent ) || ! preg_match( '/^PopupMakerUpgrader\/\d+\.\d+\.\d+$/', $user_agent ) ) { $this->throw_connection_error( self::ERROR_USER_AGENT, 'User agent invalid: ' . $user_agent ); } } /** * Assert the request has an allowed sending domain. * * @return void * @throws \Exception If referrer validation fails. */ private function assert_referrer() { $referer = isset( $_SERVER['HTTP_X_SENDING_DOMAIN'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SENDING_DOMAIN'] ) ) : ''; if ( ! $referer ) { $this->throw_connection_error( self::ERROR_REFERRER, 'Missing referrer' ); } $referer_host = $this->normalize_sending_domain( $referer ); if ( ! $referer_host ) { $this->throw_connection_error( self::ERROR_REFERRER, 'Invalid referrer' ); } if ( ! in_array( $referer_host, $this->get_allowed_sending_hosts(), true ) ) { $this->throw_connection_error( self::ERROR_REFERRER, 'Referrer doesn\'t match' ); } } /** * Assert the bearer token matches the locally stored token. * * @return void * @throws \Exception If authentication validation fails. */ private function assert_authentication() { $token = $this->get_access_token(); $auth_token = $this->get_request_token(); if ( ! $token || ! $auth_token ) { $this->throw_connection_error( self::ERROR_AUTHENTICATION, 'Missing authentication' ); } if ( ! hash_equals( $token, $auth_token ) ) { $this->throw_connection_error( self::ERROR_AUTHENTICATION, 'Invalid authentication' ); } } /** * Assert the request signature matches the signed payload. * * @param \WP_REST_Request|null $request Optional REST request object. * @return void * @throws \Exception If signature validation fails. */ private function assert_signature( $request = null ) { $signature = $this->get_request_signature(); if ( empty( $signature ) ) { $this->throw_connection_error( self::ERROR_SIGNATURE, 'Missing signature' ); } $request_data = $this->get_signature_request_data( $request ); $expected_signature = $this->generate_hash( $request_data, $this->get_access_token() ); if ( ! hash_equals( $expected_signature, $signature ) ) { $this->throw_connection_error( self::ERROR_SIGNATURE, 'Invalid signature' ); } } /** * Get the OAuth connect URL. * * @param string $license_key License key. * * @return array{url:string,back_url:string} */ public function get_connect_info( $license_key ) { $token = $this->generate_token(); $nonce = wp_create_nonce( $this->get_nonce_name( $token ) ); $webhook = rest_url( 'popup-maker/v2/connect/install' ); $redirect = add_query_arg( [ 'post_type' => 'popup', 'page' => 'pum-settings', // TODO This should be set to the Pro upgrade page? 'view' => 'upgrade', 'tab' => 'general', ], admin_url( 'edit.php' ) ); $url = add_query_arg( [ 'key' => $license_key, 'token' => $token, 'nonce' => $nonce, 'webhook' => $webhook, 'version' => plugin( 'version' ), 'siteurl' => admin_url(), 'homeurl' => home_url(), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'redirect' => rawurlencode( base64_encode( $redirect ) ), ], self::API_URL ); $this->debug_log( 'Generated new connection.' ); $this->debug_log( 'Token: ' . $token ); $this->debug_log( 'Nonce: ' . $nonce ); $this->debug_log( 'Redirect URL: ' . $redirect ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $this->debug_log( 'Base64 Redirect: ' . base64_encode( $redirect ) ); $this->debug_log( 'Final URL: ' . $url ); return [ 'url' => $url, 'back_url' => add_query_arg( [ 'action' => 'popup_maker_connect', 'token' => $token, ], $webhook ), ]; } /** * Kill the connection with no permission. * * @param int $error_no Error number. * @param string|false $message Error message. * * @return void */ public function kill_connection( $error_no = self::ERROR_REFERRER, $message = false ) { $this->debug_log( "Killing connection with error ($error_no) message: " . $message, 'ERROR' ); wp_die( esc_html( $this->debug_mode_enabled() && $message ? $message : __( 'Sorry, You Are Not Allowed to Access This Page.', 'popup-maker' ) ), esc_attr( (string) $error_no ), [ 'response' => 403 ] ); } /** * Verify the user agent. * * @return void */ public function verify_user_agent() { try { $this->assert_user_agent(); } catch ( \Exception $e ) { $this->kill_connection( self::ERROR_USER_AGENT, $e->getMessage() ); } } /** * Verify the referrer. * * @return void */ public function verify_referrer() { try { $this->assert_referrer(); } catch ( \Exception $e ) { $this->kill_connection( self::ERROR_REFERRER, $e->getMessage() ); } } /** * Verify the nonce. * * @deprecated 2.0.0 Don't use, it doesn't work as its a separate server making request. * * @return void */ public function verify_nonce() { $token = $this->get_access_token(); $nonce = $this->get_request_nonce(); if ( ! $nonce ) { $this->kill_connection( self::ERROR_NONCE, 'Missing nonce' ); } if ( false === wp_verify_nonce( $nonce, $this->get_nonce_name( $token ) ) ) { $this->debug_log( 'Nonce mismatch: ' . $nonce, 'DEBUG' ); $this->debug_log( 'Nonce Name: ' . $this->get_nonce_name( $token ) ); $this->kill_connection( self::ERROR_NONCE, 'Invalid nonce' ); } } /** * Verify the authentication token. * * @return void */ public function verify_authentication() { try { $this->assert_authentication(); } catch ( \Exception $e ) { $this->kill_connection( self::ERROR_AUTHENTICATION, $e->getMessage() ); } } /** * Generate signature hash. * * This must match the hash generated on the server. * * @see \PopupMakerUpgrader\App::generate_hash() * * @param array<string,mixed>|string $data Data to hash. * @param string $token Token to hash with. * @return string */ public function generate_hash( $data, $token ) { if ( ! is_string( $data ) ) { // Convert boolean values to their string representation. array_walk_recursive( $data, function ( &$value ) { if ( is_bool( $value ) ) { $value = $value ? '1' : '0'; } } ); // Sort the array before encoding it as JSON. ksort( $data ); // Ignored due to wp_json_encode stripping slashes and breaking hashes. // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode $data = json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } // Generate the hash binary. $hash = hash_hmac( 'sha256', $data, $token, true ); // Generate HMAC-SHA256 signature. // Encode the hash in base64 to make it URL safe. // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( $hash ); } /** * Verify the signature of the requester. * * @return void */ public function verify_signature() { try { $this->assert_signature(); } catch ( \Exception $e ) { $this->kill_connection( self::ERROR_SIGNATURE, $e->getMessage() ); } } /** * Validate the connection. * * @return void */ public function validate_connection() { // Validate connection security layers. $this->verify_user_agent(); $this->verify_referrer(); $this->verify_authentication(); $this->verify_signature(); } /** * Validate a REST webhook connection. * * @param \WP_REST_Request|null $request Optional REST request object. * @return void * @throws \Exception If validation fails. */ public function validate_rest_connection( $request = null ) { $this->assert_user_agent(); $this->assert_referrer(); $this->assert_authentication(); $this->assert_signature( $request ); } /** * Verify the connection. * * @return void */ public function process_verify_connection() { $this->validate_connection(); wp_send_json_success(); } /** * Get the webhook args. * * @return array{file:string,type:string,slug:string,force:boolean} */ public function get_webhook_args() { // First try to get data from JSON body (modern REST API approach) $json_data = json_decode( file_get_contents( 'php://input' ), true ); if ( is_array( $json_data ) && ! empty( $json_data ) ) { $this->debug_log( 'Using JSON body data for webhook args', 'DEBUG' ); $this->debug_log( 'JSON data: ' . wp_json_encode( $json_data, JSON_PRETTY_PRINT ), 'DEBUG' ); $args = [ 'file' => ! empty( $json_data['file'] ) ? esc_url_raw( $json_data['file'] ) : '', 'type' => ! empty( $json_data['type'] ) ? sanitize_text_field( $json_data['type'] ) : 'plugin', 'slug' => ! empty( $json_data['slug'] ) ? sanitize_text_field( $json_data['slug'] ) : '', 'force' => ! empty( $json_data['force'] ) ? (bool) $json_data['force'] : false, ]; } else { // Fallback to $_REQUEST for backwards compatibility $this->debug_log( 'Using $_REQUEST data for webhook args', 'DEBUG' ); $args = [ // phpcs:disable WordPress.Security.NonceVerification.Recommended 'file' => ! empty( $_REQUEST['file'] ) ? esc_url_raw( wp_unslash( $_REQUEST['file'] ) ) : '', 'type' => ! empty( $_REQUEST['type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['type'] ) ) : 'plugin', 'slug' => ! empty( $_REQUEST['slug'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['slug'] ) ) : '', 'force' => ! empty( $_REQUEST['force'] ) ? (bool) $_REQUEST['force'] : false, // phpcs:enable WordPress.Security.NonceVerification.Recommended ]; } $this->debug_log( 'Parsed webhook args: ' . wp_json_encode( $args, JSON_PRETTY_PRINT ), 'DEBUG' ); $this->verify_webhook_args( $args ); return $args; } /** * Verify and return webhook args. * * @param array{file:string,type:string,slug:string,force:bool} $args The webhook args. * * @return void */ public function verify_webhook_args( $args ) { $file_url = ! empty( $args['file'] ) ? $args['file'] : false; $file_type = ! empty( $args['type'] ) ? $args['type'] : false; $file_slug = ! empty( $args['slug'] ) ? $args['slug'] : false; $force = ! empty( $args['force'] ) ? (bool) $args['force'] : false; if ( ! $file_url || ! $file_type || ! $file_slug ) { $this->kill_connection( self::ERROR_WEBHOOK_ARGS, 'Missing webhook args' ); } if ( ! in_array( $file_type, [ 'plugin', 'theme' ], true ) ) { $this->kill_connection( self::ERROR_WEBHOOK_ARGS, 'Invalid webhook args' ); } } /** * Listen for incoming secure webhooks from the API server. * * @return void */ public function process_webhook() { // 1. Validate the connection is secure & from the API server. $this->validate_connection(); $error = esc_html__( 'There was an error while installing an upgrade. Please download the plugin from wppopupmaker.com and install it manually.', 'popup-maker' ); // 2. Get the webhook data. $args = $this->get_webhook_args(); // 3. Validate license key. if ( ! plugin( 'license' )->is_license_active() ) { $this->debug_log( 'License not active', 'DEBUG' ); wp_send_json_error( $error ); } // Set the current screen to avoid undefined notices. set_current_screen( 'settings_page_popup-maker-settings' ); // 4. Install the plugin. switch ( $args['type'] ) { case 'plugin': $this->install_plugin( $args ); break; } // 5. Delete the token to prevent abuse. Doing so last means it should be possible to retry if something goes wrong. if ( ! $this->debug_mode_enabled() ) { $this->debug_log( 'Deleting token', 'DEBUG' ); \delete_site_transient( self::TOKEN_OPTION_NAME ); } } /** * Install a plugin. * * @param array{file:string,type:string,slug:string,force:bool} $args The file args. * @return void */ public function install_plugin( $args ) { $this->debug_log( 'Installing plugin...', 'DEBUG' ); // If not forcing, and already active, return success. if ( ! $args['force'] && is_plugin_active( "{$args['slug']}/{$args['slug']}.php" ) ) { $this->debug_log( 'Plugin already installed & active.', 'DEBUG' ); wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'popup-maker' ) ); } // Get the upgrader. $upgrader = $this->container->get( 'upgrader' ); // Install the plugin. (if installed already, this will replace it using upgrade). $installed = $upgrader->install_plugin( $args['file'] ); if ( ! is_wp_error( $installed ) ) { $this->debug_log( 'Plugin installed & activated successfully.', 'DEBUG' ); wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'popup-maker' ) ); } switch ( $installed->get_error_code() ) { default: $error = $installed->get_error_message(); } if ( empty( $error ) ) { $error = esc_html__( 'There was an error while installing an upgrade. Please download the plugin from wppopupmaker.com and install it manually.', 'popup-maker' ); } $this->debug_log( 'Plugin install failed: ' . $error, 'DEBUG' ); wp_send_json_error( $installed->get_error_message() ); } /** * Verify webhook request for REST API. * * Validates webhook requests coming through REST endpoints. * This method performs the same security checks as the AJAX webhook * but is designed for REST API integration. * * @param \WP_REST_Request $request REST request object. * @return array{valid:bool,error:string|null} */ public function verify_webhook_request( $request ) { $this->debug_log( 'Verifying webhook request via REST API...', 'DEBUG' ); try { // Validate the connection security. $this->validate_rest_connection( $request ); // Validate license is active. if ( ! plugin( 'license' )->is_license_active() ) { $this->debug_log( 'License not active for webhook request', 'DEBUG' ); return [ 'valid' => false, 'error' => 'License not active', ]; } $this->debug_log( 'Webhook request verified successfully', 'DEBUG' ); return [ 'valid' => true, 'error' => null, ]; } catch ( \Exception $e ) { $this->debug_log( 'Webhook verification failed: ' . $e->getMessage(), 'ERROR' ); return [ 'valid' => false, 'error' => $e->getMessage(), ]; } } /** * Generate webhook URL for REST endpoints. * * Creates properly formatted webhook URLs for REST API endpoints * with appropriate namespace and security parameters. * * @param string $endpoint The REST endpoint (e.g., 'connect/verify', 'upgrade/install'). * @param array $args Additional query arguments. * @return string The webhook URL. */ public function generate_webhook_url( $endpoint, $args = [] ) { // Generate security token. $token = $this->generate_token(); $nonce = wp_create_nonce( $this->get_nonce_name( $token ) ); // Base REST URL with our namespace. $base_url = rest_url( 'popup-maker/v2/' . ltrim( $endpoint, '/' ) ); // Add security parameters. $default_args = [ 'token' => $token, 'nonce' => $nonce, ]; $query_args = array_merge( $default_args, $args ); $webhook_url = add_query_arg( $query_args, $base_url ); $this->debug_log( 'Generated webhook URL: ' . $webhook_url ); $this->debug_log( 'Token: ' . $token ); $this->debug_log( 'Nonce: ' . $nonce ); return $webhook_url; } }
修改文件时间
将文件时间修改为当前时间的前一年
删除文件