comments feeds, keyed by their regex pattern. */ $comments_rewrite = apply_filters( 'comments_rewrite_rules', $comments_rewrite ); // Search rewrite rules. $search_structure = $this->get_search_permastruct(); $search_rewrite = $this->generate_rewrite_rules( $search_structure, EP_SEARCH ); /** * Filters rewrite rules used for search archives. * * Likely search-related archives include `/search/search+query/` as well as * pagination and feed paths for a search. * * @since 1.5.0 * * @param string[] $search_rewrite Array of rewrite rules for search queries, keyed by their regex pattern. */ $search_rewrite = apply_filters( 'search_rewrite_rules', $search_rewrite ); // Author rewrite rules. $author_rewrite = $this->generate_rewrite_rules( $this->get_author_permastruct(), EP_AUTHORS ); /** * Filters rewrite rules used for author archives. * * Likely author archives would include `/author/author-name/`, as well as * pagination and feed paths for author archives. * * @since 1.5.0 * * @param string[] $author_rewrite Array of rewrite rules for author archives, keyed by their regex pattern. */ $author_rewrite = apply_filters( 'author_rewrite_rules', $author_rewrite ); // Pages rewrite rules. $page_rewrite = $this->page_rewrite_rules(); /** * Filters rewrite rules used for "page" post type archives. * * @since 1.5.0 * * @param string[] $page_rewrite Array of rewrite rules for the "page" post type, keyed by their regex pattern. */ $page_rewrite = apply_filters( 'page_rewrite_rules', $page_rewrite ); // Extra permastructs. foreach ( $this->extra_permastructs as $permastructname => $struct ) { if ( is_array( $struct ) ) { if ( count( $struct ) === 2 ) { $rules = $this->generate_rewrite_rules( $struct[0], $struct[1] ); } else { $rules = $this->generate_rewrite_rules( $struct['struct'], $struct['ep_mask'], $struct['paged'], $struct['feed'], $struct['forcomments'], $struct['walk_dirs'], $struct['endpoints'] ); } } else { $rules = $this->generate_rewrite_rules( $struct ); } /** * Filters rewrite rules used for individual permastructs. * * The dynamic portion of the hook name, `$permastructname`, refers * to the name of the registered permastruct. * * Possible hook names include: * * - `category_rewrite_rules` * - `post_format_rewrite_rules` * - `post_tag_rewrite_rules` * * @since 3.1.0 * * @param string[] $rules Array of rewrite rules generated for the current permastruct, keyed by their regex pattern. */ $rules = apply_filters( "{$permastructname}_rewrite_rules", $rules ); if ( 'post_tag' === $permastructname ) { /** * Filters rewrite rules used specifically for Tags. * * @since 2.3.0 * @deprecated 3.1.0 Use {@see 'post_tag_rewrite_rules'} instead. * * @param string[] $rules Array of rewrite rules generated for tags, keyed by their regex pattern. */ $rules = apply_filters_deprecated( 'tag_rewrite_rules', array( $rules ), '3.1.0', 'post_tag_rewrite_rules' ); } $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules ); } // Put them together. if ( $this->use_verbose_page_rules ) { $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules ); } else { $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $post_rewrite, $page_rewrite, $this->extra_rules ); } /** * Fires after the rewrite rules are generated. * * @since 1.5.0 * * @param WP_Rewrite $wp_rewrite Current WP_Rewrite instance (passed by reference). */ do_action_ref_array( 'generate_rewrite_rules', array( &$this ) ); /** * Filters the full set of generated rewrite rules. * * @since 1.5.0 * * @param string[] $rules The compiled array of rewrite rules, keyed by their regex pattern. */ $this->rules = apply_filters( 'rewrite_rules_array', $this->rules ); return $this->rules; } /** * Retrieves the rewrite rules. * * The difference between this method and WP_Rewrite::rewrite_rules() is that * this method stores the rewrite rules in the 'rewrite_rules' option and retrieves * it. This prevents having to process all of the permalinks to get the rewrite rules * in the form of caching. * * @since 1.5.0 * * @return string[] Array of rewrite rules keyed by their regex pattern. */ public function wp_rewrite_rules() { $this->rules = get_option( 'rewrite_rules' ); if ( empty( $this->rules ) ) { $this->refresh_rewrite_rules(); } return $this->rules; } /** * Refreshes the rewrite rules, saving the fresh value to the database. * If the `wp_loaded` action has not occurred yet, will postpone saving to the database. * * @since 6.4.0 */ private function refresh_rewrite_rules() { $this->rules = ''; $this->matches = 'matches'; $this->rewrite_rules(); if ( ! did_action( 'wp_loaded' ) ) { /* * Is not safe to save the results right now, as the rules may be partial. * Need to give all rules the chance to register. */ add_action( 'wp_loaded', array( $this, 'flush_rules' ) ); } else { update_option( 'rewrite_rules', $this->rules ); } } /** * Retrieves mod_rewrite-formatted rewrite rules to write to .htaccess. * * Does not actually write to the .htaccess file, but creates the rules for * the process that will. * * Will add the non_wp_rules property rules to the .htaccess file before * the WordPress rewrite rules one. * * @since 1.5.0 * * @return string */ public function mod_rewrite_rules() { if ( ! $this->using_permalinks() ) { return ''; } $site_root = parse_url( site_url() ); if ( isset( $site_root['path'] ) ) { $site_root = trailingslashit( $site_root['path'] ); } $home_root = parse_url( home_url() ); if ( isset( $home_root['path'] ) ) { $home_root = trailingslashit( $home_root['path'] ); } else { $home_root = '/'; } $rules = "\n"; $rules .= "RewriteEngine On\n"; $rules .= "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n"; $rules .= "RewriteBase $home_root\n"; // Prevent -f checks on index.php. $rules .= "RewriteRule ^index\.php$ - [L]\n"; // Add in the rules that don't redirect to WP's index.php (and thus shouldn't be handled by WP at all). foreach ( (array) $this->non_wp_rules as $match => $query ) { // Apache 1.3 does not support the reluctant (non-greedy) modifier. $match = str_replace( '.+?', '.+', $match ); $rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n"; } if ( $this->use_verbose_rules ) { $this->matches = ''; $rewrite = $this->rewrite_rules(); $num_rules = count( $rewrite ); $rules .= "RewriteCond %{REQUEST_FILENAME} -f [OR]\n" . "RewriteCond %{REQUEST_FILENAME} -d\n" . "RewriteRule ^.*$ - [S=$num_rules]\n"; foreach ( (array) $rewrite as $match => $query ) { // Apache 1.3 does not support the reluctant (non-greedy) modifier. $match = str_replace( '.+?', '.+', $match ); if ( str_contains( $query, $this->index ) ) { $rules .= 'RewriteRule ^' . $match . ' ' . $home_root . $query . " [QSA,L]\n"; } else { $rules .= 'RewriteRule ^' . $match . ' ' . $site_root . $query . " [QSA,L]\n"; } } } else { $rules .= "RewriteCond %{REQUEST_FILENAME} !-f\n" . "RewriteCond %{REQUEST_FILENAME} !-d\n" . "RewriteRule . {$home_root}{$this->index} [L]\n"; } $rules .= "\n"; /** * Filters the list of rewrite rules formatted for output to an .htaccess file. * * @since 1.5.0 * * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess. */ $rules = apply_filters( 'mod_rewrite_rules', $rules ); /** * Filters the list of rewrite rules formatted for output to an .htaccess file. * * @since 1.5.0 * @deprecated 1.5.0 Use the {@see 'mod_rewrite_rules'} filter instead. * * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess. */ return apply_filters_deprecated( 'rewrite_rules', array( $rules ), '1.5.0', 'mod_rewrite_rules' ); } /** * Retrieves IIS7 URL Rewrite formatted rewrite rules to write to web.config file. * * Does not actually write to the web.config file, but creates the rules for * the process that will. * * @since 2.8.0 * * @param bool $add_parent_tags Optional. Whether to add parent tags to the rewrite rule sets. * Default false. * @return string IIS7 URL rewrite rule sets. */ public function iis7_url_rewrite_rules( $add_parent_tags = false ) { if ( ! $this->using_permalinks() ) { return ''; } $rules = ''; if ( $add_parent_tags ) { $rules .= ' '; } $rules .= ' '; if ( $add_parent_tags ) { $rules .= ' '; } /** * Filters the list of rewrite rules formatted for output to a web.config. * * @since 2.8.0 * * @param string $rules Rewrite rules formatted for IIS web.config. */ return apply_filters( 'iis7_url_rewrite_rules', $rules ); } /** * Adds a rewrite rule that transforms a URL structure to a set of query vars. * * Any value in the $after parameter that isn't 'bottom' will result in the rule * being placed at the top of the rewrite rules. * * @since 2.1.0 * @since 4.4.0 Array support was added to the `$query` parameter. * * @param string $regex Regular expression to match request against. * @param string|array $query The corresponding query vars for this rewrite rule. * @param string $after Optional. Priority of the new rule. Accepts 'top' * or 'bottom'. Default 'bottom'. */ public function add_rule( $regex, $query, $after = 'bottom' ) { if ( is_array( $query ) ) { $external = false; $query = add_query_arg( $query, 'index.php' ); } else { $index = ! str_contains( $query, '?' ) ? strlen( $query ) : strpos( $query, '?' ); $front = substr( $query, 0, $index ); $external = $front !== $this->index; } // "external" = it doesn't correspond to index.php. if ( $external ) { $this->add_external_rule( $regex, $query ); } else { if ( 'bottom' === $after ) { $this->extra_rules = array_merge( $this->extra_rules, array( $regex => $query ) ); } else { $this->extra_rules_top = array_merge( $this->extra_rules_top, array( $regex => $query ) ); } } } /** * Adds a rewrite rule that doesn't correspond to index.php. * * @since 2.1.0 * * @param string $regex Regular expression to match request against. * @param string $query The corresponding query vars for this rewrite rule. */ public function add_external_rule( $regex, $query ) { $this->non_wp_rules[ $regex ] = $query; } /** * Adds an endpoint, like /trackback/. * * @since 2.1.0 * @since 3.9.0 $query_var parameter added. * @since 4.3.0 Added support for skipping query var registration by passing `false` to `$query_var`. * * @see add_rewrite_endpoint() for full documentation. * @global WP $wp Current WordPress environment instance. * * @param string $name Name of the endpoint. * @param int $places Endpoint mask describing the places the endpoint should be added. * Accepts a mask of: * - `EP_ALL` * - `EP_NONE` * - `EP_ALL_ARCHIVES` * - `EP_ATTACHMENT` * - `EP_AUTHORS` * - `EP_CATEGORIES` * - `EP_COMMENTS` * - `EP_DATE` * - `EP_DAY` * - `EP_MONTH` * - `EP_PAGES` * - `EP_PERMALINK` * - `EP_ROOT` * - `EP_SEARCH` * - `EP_TAGS` * - `EP_YEAR` * @param string|bool $query_var Optional. Name of the corresponding query variable. Pass `false` to * skip registering a query_var for this endpoint. Defaults to the * value of `$name`. */ public function add_endpoint( $name, $places, $query_var = true ) { global $wp; // For backward compatibility, if null has explicitly been passed as `$query_var`, assume `true`. if ( true === $query_var || null === $query_var ) { $query_var = $name; } $this->endpoints[] = array( $places, $name, $query_var ); if ( $query_var ) { $wp->add_query_var( $query_var ); } } /** * Adds a new permalink structure. * * A permalink structure (permastruct) is an abstract definition of a set of rewrite rules; * it is an easy way of expressing a set of regular expressions that rewrite to a set of * query strings. The new permastruct is added to the WP_Rewrite::$extra_permastructs array. * * When the rewrite rules are built by WP_Rewrite::rewrite_rules(), all of these extra * permastructs are passed to WP_Rewrite::generate_rewrite_rules() which transforms them * into the regular expressions that many love to hate. * * The `$args` parameter gives you control over how WP_Rewrite::generate_rewrite_rules() * works on the new permastruct. * * @since 2.5.0 * * @param string $name Name for permalink structure. * @param string $struct Permalink structure (e.g. category/%category%) * @param array $args { * Optional. Arguments for building rewrite rules based on the permalink structure. * Default empty array. * * @type bool $with_front Whether the structure should be prepended with `WP_Rewrite::$front`. * Default true. * @type int $ep_mask The endpoint mask defining which endpoints are added to the structure. * Accepts a mask of: * - `EP_ALL` * - `EP_NONE` * - `EP_ALL_ARCHIVES` * - `EP_ATTACHMENT` * - `EP_AUTHORS` * - `EP_CATEGORIES` * - `EP_COMMENTS` * - `EP_DATE` * - `EP_DAY` * - `EP_MONTH` * - `EP_PAGES` * - `EP_PERMALINK` * - `EP_ROOT` * - `EP_SEARCH` * - `EP_TAGS` * - `EP_YEAR` * Default `EP_NONE`. * @type bool $paged Whether archive pagination rules should be added for the structure. * Default true. * @type bool $feed Whether feed rewrite rules should be added for the structure. Default true. * @type bool $forcomments Whether the feed rules should be a query for a comments feed. Default false. * @type bool $walk_dirs Whether the 'directories' making up the structure should be walked over * and rewrite rules built for each in-turn. Default true. * @type bool $endpoints Whether endpoints should be applied to the generated rules. Default true. * } */ public function add_permastruct( $name, $struct, $args = array() ) { // Back-compat for the old parameters: $with_front and $ep_mask. if ( ! is_array( $args ) ) { $args = array( 'with_front' => $args ); } if ( func_num_args() === 4 ) { $args['ep_mask'] = func_get_arg( 3 ); } $defaults = array( 'with_front' => true, 'ep_mask' => EP_NONE, 'paged' => true, 'feed' => true, 'forcomments' => false, 'walk_dirs' => true, 'endpoints' => true, ); $args = array_intersect_key( $args, $defaults ); $args = wp_parse_args( $args, $defaults ); if ( $args['with_front'] ) { $struct = $this->front . $struct; } else { $struct = $this->root . $struct; } $args['struct'] = $struct; $this->extra_permastructs[ $name ] = $args; } /** * Removes a permalink structure. * * @since 4.5.0 * * @param string $name Name for permalink structure. */ public function remove_permastruct( $name ) { unset( $this->extra_permastructs[ $name ] ); } /** * Removes rewrite rules and then recreate rewrite rules. * * Calls WP_Rewrite::wp_rewrite_rules() after removing the 'rewrite_rules' option. * If the function named 'save_mod_rewrite_rules' exists, it will be called. * * @since 2.0.1 * * @param bool $hard Whether to update .htaccess (hard flush) or just update rewrite_rules option (soft flush). Default is true (hard). */ public function flush_rules( $hard = true ) { static $do_hard_later = null; // Prevent this action from running before everyone has registered their rewrites. if ( ! did_action( 'wp_loaded' ) ) { add_action( 'wp_loaded', array( $this, 'flush_rules' ) ); $do_hard_later = ( isset( $do_hard_later ) ) ? $do_hard_later || $hard : $hard; return; } if ( isset( $do_hard_later ) ) { $hard = $do_hard_later; unset( $do_hard_later ); } $this->refresh_rewrite_rules(); /** * Filters whether a "hard" rewrite rule flush should be performed when requested. * * A "hard" flush updates .htaccess (Apache) or web.config (IIS). * * @since 3.7.0 * * @param bool $hard Whether to flush rewrite rules "hard". Default true. */ if ( ! $hard || ! apply_filters( 'flush_rewrite_rules_hard', true ) ) { return; } if ( function_exists( 'save_mod_rewrite_rules' ) ) { save_mod_rewrite_rules(); } if ( function_exists( 'iis7_save_url_rewrite_rules' ) ) { iis7_save_url_rewrite_rules(); } } /** * Sets up the object's properties. * * The 'use_verbose_page_rules' object property will be set to true if the * permalink structure begins with one of the following: '%postname%', '%category%', * '%tag%', or '%author%'. * * @since 1.5.0 */ public function init() { $this->extra_rules = array(); $this->non_wp_rules = array(); $this->endpoints = array(); $this->permalink_structure = get_option( 'permalink_structure' ); $this->front = substr( $this->permalink_structure, 0, strpos( $this->permalink_structure, '%' ) ); $this->root = ''; if ( $this->using_index_permalinks() ) { $this->root = $this->index . '/'; } unset( $this->author_structure ); unset( $this->date_structure ); unset( $this->page_structure ); unset( $this->search_structure ); unset( $this->feed_structure ); unset( $this->comment_feed_structure ); $this->use_trailing_slashes = str_ends_with( $this->permalink_structure, '/' ); // Enable generic rules for pages if permalink structure doesn't begin with a wildcard. if ( preg_match( '/^[^%]*%(?:postname|category|tag|author)%/', $this->permalink_structure ) ) { $this->use_verbose_page_rules = true; } else { $this->use_verbose_page_rules = false; } } /** * Sets the main permalink structure for the site. * * Will update the 'permalink_structure' option, if there is a difference * between the current permalink structure and the parameter value. Calls * WP_Rewrite::init() after the option is updated. * * Fires the {@see 'permalink_structure_changed'} action once the init call has * processed passing the old and new values * * @since 1.5.0 * * @param string $permalink_structure Permalink structure. */ public function set_permalink_structure( $permalink_structure ) { if ( $this->permalink_structure !== $permalink_structure ) { $old_permalink_structure = $this->permalink_structure; update_option( 'permalink_structure', $permalink_structure ); $this->init(); /** * Fires after the permalink structure is updated. * * @since 2.8.0 * * @param string $old_permalink_structure The previous permalink structure. * @param string $permalink_structure The new permalink structure. */ do_action( 'permalink_structure_changed', $old_permalink_structure, $permalink_structure ); } } /** * Sets the category base for the category permalink. * * Will update the 'category_base' option, if there is a difference between * the current category base and the parameter value. Calls WP_Rewrite::init() * after the option is updated. * * @since 1.5.0 * * @param string $category_base Category permalink structure base. */ public function set_category_base( $category_base ) { if ( get_option( 'category_base' ) !== $category_base ) { update_option( 'category_base', $category_base ); $this->init(); } } /** * Sets the tag base for the tag permalink. * * Will update the 'tag_base' option, if there is a difference between the * current tag base and the parameter value. Calls WP_Rewrite::init() after * the option is updated. * * @since 2.3.0 * * @param string $tag_base Tag permalink structure base. */ public function set_tag_base( $tag_base ) { if ( get_option( 'tag_base' ) !== $tag_base ) { update_option( 'tag_base', $tag_base ); $this->init(); } } /** * Constructor - Calls init(), which runs setup. * * @since 1.5.0 */ public function __construct() { $this->init(); } } tion( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->get' ); return ( new Secrets() )->get( $action, $user_id ); } /** * Deletes secret tokens in case they, for example, have expired. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->delete() instead. * * @param String $action The action name. * @param Integer $user_id The user identifier. */ public function delete_secrets( $action, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->delete' ); ( new Secrets() )->delete( $action, $user_id ); } /** * Deletes all connection tokens and transients from the local Jetpack site. * If the plugin object has been provided in the constructor, the function first checks * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function delete_all_connection_tokens( $ignore_connected_plugins = false ) { // refuse to delete if we're not the last Jetpack plugin installed. if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) { return false; } \Jetpack_Options::delete_option( array( 'master_user', 'time_diff', 'fallback_no_verify_ssl_certs', ) ); ( new Secrets() )->delete_all(); $this->get_tokens()->delete_all(); // Delete cached connected user data. $transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); delete_transient( $transient_key ); // Delete all XML-RPC errors. Error_Handler::get_instance()->delete_all_errors(); return true; } /** * Tells WordPress.com to disconnect the site and clear all tokens from cached site. * If the plugin object has been provided in the constructor, the function first check * whether it's the only active connection. * If there are any other connections, the function will do nothing and return `false` * (unless `$ignore_connected_plugins` is set to `true`). * * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * * @return bool True if disconnected successfully, false otherwise. */ public function disconnect_site_wpcom( $ignore_connected_plugins = false ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } if ( ( new Status() )->is_offline_mode() && ! apply_filters( 'jetpack_connection_disconnect_site_wpcom_offline_mode', false ) ) { // Prevent potential disconnect of the live site by removing WPCOM tokens. return false; } /** * Fires upon the disconnect attempt. * Return `false` to prevent the disconnect. * * @since 1.14.2 */ if ( ! apply_filters( 'jetpack_connection_disconnect_site_wpcom', true, $this ) ) { return false; } $xml = new Jetpack_IXR_Client(); $xml->query( 'jetpack.deregister', get_current_user_id() ); return true; } /** * Disconnect the plugin and remove the tokens. * This function will automatically perform "soft" or "hard" disconnect depending on whether other plugins are using the connection. * This is a proxy method to simplify the Connection package API. * * @see Manager::disconnect_site() * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. * @return bool */ public function remove_connection( $disconnect_wpcom = true, $ignore_connected_plugins = false ) { $this->disconnect_site( $disconnect_wpcom, $ignore_connected_plugins ); return true; } /** * Completely clearing up the connection, and initiating reconnect. * * @return true|WP_Error True if reconnected successfully, a `WP_Error` object otherwise. */ public function reconnect() { ( new Tracking() )->record_user_event( 'restore_connection_reconnect' ); $this->disconnect_site_wpcom( true ); return $this->register(); } /** * Validate the tokens, and refresh the invalid ones. * * @return string|bool|WP_Error True if connection restored or string indicating what's to be done next. A `WP_Error` object or false otherwise. */ public function restore() { // If this is a site connection we need to trigger a full reconnection as our only secure means of // communication with WPCOM, aka the blog token, is compromised. if ( $this->is_site_connection() ) { return $this->reconnect(); } $validate_tokens_response = $this->get_tokens()->validate(); // If token validation failed, trigger a full reconnection. if ( is_array( $validate_tokens_response ) && isset( $validate_tokens_response['blog_token']['is_healthy'] ) && isset( $validate_tokens_response['user_token']['is_healthy'] ) ) { $blog_token_healthy = $validate_tokens_response['blog_token']['is_healthy']; $user_token_healthy = $validate_tokens_response['user_token']['is_healthy']; } else { $blog_token_healthy = false; $user_token_healthy = false; } // Tokens are both valid, or both invalid. We can't fix the problem we don't see, so the full reconnection is needed. if ( $blog_token_healthy === $user_token_healthy ) { $result = $this->reconnect(); return ( true === $result ) ? 'authorize' : $result; } if ( ! $blog_token_healthy ) { return $this->refresh_blog_token(); } if ( ! $user_token_healthy ) { return ( true === $this->refresh_user_token() ) ? 'authorize' : false; } return false; } /** * Responds to a WordPress.com call to register the current site. * Should be changed to protected. * * @param array $registration_data Array of [ secret_1, user_id ]. */ public function handle_registration( array $registration_data ) { list( $registration_secret_1, $registration_user_id ) = $registration_data; if ( empty( $registration_user_id ) ) { return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack-connection' ), 400 ); } return ( new Secrets() )->verify( 'register', $registration_secret_1, (int) $registration_user_id ); } /** * Perform the API request to validate the blog and user tokens. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->validate_tokens() instead. * * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default. * * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`. */ public function validate_tokens( $user_id = null ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->validate' ); return $this->get_tokens()->validate( $user_id ); } /** * Verify a Previously Generated Secret. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->verify() instead. * * @param string $action The type of secret to verify. * @param string $secret_1 The secret string to compare to what is stored. * @param int $user_id The user ID of the owner of the secret. * @return \WP_Error|string WP_Error on failure, secret_2 on success. */ public function verify_secrets( $action, $secret_1, $user_id ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->verify' ); return ( new Secrets() )->verify( $action, $secret_1, $user_id ); } /** * Responds to a WordPress.com call to authorize the current user. * Should be changed to protected. */ public function handle_authorization() { } /** * Obtains the auth token. * * @param array $data The request data. * @return object|\WP_Error Returns the auth token on success. * Returns a \WP_Error on failure. */ public function get_token( $data ) { return $this->get_tokens()->get( $data, $this->api_url( 'token' ) ); } /** * Builds a URL to the Jetpack connection auth page. * * @param WP_User $user (optional) defaults to the current logged in user. * @param String $redirect (optional) a redirect URL to use instead of the default. * @return string Connect URL. */ public function get_authorization_url( $user = null, $redirect = null ) { if ( empty( $user ) ) { $user = wp_get_current_user(); } $roles = new Roles(); $role = $roles->translate_user_to_role( $user ); $signed_role = $this->get_tokens()->sign_role( $role ); /** * Filter the URL of the first time the user gets redirected back to your site for connection * data processing. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site admin URL. */ $processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) ); /** * Filter the URL to redirect the user back to when the authorization process * is complete. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param string $redirect_url Defaults to the site URL. */ $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); $secrets = ( new Secrets() )->generate( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS ); /** * Filter the type of authorization. * 'calypso' completes authorization on wordpress.com/jetpack/connect * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com. * * @since 1.7.0 * @since-jetpack 4.3.3 * * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'. */ $auth_type = apply_filters( 'jetpack_auth_type', 'calypso' ); /** * Filters the user connection request data for additional property addition. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $request_data request data. */ $body = apply_filters( 'jetpack_connect_request_body', array( 'response_type' => 'code', 'client_id' => \Jetpack_Options::get_option( 'id' ), 'redirect_uri' => add_query_arg( array( 'handler' => 'jetpack-connection-webhooks', 'action' => 'authorize', '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), 'redirect' => $redirect ? rawurlencode( $redirect ) : false, ), esc_url( $processing_url ) ), 'state' => $user->ID, 'scope' => $signed_role, 'user_email' => $user->user_email, 'user_login' => $user->user_login, 'is_active' => $this->has_connected_owner(), // TODO Deprecate this. 'jp_version' => (string) Constants::get_constant( 'JETPACK__VERSION' ), 'auth_type' => $auth_type, 'secret' => $secrets['secret_1'], 'blogname' => get_option( 'blogname' ), 'site_url' => Urls::site_url(), 'home_url' => Urls::home_url(), 'site_icon' => get_site_icon_url(), 'site_lang' => get_locale(), 'site_created' => $this->get_assumed_site_creation_date(), 'allow_site_connection' => ! $this->has_connected_owner(), 'calypso_env' => ( new Host() )->get_calypso_env(), 'source' => ( new Host() )->get_source_query(), ) ); $body = static::apply_activation_source_to_args( urlencode_deep( $body ) ); $api_url = $this->api_url( 'authorize' ); $url = add_query_arg( $body, $api_url ); /** This filter is documented in plugins/jetpack/class-jetpack.php */ return apply_filters( 'jetpack_build_authorize_url', $url ); } /** * Authorizes the user by obtaining and storing the user token. * * @param array $data The request data. * @return string|\WP_Error Returns a string on success. * Returns a \WP_Error on failure. */ public function authorize( $data = array() ) { /** * Action fired when user authorization starts. * * @since 1.7.0 * @since-jetpack 8.0.0 */ do_action( 'jetpack_authorize_starting' ); $roles = new Roles(); $role = $roles->translate_current_user_to_role(); if ( ! $role ) { return new \WP_Error( 'no_role', 'Invalid request.', 400 ); } $cap = $roles->translate_role_to_cap( $role ); if ( ! $cap ) { return new \WP_Error( 'no_cap', 'Invalid request.', 400 ); } if ( ! empty( $data['error'] ) ) { return new \WP_Error( $data['error'], 'Error included in the request.', 400 ); } if ( ! isset( $data['state'] ) ) { return new \WP_Error( 'no_state', 'Request must include state.', 400 ); } if ( ! ctype_digit( $data['state'] ) ) { return new \WP_Error( $data['error'], 'State must be an integer.', 400 ); } $current_user_id = get_current_user_id(); if ( $current_user_id !== (int) $data['state'] ) { return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 ); } if ( empty( $data['code'] ) ) { return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 ); } $token = $this->get_tokens()->get( $data, $this->api_url( 'token' ) ); if ( is_wp_error( $token ) ) { $code = $token->get_error_code(); if ( empty( $code ) ) { $code = 'invalid_token'; } return new \WP_Error( $code, $token->get_error_message(), 400 ); } if ( ! $token ) { return new \WP_Error( 'no_token', 'Error generating token.', 400 ); } $is_connection_owner = ! $this->has_connected_owner(); $this->get_tokens()->update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_connection_owner ); /** * Fires after user has successfully received an auth token. * * @since 1.7.0 * @since-jetpack 3.9.0 */ do_action( 'jetpack_user_authorized' ); if ( ! $is_connection_owner ) { /** * Action fired when a secondary user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 */ do_action( 'jetpack_authorize_ending_linked' ); return 'linked'; } /** * Action fired when the master user has been authorized. * * @since 1.7.0 * @since-jetpack 8.0.0 * * @param array $data The request data. */ do_action( 'jetpack_authorize_ending_authorized', $data ); \Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' ); ( new Nonce_Handler() )->reschedule(); return 'authorized'; } /** * Disconnects from the Jetpack servers. * Forgets all connection details and tells the Jetpack servers to do the same. * * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called. * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins. */ public function disconnect_site( $disconnect_wpcom = true, $ignore_connected_plugins = true ) { if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) { return false; } wp_clear_scheduled_hook( 'jetpack_clean_nonces' ); ( new Nonce_Handler() )->clean_all(); /** * Fires when a site is disconnected. * * @since 1.36.3 */ do_action( 'jetpack_site_before_disconnected' ); // If the site is in an IDC because sync is not allowed, // let's make sure to not disconnect the production site. if ( $disconnect_wpcom ) { $tracking = new Tracking(); $tracking->record_user_event( 'disconnect_site', array() ); $this->disconnect_site_wpcom( $ignore_connected_plugins ); } $this->delete_all_connection_tokens( $ignore_connected_plugins ); // Remove tracked package versions, since they depend on the Jetpack Connection. delete_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION ); $jetpack_unique_connection = \Jetpack_Options::get_option( 'unique_connection' ); if ( $jetpack_unique_connection ) { // Check then record unique disconnection if site has never been disconnected previously. if ( - 1 === $jetpack_unique_connection['disconnected'] ) { $jetpack_unique_connection['disconnected'] = 1; } else { if ( 0 === $jetpack_unique_connection['disconnected'] ) { $a8c_mc_stats_instance = new A8c_Mc_Stats(); $a8c_mc_stats_instance->add( 'connections', 'unique-disconnect' ); $a8c_mc_stats_instance->do_server_side_stats(); } // increment number of times disconnected. $jetpack_unique_connection['disconnected'] += 1; } \Jetpack_Options::update_option( 'unique_connection', $jetpack_unique_connection ); } /** * Fires when a site is disconnected. * * @since 1.30.1 */ do_action( 'jetpack_site_disconnected' ); } /** * The Base64 Encoding of the SHA1 Hash of the Input. * * @param string $text The string to hash. * @return string */ public function sha1_base64( $text ) { return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase. * * @param string $domain The domain to check. * * @return bool|WP_Error */ public function is_usable_domain( $domain ) { // If it's empty, just fail out. if ( ! $domain ) { return new \WP_Error( 'fail_domain_empty', /* translators: %1$s is a domain name. */ sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack-connection' ), $domain ) ); } /** * Skips the usuable domain check when connecting a site. * * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com * * @since 1.7.0 * @since-jetpack 4.1.0 * * @param bool If the check should be skipped. Default false. */ if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) { return true; } // None of the explicit localhosts. $forbidden_domains = array( 'wordpress.com', 'localhost', 'localhost.localdomain', 'local.wordpress.test', // VVV pattern. 'local.wordpress-trunk.test', // VVV pattern. 'src.wordpress-develop.test', // VVV pattern. 'build.wordpress-develop.test', // VVV pattern. ); if ( in_array( $domain, $forbidden_domains, true ) ) { return new \WP_Error( 'fail_domain_forbidden', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', 'jetpack-connection' ), $domain ) ); } // No .test or .local domains. if ( preg_match( '#\.(test|local)$#i', $domain ) ) { return new \WP_Error( 'fail_domain_tld', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', 'jetpack-connection' ), $domain ) ); } // No WPCOM subdomains. if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) { return new \WP_Error( 'fail_subdomain_wpcom', sprintf( /* translators: %1$s is a domain name. */ __( 'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', 'jetpack-connection' ), $domain ) ); } // If PHP was compiled without support for the Filter module (very edge case). if ( ! function_exists( 'filter_var' ) ) { // Just pass back true for now, and let wpcom sort it out. return true; } $domain = preg_replace( '#^https?://#', '', untrailingslashit( $domain ) ); if ( filter_var( $domain, FILTER_VALIDATE_IP ) && ! filter_var( $domain, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return new \WP_Error( 'fail_ip_forbidden', sprintf( /* translators: %1$s is a domain name. */ __( 'IP address `%1$s` just failed is_usable_domain check as it is in the private network.', 'jetpack-connection' ), $domain ) ); } return true; } /** * Gets the requested token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_access_token() instead. * * @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. * @param string|false $token_key If provided, check that the token matches the provided input. * @param bool|true $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found. * * @return object|false * * @see $this->get_tokens()->get_access_token() */ public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_access_token' ); return $this->get_tokens()->get_access_token( $user_id, $token_key, $suppress_errors ); } /** * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths * since it is passed by reference to various methods. * Capture it here so we can verify the signature later. * * @param array $methods an array of available XMLRPC methods. * @return array the same array, since this method doesn't add or remove anything. */ public function xmlrpc_methods( $methods ) { $this->raw_post_data = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null; return $methods; } /** * Resets the raw post data parameter for testing purposes. */ public function reset_raw_post_data() { $this->raw_post_data = null; } /** * Registering an additional method. * * @param array $methods an array of available XMLRPC methods. * @return array the amended array in case the method is added. */ public function public_xmlrpc_methods( $methods ) { if ( array_key_exists( 'wp.getOptions', $methods ) ) { $methods['wp.getOptions'] = array( $this, 'jetpack_get_options' ); } return $methods; } /** * Handles a getOptions XMLRPC method call. * * @param array $args method call arguments. * @return an amended XMLRPC server options array. */ public function jetpack_get_options( $args ) { global $wp_xmlrpc_server; $wp_xmlrpc_server->escape( $args ); $username = $args[1]; $password = $args[2]; $user = $wp_xmlrpc_server->login( $username, $password ); if ( ! $user ) { return $wp_xmlrpc_server->error; } $options = array(); $user_data = $this->get_connected_user_data(); if ( is_array( $user_data ) ) { $options['jetpack_user_id'] = array( 'desc' => __( 'The WP.com user ID of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['ID'], ); $options['jetpack_user_login'] = array( 'desc' => __( 'The WP.com username of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['login'], ); $options['jetpack_user_email'] = array( 'desc' => __( 'The WP.com user email of the connected user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['email'], ); $options['jetpack_user_site_count'] = array( 'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack-connection' ), 'readonly' => true, 'value' => $user_data['site_count'], ); } $wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options ); $args = stripslashes_deep( $args ); return $wp_xmlrpc_server->wp_getOptions( $args ); } /** * Adds Jetpack-specific options to the output of the XMLRPC options method. * * @param array $options standard Core options. * @return array amended options. */ public function xmlrpc_options( $options ) { $jetpack_client_id = false; if ( $this->is_connected() ) { $jetpack_client_id = \Jetpack_Options::get_option( 'id' ); } $options['jetpack_version'] = array( 'desc' => __( 'Jetpack Plugin Version', 'jetpack-connection' ), 'readonly' => true, 'value' => Constants::get_constant( 'JETPACK__VERSION' ), ); $options['jetpack_client_id'] = array( 'desc' => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack-connection' ), 'readonly' => true, 'value' => $jetpack_client_id, ); return $options; } /** * Resets the saved authentication state in between testing requests. */ public function reset_saved_auth_state() { $this->xmlrpc_verification = null; } /** * Sign a user role with the master access token. * If not specified, will default to the current user. * * @access public * * @param string $role User role. * @param int $user_id ID of the user. * @return string Signed user role. */ public function sign_role( $role, $user_id = null ) { return $this->get_tokens()->sign_role( $role, $user_id ); } /** * Set the plugin instance. * * @param Plugin $plugin_instance The plugin instance. * * @return $this */ public function set_plugin_instance( Plugin $plugin_instance ) { $this->plugin = $plugin_instance; return $this; } /** * Retrieve the plugin management object. * * @return Plugin|null */ public function get_plugin() { return $this->plugin; } /** * Get all connected plugins information, excluding those disconnected by user. * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded * Even if you don't use Jetpack Config, it may be introduced later by other plugins, * so please make sure not to run the method too early in the code. * * @return array|WP_Error */ public function get_connected_plugins() { $maybe_plugins = Plugin_Storage::get_all(); if ( $maybe_plugins instanceof WP_Error ) { return $maybe_plugins; } return $maybe_plugins; } /** * Force plugin disconnect. After its called, the plugin will not be allowed to use the connection. * Note: this method does not remove any access tokens. * * @deprecated since 1.39.0 * @return bool */ public function disable_plugin() { return null; } /** * Force plugin reconnect after user-initiated disconnect. * After its called, the plugin will be allowed to use the connection again. * Note: this method does not initialize access tokens. * * @deprecated since 1.39.0. * @return bool */ public function enable_plugin() { return null; } /** * Whether the plugin is allowed to use the connection, or it's been disconnected by user. * If no plugin slug was passed into the constructor, always returns true. * * @deprecated 1.42.0 This method no longer has a purpose after the removal of the soft disconnect feature. * * @return bool */ public function is_plugin_enabled() { return true; } /** * Perform the API request to refresh the blog token. * Note that we are making this request on behalf of the Jetpack master user, * given they were (most probably) the ones that registered the site at the first place. * * @return WP_Error|bool The result of updating the blog_token option. */ public function refresh_blog_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' ); $blog_id = \Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { return new WP_Error( 'site_not_registered', 'Site not registered.' ); } $url = sprintf( '%s/%s/v%s/%s', Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ), 'wpcom', '2', 'sites/' . $blog_id . '/jetpack-refresh-blog-token' ); $method = 'POST'; $user_id = get_current_user_id(); $response = Client::remote_request( compact( 'url', 'method', 'user_id' ) ); if ( is_wp_error( $response ) ) { return new WP_Error( 'refresh_blog_token_http_request_failed', $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $entity = wp_remote_retrieve_body( $response ); if ( $entity ) { $json = json_decode( $entity ); } else { $json = false; } if ( 200 !== $code ) { if ( empty( $json->code ) ) { return new WP_Error( 'unknown', '', $code ); } /* translators: Error description string. */ $error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->message ) : ''; return new WP_Error( (string) $json->code, $error_description, $code ); } if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) { return new WP_Error( 'jetpack_secret', '', $code ); } Error_Handler::get_instance()->delete_all_errors(); return $this->get_tokens()->update_blog_token( (string) $json->jetpack_secret ); } /** * Disconnect the user from WP.com, and initiate the reconnect process. * * @return bool */ public function refresh_user_token() { ( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' ); $this->disconnect_user( null, true, true ); return true; } /** * Fetches a signed token. * * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_signed_token() instead. * * @param object $token the token. * @return WP_Error|string a signed token */ public function get_signed_token( $token ) { _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_signed_token' ); return $this->get_tokens()->get_signed_token( $token ); } /** * If the site-level connection is active, add the list of plugins using connection to the heartbeat (except Jetpack itself) * * @param array $stats The Heartbeat stats array. * @return array $stats */ public function add_stats_to_heartbeat( $stats ) { if ( ! $this->is_connected() ) { return $stats; } $active_plugins_using_connection = Plugin_Storage::get_all(); foreach ( array_keys( $active_plugins_using_connection ) as $plugin_slug ) { if ( 'jetpack' !== $plugin_slug ) { $stats_group = isset( $active_plugins_using_connection['jetpack'] ) ? 'combined-connection' : 'standalone-connection'; $stats[ $stats_group ][] = $plugin_slug; } } return $stats; } /** * Get the WPCOM or self-hosted site ID. * * @return int|WP_Error */ public static function get_site_id() { $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM ); $site_id = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' ); if ( ! $site_id ) { return new \WP_Error( 'unavailable_site_id', __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ), 403 ); } return (int) $site_id; } /** * Check if Jetpack is ready for uninstall cleanup. * * @param string $current_plugin_slug The current plugin's slug. * * @return bool */ public static function is_ready_for_cleanup( $current_plugin_slug ) { $active_plugins = get_option( Plugin_Storage::ACTIVE_PLUGINS_OPTION_NAME ); return empty( $active_plugins ) || ! is_array( $active_plugins ) || ( count( $active_plugins ) === 1 && array_key_exists( $current_plugin_slug, $active_plugins ) ); } }