get_var( $wpdb->prepare( 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $wpdb->options ) ); return is_string( $engine ) && 'innodb' === strtolower( $engine ); } /** Public home and core admin may have different paths, but must share one HTTPS origin. */ function advocate_connect_urls() { $site = rtrim( home_url(), '/' ) . '/'; $return = admin_url( 'admin-post.php?action=advocate_connect_complete' ); $a = wp_parse_url( $site ); $b = wp_parse_url( $return ); if ( null === advocate_setup_website_identity( $site ) || ! is_array( $b ) || 'https' !== ( $a['scheme'] ?? null ) || 'https' !== ( $b['scheme'] ?? null ) || strtolower( $a['host'] ?? '' ) !== strtolower( $b['host'] ?? '' ) || ( $a['port'] ?? 443 ) !== ( $b['port'] ?? 443 ) || isset( $b['user'] ) || isset( $b['pass'] ) || isset( $b['fragment'] ) || ! preg_match( '~/(?:wp-admin)/admin-post\.php$~D', $b['path'] ?? '' ) || 'action=advocate_connect_complete' !== ( $b['query'] ?? '' ) ) { return null; } return [ 'site' => $site, 'return' => $return ]; } /** Snapshot SQL bytes, not potentially stale persistent-cache values. */ function advocate_connect_guard() { $guard = []; foreach ( [ 'home', 'siteurl', ADVOCATE_LIFECYCLE_OPTION ] as $key ) { $guard[ $key ] = advocate_raw_option( $key ); advocate_invalidate_option( $key ); } return $guard; } /** Invoked only by the capability- and nonce-checked administrator POST below. */ function advocate_begin_connect() { if ( ! current_user_can( 'manage_options' ) || get_current_user_id() < 1 || ! advocate_connect_storage_supported() ) { return null; } $guard = advocate_connect_guard(); if ( ! is_string( $guard[ ADVOCATE_LIFECYCLE_OPTION ] ) || '' === $guard[ ADVOCATE_LIFECYCLE_OPTION ] || 'inactive' === $guard[ ADVOCATE_LIFECYCLE_OPTION ] ) { return null; } $urls = advocate_connect_urls(); if ( null === $urls ) { return null; } // Ensure both rows exist for the guarded single-statement InnoDB completion. add_option( ADVOCATE_OPTION, [], '', 'no' ); $raw_settings = advocate_raw_option( ADVOCATE_OPTION ); if ( ! is_string( $raw_settings ) || ! is_array( maybe_unserialize( $raw_settings ) ) ) { return null; } // Starting a manual connection is intentional; an old prepared ZIP must not configure over it. add_option( ADVOCATE_SETUP_IMPORT_OPTION, 'processed', '', 'no' ); try { $state = rtrim( strtr( base64_encode( random_bytes( 32 ) ), '+/', '-_' ), '=' ); } catch ( Exception $error ) { return null; } $attempt = [ 'version' => 1, 'state_hash' => hash( 'sha256', $state ), 'user' => get_current_user_id(), 'blog' => get_current_blog_id(), 'site' => $urls['site'], 'guard' => $guard, 'settings' => $raw_settings, 'expires' => time() + ADVOCATE_CONNECT_TTL, 'status' => 'pending' ]; update_option( ADVOCATE_CONNECT_OPTION, $attempt, 'no' ); if ( advocate_raw_option( ADVOCATE_CONNECT_OPTION ) !== maybe_serialize( $attempt ) ) { return null; } return ADVOCATE_APP_BASE . '/connect/wordpress?' . http_build_query( [ 'v' => '1', 'site' => $urls['site'], 'return' => $urls['return'], 'state' => $state ], '', '&', PHP_QUERY_RFC3986 ); } /** Reject duplicate/array/extra keys before PHP's normal query parsing can collapse them. */ function advocate_connect_callback_input( $query ) { if ( ! is_string( $query ) || strlen( $query ) > 8192 || preg_match( '/%(?![a-f0-9]{2})/i', $query ) ) { return null; } $input = []; foreach ( explode( '&', $query ) as $pair ) { $parts = explode( '=', $pair, 2 ); if ( 2 !== count( $parts ) ) { return null; } $key = urldecode( $parts[0] ); $value = urldecode( $parts[1] ); if ( array_key_exists( $key, $input ) || ! in_array( $key, [ 'action', 'v', 'state', 'businessSlug', 'website', 'next' ], true ) ) { return null; } $input[ $key ] = $value; } if ( 6 !== count( $input ) || 'advocate_connect_complete' !== ( $input['action'] ?? '' ) || '1' !== ( $input['v'] ?? '' ) || ! preg_match( '/^[A-Za-z0-9_-]{43}$/D', $input['state'] ?? '' ) || ! in_array( $input['next'] ?? '', [ 'onboard', 'wordpress-setup' ], true ) ) { return null; } $setup = advocate_parse_packaged_setup( json_encode( [ 'version' => 1, 'businessSlug' => $input['businessSlug'] ?? null, 'website' => $input['website'] ?? null ] ) ); return null === $setup ? null : $input; } /** One SQL statement consumes this attempt and changes settings together. No remote HTTP. */ function advocate_apply_connect( $input ) { global $wpdb; if ( ! is_array( $input ) || ! current_user_can( 'manage_options' ) || get_current_user_id() < 1 || ! advocate_connect_storage_supported() ) { return null; } $input = advocate_connect_callback_input( http_build_query( $input, '', '&', PHP_QUERY_RFC3986 ) ); if ( null === $input ) { return null; } $raw = advocate_raw_option( ADVOCATE_CONNECT_OPTION ); $attempt = maybe_unserialize( $raw ); if ( ! is_array( $attempt ) || 1 !== ( $attempt['version'] ?? null ) || ( $attempt['user'] ?? null ) !== get_current_user_id() || ( $attempt['blog'] ?? null ) !== get_current_blog_id() || ! is_string( $attempt['state_hash'] ?? null ) || ! hash_equals( $attempt['state_hash'], hash( 'sha256', $input['state'] ) ) || ! is_int( $attempt['expires'] ?? null ) || $attempt['expires'] <= time() || ! is_array( $attempt['guard'] ?? null ) || $attempt['guard'] !== advocate_connect_guard() || ( advocate_connect_urls()['site'] ?? null ) !== ( $attempt['site'] ?? null ) || advocate_setup_website_identity( $input['website'] ) !== advocate_setup_website_identity( $attempt['site'] ) ) { return null; } $result = [ 'businessSlug' => $input['businessSlug'], 'website' => $input['website'], 'next' => $input['next'] ]; $destination = ADVOCATE_APP_BASE . ( 'onboard' === $input['next'] ? '/onboard' : '/owner/wordpress-setup' ); $current_settings = advocate_raw_option( ADVOCATE_OPTION ); if ( 'completed' === ( $attempt['status'] ?? null ) ) { // Replay may navigate, but can never recreate or re-enable a locally changed configuration. return ( $attempt['result'] ?? null ) === $result && ( $attempt['applied_settings'] ?? null ) === $current_settings ? $destination : null; } if ( 'pending' !== ( $attempt['status'] ?? null ) || ( $attempt['settings'] ?? null ) !== $current_settings ) { return null; } $settings = maybe_unserialize( $current_settings ); if ( ! is_array( $settings ) ) { return null; } $old_slug = is_string( $settings['agent_id'] ?? null ) ? trim( $settings['agent_id'] ) : ''; $settings['agent_id'] = $input['businessSlug']; // An explicit off remains off. Configuration and permission to publish are separate. if ( ! array_key_exists( 'inject_jsonld', $settings ) ) { $settings['inject_jsonld'] = true; } $next_settings = maybe_serialize( $settings ); $completed = $attempt; $completed['status'] = 'completed'; $completed['result'] = $result; $completed['applied_settings'] = $next_settings; $joins = ''; $guards = ''; $args = [ ADVOCATE_OPTION ]; foreach ( array_keys( $attempt['guard'] ) as $i => $key ) { $joins .= "LEFT JOIN $wpdb->options AS cfg$i ON cfg$i.option_name = %s "; $args[] = $key; } $args = array_merge( $args, [ maybe_serialize( $completed ), $next_settings, ADVOCATE_CONNECT_OPTION, $raw, $current_settings ] ); foreach ( array_values( $attempt['guard'] ) as $i => $value ) { if ( null === $value ) { $guards .= " AND cfg$i.option_name IS NULL"; } else { $guards .= " AND BINARY cfg$i.option_value = BINARY %s"; $args[] = $value; } } $args[] = $attempt['expires']; $sql = "UPDATE $wpdb->options AS attempt JOIN $wpdb->options AS settings ON settings.option_name = %s $joins" . "SET attempt.option_value = %s, settings.option_value = %s, settings.autoload = 'no' " . "WHERE attempt.option_name = %s AND BINARY attempt.option_value = BINARY %s AND BINARY settings.option_value = BINARY %s" . $guards . ' AND %d > UNIX_TIMESTAMP()'; // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.NotPrepared -- Only fixed SQL clauses/trusted WP table names are assembled; all attempt/settings/config values use prepare placeholders. $changed = $wpdb->query( $wpdb->prepare( $sql, ...$args ) ); advocate_invalidate_option( ADVOCATE_OPTION ); advocate_invalidate_option( ADVOCATE_CONNECT_OPTION ); // MySQL counts one changed attempt row and optionally one changed settings row. if ( 1 !== $changed && 2 !== $changed ) { return null; } // Raw SQL bypasses the normal option hook; retain its former-business cleanup. advocate_clear_previous_business_cache( $old_slug, $input['businessSlug'] ); advocate_schedule_background_refresh(); return $destination; } /** * The raw callback query through WordPress's URL sanitizer (which keeps the * `%XX & = . - _ ~` bytes a well-formed callback is made of). The strict parser * then rejects anything the sanitizer altered. sanitize_text_field() is the * wrong tool here: it strips every percent-encoded byte, so `state` and * `website` would never survive it. */ function advocate_connect_callback_query() { return isset( $_SERVER['QUERY_STRING'] ) ? filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_SANITIZE_URL ) : null; } /** The parsed callback input for a GET with a well-formed query, else null. */ function advocate_connect_request_input() { if ( 'GET' !== advocate_request_method() ) { return null; } $query = advocate_connect_callback_query(); return is_string( $query ) ? advocate_connect_callback_input( $query ) : null; } /** Display only: the error redirect below sets this flag, and reading it changes nothing. */ function advocate_connect_retry_requested() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only notice flag set by this plugin's own redirect; no state is read from or written through it. return isset( $_GET['advocate_connect'] ) && 'retry' === sanitize_key( wp_unslash( $_GET['advocate_connect'] ) ); } function advocate_connect_error_redirect() { wp_safe_redirect( add_query_arg( [ 'page' => ADVOCATE_SETTINGS_PAGE, 'advocate_connect' => 'retry' ], admin_url( 'options-general.php' ) ) ); exit; } function advocate_connect_redirect_hosts( $hosts, $host ) { $app_host = wp_parse_url( ADVOCATE_APP_BASE, PHP_URL_HOST ); if ( $host === $app_host ) { $hosts[] = $app_host; } return $hosts; } /** Only this handoff's fixed application routes may leave WordPress. */ function advocate_connect_app_target( $href ) { if ( ! is_string( $href ) || strlen( $href ) > 8192 ) { return false; } $parts = wp_parse_url( $href ); if ( ! is_array( $parts ) || 'https' !== ( $parts['scheme'] ?? null ) || wp_parse_url( ADVOCATE_APP_BASE, PHP_URL_HOST ) !== ( $parts['host'] ?? null ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['port'] ) || isset( $parts['fragment'] ) || ! in_array( $parts['path'] ?? '', [ '/connect/wordpress', '/onboard', '/owner/wordpress-setup' ], true ) || ( '/connect/wordpress' !== $parts['path'] && isset( $parts['query'] ) ) ) { return false; } return true; } function advocate_connect_app_redirect( $href ) { if ( ! advocate_connect_app_target( $href ) ) { advocate_connect_error_redirect(); } add_filter( 'allowed_redirect_hosts', 'advocate_connect_redirect_hosts', 10, 2 ); wp_safe_redirect( $href ); remove_filter( 'allowed_redirect_hosts', 'advocate_connect_redirect_hosts', 10 ); exit; } add_action( 'admin_post_advocate_connect_start', function () { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'advocatemcp-structured-data' ) ); } check_admin_referer( 'advocate_connect_start' ); nocache_headers(); header( 'Referrer-Policy: no-referrer' ); if ( 'POST' !== advocate_request_method() ) { advocate_connect_error_redirect(); } $href = advocate_begin_connect(); if ( null === $href ) { advocate_connect_error_redirect(); } // Fixed trusted origin, never a caller-controlled redirect destination. advocate_connect_app_redirect( $href ); } ); add_action( 'admin_post_advocate_connect_complete', function () { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'advocatemcp-structured-data' ) ); } nocache_headers(); header( 'Referrer-Policy: no-referrer' ); $input = advocate_connect_request_input(); $href = null === $input ? null : advocate_apply_connect( $input ); if ( null === $href ) { advocate_connect_error_redirect(); } advocate_connect_app_redirect( $href ); } ); function advocate_connect_settings() { if ( advocate_connect_retry_requested() ) { echo '
' . esc_html__( 'Connection was not applied. Your settings were kept. Start again, or use the manual Business ID below. HTTPS and a WordPress admin on the same website are required.', 'advocatemcp-structured-data' ) . '