Файловый менеджер - Редактировать - /home/bean7936/perfect-community.com/442aa3/wordpress-seo.tar
Назад
wp-seo-main.php 0000644 00000042340 15174712003 0007414 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Main */ if ( ! function_exists( 'add_filter' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * {@internal Nobody should be able to overrule the real version number as this can cause * serious issues with the options, so no if ( ! defined() ).}} */ define( 'WPSEO_VERSION', '27.1.1' ); if ( ! defined( 'WPSEO_PATH' ) ) { define( 'WPSEO_PATH', plugin_dir_path( WPSEO_FILE ) ); } if ( ! defined( 'WPSEO_BASENAME' ) ) { define( 'WPSEO_BASENAME', plugin_basename( WPSEO_FILE ) ); } /* * {@internal The prefix constants are used to build prefixed versions of dependencies. * These should not be changed on run-time, thus missing the ! defined() check.}} */ define( 'YOAST_VENDOR_NS_PREFIX', 'YoastSEO_Vendor' ); define( 'YOAST_VENDOR_DEFINE_PREFIX', 'YOASTSEO_VENDOR__' ); define( 'YOAST_VENDOR_PREFIX_DIRECTORY', 'vendor_prefixed' ); define( 'YOAST_SEO_PHP_REQUIRED', '7.4' ); define( 'YOAST_SEO_WP_TESTED', '6.9.1' ); define( 'YOAST_SEO_WP_REQUIRED', '6.8' ); if ( ! defined( 'WPSEO_NAMESPACES' ) ) { define( 'WPSEO_NAMESPACES', true ); } /* ***************************** CLASS AUTOLOADING *************************** */ /** * Autoload our class files. * * @param string $class_name Class name. * * @return void */ function wpseo_auto_load( $class_name ) { static $classes = null; $classes ??= [ 'wp_list_table' => ABSPATH . 'wp-admin/includes/class-wp-list-table.php', 'walker_category' => ABSPATH . 'wp-includes/category-template.php', ]; $cn = strtolower( $class_name ); if ( ! class_exists( $class_name ) && isset( $classes[ $cn ] ) ) { require_once $classes[ $cn ]; } } $yoast_autoload_file = WPSEO_PATH . 'vendor/autoload.php'; if ( is_readable( $yoast_autoload_file ) ) { $yoast_autoloader = require $yoast_autoload_file; } elseif ( ! class_exists( 'WPSEO_Options' ) ) { // Still checking since might be site-level autoload R. add_action( 'admin_init', 'yoast_wpseo_missing_autoload', 1 ); return; } /** * Include the file from the `symfony/deprecation-contracts` dependency instead of autoloading it via composer. * * We need to do that because autoloading via composer prevents the vendor-prefixing of the dependency itself. * Note that we don't expect the function to be ever called since the OAuth2 library should not provide invalid input. */ $deprecation_contracts_file = WPSEO_PATH . 'vendor_prefixed/symfony/deprecation-contracts/functions.php'; if ( is_readable( $deprecation_contracts_file ) ) { include $deprecation_contracts_file; } if ( function_exists( 'spl_autoload_register' ) ) { spl_autoload_register( 'wpseo_auto_load' ); } require_once WPSEO_PATH . 'src/functions.php'; /* ********************* DEFINES DEPENDING ON AUTOLOADED CODE ********************* */ /** * Defaults to production, for safety. */ if ( ! defined( 'YOAST_ENVIRONMENT' ) ) { define( 'YOAST_ENVIRONMENT', 'production' ); } if ( YOAST_ENVIRONMENT === 'development' && isset( $yoast_autoloader ) ) { add_action( 'plugins_loaded', /** * Reregisters the autoloader so that Yoast SEO is at the front. * This prevents conflicts with the development versions of our addons. * An anonymous function is used so we can use the autoloader variable. * As this is only loaded in development removing this action is not a concern. * * @return void */ static function () use ( $yoast_autoloader ) { $yoast_autoloader->unregister(); $yoast_autoloader->register( true ); }, 1, ); } /** * Only use minified assets when we are in a production environment. */ if ( ! defined( 'WPSEO_CSSJS_SUFFIX' ) ) { define( 'WPSEO_CSSJS_SUFFIX', ( YOAST_ENVIRONMENT !== 'development' ) ? '.min' : '' ); } /* ***************************** PLUGIN (DE-)ACTIVATION *************************** */ /** * Run single site / network-wide activation of the plugin. * * @param bool $networkwide Whether the plugin is being activated network-wide. * * @return void */ function wpseo_activate( $networkwide = false ) { if ( ! is_multisite() || ! $networkwide ) { _wpseo_activate(); } else { /* Multi-site network activation - activate the plugin for all blogs. */ wpseo_network_activate_deactivate( true ); } // This is done so that the 'uninstall_{$file}' is triggered. register_uninstall_hook( WPSEO_FILE, '__return_false' ); } /** * Run single site / network-wide de-activation of the plugin. * * @param bool $networkwide Whether the plugin is being de-activated network-wide. * * @return void */ function wpseo_deactivate( $networkwide = false ) { if ( ! is_multisite() || ! $networkwide ) { _wpseo_deactivate(); } else { /* Multi-site network activation - de-activate the plugin for all blogs. */ wpseo_network_activate_deactivate( false ); } } /** * Run network-wide (de-)activation of the plugin. * * @param bool $activate True for plugin activation, false for de-activation. * * @return void */ function wpseo_network_activate_deactivate( $activate = true ) { global $wpdb; $network_blogs = $wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE site_id = %d", $wpdb->siteid ) ); if ( is_array( $network_blogs ) && $network_blogs !== [] ) { foreach ( $network_blogs as $blog_id ) { switch_to_blog( $blog_id ); if ( $activate === true ) { _wpseo_activate(); } else { _wpseo_deactivate(); } restore_current_blog(); } } } /** * Runs on activation of the plugin. * * @return void */ function _wpseo_activate() { require_once WPSEO_PATH . 'inc/wpseo-functions.php'; require_once WPSEO_PATH . 'inc/class-wpseo-installation.php'; new WPSEO_Installation(); WPSEO_Options::get_instance(); if ( ! is_multisite() ) { WPSEO_Options::initialize(); } else { WPSEO_Options::maybe_set_multisite_defaults( true ); } WPSEO_Options::ensure_options_exist(); if ( ! is_multisite() || ! ms_is_switched() ) { // Constructor has side effects so this registers all hooks. $GLOBALS['wpseo_rewrite'] = new WPSEO_Rewrite(); } add_action( 'shutdown', [ 'WPSEO_Utils', 'clear_rewrites' ] ); WPSEO_Options::set( 'indexing_reason', 'first_install' ); WPSEO_Options::set( 'first_time_install', true ); if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { WPSEO_Options::set( 'should_redirect_after_install_free', true ); } else { WPSEO_Options::set( 'activation_redirect_timestamp_free', time() ); } // Reset tracking to be disabled by default. if ( ! YoastSEO()->helpers->product->is_premium() && WPSEO_Options::get( 'toggled_tracking' ) !== true ) { WPSEO_Options::set( 'tracking', false ); } do_action( 'wpseo_register_roles' ); WPSEO_Role_Manager_Factory::get()->add(); do_action( 'wpseo_register_capabilities' ); WPSEO_Capability_Manager_Factory::get()->add(); // Clear cache so the changes are obvious. WPSEO_Utils::clear_cache(); do_action( 'wpseo_activate' ); } /** * On deactivation, flush the rewrite rules so XML sitemaps stop working. * * @return void */ function _wpseo_deactivate() { require_once WPSEO_PATH . 'inc/wpseo-functions.php'; add_action( 'shutdown', [ 'WPSEO_Utils', 'clear_rewrites' ] ); // Register capabilities, to make sure they are cleaned up. do_action( 'wpseo_register_roles' ); do_action( 'wpseo_register_capabilities' ); // Clean up capabilities. WPSEO_Role_Manager_Factory::get()->remove(); WPSEO_Capability_Manager_Factory::get()->remove(); // Clear cache so the changes are obvious. WPSEO_Utils::clear_cache(); do_action( 'wpseo_deactivate' ); } /** * Run wpseo activation routine on creation / activation of a multisite blog if WPSEO is activated * network-wide. * * Will only be called by multisite actions. * * {@internal Unfortunately will fail if the plugin is in the must-use directory. * {@link https://core.trac.wordpress.org/ticket/24205} }} * * @param int|WP_Site $blog_id Blog ID. * * @return void */ function wpseo_on_activate_blog( $blog_id ) { if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( $blog_id instanceof WP_Site ) { $blog_id = (int) $blog_id->blog_id; } if ( is_plugin_active_for_network( WPSEO_BASENAME ) ) { switch_to_blog( $blog_id ); wpseo_activate( false ); restore_current_blog(); } } /* ***************************** PLUGIN LOADING *************************** */ /** * Load translations. * * @deprecated 27.0 * @codeCoverageIgnore * * @return void */ function wpseo_load_textdomain() { _deprecated_function( __FUNCTION__, 'Yoast SEO 27.0' ); } /** * On plugins_loaded: load the minimum amount of essential files for this plugin. * * @return void */ function wpseo_init() { require_once WPSEO_PATH . 'inc/wpseo-functions.php'; require_once WPSEO_PATH . 'inc/wpseo-functions-deprecated.php'; // Make sure our option and meta value validation routines and default values are always registered and available. WPSEO_Options::get_instance(); WPSEO_Meta::init(); if ( version_compare( WPSEO_Options::get( 'version', 1, [ 'wpseo' ] ), WPSEO_VERSION, '<' ) ) { // Invalidate the opcache in 50% of the cases, randomly staggered based on the site URL. // @TODO: Move the staggering logic to its own class, but only after a few releases after the complete sunset of the opcache invalidation. Make sure to document that in the future, maybe `12` should be used as modulus, so that it's easier to tinker the percentage (divisible by 2, 3, 4, 6). (see the technical choices of https://github.com/Yoast/wordpress-seo/pull/22812). $random_seed = hexdec( substr( hash( 'sha256', site_url() ), 0, 8 ) ); $should_invalidate_opcache = ( $random_seed % 2 ) !== 0; /** * Filter: 'Yoast\WP\SEO\should_invalidate_opcache' - Allow developers to enable / disable * opcache invalidation upon upgrade of the Yoast SEO plugin. * * @since 26.1 * * @param bool $should_invalidate Whether opcache should be invalidated. */ $should_invalidate_opcache = (bool) apply_filters( 'Yoast\WP\SEO\should_invalidate_opcache', $should_invalidate_opcache ); if ( $should_invalidate_opcache && function_exists( 'opcache_reset' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Prevent notices when opcache.restrict_api is set. @opcache_reset(); } new WPSEO_Upgrade(); // Get a cleaned up version of the $options. } $GLOBALS['wpseo_rewrite'] = new WPSEO_Rewrite(); if ( WPSEO_Options::get( 'enable_xml_sitemap', null, [ 'wpseo' ] ) === true ) { $GLOBALS['wpseo_sitemaps'] = new WPSEO_Sitemaps(); } if ( ! wp_doing_ajax() ) { require_once WPSEO_PATH . 'inc/wpseo-non-ajax-functions.php'; } $integrations = []; $integrations[] = new WPSEO_Slug_Change_Watcher(); foreach ( $integrations as $integration ) { $integration->register_hooks(); } } /** * Loads the rest api endpoints. * * @return void */ function wpseo_init_rest_api() { // We can't do anything when requirements are not met. if ( ! WPSEO_Utils::is_api_available() ) { return; } // Boot up REST API. $statistics_service = new WPSEO_Statistics_Service( new WPSEO_Statistics() ); $endpoints = []; $endpoints[] = new WPSEO_Endpoint_File_Size( new WPSEO_File_Size_Service() ); $endpoints[] = new WPSEO_Endpoint_Statistics( $statistics_service ); foreach ( $endpoints as $endpoint ) { $endpoint->register(); } } /** * Used to load the required files on the plugins_loaded hook, instead of immediately. * * @return void */ function wpseo_admin_init() { new WPSEO_Admin_Init(); } /* ***************************** BOOTSTRAP / HOOK INTO WP *************************** */ $spl_autoload_exists = function_exists( 'spl_autoload_register' ); if ( ! $spl_autoload_exists ) { add_action( 'admin_init', 'yoast_wpseo_missing_spl', 1 ); } if ( ! wp_installing() && ( $spl_autoload_exists ) ) { add_action( 'plugins_loaded', 'wpseo_init', 14 ); add_action( 'setup_theme', [ 'Yoast_Dynamic_Rewrites', 'instance' ], 1 ); add_action( 'rest_api_init', 'wpseo_init_rest_api' ); if ( is_admin() ) { new Yoast_Notifications(); $yoast_addon_manager = new WPSEO_Addon_Manager(); $yoast_addon_manager->register_hooks(); if ( wp_doing_ajax() ) { require_once WPSEO_PATH . 'admin/ajax.php'; // Plugin conflict ajax hooks. new Yoast_Plugin_Conflict_Ajax(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information but only loading the admin init class. if ( isset( $_POST['action'] ) && is_string( $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information but only loading the admin init class, We are strictly comparing only. if ( wp_unslash( $_POST['action'] ) === 'inline-save' ) { add_action( 'plugins_loaded', 'wpseo_admin_init', 15 ); } } } else { add_action( 'plugins_loaded', 'wpseo_admin_init', 15 ); } } add_action( 'plugins_loaded', 'load_yoast_notifications' ); add_action( 'init', [ 'WPSEO_Replace_Vars', 'setup_statics_once' ] ); // Initializes the Yoast indexables for the first time. YoastSEO(); /** * Action called when the Yoast SEO plugin file has loaded. */ do_action( 'wpseo_loaded' ); } // Activation and deactivation hook. register_activation_hook( WPSEO_FILE, 'wpseo_activate' ); register_deactivation_hook( WPSEO_FILE, 'wpseo_deactivate' ); add_action( 'wp_initialize_site', 'wpseo_on_activate_blog', 99 ); add_action( 'activate_blog', 'wpseo_on_activate_blog' ); // Registers SEO capabilities. $wpseo_register_capabilities = new WPSEO_Register_Capabilities(); $wpseo_register_capabilities->register_hooks(); // Registers SEO roles. $wpseo_register_capabilities = new WPSEO_Register_Roles(); $wpseo_register_capabilities->register_hooks(); /** * Wraps for notifications center class. * * @return void */ function load_yoast_notifications() { // Init Yoast_Notification_Center class. Yoast_Notification_Center::get(); } /** * Throw an error if the PHP SPL extension is disabled (prevent white screens) and self-deactivate plugin. * * @since 1.5.4 * * @return void */ function yoast_wpseo_missing_spl() { if ( is_admin() ) { add_action( 'admin_notices', 'yoast_wpseo_missing_spl_notice' ); yoast_wpseo_self_deactivate(); } } /** * Returns the notice in case of missing spl extension. * * @return void */ function yoast_wpseo_missing_spl_notice() { $message = esc_html__( 'The Standard PHP Library (SPL) extension seem to be unavailable. Please ask your web host to enable it.', 'wordpress-seo' ); yoast_wpseo_activation_failed_notice( $message ); } /** * Throw an error if the Composer autoload is missing and self-deactivate plugin. * * @return void */ function yoast_wpseo_missing_autoload() { if ( is_admin() ) { add_action( 'admin_notices', 'yoast_wpseo_missing_autoload_notice' ); yoast_wpseo_self_deactivate(); } } /** * Returns the notice in case of missing Composer autoload. * * @return void */ function yoast_wpseo_missing_autoload_notice() { /* translators: %1$s expands to Yoast SEO, %2$s / %3$s: links to the installation manual in the Readme for the Yoast SEO code repository on GitHub */ $message = esc_html__( 'The %1$s plugin installation is incomplete. Please refer to %2$sinstallation instructions%3$s.', 'wordpress-seo' ); $message = sprintf( $message, 'Yoast SEO', '<a href="https://github.com/Yoast/wordpress-seo#installation">', '</a>' ); yoast_wpseo_activation_failed_notice( $message ); } /** * Throw an error if the filter extension is disabled (prevent white screens) and self-deactivate plugin. * * @since 2.0 * @deprecated 23.3 * @codeCoverageIgnore * * @return void */ function yoast_wpseo_missing_filter() { _deprecated_function( __FUNCTION__, 'Yoast SEO 23.3' ); } /** * Returns the notice in case of missing filter extension. * * @deprecated 23.3 * @codeCoverageIgnore * * @return void */ function yoast_wpseo_missing_filter_notice() { _deprecated_function( __FUNCTION__, 'Yoast SEO 23.3' ); } /** * Echo's the Activation failed notice with any given message. * * @param string $message Message string. * * @return void */ function yoast_wpseo_activation_failed_notice( $message ) { $title = sprintf( /* translators: %s: Yoast SEO. */ esc_html__( '%s activation failed', 'wordpress-seo' ), 'Yoast SEO', ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This function is only called in 3 places that are safe. echo '<div class="error yoast-migrated-notice"><h4 class="yoast-notice-migrated-header">' . $title . '</h4><div class="notice-yoast-content"><p>' . strip_tags( $message, '<a>' ) . '</p></div></div>'; } /** * The method will deactivate the plugin, but only once, done by the static $is_deactivated. * * @return void */ function yoast_wpseo_self_deactivate() { static $is_deactivated; if ( $is_deactivated === null ) { $is_deactivated = true; deactivate_plugins( WPSEO_BASENAME ); if ( isset( $_GET['activate'] ) ) { unset( $_GET['activate'] ); } } } /** * Aliasses added in order to keep compatibility with Yoast SEO: Local. */ class_alias( '\Yoast\WP\SEO\Initializers\Initializer_Interface', '\Yoast\WP\SEO\WordPress\Initializer' ); class_alias( '\Yoast\WP\SEO\Loadable_Interface', '\Yoast\WP\SEO\WordPress\Loadable' ); class_alias( '\Yoast\WP\SEO\Integrations\Integration_Interface', '\Yoast\WP\SEO\WordPress\Integration' ); blocks/dynamic-blocks/breadcrumbs/block.json 0000644 00000001057 15174712003 0015201 0 ustar 00 { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "version": "22.8", "name": "yoast-seo/breadcrumbs", "title": "Yoast Breadcrumbs", "description": "Adds the Yoast SEO breadcrumbs to your template or content.", "category": "yoast-internal-linking-blocks", "icon": "admin-links", "keywords": [ "SEO", "breadcrumbs", "internal linking", "site structure" ], "textdomain": "wordpress-seo", "attributes": { "className": { "type": "string" } }, "example": { "attributes": {} } } blocks/structured-data-blocks/faq/block.json 0000644 00000001756 15174712003 0015154 0 ustar 00 { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "version": "22.7", "name": "yoast/faq-block", "title": "Yoast FAQ", "description": "List your Frequently Asked Questions in an SEO-friendly way.", "category": "yoast-structured-data-blocks", "icon": "editor-ul", "keywords": [ "FAQ", "Frequently Asked Questions", "Schema", "SEO", "Structured Data" ], "textdomain": "wordpress-seo", "attributes": { "questions": { "type": "array" }, "additionalListCssClasses": { "type": "string" } }, "example": { "attributes": { "questions": [ { "id": "faq-question-1", "question": "", "answer": "", "images": [] }, { "id": "faq-question-2", "question": "", "answer": "", "images": [] } ] } }, "editorScript": "yoast-seo-faq-block", "editorStyle": "yoast-seo-structured-data-blocks" } blocks/structured-data-blocks/how-to/block.json 0000644 00000003073 15174712003 0015614 0 ustar 00 { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "version": "22.7", "name": "yoast/how-to-block", "title": "Yoast How-to", "description": "Create a How-to guide in an SEO-friendly way. You can only use one How-to block per post.", "category": "yoast-structured-data-blocks", "icon": "editor-ol", "keywords": [ "How-to", "How to", "Schema", "SEO", "Structured Data" ], "supports": { "multiple": false }, "textdomain": "wordpress-seo", "attributes": { "hasDuration": { "type": "boolean" }, "days": { "type": "string" }, "hours": { "type": "string" }, "minutes": { "type": "string" }, "description": { "type": "string", "source": "html", "selector": ".schema-how-to-description" }, "jsonDescription": { "type": "string" }, "steps": { "type": "array" }, "additionalListCssClasses": { "type": "string" }, "unorderedList": { "type": "boolean" }, "durationText": { "type": "string" }, "defaultDurationText": { "type": "string" } }, "example": { "attributes": { "steps": [ { "id": "how-to-step-example-1", "name": "", "text": "", "images": [] }, { "id": "how-to-step-example-2", "name": "", "text": "", "images": [] } ] } }, "editorScript": "yoast-seo-how-to-block", "editorStyle": "yoast-seo-structured-data-blocks" } wp-seo.php 0000644 00000003175 15174712003 0006475 0 ustar 00 <?php /** * Yoast SEO Plugin. * * @package WPSEO\Main * @copyright Copyright (C) 2008-2024, Yoast BV - support@yoast.com * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License, version 3 or higher * * @wordpress-plugin * Plugin Name: Yoast SEO * Version: 27.1.1 * Plugin URI: https://yoa.st/1uj * Description: The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more. * Author: Team Yoast * Author URI: https://yoa.st/1uk * Text Domain: wordpress-seo * Domain Path: /languages/ * License: GPL v3 * Requires at least: 6.8 * Requires PHP: 7.4 * * WC requires at least: 7.1 * WC tested up to: 10.5 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ if ( ! function_exists( 'add_filter' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } if ( ! defined( 'WPSEO_FILE' ) ) { define( 'WPSEO_FILE', __FILE__ ); } // Load the Yoast SEO plugin. require_once dirname( WPSEO_FILE ) . '/wp-seo-main.php'; vendor_prefixed/guzzlehttp/guzzle/src/Utils.php 0000644 00000032744 15174712003 0016101 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException; use YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler; use YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler; use YoastSEO_Vendor\GuzzleHttp\Handler\Proxy; use YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; final class Utils { /** * Debug function used to describe the provided value type and class. * * @param mixed $input * * @return string Returns a string containing the type of the variable and * if a class is provided, the class name. */ public static function describeType($input) : string { switch (\gettype($input)) { case 'object': return 'object(' . \get_class($input) . ')'; case 'array': return 'array(' . \count($input) . ')'; default: \ob_start(); \var_dump($input); // normalize float vs double /** @var string $varDumpContent */ $varDumpContent = \ob_get_clean(); return \str_replace('double(', 'float(', \rtrim($varDumpContent)); } } /** * Parses an array of header lines into an associative array of headers. * * @param iterable $lines Header lines array of strings in the following * format: "Name: Value" */ public static function headersFromLines(iterable $lines) : array { $headers = []; foreach ($lines as $line) { $parts = \explode(':', $line, 2); $headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null; } return $headers; } /** * Returns a debug stream based on the provided variable. * * @param mixed $value Optional value * * @return resource */ public static function debugResource($value = null) { if (\is_resource($value)) { return $value; } if (\defined('STDOUT')) { return \STDOUT; } return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://output', 'w'); } /** * Chooses and creates a default handler to use based on the environment. * * The returned handler is not wrapped by any default middlewares. * * @return callable(\Psr\Http\Message\RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system. * * @throws \RuntimeException if no viable Handler is available. */ public static function chooseHandler() : callable { $handler = null; if (\defined('CURLOPT_CUSTOMREQUEST') && \function_exists('curl_version') && \version_compare(\curl_version()['version'], '7.21.2') >= 0) { if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) { $handler = \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapSync(new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler(), new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler()); } elseif (\function_exists('curl_exec')) { $handler = new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlHandler(); } elseif (\function_exists('curl_multi_exec')) { $handler = new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlMultiHandler(); } } if (\ini_get('allow_url_fopen')) { $handler = $handler ? \YoastSEO_Vendor\GuzzleHttp\Handler\Proxy::wrapStreaming($handler, new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler()) : new \YoastSEO_Vendor\GuzzleHttp\Handler\StreamHandler(); } elseif (!$handler) { throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.'); } return $handler; } /** * Get the default User-Agent string to use with Guzzle. */ public static function defaultUserAgent() : string { return \sprintf('GuzzleHttp/%d', \YoastSEO_Vendor\GuzzleHttp\ClientInterface::MAJOR_VERSION); } /** * Returns the default cacert bundle for the current system. * * First, the openssl.cafile and curl.cainfo php.ini settings are checked. * If those settings are not configured, then the common locations for * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X * and Windows are checked. If any of these file locations are found on * disk, they will be utilized. * * Note: the result of this function is cached for subsequent calls. * * @throws \RuntimeException if no bundle can be found. * * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+. */ public static function defaultCaBundle() : string { static $cached = null; static $cafiles = [ // Red Hat, CentOS, Fedora (provided by the ca-certificates package) '/etc/pki/tls/certs/ca-bundle.crt', // Ubuntu, Debian (provided by the ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // FreeBSD (provided by the ca_root_nss package) '/usr/local/share/certs/ca-root-nss.crt', // SLES 12 (provided by the ca-certificates package) '/var/lib/ca-certificates/ca-bundle.pem', // OS X provided by homebrew (using the default path) '/usr/local/etc/openssl/cert.pem', // Google app engine '/etc/ca-certificates.crt', // Windows? 'C:\\windows\\system32\\curl-ca-bundle.crt', 'C:\\windows\\curl-ca-bundle.crt', ]; if ($cached) { return $cached; } if ($ca = \ini_get('openssl.cafile')) { return $cached = $ca; } if ($ca = \ini_get('curl.cainfo')) { return $cached = $ca; } foreach ($cafiles as $filename) { if (\file_exists($filename)) { return $cached = $filename; } } throw new \RuntimeException(<<<EOT No system CA bundle could be found in any of the the common system locations. PHP versions earlier than 5.6 are not properly configured to use the system's CA bundle by default. In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle to the 'verify' request option: https://docs.guzzlephp.org/en/latest/request-options.html#verify. If you do not need a specific certificate bundle, then Mozilla provides a commonly used CA bundle which can be downloaded here (provided by the maintainer of cURL): https://curl.haxx.se/ca/cacert.pem. Once you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to the file, allowing you to omit the 'verify' request option. See https://curl.haxx.se/docs/sslcerts.html for more information. EOT ); } /** * Creates an associative array of lowercase header names to the actual * header casing. */ public static function normalizeHeaderKeys(array $headers) : array { $result = []; foreach (\array_keys($headers) as $key) { $result[\strtolower($key)] = $key; } return $result; } /** * Returns true if the provided host matches any of the no proxy areas. * * This method will strip a port from the host if it is present. Each pattern * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == * "baz.foo.com", but ".foo.com" != "foo.com"). * * Areas are matched in the following cases: * 1. "*" (without quotes) always matches any hosts. * 2. An exact match. * 3. The area starts with "." and the area is the last part of the host. e.g. * '.mit.edu' will match any host that ends with '.mit.edu'. * * @param string $host Host to check against the patterns. * @param string[] $noProxyArray An array of host patterns. * * @throws InvalidArgumentException */ public static function isHostInNoProxy(string $host, array $noProxyArray) : bool { if (\strlen($host) === 0) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Empty host provided'); } // Strip port if present. [$host] = \explode(':', $host, 2); foreach ($noProxyArray as $area) { // Always match on wildcards. if ($area === '*') { return \true; } if (empty($area)) { // Don't match on empty values. continue; } if ($area === $host) { // Exact matches. return \true; } // Special match if the area when prefixed with ".". Remove any // existing leading "." and add a new leading ".". $area = '.' . \ltrim($area, '.'); if (\substr($host, -\strlen($area)) === $area) { return \true; } } return \false; } /** * Wrapper for json_decode that throws when an error occurs. * * @param string $json JSON data to parse * @param bool $assoc When true, returned objects will be converted * into associative arrays. * @param int $depth User specified recursion depth. * @param int $options Bitmask of JSON decode options. * * @return object|array|string|int|float|bool|null * * @throws InvalidArgumentException if the JSON cannot be decoded. * * @see https://www.php.net/manual/en/function.json-decode.php */ public static function jsonDecode(string $json, bool $assoc = \false, int $depth = 512, int $options = 0) { $data = \json_decode($json, $assoc, $depth, $options); if (\JSON_ERROR_NONE !== \json_last_error()) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_decode error: ' . \json_last_error_msg()); } return $data; } /** * Wrapper for JSON encoding that throws when an error occurs. * * @param mixed $value The value being encoded * @param int $options JSON encode option bitmask * @param int $depth Set the maximum depth. Must be greater than zero. * * @throws InvalidArgumentException if the JSON cannot be encoded. * * @see https://www.php.net/manual/en/function.json-encode.php */ public static function jsonEncode($value, int $options = 0, int $depth = 512) : string { $json = \json_encode($value, $options, $depth); if (\JSON_ERROR_NONE !== \json_last_error()) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('json_encode error: ' . \json_last_error_msg()); } /** @var string */ return $json; } /** * Wrapper for the hrtime() or microtime() functions * (depending on the PHP version, one of the two is used) * * @return float UNIX timestamp * * @internal */ public static function currentTime() : float { return (float) \function_exists('hrtime') ? \hrtime(\true) / 1000000000.0 : \microtime(\true); } /** * @throws InvalidArgumentException * * @internal */ public static function idnUriConvert(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, int $options = 0) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ($uri->getHost()) { $asciiHost = self::idnToAsci($uri->getHost(), $options, $info); if ($asciiHost === \false) { $errorBitSet = $info['errors'] ?? 0; $errorConstants = \array_filter(\array_keys(\get_defined_constants()), static function (string $name) : bool { return \substr($name, 0, 11) === 'IDNA_ERROR_'; }); $errors = []; foreach ($errorConstants as $errorConstant) { if ($errorBitSet & \constant($errorConstant)) { $errors[] = $errorConstant; } } $errorMessage = 'IDN conversion failed'; if ($errors) { $errorMessage .= ' (errors: ' . \implode(', ', $errors) . ')'; } throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException($errorMessage); } if ($uri->getHost() !== $asciiHost) { // Replace URI only if the ASCII version is different $uri = $uri->withHost($asciiHost); } } return $uri; } /** * @internal */ public static function getenv(string $name) : ?string { if (isset($_SERVER[$name])) { return (string) $_SERVER[$name]; } if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== \false && $value !== null) { return (string) $value; } return null; } /** * @return string|false */ private static function idnToAsci(string $domain, int $options, ?array &$info = []) { if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) { return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info); } throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old'); } } vendor_prefixed/guzzlehttp/guzzle/src/Exception/RequestException.php 0000644 00000011346 15174712003 0022241 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; use YoastSEO_Vendor\GuzzleHttp\BodySummarizer; use YoastSEO_Vendor\GuzzleHttp\BodySummarizerInterface; use YoastSEO_Vendor\Psr\Http\Client\RequestExceptionInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * HTTP Request exception */ class RequestException extends \YoastSEO_Vendor\GuzzleHttp\Exception\TransferException implements \YoastSEO_Vendor\Psr\Http\Client\RequestExceptionInterface { /** * @var RequestInterface */ private $request; /** * @var ResponseInterface|null */ private $response; /** * @var array */ private $handlerContext; public function __construct(string $message, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $previous = null, array $handlerContext = []) { // Set the code of the exception if the response is set and not future. $code = $response ? $response->getStatusCode() : 0; parent::__construct($message, $code, $previous); $this->request = $request; $this->response = $response; $this->handlerContext = $handlerContext; } /** * Wrap non-RequestExceptions with a RequestException */ public static function wrapException(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \Throwable $e) : \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException { return $e instanceof \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException ? $e : new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException($e->getMessage(), $request, null, $e); } /** * Factory method to create a new exception with a normalized error message * * @param RequestInterface $request Request sent * @param ResponseInterface $response Response received * @param \Throwable|null $previous Previous exception * @param array $handlerContext Optional handler context * @param BodySummarizerInterface|null $bodySummarizer Optional body summarizer */ public static function create(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $previous = null, array $handlerContext = [], ?\YoastSEO_Vendor\GuzzleHttp\BodySummarizerInterface $bodySummarizer = null) : self { if (!$response) { return new self('Error completing request', $request, null, $previous, $handlerContext); } $level = (int) \floor($response->getStatusCode() / 100); if ($level === 4) { $label = 'Client error'; $className = \YoastSEO_Vendor\GuzzleHttp\Exception\ClientException::class; } elseif ($level === 5) { $label = 'Server error'; $className = \YoastSEO_Vendor\GuzzleHttp\Exception\ServerException::class; } else { $label = 'Unsuccessful request'; $className = __CLASS__; } $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($request->getUri()); // Client Error: `GET /` resulted in a `404 Not Found` response: // <html> ... (truncated) $message = \sprintf('%s: `%s %s` resulted in a `%s %s` response', $label, $request->getMethod(), $uri->__toString(), $response->getStatusCode(), $response->getReasonPhrase()); $summary = ($bodySummarizer ?? new \YoastSEO_Vendor\GuzzleHttp\BodySummarizer())->summarize($response); if ($summary !== null) { $message .= ":\n{$summary}\n"; } return new $className($message, $request, $response, $previous, $handlerContext); } /** * Get the request that caused the exception */ public function getRequest() : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { return $this->request; } /** * Get the associated response */ public function getResponse() : ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->response; } /** * Check if a response was received */ public function hasResponse() : bool { return $this->response !== null; } /** * Get contextual information about the error from the underlying handler. * * The contents of this array will vary depending on which handler you are * using. It may also be just an empty array. Relying on this data will * couple you to a specific handler, but can give more debug information * when needed. */ public function getHandlerContext() : array { return $this->handlerContext; } } vendor_prefixed/guzzlehttp/guzzle/src/Exception/ConnectException.php 0000644 00000003027 15174712003 0022177 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; use YoastSEO_Vendor\Psr\Http\Client\NetworkExceptionInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Exception thrown when a connection cannot be established. * * Note that no response is present for a ConnectException */ class ConnectException extends \YoastSEO_Vendor\GuzzleHttp\Exception\TransferException implements \YoastSEO_Vendor\Psr\Http\Client\NetworkExceptionInterface { /** * @var RequestInterface */ private $request; /** * @var array */ private $handlerContext; public function __construct(string $message, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\Throwable $previous = null, array $handlerContext = []) { parent::__construct($message, 0, $previous); $this->request = $request; $this->handlerContext = $handlerContext; } /** * Get the request that caused the exception */ public function getRequest() : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { return $this->request; } /** * Get contextual information about the error from the underlying handler. * * The contents of this array will vary depending on which handler you are * using. It may also be just an empty array. Relying on this data will * couple you to a specific handler, but can give more debug information * when needed. */ public function getHandlerContext() : array { return $this->handlerContext; } } vendor_prefixed/guzzlehttp/guzzle/src/Exception/ClientException.php 0000644 00000000331 15174712003 0022017 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; /** * Exception when a client error is encountered (4xx codes) */ class ClientException extends \YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException { } vendor_prefixed/guzzlehttp/guzzle/src/Exception/GuzzleException.php 0000644 00000000326 15174712003 0022065 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; use YoastSEO_Vendor\Psr\Http\Client\ClientExceptionInterface; interface GuzzleException extends \YoastSEO_Vendor\Psr\Http\Client\ClientExceptionInterface { } vendor_prefixed/guzzlehttp/guzzle/src/Exception/BadResponseException.php 0000644 00000002150 15174712003 0023007 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Exception when an HTTP error occurs (4xx or 5xx error) */ class BadResponseException extends \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException { public function __construct(string $message, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, ?\Throwable $previous = null, array $handlerContext = []) { parent::__construct($message, $request, $response, $previous, $handlerContext); } /** * Current exception and the ones that extend it will always have a response. */ public function hasResponse() : bool { return \true; } /** * This function narrows the return type from the parent class and does not allow it to be nullable. */ public function getResponse() : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { /** @var ResponseInterface */ return parent::getResponse(); } } vendor_prefixed/guzzlehttp/guzzle/src/Exception/ServerException.php 0000644 00000000331 15174712003 0022047 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; /** * Exception when a server error is encountered (5xx codes) */ class ServerException extends \YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException { } vendor_prefixed/guzzlehttp/guzzle/src/Exception/TransferException.php 0000644 00000000257 15174712003 0022374 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; class TransferException extends \RuntimeException implements \YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException { } vendor_prefixed/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php 0000644 00000000233 15174712003 0024035 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; class TooManyRedirectsException extends \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException { } vendor_prefixed/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php 0000644 00000000304 15174712003 0023672 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Exception; final class InvalidArgumentException extends \InvalidArgumentException implements \YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException { } vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatter.php 0000644 00000016170 15174712003 0020244 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\Psr\Http\Message\MessageInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Formats log messages using variable substitutions for requests, responses, * and other transactional data. * * The following variable substitutions are supported: * * - {request}: Full HTTP request message * - {response}: Full HTTP response message * - {ts}: ISO 8601 date in GMT * - {date_iso_8601} ISO 8601 date in GMT * - {date_common_log} Apache common log date using the configured timezone. * - {host}: Host of the request * - {method}: Method of the request * - {uri}: URI of the request * - {version}: Protocol version * - {target}: Request target of the request (path + query + fragment) * - {hostname}: Hostname of the machine that sent the request * - {code}: Status code of the response (if available) * - {phrase}: Reason phrase of the response (if available) * - {error}: Any error messages (if available) * - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message * - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message * - {req_headers}: Request headers * - {res_headers}: Response headers * - {req_body}: Request body * - {res_body}: Response body * * @final */ class MessageFormatter implements \YoastSEO_Vendor\GuzzleHttp\MessageFormatterInterface { /** * Apache Common Log Format. * * @see https://httpd.apache.org/docs/2.4/logs.html#common * * @var string */ public const CLF = '{hostname} {req_header_User-Agent} - [{date_common_log}] "{method} {target} HTTP/{version}" {code} {res_header_Content-Length}'; public const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; /** * @var string Template used to format log messages */ private $template; /** * @param string $template Log message template */ public function __construct(?string $template = self::CLF) { $this->template = $template ?: self::CLF; } /** * Returns a formatted message string. * * @param RequestInterface $request Request that was sent * @param ResponseInterface|null $response Response that was received * @param \Throwable|null $error Exception that was received */ public function format(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $error = null) : string { $cache = []; /** @var string */ return \preg_replace_callback('/{\\s*([A-Za-z_\\-\\.0-9]+)\\s*}/', function (array $matches) use($request, $response, $error, &$cache) { if (isset($cache[$matches[1]])) { return $cache[$matches[1]]; } $result = ''; switch ($matches[1]) { case 'request': $result = \YoastSEO_Vendor\GuzzleHttp\Psr7\Message::toString($request); break; case 'response': $result = $response ? \YoastSEO_Vendor\GuzzleHttp\Psr7\Message::toString($response) : ''; break; case 'req_headers': $result = \trim($request->getMethod() . ' ' . $request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . "\r\n" . $this->headers($request); break; case 'res_headers': $result = $response ? \sprintf('HTTP/%s %d %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase()) . "\r\n" . $this->headers($response) : 'NULL'; break; case 'req_body': $result = $request->getBody()->__toString(); break; case 'res_body': if (!$response instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface) { $result = 'NULL'; break; } $body = $response->getBody(); if (!$body->isSeekable()) { $result = 'RESPONSE_NOT_LOGGEABLE'; break; } $result = $response->getBody()->__toString(); break; case 'ts': case 'date_iso_8601': $result = \gmdate('c'); break; case 'date_common_log': $result = \date('d/M/Y:H:i:s O'); break; case 'method': $result = $request->getMethod(); break; case 'version': $result = $request->getProtocolVersion(); break; case 'uri': case 'url': $result = $request->getUri()->__toString(); break; case 'target': $result = $request->getRequestTarget(); break; case 'req_version': $result = $request->getProtocolVersion(); break; case 'res_version': $result = $response ? $response->getProtocolVersion() : 'NULL'; break; case 'host': $result = $request->getHeaderLine('Host'); break; case 'hostname': $result = \gethostname(); break; case 'code': $result = $response ? $response->getStatusCode() : 'NULL'; break; case 'phrase': $result = $response ? $response->getReasonPhrase() : 'NULL'; break; case 'error': $result = $error ? $error->getMessage() : 'NULL'; break; default: // handle prefixed dynamic headers if (\strpos($matches[1], 'req_header_') === 0) { $result = $request->getHeaderLine(\substr($matches[1], 11)); } elseif (\strpos($matches[1], 'res_header_') === 0) { $result = $response ? $response->getHeaderLine(\substr($matches[1], 11)) : 'NULL'; } } $cache[$matches[1]] = $result; return $result; }, $this->template); } /** * Get headers from message as string */ private function headers(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : string { $result = ''; foreach ($message->getHeaders() as $name => $values) { $result .= $name . ': ' . \implode(', ', $values) . "\r\n"; } return \trim($result); } } vendor_prefixed/guzzlehttp/guzzle/src/functions_include.php 0000644 00000000320 15174712003 0020475 0 ustar 00 <?php namespace YoastSEO_Vendor; // Don't redefine the functions if included multiple times. if (!\function_exists('YoastSEO_Vendor\\GuzzleHttp\\describe_type')) { require __DIR__ . '/functions.php'; } vendor_prefixed/guzzlehttp/guzzle/src/RedirectMiddleware.php 0000644 00000020552 15174712003 0020532 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException; use YoastSEO_Vendor\GuzzleHttp\Exception\TooManyRedirectsException; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Request redirect middleware. * * Apply this middleware like other middleware using * {@see \GuzzleHttp\Middleware::redirect()}. * * @final */ class RedirectMiddleware { public const HISTORY_HEADER = 'X-Guzzle-Redirect-History'; public const STATUS_HISTORY_HEADER = 'X-Guzzle-Redirect-Status-History'; /** * @var array */ public static $defaultSettings = ['max' => 5, 'protocols' => ['http', 'https'], 'strict' => \false, 'referer' => \false, 'track_redirects' => \false]; /** * @var callable(RequestInterface, array): PromiseInterface */ private $nextHandler; /** * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. */ public function __construct(callable $nextHandler) { $this->nextHandler = $nextHandler; } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $fn = $this->nextHandler; if (empty($options['allow_redirects'])) { return $fn($request, $options); } if ($options['allow_redirects'] === \true) { $options['allow_redirects'] = self::$defaultSettings; } elseif (!\is_array($options['allow_redirects'])) { throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); } else { // Merge the default settings with the provided settings $options['allow_redirects'] += self::$defaultSettings; } if (empty($options['allow_redirects']['max'])) { return $fn($request, $options); } return $fn($request, $options)->then(function (\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) use($request, $options) { return $this->checkRedirect($request, $options, $response); }); } /** * @return ResponseInterface|PromiseInterface */ public function checkRedirect(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { if (\strpos((string) $response->getStatusCode(), '3') !== 0 || !$response->hasHeader('Location')) { return $response; } $this->guardMax($request, $response, $options); $nextRequest = $this->modifyRequest($request, $options, $response); // If authorization is handled by curl, unset it if URI is cross-origin. if (\YoastSEO_Vendor\GuzzleHttp\Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && \defined('\\CURLOPT_HTTPAUTH')) { unset($options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD]); } if (isset($options['allow_redirects']['on_redirect'])) { $options['allow_redirects']['on_redirect']($request, $response, $nextRequest->getUri()); } $promise = $this($nextRequest, $options); // Add headers to be able to track history of redirects. if (!empty($options['allow_redirects']['track_redirects'])) { return $this->withTracking($promise, (string) $nextRequest->getUri(), $response->getStatusCode()); } return $promise; } /** * Enable tracking on promise. */ private function withTracking(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise, string $uri, int $statusCode) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $promise->then(static function (\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) use($uri, $statusCode) { // Note that we are pushing to the front of the list as this // would be an earlier response than what is currently present // in the history header. $historyHeader = $response->getHeader(self::HISTORY_HEADER); $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); \array_unshift($historyHeader, $uri); \array_unshift($statusHeader, (string) $statusCode); return $response->withHeader(self::HISTORY_HEADER, $historyHeader)->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); }); } /** * Check for too many redirects. * * @throws TooManyRedirectsException Too many redirects. */ private function guardMax(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, array &$options) : void { $current = $options['__redirect_count'] ?? 0; $options['__redirect_count'] = $current + 1; $max = $options['allow_redirects']['max']; if ($options['__redirect_count'] > $max) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\TooManyRedirectsException("Will not follow more than {$max} redirects", $request, $response); } } public function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { // Request modifications to apply. $modify = []; $protocols = $options['allow_redirects']['protocols']; // Use a GET request if this is an entity enclosing request and we are // not forcing RFC compliance, but rather emulating what all browsers // would do. $statusCode = $response->getStatusCode(); if ($statusCode == 303 || $statusCode <= 302 && !$options['allow_redirects']['strict']) { $safeMethods = ['GET', 'HEAD', 'OPTIONS']; $requestMethod = $request->getMethod(); $modify['method'] = \in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET'; $modify['body'] = ''; } $uri = self::redirectUri($request, $response, $protocols); if (isset($options['idn_conversion']) && $options['idn_conversion'] !== \false) { $idnOptions = $options['idn_conversion'] === \true ? \IDNA_DEFAULT : $options['idn_conversion']; $uri = \YoastSEO_Vendor\GuzzleHttp\Utils::idnUriConvert($uri, $idnOptions); } $modify['uri'] = $uri; \YoastSEO_Vendor\GuzzleHttp\Psr7\Message::rewindBody($request); // Add the Referer header if it is told to do so and only // add the header if we are not redirecting from https to http. if ($options['allow_redirects']['referer'] && $modify['uri']->getScheme() === $request->getUri()->getScheme()) { $uri = $request->getUri()->withUserInfo(''); $modify['set_headers']['Referer'] = (string) $uri; } else { $modify['remove_headers'][] = 'Referer'; } // Remove Authorization and Cookie headers if URI is cross-origin. if (\YoastSEO_Vendor\GuzzleHttp\Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) { $modify['remove_headers'][] = 'Authorization'; $modify['remove_headers'][] = 'Cookie'; } return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify); } /** * Set the appropriate URL on the request based on the location header. */ private static function redirectUri(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, array $protocols) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $location = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve($request->getUri(), new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($response->getHeaderLine('Location'))); // Ensure that the redirect URI is allowed based on the protocols. if (!\in_array($location->getScheme(), $protocols)) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException(\sprintf('Redirect URI, %s, does not use one of the allowed redirect protocols: %s', $location, \implode(', ', $protocols)), $request, $response); } return $location; } } vendor_prefixed/guzzlehttp/guzzle/src/HandlerStack.php 0000644 00000021251 15174712003 0017333 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Creates a composed Guzzle handler function by stacking middlewares on top of * an HTTP handler function. * * @final */ class HandlerStack { /** * @var (callable(RequestInterface, array): PromiseInterface)|null */ private $handler; /** * @var array{(callable(callable(RequestInterface, array): PromiseInterface): callable), (string|null)}[] */ private $stack = []; /** * @var (callable(RequestInterface, array): PromiseInterface)|null */ private $cached; /** * Creates a default handler stack that can be used by clients. * * The returned handler will wrap the provided handler or use the most * appropriate default handler for your system. The returned HandlerStack has * support for cookies, redirects, HTTP error exceptions, and preparing a body * before sending. * * The returned handler stack can be passed to a client in the "handler" * option. * * @param (callable(RequestInterface, array): PromiseInterface)|null $handler HTTP handler function to use with the stack. If no * handler is provided, the best handler for your * system will be utilized. */ public static function create(?callable $handler = null) : self { $stack = new self($handler ?: \YoastSEO_Vendor\GuzzleHttp\Utils::chooseHandler()); $stack->push(\YoastSEO_Vendor\GuzzleHttp\Middleware::httpErrors(), 'http_errors'); $stack->push(\YoastSEO_Vendor\GuzzleHttp\Middleware::redirect(), 'allow_redirects'); $stack->push(\YoastSEO_Vendor\GuzzleHttp\Middleware::cookies(), 'cookies'); $stack->push(\YoastSEO_Vendor\GuzzleHttp\Middleware::prepareBody(), 'prepare_body'); return $stack; } /** * @param (callable(RequestInterface, array): PromiseInterface)|null $handler Underlying HTTP handler. */ public function __construct(?callable $handler = null) { $this->handler = $handler; } /** * Invokes the handler stack as a composed handler * * @return ResponseInterface|PromiseInterface */ public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) { $handler = $this->resolve(); return $handler($request, $options); } /** * Dumps a string representation of the stack. * * @return string */ public function __toString() { $depth = 0; $stack = []; if ($this->handler !== null) { $stack[] = '0) Handler: ' . $this->debugCallable($this->handler); } $result = ''; foreach (\array_reverse($this->stack) as $tuple) { ++$depth; $str = "{$depth}) Name: '{$tuple[1]}', "; $str .= 'Function: ' . $this->debugCallable($tuple[0]); $result = "> {$str}\n{$result}"; $stack[] = $str; } foreach (\array_keys($stack) as $k) { $result .= "< {$stack[$k]}\n"; } return $result; } /** * Set the HTTP handler that actually returns a promise. * * @param callable(RequestInterface, array): PromiseInterface $handler Accepts a request and array of options and * returns a Promise. */ public function setHandler(callable $handler) : void { $this->handler = $handler; $this->cached = null; } /** * Returns true if the builder has a handler. */ public function hasHandler() : bool { return $this->handler !== null; } /** * Unshift a middleware to the bottom of the stack. * * @param callable(callable): callable $middleware Middleware function * @param string $name Name to register for this middleware. */ public function unshift(callable $middleware, ?string $name = null) : void { \array_unshift($this->stack, [$middleware, $name]); $this->cached = null; } /** * Push a middleware to the top of the stack. * * @param callable(callable): callable $middleware Middleware function * @param string $name Name to register for this middleware. */ public function push(callable $middleware, string $name = '') : void { $this->stack[] = [$middleware, $name]; $this->cached = null; } /** * Add a middleware before another middleware by name. * * @param string $findName Middleware to find * @param callable(callable): callable $middleware Middleware function * @param string $withName Name to register for this middleware. */ public function before(string $findName, callable $middleware, string $withName = '') : void { $this->splice($findName, $withName, $middleware, \true); } /** * Add a middleware after another middleware by name. * * @param string $findName Middleware to find * @param callable(callable): callable $middleware Middleware function * @param string $withName Name to register for this middleware. */ public function after(string $findName, callable $middleware, string $withName = '') : void { $this->splice($findName, $withName, $middleware, \false); } /** * Remove a middleware by instance or name from the stack. * * @param callable|string $remove Middleware to remove by instance or name. */ public function remove($remove) : void { if (!\is_string($remove) && !\is_callable($remove)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->cached = null; $idx = \is_callable($remove) ? 0 : 1; $this->stack = \array_values(\array_filter($this->stack, static function ($tuple) use($idx, $remove) { return $tuple[$idx] !== $remove; })); } /** * Compose the middleware and handler into a single callable function. * * @return callable(RequestInterface, array): PromiseInterface */ public function resolve() : callable { if ($this->cached === null) { if (($prev = $this->handler) === null) { throw new \LogicException('No handler has been specified'); } foreach (\array_reverse($this->stack) as $fn) { /** @var callable(RequestInterface, array): PromiseInterface $prev */ $prev = $fn[0]($prev); } $this->cached = $prev; } return $this->cached; } private function findByName(string $name) : int { foreach ($this->stack as $k => $v) { if ($v[1] === $name) { return $k; } } throw new \InvalidArgumentException("Middleware not found: {$name}"); } /** * Splices a function into the middleware list at a specific position. */ private function splice(string $findName, string $withName, callable $middleware, bool $before) : void { $this->cached = null; $idx = $this->findByName($findName); $tuple = [$middleware, $withName]; if ($before) { if ($idx === 0) { \array_unshift($this->stack, $tuple); } else { $replacement = [$tuple, $this->stack[$idx]]; \array_splice($this->stack, $idx, 1, $replacement); } } elseif ($idx === \count($this->stack) - 1) { $this->stack[] = $tuple; } else { $replacement = [$this->stack[$idx], $tuple]; \array_splice($this->stack, $idx, 1, $replacement); } } /** * Provides a debug string for a given callable. * * @param callable|string $fn Function to write as a string. */ private function debugCallable($fn) : string { if (\is_string($fn)) { return "callable({$fn})"; } if (\is_array($fn)) { return \is_string($fn[0]) ? "callable({$fn[0]}::{$fn[1]})" : "callable(['" . \get_class($fn[0]) . "', '{$fn[1]}'])"; } /** @var object $fn */ return 'callable(' . \spl_object_hash($fn) . ')'; } } vendor_prefixed/guzzlehttp/guzzle/src/Client.php 0000644 00000047011 15174712003 0016210 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar; use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; use YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * @final */ class Client implements \YoastSEO_Vendor\GuzzleHttp\ClientInterface, \YoastSEO_Vendor\Psr\Http\Client\ClientInterface { use ClientTrait; /** * @var array Default request options */ private $config; /** * Clients accept an array of constructor parameters. * * Here's an example of creating a client using a base_uri and an array of * default request options to apply to each request: * * $client = new Client([ * 'base_uri' => 'http://www.foo.com/1.0/', * 'timeout' => 0, * 'allow_redirects' => false, * 'proxy' => '192.168.16.1:10' * ]); * * Client configuration settings include the following options: * * - handler: (callable) Function that transfers HTTP requests over the * wire. The function is called with a Psr7\Http\Message\RequestInterface * and array of transfer options, and must return a * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a * Psr7\Http\Message\ResponseInterface on success. * If no handler is provided, a default handler will be created * that enables all of the request options below by attaching all of the * default middleware to the handler. * - base_uri: (string|UriInterface) Base URI of the client that is merged * into relative URIs. Can be a string or instance of UriInterface. * - **: any request option * * @param array $config Client configuration settings. * * @see RequestOptions for a list of available request options. */ public function __construct(array $config = []) { if (!isset($config['handler'])) { $config['handler'] = \YoastSEO_Vendor\GuzzleHttp\HandlerStack::create(); } elseif (!\is_callable($config['handler'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('handler must be a callable'); } // Convert the base_uri to a UriInterface if (isset($config['base_uri'])) { $config['base_uri'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']); } $this->configureDefaults($config); } /** * @param string $method * @param array $args * * @return PromiseInterface|ResponseInterface * * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. */ public function __call($method, $args) { if (\count($args) < 1) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Magic request methods require a URI and optional options array'); } $uri = $args[0]; $opts = $args[1] ?? []; return \substr($method, -5) === 'Async' ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) : $this->request($method, $uri, $opts); } /** * Asynchronously send an HTTP request. * * @param array $options Request options to apply to the given * request and to the transfer. See \GuzzleHttp\RequestOptions. */ public function sendAsync(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { // Merge the base URI into the request URI if needed. $options = $this->prepareDefaults($options); return $this->transfer($request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), $options); } /** * Send an HTTP request. * * @param array $options Request options to apply to the given * request and to the transfer. See \GuzzleHttp\RequestOptions. * * @throws GuzzleException */ public function send(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true; return $this->sendAsync($request, $options)->wait(); } /** * The HttpClient PSR (PSR-18) specify this method. * * {@inheritDoc} */ public function sendRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true; $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::ALLOW_REDIRECTS] = \false; $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::HTTP_ERRORS] = \false; return $this->sendAsync($request, $options)->wait(); } /** * Create and send an asynchronous HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string $method HTTP method * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. */ public function requestAsync(string $method, $uri = '', array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $options = $this->prepareDefaults($options); // Remove request modifying parameter because it can be done up-front. $headers = $options['headers'] ?? []; $body = $options['body'] ?? null; $version = $options['version'] ?? '1.1'; // Merge the URI into the base URI. $uri = $this->buildUri(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($uri), $options); if (\is_array($body)) { throw $this->invalidBody(); } $request = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($method, $uri, $headers, $body, $version); // Remove the option so that they are not doubly-applied. unset($options['headers'], $options['body'], $options['version']); return $this->transfer($request, $options); } /** * Create and send an HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string $method HTTP method. * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. * * @throws GuzzleException */ public function request(string $method, $uri = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { $options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS] = \true; return $this->requestAsync($method, $uri, $options)->wait(); } /** * Get a client configuration option. * * These options include default request options of the client, a "handler" * (if utilized by the concrete client), and a "base_uri" if utilized by * the concrete client. * * @param string|null $option The config option to retrieve. * * @return mixed * * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. */ public function getConfig(?string $option = null) { return $option === null ? $this->config : $this->config[$option] ?? null; } private function buildUri(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $config) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if (isset($config['base_uri'])) { $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::uriFor($config['base_uri']), $uri); } if (isset($config['idn_conversion']) && $config['idn_conversion'] !== \false) { $idnOptions = $config['idn_conversion'] === \true ? \IDNA_DEFAULT : $config['idn_conversion']; $uri = \YoastSEO_Vendor\GuzzleHttp\Utils::idnUriConvert($uri, $idnOptions); } return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; } /** * Configures the default options for a client. */ private function configureDefaults(array $config) : void { $defaults = ['allow_redirects' => \YoastSEO_Vendor\GuzzleHttp\RedirectMiddleware::$defaultSettings, 'http_errors' => \true, 'decode_content' => \true, 'verify' => \true, 'cookies' => \false, 'idn_conversion' => \false]; // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. // We can only trust the HTTP_PROXY environment variable in a CLI // process due to the fact that PHP has no reliable mechanism to // get environment variables that start with "HTTP_". if (\PHP_SAPI === 'cli' && ($proxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('HTTP_PROXY'))) { $defaults['proxy']['http'] = $proxy; } if ($proxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('HTTPS_PROXY')) { $defaults['proxy']['https'] = $proxy; } if ($noProxy = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('NO_PROXY')) { $cleanedNoProxy = \str_replace(' ', '', $noProxy); $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); } $this->config = $config + $defaults; if (!empty($config['cookies']) && $config['cookies'] === \true) { $this->config['cookies'] = new \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar(); } // Add the default user-agent header. if (!isset($this->config['headers'])) { $this->config['headers'] = ['User-Agent' => \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent()]; } else { // Add the User-Agent header if one was not already set. foreach (\array_keys($this->config['headers']) as $name) { if (\strtolower($name) === 'user-agent') { return; } } $this->config['headers']['User-Agent'] = \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent(); } } /** * Merges default options into the array. * * @param array $options Options to modify by reference */ private function prepareDefaults(array $options) : array { $defaults = $this->config; if (!empty($defaults['headers'])) { // Default headers are only added if they are not present. $defaults['_conditional'] = $defaults['headers']; unset($defaults['headers']); } // Special handling for headers is required as they are added as // conditional headers and as headers passed to a request ctor. if (\array_key_exists('headers', $options)) { // Allows default headers to be unset. if ($options['headers'] === null) { $defaults['_conditional'] = []; unset($options['headers']); } elseif (!\is_array($options['headers'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('headers must be an array'); } } // Shallow merge defaults underneath options. $result = $options + $defaults; // Remove null values. foreach ($result as $k => $v) { if ($v === null) { unset($result[$k]); } } return $result; } /** * Transfers the given request and applies request options. * * The URI of the request is not modified and the request options are used * as-is without merging in default options. * * @param array $options See \GuzzleHttp\RequestOptions. */ private function transfer(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $request = $this->applyOptions($request, $options); /** @var HandlerStack $handler */ $handler = $options['handler']; try { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($handler($request, $options)); } catch (\Exception $e) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e); } } /** * Applies the array of request options to a request. */ private function applyOptions(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { $modify = ['set_headers' => []]; if (isset($options['headers'])) { if (\array_keys($options['headers']) === \range(0, \count($options['headers']) - 1)) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('The headers array must have header name as keys.'); } $modify['set_headers'] = $options['headers']; unset($options['headers']); } if (isset($options['form_params'])) { if (isset($options['multipart'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('You cannot use ' . 'form_params and multipart at the same time. Use the ' . 'form_params option if you want to send application/' . 'x-www-form-urlencoded requests, and the multipart ' . 'option to send multipart/form-data requests.'); } $options['body'] = \http_build_query($options['form_params'], '', '&'); unset($options['form_params']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; } if (isset($options['multipart'])) { $options['body'] = new \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream($options['multipart']); unset($options['multipart']); } if (isset($options['json'])) { $options['body'] = \YoastSEO_Vendor\GuzzleHttp\Utils::jsonEncode($options['json']); unset($options['json']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/json'; } if (!empty($options['decode_content']) && $options['decode_content'] !== \true) { // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; } if (isset($options['body'])) { if (\is_array($options['body'])) { throw $this->invalidBody(); } $modify['body'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($options['body']); unset($options['body']); } if (!empty($options['auth']) && \is_array($options['auth'])) { $value = $options['auth']; $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; switch ($type) { case 'basic': // Ensure that we don't have the header in different case and set the new value. $modify['set_headers'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); $modify['set_headers']['Authorization'] = 'Basic ' . \base64_encode("{$value[0]}:{$value[1]}"); break; case 'digest': // @todo: Do not rely on curl $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; break; case 'ntlm': $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; break; } } if (isset($options['query'])) { $value = $options['query']; if (\is_array($value)) { $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); } if (!\is_string($value)) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('query must be a string or array'); } $modify['query'] = $value; unset($options['query']); } // Ensure that sink is not an invalid value. if (isset($options['sink'])) { // TODO: Add more sink validation? if (\is_bool($options['sink'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('sink must not be a boolean'); } } if (isset($options['version'])) { $modify['version'] = $options['version']; } $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify); if ($request->getBody() instanceof \YoastSEO_Vendor\GuzzleHttp\Psr7\MultipartStream) { // Use a multipart/form-data POST if a Content-Type is not set. // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' . $request->getBody()->getBoundary(); } // Merge in conditional headers if they are not present. if (isset($options['_conditional'])) { // Build up the changes so it's in a single clone of the message. $modify = []; foreach ($options['_conditional'] as $k => $v) { if (!$request->hasHeader($k)) { $modify['set_headers'][$k] = $v; } } $request = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify); // Don't pass this internal value along to middleware/handlers. unset($options['_conditional']); } return $request; } /** * Return an InvalidArgumentException with pre-set message. */ private function invalidBody() : \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException { return new \YoastSEO_Vendor\GuzzleHttp\Exception\InvalidArgumentException('Passing in the "body" request ' . 'option as an array to send a request is not supported. ' . 'Please use the "form_params" request option to send a ' . 'application/x-www-form-urlencoded request, or the "multipart" ' . 'request option to send a multipart/form-data request.'); } } vendor_prefixed/guzzlehttp/guzzle/src/RequestOptions.php 0000644 00000025300 15174712003 0017773 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; /** * This class contains a list of built-in Guzzle request options. * * @see https://docs.guzzlephp.org/en/latest/request-options.html */ final class RequestOptions { /** * allow_redirects: (bool|array) Controls redirect behavior. Pass false * to disable redirects, pass true to enable redirects, pass an * associative to provide custom redirect settings. Defaults to "false". * This option only works if your handler has the RedirectMiddleware. When * passing an associative array, you can provide the following key value * pairs: * * - max: (int, default=5) maximum number of allowed redirects. * - strict: (bool, default=false) Set to true to use strict redirects * meaning redirect POST requests with POST requests vs. doing what most * browsers do which is redirect POST requests with GET requests * - referer: (bool, default=false) Set to true to enable the Referer * header. * - protocols: (array, default=['http', 'https']) Allowed redirect * protocols. * - on_redirect: (callable) PHP callable that is invoked when a redirect * is encountered. The callable is invoked with the request, the redirect * response that was received, and the effective URI. Any return value * from the on_redirect function is ignored. */ public const ALLOW_REDIRECTS = 'allow_redirects'; /** * auth: (array) Pass an array of HTTP authentication parameters to use * with the request. The array must contain the username in index [0], * the password in index [1], and you can optionally provide a built-in * authentication type in index [2]. Pass null to disable authentication * for a request. */ public const AUTH = 'auth'; /** * body: (resource|string|null|int|float|StreamInterface|callable|\Iterator) * Body to send in the request. */ public const BODY = 'body'; /** * cert: (string|array) Set to a string to specify the path to a file * containing a PEM formatted SSL client side certificate. If a password * is required, then set cert to an array containing the path to the PEM * file in the first array element followed by the certificate password * in the second array element. */ public const CERT = 'cert'; /** * cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false) * Specifies whether or not cookies are used in a request or what cookie * jar to use or what cookies to send. This option only works if your * handler has the `cookie` middleware. Valid values are `false` and * an instance of {@see Cookie\CookieJarInterface}. */ public const COOKIES = 'cookies'; /** * connect_timeout: (float, default=0) Float describing the number of * seconds to wait while trying to connect to a server. Use 0 to wait * 300 seconds (the default behavior). */ public const CONNECT_TIMEOUT = 'connect_timeout'; /** * crypto_method: (int) A value describing the minimum TLS protocol * version to use. * * This setting must be set to one of the * ``STREAM_CRYPTO_METHOD_TLS*_CLIENT`` constants. PHP 7.4 or higher is * required in order to use TLS 1.3, and cURL 7.34.0 or higher is required * in order to specify a crypto method, with cURL 7.52.0 or higher being * required to use TLS 1.3. */ public const CRYPTO_METHOD = 'crypto_method'; /** * debug: (bool|resource) Set to true or set to a PHP stream returned by * fopen() enable debug output with the HTTP handler used to send a * request. */ public const DEBUG = 'debug'; /** * decode_content: (bool, default=true) Specify whether or not * Content-Encoding responses (gzip, deflate, etc.) are automatically * decoded. */ public const DECODE_CONTENT = 'decode_content'; /** * delay: (int) The amount of time to delay before sending in milliseconds. */ public const DELAY = 'delay'; /** * expect: (bool|integer) Controls the behavior of the * "Expect: 100-Continue" header. * * Set to `true` to enable the "Expect: 100-Continue" header for all * requests that sends a body. Set to `false` to disable the * "Expect: 100-Continue" header for all requests. Set to a number so that * the size of the payload must be greater than the number in order to send * the Expect header. Setting to a number will send the Expect header for * all requests in which the size of the payload cannot be determined or * where the body is not rewindable. * * By default, Guzzle will add the "Expect: 100-Continue" header when the * size of the body of a request is greater than 1 MB and a request is * using HTTP/1.1. */ public const EXPECT = 'expect'; /** * form_params: (array) Associative array of form field names to values * where each value is a string or array of strings. Sets the Content-Type * header to application/x-www-form-urlencoded when no Content-Type header * is already present. */ public const FORM_PARAMS = 'form_params'; /** * headers: (array) Associative array of HTTP headers. Each value MUST be * a string or array of strings. */ public const HEADERS = 'headers'; /** * http_errors: (bool, default=true) Set to false to disable exceptions * when a non- successful HTTP response is received. By default, * exceptions will be thrown for 4xx and 5xx responses. This option only * works if your handler has the `httpErrors` middleware. */ public const HTTP_ERRORS = 'http_errors'; /** * idn: (bool|int, default=true) A combination of IDNA_* constants for * idn_to_ascii() PHP's function (see "options" parameter). Set to false to * disable IDN support completely, or to true to use the default * configuration (IDNA_DEFAULT constant). */ public const IDN_CONVERSION = 'idn_conversion'; /** * json: (mixed) Adds JSON data to a request. The provided value is JSON * encoded and a Content-Type header of application/json will be added to * the request if no Content-Type header is already present. */ public const JSON = 'json'; /** * multipart: (array) Array of associative arrays, each containing a * required "name" key mapping to the form field, name, a required * "contents" key mapping to a StreamInterface|resource|string, an * optional "headers" associative array of custom headers, and an * optional "filename" key mapping to a string to send as the filename in * the part. If no "filename" key is present, then no "filename" attribute * will be added to the part. */ public const MULTIPART = 'multipart'; /** * on_headers: (callable) A callable that is invoked when the HTTP headers * of the response have been received but the body has not yet begun to * download. */ public const ON_HEADERS = 'on_headers'; /** * on_stats: (callable) allows you to get access to transfer statistics of * a request and access the lower level transfer details of the handler * associated with your client. ``on_stats`` is a callable that is invoked * when a handler has finished sending a request. The callback is invoked * with transfer statistics about the request, the response received, or * the error encountered. Included in the data is the total amount of time * taken to send the request. */ public const ON_STATS = 'on_stats'; /** * progress: (callable) Defines a function to invoke when transfer * progress is made. The function accepts the following positional * arguments: the total number of bytes expected to be downloaded, the * number of bytes downloaded so far, the number of bytes expected to be * uploaded, the number of bytes uploaded so far. */ public const PROGRESS = 'progress'; /** * proxy: (string|array) Pass a string to specify an HTTP proxy, or an * array to specify different proxies for different protocols (where the * key is the protocol and the value is a proxy string). */ public const PROXY = 'proxy'; /** * query: (array|string) Associative array of query string values to add * to the request. This option uses PHP's http_build_query() to create * the string representation. Pass a string value if you need more * control than what this method provides */ public const QUERY = 'query'; /** * sink: (resource|string|StreamInterface) Where the data of the * response is written to. Defaults to a PHP temp stream. Providing a * string will write data to a file by the given name. */ public const SINK = 'sink'; /** * synchronous: (bool) Set to true to inform HTTP handlers that you intend * on waiting on the response. This can be useful for optimizations. Note * that a promise is still returned if you are using one of the async * client methods. */ public const SYNCHRONOUS = 'synchronous'; /** * ssl_key: (array|string) Specify the path to a file containing a private * SSL key in PEM format. If a password is required, then set to an array * containing the path to the SSL key in the first array element followed * by the password required for the certificate in the second element. */ public const SSL_KEY = 'ssl_key'; /** * stream: Set to true to attempt to stream a response rather than * download it all up-front. */ public const STREAM = 'stream'; /** * verify: (bool|string, default=true) Describes the SSL certificate * verification behavior of a request. Set to true to enable SSL * certificate verification using the system CA bundle when available * (the default). Set to false to disable certificate verification (this * is insecure!). Set to a string to provide the path to a CA bundle on * disk to enable verification using a custom certificate. */ public const VERIFY = 'verify'; /** * timeout: (float, default=0) Float describing the timeout of the * request in seconds. Use 0 to wait indefinitely (the default behavior). */ public const TIMEOUT = 'timeout'; /** * read_timeout: (float, default=default_socket_timeout ini setting) Float describing * the body read timeout, for stream requests. */ public const READ_TIMEOUT = 'read_timeout'; /** * version: (float) Specifies the HTTP protocol version to attempt to use. */ public const VERSION = 'version'; /** * force_ip_resolve: (bool) Force client to use only ipv4 or ipv6 protocol */ public const FORCE_IP_RESOLVE = 'force_ip_resolve'; } vendor_prefixed/guzzlehttp/guzzle/src/functions.php 0000644 00000013524 15174712003 0017004 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; /** * Debug function used to describe the provided value type and class. * * @param mixed $input Any type of variable to describe the type of. This * parameter misses a typehint because of that. * * @return string Returns a string containing the type of the variable and * if a class is provided, the class name. * * @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use Utils::describeType instead. */ function describe_type($input) : string { return \YoastSEO_Vendor\GuzzleHttp\Utils::describeType($input); } /** * Parses an array of header lines into an associative array of headers. * * @param iterable $lines Header lines array of strings in the following * format: "Name: Value" * * @deprecated headers_from_lines will be removed in guzzlehttp/guzzle:8.0. Use Utils::headersFromLines instead. */ function headers_from_lines(iterable $lines) : array { return \YoastSEO_Vendor\GuzzleHttp\Utils::headersFromLines($lines); } /** * Returns a debug stream based on the provided variable. * * @param mixed $value Optional value * * @return resource * * @deprecated debug_resource will be removed in guzzlehttp/guzzle:8.0. Use Utils::debugResource instead. */ function debug_resource($value = null) { return \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($value); } /** * Chooses and creates a default handler to use based on the environment. * * The returned handler is not wrapped by any default middlewares. * * @return callable(\Psr\Http\Message\RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system. * * @throws \RuntimeException if no viable Handler is available. * * @deprecated choose_handler will be removed in guzzlehttp/guzzle:8.0. Use Utils::chooseHandler instead. */ function choose_handler() : callable { return \YoastSEO_Vendor\GuzzleHttp\Utils::chooseHandler(); } /** * Get the default User-Agent string to use with Guzzle. * * @deprecated default_user_agent will be removed in guzzlehttp/guzzle:8.0. Use Utils::defaultUserAgent instead. */ function default_user_agent() : string { return \YoastSEO_Vendor\GuzzleHttp\Utils::defaultUserAgent(); } /** * Returns the default cacert bundle for the current system. * * First, the openssl.cafile and curl.cainfo php.ini settings are checked. * If those settings are not configured, then the common locations for * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X * and Windows are checked. If any of these file locations are found on * disk, they will be utilized. * * Note: the result of this function is cached for subsequent calls. * * @throws \RuntimeException if no bundle can be found. * * @deprecated default_ca_bundle will be removed in guzzlehttp/guzzle:8.0. This function is not needed in PHP 5.6+. */ function default_ca_bundle() : string { return \YoastSEO_Vendor\GuzzleHttp\Utils::defaultCaBundle(); } /** * Creates an associative array of lowercase header names to the actual * header casing. * * @deprecated normalize_header_keys will be removed in guzzlehttp/guzzle:8.0. Use Utils::normalizeHeaderKeys instead. */ function normalize_header_keys(array $headers) : array { return \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers); } /** * Returns true if the provided host matches any of the no proxy areas. * * This method will strip a port from the host if it is present. Each pattern * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == * "baz.foo.com", but ".foo.com" != "foo.com"). * * Areas are matched in the following cases: * 1. "*" (without quotes) always matches any hosts. * 2. An exact match. * 3. The area starts with "." and the area is the last part of the host. e.g. * '.mit.edu' will match any host that ends with '.mit.edu'. * * @param string $host Host to check against the patterns. * @param string[] $noProxyArray An array of host patterns. * * @throws Exception\InvalidArgumentException * * @deprecated is_host_in_noproxy will be removed in guzzlehttp/guzzle:8.0. Use Utils::isHostInNoProxy instead. */ function is_host_in_noproxy(string $host, array $noProxyArray) : bool { return \YoastSEO_Vendor\GuzzleHttp\Utils::isHostInNoProxy($host, $noProxyArray); } /** * Wrapper for json_decode that throws when an error occurs. * * @param string $json JSON data to parse * @param bool $assoc When true, returned objects will be converted * into associative arrays. * @param int $depth User specified recursion depth. * @param int $options Bitmask of JSON decode options. * * @return object|array|string|int|float|bool|null * * @throws Exception\InvalidArgumentException if the JSON cannot be decoded. * * @see https://www.php.net/manual/en/function.json-decode.php * @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonDecode instead. */ function json_decode(string $json, bool $assoc = \false, int $depth = 512, int $options = 0) { return \YoastSEO_Vendor\GuzzleHttp\Utils::jsonDecode($json, $assoc, $depth, $options); } /** * Wrapper for JSON encoding that throws when an error occurs. * * @param mixed $value The value being encoded * @param int $options JSON encode option bitmask * @param int $depth Set the maximum depth. Must be greater than zero. * * @throws Exception\InvalidArgumentException if the JSON cannot be encoded. * * @see https://www.php.net/manual/en/function.json-encode.php * @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonEncode instead. */ function json_encode($value, int $options = 0, int $depth = 512) : string { return \YoastSEO_Vendor\GuzzleHttp\Utils::jsonEncode($value, $options, $depth); } vendor_prefixed/guzzlehttp/guzzle/src/Middleware.php 0000644 00000026133 15174712003 0017051 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJarInterface; use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Log\LoggerInterface; /** * Functions used to create and wrap handlers with handler middleware. */ final class Middleware { /** * Middleware that adds cookies to requests. * * The options array must be set to a CookieJarInterface in order to use * cookies. This is typically handled for you by a client. * * @return callable Returns a function that accepts the next handler. */ public static function cookies() : callable { return static function (callable $handler) : callable { return static function ($request, array $options) use($handler) { if (empty($options['cookies'])) { return $handler($request, $options); } elseif (!$options['cookies'] instanceof \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJarInterface) { throw new \InvalidArgumentException('cookies must be an instance of YoastSEO_Vendor\\GuzzleHttp\\Cookie\\CookieJarInterface'); } $cookieJar = $options['cookies']; $request = $cookieJar->withCookieHeader($request); return $handler($request, $options)->then(static function (\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) use($cookieJar, $request) : ResponseInterface { $cookieJar->extractCookies($request, $response); return $response; }); }; }; } /** * Middleware that throws exceptions for 4xx or 5xx responses when the * "http_errors" request option is set to true. * * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. * * @return callable(callable): callable Returns a function that accepts the next handler. */ public static function httpErrors(?\YoastSEO_Vendor\GuzzleHttp\BodySummarizerInterface $bodySummarizer = null) : callable { return static function (callable $handler) use($bodySummarizer) : callable { return static function ($request, array $options) use($handler, $bodySummarizer) { if (empty($options['http_errors'])) { return $handler($request, $options); } return $handler($request, $options)->then(static function (\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) use($request, $bodySummarizer) { $code = $response->getStatusCode(); if ($code < 400) { return $response; } throw \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException::create($request, $response, null, [], $bodySummarizer); }); }; }; } /** * Middleware that pushes history data to an ArrayAccess container. * * @param array|\ArrayAccess<int, array> $container Container to hold the history (by reference). * * @return callable(callable): callable Returns a function that accepts the next handler. * * @throws \InvalidArgumentException if container is not an array or ArrayAccess. */ public static function history(&$container) : callable { if (!\is_array($container) && !$container instanceof \ArrayAccess) { throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); } return static function (callable $handler) use(&$container) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($handler, &$container) { return $handler($request, $options)->then(static function ($value) use($request, &$container, $options) { $container[] = ['request' => $request, 'response' => $value, 'error' => null, 'options' => $options]; return $value; }, static function ($reason) use($request, &$container, $options) { $container[] = ['request' => $request, 'response' => null, 'error' => $reason, 'options' => $options]; return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($reason); }); }; }; } /** * Middleware that invokes a callback before and after sending a request. * * The provided listener cannot modify or alter the response. It simply * "taps" into the chain to be notified before returning the promise. The * before listener accepts a request and options array, and the after * listener accepts a request, options array, and response promise. * * @param callable $before Function to invoke before forwarding the request. * @param callable $after Function invoked after forwarding. * * @return callable Returns a function that accepts the next handler. */ public static function tap(?callable $before = null, ?callable $after = null) : callable { return static function (callable $handler) use($before, $after) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($handler, $before, $after) { if ($before) { $before($request, $options); } $response = $handler($request, $options); if ($after) { $after($request, $options, $response); } return $response; }; }; } /** * Middleware that handles request redirects. * * @return callable Returns a function that accepts the next handler. */ public static function redirect() : callable { return static function (callable $handler) : RedirectMiddleware { return new \YoastSEO_Vendor\GuzzleHttp\RedirectMiddleware($handler); }; } /** * Middleware that retries requests based on the boolean result of * invoking the provided "decider" function. * * If no delay function is provided, a simple implementation of exponential * backoff will be utilized. * * @param callable $decider Function that accepts the number of retries, * a request, [response], and [exception] and * returns true if the request is to be retried. * @param callable $delay Function that accepts the number of retries and * returns the number of milliseconds to delay. * * @return callable Returns a function that accepts the next handler. */ public static function retry(callable $decider, ?callable $delay = null) : callable { return static function (callable $handler) use($decider, $delay) : RetryMiddleware { return new \YoastSEO_Vendor\GuzzleHttp\RetryMiddleware($decider, $handler, $delay); }; } /** * Middleware that logs requests, responses, and errors using a message * formatter. * * @param LoggerInterface $logger Logs messages. * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. * @param string $logLevel Level at which to log requests. * * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. * * @return callable Returns a function that accepts the next handler. */ public static function log(\YoastSEO_Vendor\Psr\Log\LoggerInterface $logger, $formatter, string $logLevel = 'info') : callable { // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter if (!$formatter instanceof \YoastSEO_Vendor\GuzzleHttp\MessageFormatter && !$formatter instanceof \YoastSEO_Vendor\GuzzleHttp\MessageFormatterInterface) { throw new \LogicException(\sprintf('Argument 2 to %s::log() must be of type %s', self::class, \YoastSEO_Vendor\GuzzleHttp\MessageFormatterInterface::class)); } return static function (callable $handler) use($logger, $formatter, $logLevel) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) use($handler, $logger, $formatter, $logLevel) { return $handler($request, $options)->then(static function ($response) use($logger, $request, $formatter, $logLevel) : ResponseInterface { $message = $formatter->format($request, $response); $logger->log($logLevel, $message); return $response; }, static function ($reason) use($logger, $request, $formatter) : PromiseInterface { $response = $reason instanceof \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException ? $reason->getResponse() : null; $message = $formatter->format($request, $response, \YoastSEO_Vendor\GuzzleHttp\Promise\Create::exceptionFor($reason)); $logger->error($message); return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($reason); }); }; }; } /** * This middleware adds a default content-type if possible, a default * content-length or transfer-encoding header, and the expect header. */ public static function prepareBody() : callable { return static function (callable $handler) : PrepareBodyMiddleware { return new \YoastSEO_Vendor\GuzzleHttp\PrepareBodyMiddleware($handler); }; } /** * Middleware that applies a map function to the request before passing to * the next handler. * * @param callable $fn Function that accepts a RequestInterface and returns * a RequestInterface. */ public static function mapRequest(callable $fn) : callable { return static function (callable $handler) use($fn) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($handler, $fn) { return $handler($fn($request), $options); }; }; } /** * Middleware that applies a map function to the resolved promise's * response. * * @param callable $fn Function that accepts a ResponseInterface and * returns a ResponseInterface. */ public static function mapResponse(callable $fn) : callable { return static function (callable $handler) use($fn) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($handler, $fn) { return $handler($request, $options)->then($fn); }; }; } } vendor_prefixed/guzzlehttp/guzzle/src/ClientInterface.php 0000644 00000006203 15174712003 0020027 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Client interface for sending HTTP requests. */ interface ClientInterface { /** * The Guzzle major version. */ public const MAJOR_VERSION = 7; /** * Send an HTTP request. * * @param RequestInterface $request Request to send * @param array $options Request options to apply to the given * request and to the transfer. * * @throws GuzzleException */ public function send(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Asynchronously send an HTTP request. * * @param RequestInterface $request Request to send * @param array $options Request options to apply to the given * request and to the transfer. */ public function sendAsync(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; /** * Create and send an HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string $method HTTP method. * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function request(string $method, $uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Create and send an asynchronous HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string $method HTTP method * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function requestAsync(string $method, $uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; /** * Get a client configuration option. * * These options include default request options of the client, a "handler" * (if utilized by the concrete client), and a "base_uri" if utilized by * the concrete client. * * @param string|null $option The config option to retrieve. * * @return mixed * * @deprecated ClientInterface::getConfig will be removed in guzzlehttp/guzzle:8.0. */ public function getConfig(?string $option = null); } vendor_prefixed/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php 0000644 00000005604 15174712003 0020653 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Cookie; use YoastSEO_Vendor\GuzzleHttp\Utils; /** * Persists non-session cookies using a JSON formatted file */ class FileCookieJar extends \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar { /** * @var string filename */ private $filename; /** * @var bool Control whether to persist session cookies or not. */ private $storeSessionCookies; /** * Create a new FileCookieJar object * * @param string $cookieFile File to store the cookie data * @param bool $storeSessionCookies Set to true to store session cookies * in the cookie jar. * * @throws \RuntimeException if the file cannot be found or created */ public function __construct(string $cookieFile, bool $storeSessionCookies = \false) { parent::__construct(); $this->filename = $cookieFile; $this->storeSessionCookies = $storeSessionCookies; if (\file_exists($cookieFile)) { $this->load($cookieFile); } } /** * Saves the file when shutting down */ public function __destruct() { $this->save($this->filename); } /** * Saves the cookies to a file. * * @param string $filename File to save * * @throws \RuntimeException if the file cannot be found or created */ public function save(string $filename) : void { $json = []; /** @var SetCookie $cookie */ foreach ($this as $cookie) { if (\YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $jsonStr = \YoastSEO_Vendor\GuzzleHttp\Utils::jsonEncode($json); if (\false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) { throw new \RuntimeException("Unable to save file {$filename}"); } } /** * Load cookies from a JSON formatted file. * * Old cookies are kept unless overwritten by newly loaded ones. * * @param string $filename Cookie file to load. * * @throws \RuntimeException if the file cannot be loaded. */ public function load(string $filename) : void { $json = \file_get_contents($filename); if (\false === $json) { throw new \RuntimeException("Unable to load file {$filename}"); } if ($json === '') { return; } $data = \YoastSEO_Vendor\GuzzleHttp\Utils::jsonDecode($json, \true); if (\is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie($cookie)); } } elseif (\is_scalar($data) && !empty($data)) { throw new \RuntimeException("Invalid cookie file: {$filename}"); } } } vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SetCookie.php 0000644 00000034041 15174712003 0020067 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Cookie; /** * Set-Cookie object */ class SetCookie { /** * @var array */ private static $defaults = ['Name' => null, 'Value' => null, 'Domain' => null, 'Path' => '/', 'Max-Age' => null, 'Expires' => null, 'Secure' => \false, 'Discard' => \false, 'HttpOnly' => \false]; /** * @var array Cookie data */ private $data; /** * Create a new SetCookie object from a string. * * @param string $cookie Set-Cookie header string */ public static function fromString(string $cookie) : self { // Create the default return array $data = self::$defaults; // Explode the cookie string using a series of semicolons $pieces = \array_filter(\array_map('trim', \explode(';', $cookie))); // The name of the cookie (first kvp) must exist and include an equal sign. if (!isset($pieces[0]) || \strpos($pieces[0], '=') === \false) { return new self($data); } // Add the cookie pieces into the parsed data array foreach ($pieces as $part) { $cookieParts = \explode('=', $part, 2); $key = \trim($cookieParts[0]); $value = isset($cookieParts[1]) ? \trim($cookieParts[1], " \n\r\t\x00\v") : \true; // Only check for non-cookies when cookies have been found if (!isset($data['Name'])) { $data['Name'] = $key; $data['Value'] = $value; } else { foreach (\array_keys(self::$defaults) as $search) { if (!\strcasecmp($search, $key)) { if ($search === 'Max-Age') { if (\is_numeric($value)) { $data[$search] = (int) $value; } } elseif ($search === 'Secure' || $search === 'Discard' || $search === 'HttpOnly') { if ($value) { $data[$search] = \true; } } else { $data[$search] = $value; } continue 2; } } $data[$key] = $value; } } return new self($data); } /** * @param array $data Array of cookie data provided by a Cookie parser */ public function __construct(array $data = []) { $this->data = self::$defaults; if (isset($data['Name'])) { $this->setName($data['Name']); } if (isset($data['Value'])) { $this->setValue($data['Value']); } if (isset($data['Domain'])) { $this->setDomain($data['Domain']); } if (isset($data['Path'])) { $this->setPath($data['Path']); } if (isset($data['Max-Age'])) { $this->setMaxAge($data['Max-Age']); } if (isset($data['Expires'])) { $this->setExpires($data['Expires']); } if (isset($data['Secure'])) { $this->setSecure($data['Secure']); } if (isset($data['Discard'])) { $this->setDiscard($data['Discard']); } if (isset($data['HttpOnly'])) { $this->setHttpOnly($data['HttpOnly']); } // Set the remaining values that don't have extra validation logic foreach (\array_diff(\array_keys($data), \array_keys(self::$defaults)) as $key) { $this->data[$key] = $data[$key]; } // Extract the Expires value and turn it into a UNIX timestamp if needed if (!$this->getExpires() && $this->getMaxAge()) { // Calculate the Expires date $this->setExpires(\time() + $this->getMaxAge()); } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) { $this->setExpires($expires); } } public function __toString() { $str = $this->data['Name'] . '=' . ($this->data['Value'] ?? '') . '; '; foreach ($this->data as $k => $v) { if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== \false) { if ($k === 'Expires') { $str .= 'Expires=' . \gmdate('D, d M Y H:i:s \\G\\M\\T', $v) . '; '; } else { $str .= ($v === \true ? $k : "{$k}={$v}") . '; '; } } } return \rtrim($str, '; '); } public function toArray() : array { return $this->data; } /** * Get the cookie name. * * @return string */ public function getName() { return $this->data['Name']; } /** * Set the cookie name. * * @param string $name Cookie name */ public function setName($name) : void { if (!\is_string($name)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Name'] = (string) $name; } /** * Get the cookie value. * * @return string|null */ public function getValue() { return $this->data['Value']; } /** * Set the cookie value. * * @param string $value Cookie value */ public function setValue($value) : void { if (!\is_string($value)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Value'] = (string) $value; } /** * Get the domain. * * @return string|null */ public function getDomain() { return $this->data['Domain']; } /** * Set the domain of the cookie. * * @param string|null $domain */ public function setDomain($domain) : void { if (!\is_string($domain) && null !== $domain) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Domain'] = null === $domain ? null : (string) $domain; } /** * Get the path. * * @return string */ public function getPath() { return $this->data['Path']; } /** * Set the path of the cookie. * * @param string $path Path of the cookie */ public function setPath($path) : void { if (!\is_string($path)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Path'] = (string) $path; } /** * Maximum lifetime of the cookie in seconds. * * @return int|null */ public function getMaxAge() { return null === $this->data['Max-Age'] ? null : (int) $this->data['Max-Age']; } /** * Set the max-age of the cookie. * * @param int|null $maxAge Max age of the cookie in seconds */ public function setMaxAge($maxAge) : void { if (!\is_int($maxAge) && null !== $maxAge) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge; } /** * The UNIX timestamp when the cookie Expires. * * @return string|int|null */ public function getExpires() { return $this->data['Expires']; } /** * Set the unix timestamp for which the cookie will expire. * * @param int|string|null $timestamp Unix timestamp or any English textual datetime description. */ public function setExpires($timestamp) : void { if (!\is_int($timestamp) && !\is_string($timestamp) && null !== $timestamp) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp)); } /** * Get whether or not this is a secure cookie. * * @return bool */ public function getSecure() { return $this->data['Secure']; } /** * Set whether or not the cookie is secure. * * @param bool $secure Set to true or false if secure */ public function setSecure($secure) : void { if (!\is_bool($secure)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Secure'] = (bool) $secure; } /** * Get whether or not this is a session cookie. * * @return bool|null */ public function getDiscard() { return $this->data['Discard']; } /** * Set whether or not this is a session cookie. * * @param bool $discard Set to true or false if this is a session cookie */ public function setDiscard($discard) : void { if (!\is_bool($discard)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Discard'] = (bool) $discard; } /** * Get whether or not this is an HTTP only cookie. * * @return bool */ public function getHttpOnly() { return $this->data['HttpOnly']; } /** * Set whether or not this is an HTTP only cookie. * * @param bool $httpOnly Set to true or false if this is HTTP only */ public function setHttpOnly($httpOnly) : void { if (!\is_bool($httpOnly)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['HttpOnly'] = (bool) $httpOnly; } /** * Check if the cookie matches a path value. * * A request-path path-matches a given cookie-path if at least one of * the following conditions holds: * * - The cookie-path and the request-path are identical. * - The cookie-path is a prefix of the request-path, and the last * character of the cookie-path is %x2F ("/"). * - The cookie-path is a prefix of the request-path, and the first * character of the request-path that is not included in the cookie- * path is a %x2F ("/") character. * * @param string $requestPath Path to check against */ public function matchesPath(string $requestPath) : bool { $cookiePath = $this->getPath(); // Match on exact matches or when path is the default empty "/" if ($cookiePath === '/' || $cookiePath == $requestPath) { return \true; } // Ensure that the cookie-path is a prefix of the request path. if (0 !== \strpos($requestPath, $cookiePath)) { return \false; } // Match if the last character of the cookie-path is "/" if (\substr($cookiePath, -1, 1) === '/') { return \true; } // Match if the first character not included in cookie path is "/" return \substr($requestPath, \strlen($cookiePath), 1) === '/'; } /** * Check if the cookie matches a domain value. * * @param string $domain Domain to check against */ public function matchesDomain(string $domain) : bool { $cookieDomain = $this->getDomain(); if (null === $cookieDomain) { return \true; } // Remove the leading '.' as per spec in RFC 6265. // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3 $cookieDomain = \ltrim(\strtolower($cookieDomain), '.'); $domain = \strtolower($domain); // Domain not set or exact match. if ('' === $cookieDomain || $domain === $cookieDomain) { return \true; } // Matching the subdomain according to RFC 6265. // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3 if (\filter_var($domain, \FILTER_VALIDATE_IP)) { return \false; } return (bool) \preg_match('/\\.' . \preg_quote($cookieDomain, '/') . '$/', $domain); } /** * Check if the cookie is expired. */ public function isExpired() : bool { return $this->getExpires() !== null && \time() > $this->getExpires(); } /** * Check if the cookie is valid according to RFC 6265. * * @return bool|string Returns true if valid or an error message if invalid */ public function validate() { $name = $this->getName(); if ($name === '') { return 'The cookie name must not be empty'; } // Check if any of the invalid characters are present in the cookie name if (\preg_match('/[\\x00-\\x20\\x22\\x28-\\x29\\x2c\\x2f\\x3a-\\x40\\x5c\\x7b\\x7d\\x7f]/', $name)) { return 'Cookie name must not contain invalid characters: ASCII ' . 'Control characters (0-31;127), space, tab and the ' . 'following characters: ()<>@,;:\\"/?={}'; } // Value must not be null. 0 and empty string are valid. Empty strings // are technically against RFC 6265, but known to happen in the wild. $value = $this->getValue(); if ($value === null) { return 'The cookie value must not be empty'; } // Domains must not be empty, but can be 0. "0" is not a valid internet // domain, but may be used as server name in a private network. $domain = $this->getDomain(); if ($domain === null || $domain === '') { return 'The cookie domain must not be empty'; } return \true; } } vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJar.php 0000644 00000022711 15174712003 0020051 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Cookie; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Cookie jar that stores cookies as an array */ class CookieJar implements \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJarInterface { /** * @var SetCookie[] Loaded cookie data */ private $cookies = []; /** * @var bool */ private $strictMode; /** * @param bool $strictMode Set to true to throw exceptions when invalid * cookies are added to the cookie jar. * @param array $cookieArray Array of SetCookie objects or a hash of * arrays that can be used with the SetCookie * constructor */ public function __construct(bool $strictMode = \false, array $cookieArray = []) { $this->strictMode = $strictMode; foreach ($cookieArray as $cookie) { if (!$cookie instanceof \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie) { $cookie = new \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie($cookie); } $this->setCookie($cookie); } } /** * Create a new Cookie jar from an associative array and domain. * * @param array $cookies Cookies to create the jar from * @param string $domain Domain to set the cookies to */ public static function fromArray(array $cookies, string $domain) : self { $cookieJar = new self(); foreach ($cookies as $name => $value) { $cookieJar->setCookie(new \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie(['Domain' => $domain, 'Name' => $name, 'Value' => $value, 'Discard' => \true])); } return $cookieJar; } /** * Evaluate if this cookie should be persisted to storage * that survives between requests. * * @param SetCookie $cookie Being evaluated. * @param bool $allowSessionCookies If we should persist session cookies */ public static function shouldPersist(\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie, bool $allowSessionCookies = \false) : bool { if ($cookie->getExpires() || $allowSessionCookies) { if (!$cookie->getDiscard()) { return \true; } } return \false; } /** * Finds and returns the cookie based on the name * * @param string $name cookie name to search for * * @return SetCookie|null cookie that was found or null if not found */ public function getCookieByName(string $name) : ?\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie { foreach ($this->cookies as $cookie) { if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) { return $cookie; } } return null; } public function toArray() : array { return \array_map(static function (\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) : array { return $cookie->toArray(); }, $this->getIterator()->getArrayCopy()); } public function clear(?string $domain = null, ?string $path = null, ?string $name = null) : void { if (!$domain) { $this->cookies = []; return; } elseif (!$path) { $this->cookies = \array_filter($this->cookies, static function (\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) use($domain) : bool { return !$cookie->matchesDomain($domain); }); } elseif (!$name) { $this->cookies = \array_filter($this->cookies, static function (\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) use($path, $domain) : bool { return !($cookie->matchesPath($path) && $cookie->matchesDomain($domain)); }); } else { $this->cookies = \array_filter($this->cookies, static function (\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) use($path, $domain, $name) { return !($cookie->getName() == $name && $cookie->matchesPath($path) && $cookie->matchesDomain($domain)); }); } } public function clearSessionCookies() : void { $this->cookies = \array_filter($this->cookies, static function (\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) : bool { return !$cookie->getDiscard() && $cookie->getExpires(); }); } public function setCookie(\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) : bool { // If the name string is empty (but not 0), ignore the set-cookie // string entirely. $name = $cookie->getName(); if (!$name && $name !== '0') { return \false; } // Only allow cookies with set and valid domain, name, value $result = $cookie->validate(); if ($result !== \true) { if ($this->strictMode) { throw new \RuntimeException('Invalid cookie: ' . $result); } $this->removeCookieIfEmpty($cookie); return \false; } // Resolve conflicts with previously set cookies foreach ($this->cookies as $i => $c) { // Two cookies are identical, when their path, and domain are // identical. if ($c->getPath() != $cookie->getPath() || $c->getDomain() != $cookie->getDomain() || $c->getName() != $cookie->getName()) { continue; } // The previously set cookie is a discard cookie and this one is // not so allow the new cookie to be set if (!$cookie->getDiscard() && $c->getDiscard()) { unset($this->cookies[$i]); continue; } // If the new cookie's expiration is further into the future, then // replace the old cookie if ($cookie->getExpires() > $c->getExpires()) { unset($this->cookies[$i]); continue; } // If the value has changed, we better change it if ($cookie->getValue() !== $c->getValue()) { unset($this->cookies[$i]); continue; } // The cookie exists, so no need to continue return \false; } $this->cookies[] = $cookie; return \true; } public function count() : int { return \count($this->cookies); } /** * @return \ArrayIterator<int, SetCookie> */ public function getIterator() : \ArrayIterator { return new \ArrayIterator(\array_values($this->cookies)); } public function extractCookies(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) : void { if ($cookieHeader = $response->getHeader('Set-Cookie')) { foreach ($cookieHeader as $cookie) { $sc = \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie::fromString($cookie); if (!$sc->getDomain()) { $sc->setDomain($request->getUri()->getHost()); } if (0 !== \strpos($sc->getPath(), '/')) { $sc->setPath($this->getCookiePathFromRequest($request)); } if (!$sc->matchesDomain($request->getUri()->getHost())) { continue; } // Note: At this point `$sc->getDomain()` being a public suffix should // be rejected, but we don't want to pull in the full PSL dependency. $this->setCookie($sc); } } } /** * Computes cookie path following RFC 6265 section 5.1.4 * * @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 */ private function getCookiePathFromRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : string { $uriPath = $request->getUri()->getPath(); if ('' === $uriPath) { return '/'; } if (0 !== \strpos($uriPath, '/')) { return '/'; } if ('/' === $uriPath) { return '/'; } $lastSlashPos = \strrpos($uriPath, '/'); if (0 === $lastSlashPos || \false === $lastSlashPos) { return '/'; } return \substr($uriPath, 0, $lastSlashPos); } public function withCookieHeader(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { $values = []; $uri = $request->getUri(); $scheme = $uri->getScheme(); $host = $uri->getHost(); $path = $uri->getPath() ?: '/'; foreach ($this->cookies as $cookie) { if ($cookie->matchesPath($path) && $cookie->matchesDomain($host) && !$cookie->isExpired() && (!$cookie->getSecure() || $scheme === 'https')) { $values[] = $cookie->getName() . '=' . $cookie->getValue(); } } return $values ? $request->withHeader('Cookie', \implode('; ', $values)) : $request; } /** * If a cookie already exists and the server asks to set it again with a * null value, the cookie must be deleted. */ private function removeCookieIfEmpty(\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) : void { $cookieValue = $cookie->getValue(); if ($cookieValue === null || $cookieValue === '') { $this->clear($cookie->getDomain(), $cookie->getPath(), $cookie->getName()); } } } vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php 0000644 00000005750 15174712003 0021676 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Cookie; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Stores HTTP cookies. * * It extracts cookies from HTTP requests, and returns them in HTTP responses. * CookieJarInterface instances automatically expire contained cookies when * necessary. Subclasses are also responsible for storing and retrieving * cookies from a file, database, etc. * * @see https://docs.python.org/2/library/cookielib.html Inspiration * * @extends \IteratorAggregate<SetCookie> */ interface CookieJarInterface extends \Countable, \IteratorAggregate { /** * Create a request with added cookie headers. * * If no matching cookies are found in the cookie jar, then no Cookie * header is added to the request and the same request is returned. * * @param RequestInterface $request Request object to modify. * * @return RequestInterface returns the modified request. */ public function withCookieHeader(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Extract cookies from an HTTP response and store them in the CookieJar. * * @param RequestInterface $request Request that was sent * @param ResponseInterface $response Response that was received */ public function extractCookies(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) : void; /** * Sets a cookie in the cookie jar. * * @param SetCookie $cookie Cookie to set. * * @return bool Returns true on success or false on failure */ public function setCookie(\YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie $cookie) : bool; /** * Remove cookies currently held in the cookie jar. * * Invoking this method without arguments will empty the whole cookie jar. * If given a $domain argument only cookies belonging to that domain will * be removed. If given a $domain and $path argument, cookies belonging to * the specified path within that domain are removed. If given all three * arguments, then the cookie with the specified name, path and domain is * removed. * * @param string|null $domain Clears cookies matching a domain * @param string|null $path Clears cookies matching a domain and path * @param string|null $name Clears cookies matching a domain, path, and name */ public function clear(?string $domain = null, ?string $path = null, ?string $name = null) : void; /** * Discard all sessions cookies. * * Removes cookies that don't have an expire field or a have a discard * field set to true. To be called when the user agent shuts down according * to RFC 2965. */ public function clearSessionCookies() : void; /** * Converts the cookie jar to an array. */ public function toArray() : array; } vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php 0000644 00000004112 15174712003 0021410 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Cookie; /** * Persists cookies in the client session */ class SessionCookieJar extends \YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar { /** * @var string session key */ private $sessionKey; /** * @var bool Control whether to persist session cookies or not. */ private $storeSessionCookies; /** * Create a new SessionCookieJar object * * @param string $sessionKey Session key name to store the cookie * data in session * @param bool $storeSessionCookies Set to true to store session cookies * in the cookie jar. */ public function __construct(string $sessionKey, bool $storeSessionCookies = \false) { parent::__construct(); $this->sessionKey = $sessionKey; $this->storeSessionCookies = $storeSessionCookies; $this->load(); } /** * Saves cookies to session when shutting down */ public function __destruct() { $this->save(); } /** * Save cookies to the client session */ public function save() : void { $json = []; /** @var SetCookie $cookie */ foreach ($this as $cookie) { if (\YoastSEO_Vendor\GuzzleHttp\Cookie\CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $_SESSION[$this->sessionKey] = \json_encode($json); } /** * Load the contents of the client session into the data array */ protected function load() : void { if (!isset($_SESSION[$this->sessionKey])) { return; } $data = \json_decode($_SESSION[$this->sessionKey], \true); if (\is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new \YoastSEO_Vendor\GuzzleHttp\Cookie\SetCookie($cookie)); } } elseif (\strlen($data)) { throw new \RuntimeException('Invalid cookie data'); } } } vendor_prefixed/guzzlehttp/guzzle/src/TransferStats.php 0000644 00000006444 15174712003 0017602 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Represents data at the point after it was transferred either successfully * or after a network error. */ final class TransferStats { /** * @var RequestInterface */ private $request; /** * @var ResponseInterface|null */ private $response; /** * @var float|null */ private $transferTime; /** * @var array */ private $handlerStats; /** * @var mixed|null */ private $handlerErrorData; /** * @param RequestInterface $request Request that was sent. * @param ResponseInterface|null $response Response received (if any) * @param float|null $transferTime Total handler transfer time. * @param mixed $handlerErrorData Handler error data. * @param array $handlerStats Handler specific stats. */ public function __construct(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?float $transferTime = null, $handlerErrorData = null, array $handlerStats = []) { $this->request = $request; $this->response = $response; $this->transferTime = $transferTime; $this->handlerErrorData = $handlerErrorData; $this->handlerStats = $handlerStats; } public function getRequest() : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { return $this->request; } /** * Returns the response that was received (if any). */ public function getResponse() : ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->response; } /** * Returns true if a response was received. */ public function hasResponse() : bool { return $this->response !== null; } /** * Gets handler specific error data. * * This might be an exception, a integer representing an error code, or * anything else. Relying on this value assumes that you know what handler * you are using. * * @return mixed */ public function getHandlerErrorData() { return $this->handlerErrorData; } /** * Get the effective URI the request was sent to. */ public function getEffectiveUri() : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { return $this->request->getUri(); } /** * Get the estimated time the request was being transferred by the handler. * * @return float|null Time in seconds. */ public function getTransferTime() : ?float { return $this->transferTime; } /** * Gets an array of all of the handler specific transfer data. */ public function getHandlerStats() : array { return $this->handlerStats; } /** * Get a specific handler statistic from the handler by name. * * @param string $stat Handler specific transfer stat to retrieve. * * @return mixed|null */ public function getHandlerStat(string $stat) { return $this->handlerStats[$stat] ?? null; } } vendor_prefixed/guzzlehttp/guzzle/src/Pool.php 0000644 00000011651 15174712003 0015704 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\EachPromise; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\Promise\PromisorInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Sends an iterator of requests concurrently using a capped pool size. * * The pool will read from an iterator until it is cancelled or until the * iterator is consumed. When a request is yielded, the request is sent after * applying the "request_options" request options (if provided in the ctor). * * When a function is yielded by the iterator, the function is provided the * "request_options" array that should be merged on top of any existing * options, and the function MUST then return a wait-able promise. * * @final */ class Pool implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromisorInterface { /** * @var EachPromise */ private $each; /** * @param ClientInterface $client Client used to send the requests. * @param array|\Iterator $requests Requests or functions that return * requests to send concurrently. * @param array $config Associative array of options * - concurrency: (int) Maximum number of requests to send concurrently * - options: Array of request options to apply to each request. * - fulfilled: (callable) Function to invoke when a request completes. * - rejected: (callable) Function to invoke when a request is rejected. */ public function __construct(\YoastSEO_Vendor\GuzzleHttp\ClientInterface $client, $requests, array $config = []) { if (!isset($config['concurrency'])) { $config['concurrency'] = 25; } if (isset($config['options'])) { $opts = $config['options']; unset($config['options']); } else { $opts = []; } $iterable = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::iterFor($requests); $requests = static function () use($iterable, $client, $opts) { foreach ($iterable as $key => $rfn) { if ($rfn instanceof \YoastSEO_Vendor\Psr\Http\Message\RequestInterface) { (yield $key => $client->sendAsync($rfn, $opts)); } elseif (\is_callable($rfn)) { (yield $key => $rfn($opts)); } else { throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\\Http\\Message\\RequestInterface or a callable that returns a promise that fulfills with a Psr7\\Message\\Http\\ResponseInterface object.'); } } }; $this->each = new \YoastSEO_Vendor\GuzzleHttp\Promise\EachPromise($requests(), $config); } /** * Get promise */ public function promise() : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->each->promise(); } /** * Sends multiple requests concurrently and returns an array of responses * and exceptions that uses the same ordering as the provided requests. * * IMPORTANT: This method keeps every request and response in memory, and * as such, is NOT recommended when sending a large number or an * indeterminate number of requests concurrently. * * @param ClientInterface $client Client used to send the requests * @param array|\Iterator $requests Requests to send concurrently. * @param array $options Passes through the options available in * {@see Pool::__construct} * * @return array Returns an array containing the response or an exception * in the same order that the requests were sent. * * @throws \InvalidArgumentException if the event format is incorrect. */ public static function batch(\YoastSEO_Vendor\GuzzleHttp\ClientInterface $client, $requests, array $options = []) : array { $res = []; self::cmpCallback($options, 'fulfilled', $res); self::cmpCallback($options, 'rejected', $res); $pool = new static($client, $requests, $options); $pool->promise()->wait(); \ksort($res); return $res; } /** * Execute callback(s) */ private static function cmpCallback(array &$options, string $name, array &$results) : void { if (!isset($options[$name])) { $options[$name] = static function ($v, $k) use(&$results) { $results[$k] = $v; }; } else { $currentFn = $options[$name]; $options[$name] = static function ($v, $k) use(&$results, $currentFn) { $currentFn($v, $k); $results[$k] = $v; }; } } } vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatterInterface.php 0000644 00000001245 15174712003 0022062 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; interface MessageFormatterInterface { /** * Returns a formatted message string. * * @param RequestInterface $request Request that was sent * @param ResponseInterface|null $response Response that was received * @param \Throwable|null $error Exception that was received */ public function format(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $error = null) : string; } vendor_prefixed/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php 0000644 00000006350 15174712003 0021205 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Prepares requests that contain a body, adding the Content-Length, * Content-Type, and Expect headers. * * @final */ class PrepareBodyMiddleware { /** * @var callable(RequestInterface, array): PromiseInterface */ private $nextHandler; /** * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. */ public function __construct(callable $nextHandler) { $this->nextHandler = $nextHandler; } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $fn = $this->nextHandler; // Don't do anything if the request has no body. if ($request->getBody()->getSize() === 0) { return $fn($request, $options); } $modify = []; // Add a default content-type if possible. if (!$request->hasHeader('Content-Type')) { if ($uri = $request->getBody()->getMetadata('uri')) { if (\is_string($uri) && ($type = \YoastSEO_Vendor\GuzzleHttp\Psr7\MimeType::fromFilename($uri))) { $modify['set_headers']['Content-Type'] = $type; } } } // Add a default content-length or transfer-encoding header. if (!$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding')) { $size = $request->getBody()->getSize(); if ($size !== null) { $modify['set_headers']['Content-Length'] = $size; } else { $modify['set_headers']['Transfer-Encoding'] = 'chunked'; } } // Add the expect header if needed. $this->addExpectHeader($request, $options, $modify); return $fn(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::modifyRequest($request, $modify), $options); } /** * Add expect header */ private function addExpectHeader(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, array &$modify) : void { // Determine if the Expect header should be used if ($request->hasHeader('Expect')) { return; } $expect = $options['expect'] ?? null; // Return if disabled or using HTTP/1.0 if ($expect === \false || $request->getProtocolVersion() === '1.0') { return; } // The expect header is unconditionally enabled if ($expect === \true) { $modify['set_headers']['Expect'] = '100-Continue'; return; } // By default, send the expect header when the payload is > 1mb if ($expect === null) { $expect = 1048576; } // Always add if the body cannot be rewound, the size cannot be // determined, or the size is greater than the cutoff threshold $body = $request->getBody(); $size = $body->getSize(); if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { $modify['set_headers']['Expect'] = '100-Continue'; } } } vendor_prefixed/guzzlehttp/guzzle/src/RetryMiddleware.php 0000644 00000007243 15174712003 0020100 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Middleware that retries requests based on the boolean result of * invoking the provided "decider" function. * * @final */ class RetryMiddleware { /** * @var callable(RequestInterface, array): PromiseInterface */ private $nextHandler; /** * @var callable */ private $decider; /** * @var callable(int) */ private $delay; /** * @param callable $decider Function that accepts the number of retries, * a request, [response], and [exception] and * returns true if the request is to be * retried. * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. * @param (callable(int): int)|null $delay Function that accepts the number of retries * and returns the number of * milliseconds to delay. */ public function __construct(callable $decider, callable $nextHandler, ?callable $delay = null) { $this->decider = $decider; $this->nextHandler = $nextHandler; $this->delay = $delay ?: __CLASS__ . '::exponentialDelay'; } /** * Default exponential backoff delay function. * * @return int milliseconds. */ public static function exponentialDelay(int $retries) : int { return (int) 2 ** ($retries - 1) * 1000; } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if (!isset($options['retries'])) { $options['retries'] = 0; } $fn = $this->nextHandler; return $fn($request, $options)->then($this->onFulfilled($request, $options), $this->onRejected($request, $options)); } /** * Execute fulfilled closure */ private function onFulfilled(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : callable { return function ($value) use($request, $options) { if (!($this->decider)($options['retries'], $request, $value, null)) { return $value; } return $this->doRetry($request, $options, $value); }; } /** * Execute rejected closure */ private function onRejected(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $req, array $options) : callable { return function ($reason) use($req, $options) { if (!($this->decider)($options['retries'], $req, null, $reason)) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($reason); } return $this->doRetry($req, $options); }; } private function doRetry(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $options['delay'] = ($this->delay)(++$options['retries'], $response, $request); return $this($request, $options); } } vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizerInterface.php 0000644 00000000453 15174712003 0021566 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\Psr\Http\Message\MessageInterface; interface BodySummarizerInterface { /** * Returns a summarized message body. */ public function summarize(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : ?string; } vendor_prefixed/guzzlehttp/guzzle/src/Handler/Proxy.php 0000644 00000004252 15174712003 0017470 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\RequestOptions; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Provides basic proxies for handlers. * * @final */ class Proxy { /** * Sends synchronous requests to a specific handler while sending all other * requests to another handler. * * @param callable(RequestInterface, array): PromiseInterface $default Handler used for normal responses * @param callable(RequestInterface, array): PromiseInterface $sync Handler used for synchronous responses. * * @return callable(RequestInterface, array): PromiseInterface Returns the composed handler. */ public static function wrapSync(callable $default, callable $sync) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($default, $sync) : PromiseInterface { return empty($options[\YoastSEO_Vendor\GuzzleHttp\RequestOptions::SYNCHRONOUS]) ? $default($request, $options) : $sync($request, $options); }; } /** * Sends streaming requests to a streaming compatible handler while sending * all other requests to a default handler. * * This, for example, could be useful for taking advantage of the * performance benefits of curl while still supporting true streaming * through the StreamHandler. * * @param callable(RequestInterface, array): PromiseInterface $default Handler used for non-streaming responses * @param callable(RequestInterface, array): PromiseInterface $streaming Handler used for streaming responses * * @return callable(RequestInterface, array): PromiseInterface Returns the composed handler. */ public static function wrapStreaming(callable $default, callable $streaming) : callable { return static function (\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) use($default, $streaming) : PromiseInterface { return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options); }; } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/EasyHandle.php 0000644 00000005703 15174712003 0020366 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Psr7\Response; use YoastSEO_Vendor\GuzzleHttp\Utils; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Represents a cURL easy handle and the data it populates. * * @internal */ final class EasyHandle { /** * @var resource|\CurlHandle cURL resource */ public $handle; /** * @var StreamInterface Where data is being written */ public $sink; /** * @var array Received HTTP headers so far */ public $headers = []; /** * @var ResponseInterface|null Received response (if any) */ public $response; /** * @var RequestInterface Request being sent */ public $request; /** * @var array Request options */ public $options = []; /** * @var int cURL error number (if any) */ public $errno = 0; /** * @var \Throwable|null Exception during on_headers (if any) */ public $onHeadersException; /** * @var \Exception|null Exception during createResponse (if any) */ public $createResponseException; /** * Attach a response to the easy handle based on the received headers. * * @throws \RuntimeException if no headers have been received or the first * header line is invalid. */ public function createResponse() : void { [$ver, $status, $reason, $headers] = \YoastSEO_Vendor\GuzzleHttp\Handler\HeaderProcessor::parseHeaders($this->headers); $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers); if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; unset($headers[$normalizedKeys['content-encoding']]); if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $bodyLength = (int) $this->sink->getSize(); if ($bodyLength) { $headers[$normalizedKeys['content-length']] = $bodyLength; } else { unset($headers[$normalizedKeys['content-length']]); } } } // Attach a response to the easy handle with the parsed headers. $this->response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $this->sink, $ver, $reason); } /** * @param string $name * * @return void * * @throws \BadMethodCallException */ public function __get($name) { $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: ' . $name; throw new \BadMethodCallException($msg); } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php 0000644 00000002127 15174712003 0021436 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Utils; /** * @internal */ final class HeaderProcessor { /** * Returns the HTTP version, status code, reason phrase, and headers. * * @param string[] $headers * * @return array{0:string, 1:int, 2:?string, 3:array} * * @throws \RuntimeException */ public static function parseHeaders(array $headers) : array { if ($headers === []) { throw new \RuntimeException('Expected a non-empty array of header data'); } $parts = \explode(' ', \array_shift($headers), 3); $version = \explode('/', $parts[0])[1] ?? null; if ($version === null) { throw new \RuntimeException('HTTP version missing from header data'); } $status = $parts[1] ?? null; if ($status === null) { throw new \RuntimeException('HTTP status code missing from header data'); } return [$version, (int) $status, $parts[2] ?? null, \YoastSEO_Vendor\GuzzleHttp\Utils::headersFromLines($headers)]; } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php 0000644 00000021613 15174712003 0021565 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use Closure; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\Promise; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\Utils; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Returns an asynchronous response using curl_multi_* functions. * * When using the CurlMultiHandler, custom curl options can be specified as an * associative array of curl option constants mapping to values in the * **curl** key of the provided request options. * * @final */ class CurlMultiHandler { /** * @var CurlFactoryInterface */ private $factory; /** * @var int */ private $selectTimeout; /** * @var int Will be higher than 0 when `curl_multi_exec` is still running. */ private $active = 0; /** * @var array Request entry handles, indexed by handle id in `addRequest`. * * @see CurlMultiHandler::addRequest */ private $handles = []; /** * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`. * * @see CurlMultiHandler::addRequest */ private $delays = []; /** * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() */ private $options = []; /** @var resource|\CurlMultiHandle */ private $_mh; /** * This handler accepts the following options: * * - handle_factory: An optional factory used to create curl handles * - select_timeout: Optional timeout (in seconds) to block before timing * out while selecting curl handles. Defaults to 1 second. * - options: An associative array of CURLMOPT_* options and * corresponding values for curl_multi_setopt() */ public function __construct(array $options = []) { $this->factory = $options['handle_factory'] ?? new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(50); if (isset($options['select_timeout'])) { $this->selectTimeout = $options['select_timeout']; } elseif ($selectTimeout = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { @\trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); $this->selectTimeout = (int) $selectTimeout; } else { $this->selectTimeout = 1; } $this->options = $options['options'] ?? []; // unsetting the property forces the first access to go through // __get(). unset($this->_mh); } /** * @param string $name * * @return resource|\CurlMultiHandle * * @throws \BadMethodCallException when another field as `_mh` will be gotten * @throws \RuntimeException when curl can not initialize a multi handle */ public function __get($name) { if ($name !== '_mh') { throw new \BadMethodCallException("Can not get other property as '_mh'."); } $multiHandle = \curl_multi_init(); if (\false === $multiHandle) { throw new \RuntimeException('Can not initialize curl multi handle.'); } $this->_mh = $multiHandle; foreach ($this->options as $option => $value) { // A warning is raised in case of a wrong option. \curl_multi_setopt($this->_mh, $option, $value); } return $this->_mh; } public function __destruct() { if (isset($this->_mh)) { \curl_multi_close($this->_mh); unset($this->_mh); } } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $easy = $this->factory->create($request, $options); $id = (int) $easy->handle; $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise([$this, 'execute'], function () use($id) { return $this->cancel($id); }); $this->addRequest(['easy' => $easy, 'deferred' => $promise]); return $promise; } /** * Ticks the curl event loop. */ public function tick() : void { // Add any delayed handles if needed. if ($this->delays) { $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime(); foreach ($this->delays as $id => $delay) { if ($currentTime >= $delay) { unset($this->delays[$id]); \curl_multi_add_handle($this->_mh, $this->handles[$id]['easy']->handle); } } } // Run curl_multi_exec in the queue to enable other async tasks to run \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue'])); // Step through the task queue which may add additional requests. \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->run(); if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { // Perform a usleep if a select returns -1. // See: https://bugs.php.net/bug.php?id=61141 \usleep(250); } while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { // Prevent busy looping for slow HTTP requests. \curl_multi_select($this->_mh, $this->selectTimeout); } $this->processMessages(); } /** * Runs \curl_multi_exec() inside the event loop, to prevent busy looping */ private function tickInQueue() : void { if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { \curl_multi_select($this->_mh, 0); \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue'])); } } /** * Runs until all outstanding connections have completed. */ public function execute() : void { $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue(); while ($this->handles || !$queue->isEmpty()) { // If there are no transfers, then sleep for the next delay if (!$this->active && $this->delays) { \usleep($this->timeToNext()); } $this->tick(); } } private function addRequest(array $entry) : void { $easy = $entry['easy']; $id = (int) $easy->handle; $this->handles[$id] = $entry; if (empty($easy->options['delay'])) { \curl_multi_add_handle($this->_mh, $easy->handle); } else { $this->delays[$id] = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() + $easy->options['delay'] / 1000; } } /** * Cancels a handle from sending and removes references to it. * * @param int $id Handle ID to cancel and remove. * * @return bool True on success, false on failure. */ private function cancel($id) : bool { if (!\is_int($id)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } // Cannot cancel if it has been processed. if (!isset($this->handles[$id])) { return \false; } $handle = $this->handles[$id]['easy']->handle; unset($this->delays[$id], $this->handles[$id]); \curl_multi_remove_handle($this->_mh, $handle); if (\PHP_VERSION_ID < 80000) { \curl_close($handle); } return \true; } private function processMessages() : void { while ($done = \curl_multi_info_read($this->_mh)) { if ($done['msg'] !== \CURLMSG_DONE) { // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 continue; } $id = (int) $done['handle']; \curl_multi_remove_handle($this->_mh, $done['handle']); if (!isset($this->handles[$id])) { // Probably was cancelled. continue; } $entry = $this->handles[$id]; unset($this->handles[$id], $this->delays[$id]); $entry['easy']->errno = $done['result']; $entry['deferred']->resolve(\YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory::finish($this, $entry['easy'], $this->factory)); } } private function timeToNext() : int { $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime(); $nextTime = \PHP_INT_MAX; foreach ($this->delays as $time) { if ($time < $nextTime) { $nextTime = $time; } } return (int) \max(0, $nextTime - $currentTime) * 1000000; } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php 0000644 00000067601 15174712003 0020613 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException; use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream; use YoastSEO_Vendor\GuzzleHttp\TransferStats; use YoastSEO_Vendor\GuzzleHttp\Utils; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Creates curl resources from a request * * @final */ class CurlFactory implements \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface { public const CURL_VERSION_STR = 'curl_version'; /** * @deprecated */ public const LOW_CURL_VERSION_NUMBER = '7.21.2'; /** * @var resource[]|\CurlHandle[] */ private $handles = []; /** * @var int Total number of idle handles to keep in cache */ private $maxHandles; /** * @param int $maxHandles Maximum number of idle handles. */ public function __construct(int $maxHandles) { $this->maxHandles = $maxHandles; } public function create(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle { $protocolVersion = $request->getProtocolVersion(); if ('2' === $protocolVersion || '2.0' === $protocolVersion) { if (!self::supportsHttp2()) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request); } } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request); } if (isset($options['curl']['body_as_string'])) { $options['_body_as_string'] = $options['curl']['body_as_string']; unset($options['curl']['body_as_string']); } $easy = new \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle(); $easy->request = $request; $easy->options = $options; $conf = $this->getDefaultConf($easy); $this->applyMethod($easy, $conf); $this->applyHandlerOptions($easy, $conf); $this->applyHeaders($easy, $conf); unset($conf['_headers']); // Add handler options from the request configuration options if (isset($options['curl'])) { $conf = \array_replace($conf, $options['curl']); } $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); \curl_setopt_array($easy->handle, $conf); return $easy; } private static function supportsHttp2() : bool { static $supportsHttp2 = null; if (null === $supportsHttp2) { $supportsHttp2 = self::supportsTls12() && \defined('CURL_VERSION_HTTP2') && \CURL_VERSION_HTTP2 & \curl_version()['features']; } return $supportsHttp2; } private static function supportsTls12() : bool { static $supportsTls12 = null; if (null === $supportsTls12) { $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features']; } return $supportsTls12; } private static function supportsTls13() : bool { static $supportsTls13 = null; if (null === $supportsTls13) { $supportsTls13 = \defined('CURL_SSLVERSION_TLSv1_3') && \CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']; } return $supportsTls13; } public function release(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void { $resource = $easy->handle; unset($easy->handle); if (\count($this->handles) >= $this->maxHandles) { if (\PHP_VERSION_ID < 80000) { \curl_close($resource); } } else { // Remove all callback functions as they can hold onto references // and are not cleaned up by curl_reset. Using curl_setopt_array // does not work for some reason, so removing each one // individually. \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); \curl_setopt($resource, \CURLOPT_READFUNCTION, null); \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); \curl_reset($resource); $this->handles[] = $resource; } } /** * Completes a cURL transaction, either returning a response promise or a * rejected promise. * * @param callable(RequestInterface, array): PromiseInterface $handler * @param CurlFactoryInterface $factory Dictates how the handle is released */ public static function finish(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface $factory) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if (isset($easy->options['on_stats'])) { self::invokeStats($easy); } if (!$easy->response || $easy->errno) { return self::finishError($handler, $easy, $factory); } // Return the response if it is present and there is no error. $factory->release($easy); // Rewind the body of the response if possible. $body = $easy->response->getBody(); if ($body->isSeekable()) { $body->rewind(); } return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($easy->response); } private static function invokeStats(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void { $curlStats = \curl_getinfo($easy->handle); $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats); $easy->options['on_stats']($stats); } /** * @param callable(RequestInterface, array): PromiseInterface $handler */ private static function finishError(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface $factory) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { // Get error information and release the handle to the factory. $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle); $ctx[self::CURL_VERSION_STR] = self::getCurlVersion(); $factory->release($easy); // Retry when nothing is present or when curl failed to rewind. if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { return self::retryFailedRewind($handler, $easy, $ctx); } return self::createRejection($easy, $ctx); } private static function getCurlVersion() : string { static $curlVersion = null; if (null === $curlVersion) { $curlVersion = \curl_version()['version']; } return $curlVersion; } private static function createRejection(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $ctx) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true]; if ($easy->createResponseException) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx)); } // If an exception was encountered during the onHeaders event, then // return a rejected promise that wraps that exception. if ($easy->onHeadersException) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx)); } $uri = $easy->request->getUri(); $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri); $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $sanitizedError, 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'); if ('' !== $sanitizedError) { $redactedUriString = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString(); if ($redactedUriString !== '' && \false === \strpos($sanitizedError, $redactedUriString)) { $message .= \sprintf(' for %s', $redactedUriString); } } // Create a connection exception if it was a specific error code. $error = isset($connectionErrors[$easy->errno]) ? new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($message, $easy->request, null, $ctx) : new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException($message, $easy->request, $easy->response, null, $ctx); return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($error); } private static function sanitizeCurlError(string $error, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : string { if ('' === $error) { return $error; } $baseUri = $uri->withQuery('')->withFragment(''); $baseUriString = $baseUri->__toString(); if ('' === $baseUriString) { return $error; } $redactedUriString = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString(); return \str_replace($baseUriString, $redactedUriString, $error); } /** * @return array<int|string, mixed> */ private function getDefaultConf(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : array { $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300]; if (\defined('CURLOPT_PROTOCOLS')) { $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; } $version = $easy->request->getProtocolVersion(); if ('2' === $version || '2.0' === $version) { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; } elseif ('1.1' === $version) { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; } else { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; } return $conf; } private function applyMethod(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf) : void { $body = $easy->request->getBody(); $size = $body->getSize(); if ($size === null || $size > 0) { $this->applyBody($easy->request, $easy->options, $conf); return; } $method = $easy->request->getMethod(); if ($method === 'PUT' || $method === 'POST') { // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 if (!$easy->request->hasHeader('Content-Length')) { $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; } } elseif ($method === 'HEAD') { $conf[\CURLOPT_NOBODY] = \true; unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]); } } private function applyBody(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, array &$conf) : void { $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; // Send the body as a string if the size is less than 1MB OR if the // [curl][body_as_string] request value is set. if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) { $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); // Don't duplicate the Content-Length header $this->removeHeader('Content-Length', $conf); $this->removeHeader('Transfer-Encoding', $conf); } else { $conf[\CURLOPT_UPLOAD] = \true; if ($size !== null) { $conf[\CURLOPT_INFILESIZE] = $size; $this->removeHeader('Content-Length', $conf); } $body = $request->getBody(); if ($body->isSeekable()) { $body->rewind(); } $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use($body) { return $body->read($length); }; } // If the Expect header is not present, prevent curl from adding it if (!$request->hasHeader('Expect')) { $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; } // cURL sometimes adds a content-type by default. Prevent this. if (!$request->hasHeader('Content-Type')) { $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; } } private function applyHeaders(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf) : void { foreach ($conf['_headers'] as $name => $values) { foreach ($values as $value) { $value = (string) $value; if ($value === '') { // cURL requires a special format for empty headers. // See https://github.com/guzzle/guzzle/issues/1882 for more details. $conf[\CURLOPT_HTTPHEADER][] = "{$name};"; } else { $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}"; } } } // Remove the Accept header if one was not set if (!$easy->request->hasHeader('Accept')) { $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; } } /** * Remove a header from the options array. * * @param string $name Case-insensitive header to remove * @param array $options Array of options to modify */ private function removeHeader(string $name, array &$options) : void { foreach (\array_keys($options['_headers']) as $key) { if (!\strcasecmp($key, $name)) { unset($options['_headers'][$key]); return; } } } private function applyHandlerOptions(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf) : void { $options = $easy->options; if (isset($options['verify'])) { if ($options['verify'] === \false) { unset($conf[\CURLOPT_CAINFO]); $conf[\CURLOPT_SSL_VERIFYHOST] = 0; $conf[\CURLOPT_SSL_VERIFYPEER] = \false; } else { $conf[\CURLOPT_SSL_VERIFYHOST] = 2; $conf[\CURLOPT_SSL_VERIFYPEER] = \true; if (\is_string($options['verify'])) { // Throw an error if the file/folder/link path is not valid or doesn't exist. if (!\file_exists($options['verify'])) { throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); } // If it's a directory or a link to a directory use CURLOPT_CAPATH. // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. if (\is_dir($options['verify']) || \is_link($options['verify']) === \true && ($verifyLink = \readlink($options['verify'])) !== \false && \is_dir($verifyLink)) { $conf[\CURLOPT_CAPATH] = $options['verify']; } else { $conf[\CURLOPT_CAINFO] = $options['verify']; } } } } if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { $accept = $easy->request->getHeaderLine('Accept-Encoding'); if ($accept) { $conf[\CURLOPT_ENCODING] = $accept; } else { // The empty string enables all available decoders and implicitly // sets a matching 'Accept-Encoding' header. $conf[\CURLOPT_ENCODING] = ''; // But as the user did not specify any encoding preference, // let's leave it up to server by preventing curl from sending // the header, which will be interpreted as 'Accept-Encoding: *'. // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; } } if (!isset($options['sink'])) { // Use a default temp stream if no sink was set. $options['sink'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); } $sink = $options['sink']; if (!\is_string($sink)) { $sink = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink); } elseif (!\is_dir(\dirname($sink))) { // Ensure that the directory exists before failing in curl. throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); } else { $sink = new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+'); } $easy->sink = $sink; $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use($sink) : int { return $sink->write($write); }; $timeoutRequiresNoSignal = \false; if (isset($options['timeout'])) { $timeoutRequiresNoSignal |= $options['timeout'] < 1; $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; } // CURL default value is CURL_IPRESOLVE_WHATEVER if (isset($options['force_ip_resolve'])) { if ('v4' === $options['force_ip_resolve']) { $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; } elseif ('v6' === $options['force_ip_resolve']) { $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; } } if (isset($options['connect_timeout'])) { $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; } if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { $conf[\CURLOPT_NOSIGNAL] = \true; } if (isset($options['proxy'])) { if (!\is_array($options['proxy'])) { $conf[\CURLOPT_PROXY] = $options['proxy']; } else { $scheme = $easy->request->getUri()->getScheme(); if (isset($options['proxy'][$scheme])) { $host = $easy->request->getUri()->getHost(); if (isset($options['proxy']['no']) && \YoastSEO_Vendor\GuzzleHttp\Utils::isHostInNoProxy($host, $options['proxy']['no'])) { unset($conf[\CURLOPT_PROXY]); } else { $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; } } } } if (isset($options['crypto_method'])) { $protocolVersion = $easy->request->getProtocolVersion(); // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2 if ('2' === $protocolVersion || '2.0' === $protocolVersion) { if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { if (!self::supportsTls13()) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; } else { throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { if (!self::supportsTls12()) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { if (!self::supportsTls13()) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; } else { throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } } if (isset($options['cert'])) { $cert = $options['cert']; if (\is_array($cert)) { $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; $cert = $cert[0]; } if (!\file_exists($cert)) { throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); } // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html $ext = \pathinfo($cert, \PATHINFO_EXTENSION); if (\preg_match('#^(der|p12)$#i', $ext)) { $conf[\CURLOPT_SSLCERTTYPE] = \strtoupper($ext); } $conf[\CURLOPT_SSLCERT] = $cert; } if (isset($options['ssl_key'])) { if (\is_array($options['ssl_key'])) { if (\count($options['ssl_key']) === 2) { [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; } else { [$sslKey] = $options['ssl_key']; } } $sslKey = $sslKey ?? $options['ssl_key']; if (!\file_exists($sslKey)) { throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); } $conf[\CURLOPT_SSLKEY] = $sslKey; } if (isset($options['progress'])) { $progress = $options['progress']; if (!\is_callable($progress)) { throw new \InvalidArgumentException('progress client option must be callable'); } $conf[\CURLOPT_NOPROGRESS] = \false; $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) { $progress($downloadSize, $downloaded, $uploadSize, $uploaded); }; } if (!empty($options['debug'])) { $conf[\CURLOPT_STDERR] = \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($options['debug']); $conf[\CURLOPT_VERBOSE] = \true; } } /** * This function ensures that a response was set on a transaction. If one * was not set, then the request is retried if possible. This error * typically means you are sending a payload, curl encountered a * "Connection died, retrying a fresh connect" error, tried to rewind the * stream, and then encountered a "necessary data rewind wasn't possible" * error, causing the request to be sent through curl_multi_info_read() * without an error status. * * @param callable(RequestInterface, array): PromiseInterface $handler */ private static function retryFailedRewind(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $ctx) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { try { // Only rewind if the body has been read from. $body = $easy->request->getBody(); if ($body->tell() > 0) { $body->rewind(); } } catch (\RuntimeException $e) { $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e; return self::createRejection($easy, $ctx); } // Retry no more than 3 times before giving up. if (!isset($easy->options['_curl_retries'])) { $easy->options['_curl_retries'] = 1; } elseif ($easy->options['_curl_retries'] == 2) { $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.'; return self::createRejection($easy, $ctx); } else { ++$easy->options['_curl_retries']; } return $handler($easy->request, $easy->options); } private function createHeaderFn(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : callable { if (isset($easy->options['on_headers'])) { $onHeaders = $easy->options['on_headers']; if (!\is_callable($onHeaders)) { throw new \InvalidArgumentException('on_headers must be callable'); } } else { $onHeaders = null; } return static function ($ch, $h) use($onHeaders, $easy, &$startingResponse) { $value = \trim($h); if ($value === '') { $startingResponse = \true; try { $easy->createResponse(); } catch (\Exception $e) { $easy->createResponseException = $e; return -1; } if ($onHeaders !== null) { try { $onHeaders($easy->response); } catch (\Exception $e) { // Associate the exception with the handle and trigger // a curl header write error by returning 0. $easy->onHeadersException = $e; return -1; } } } elseif ($startingResponse) { $startingResponse = \false; $easy->headers = [$value]; } else { $easy->headers[] = $value; } return \strlen($h); }; } public function __destruct() { foreach ($this->handles as $id => $handle) { if (\PHP_VERSION_ID < 80000) { \curl_close($handle); } unset($this->handles[$id]); } } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlHandler.php 0000644 00000002740 15174712003 0020552 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * HTTP handler that uses cURL easy handles as a transport layer. * * When using the CurlHandler, custom curl options can be specified as an * associative array of curl option constants mapping to values in the * **curl** key of the "client" key of the request. * * @final */ class CurlHandler { /** * @var CurlFactoryInterface */ private $factory; /** * Accepts an associative array of options: * * - handle_factory: Optional curl factory used to create cURL handles. * * @param array{handle_factory?: ?CurlFactoryInterface} $options Array of options to use with the handler */ public function __construct(array $options = []) { $this->factory = $options['handle_factory'] ?? new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(3); } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if (isset($options['delay'])) { \usleep($options['delay'] * 1000); } $easy = $this->factory->create($request, $options); \curl_exec($easy->handle); $easy->errno = \curl_errno($easy->handle); return \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory::finish($this, $easy, $this->factory); } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php 0000644 00000001433 15174712003 0022423 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; interface CurlFactoryInterface { /** * Creates a cURL handle resource. * * @param RequestInterface $request Request * @param array $options Transfer options * * @throws \RuntimeException when an option cannot be applied */ public function create(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle; /** * Release an easy handle, allowing it to be reused or closed. * * This function must call unset on the easy handle's "handle" property. */ public function release(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void; } vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php 0000644 00000054426 15174712003 0021110 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException; use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\GuzzleHttp\TransferStats; use YoastSEO_Vendor\GuzzleHttp\Utils; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * HTTP handler that uses PHP's HTTP stream wrapper. * * @final */ class StreamHandler { /** * @var array */ private $lastHeaders = []; /** * Sends an HTTP request. * * @param RequestInterface $request Request to send. * @param array $options Request transfer options. */ public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { // Sleep if there is a delay specified. if (isset($options['delay'])) { \usleep($options['delay'] * 1000); } $protocolVersion = $request->getProtocolVersion(); if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request); } $startTime = isset($options['on_stats']) ? \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() : null; try { // Does not support the expect header. $request = $request->withoutHeader('Expect'); // Append a content-length header if body size is zero to match // the behavior of `CurlHandler` if ((0 === \strcasecmp('PUT', $request->getMethod()) || 0 === \strcasecmp('POST', $request->getMethod())) && 0 === $request->getBody()->getSize()) { $request = $request->withHeader('Content-Length', '0'); } return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime); } catch (\InvalidArgumentException $e) { throw $e; } catch (\Exception $e) { // Determine if the error was a networking error. $message = $e->getMessage(); // This list can probably get more comprehensive. if (\false !== \strpos($message, 'getaddrinfo') || \false !== \strpos($message, 'Connection refused') || \false !== \strpos($message, "couldn't connect to host") || \false !== \strpos($message, 'connection attempt failed')) { $e = new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($e->getMessage(), $request, $e); } else { $e = \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException::wrapException($request, $e); } $this->invokeStats($options, $request, $startTime, null, $e); return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e); } } private function invokeStats(array $options, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?float $startTime, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $error = null) : void { if (isset($options['on_stats'])) { $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() - $startTime, $error, []); $options['on_stats']($stats); } } /** * @param resource $stream */ private function createResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, $stream, ?float $startTime) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $hdrs = $this->lastHeaders; $this->lastHeaders = []; try { [$ver, $status, $reason, $headers] = \YoastSEO_Vendor\GuzzleHttp\Handler\HeaderProcessor::parseHeaders($hdrs); } catch (\Exception $e) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e)); } [$stream, $headers] = $this->checkDecode($options, $headers, $stream); $stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream); $sink = $stream; if (\strcasecmp('HEAD', $request->getMethod())) { $sink = $this->createSink($stream, $options); } try { $response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $sink, $ver, $reason); } catch (\Exception $e) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e)); } if (isset($options['on_headers'])) { try { $options['on_headers']($response); } catch (\Exception $e) { return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered during the on_headers event', $request, $response, $e)); } } // Do not drain when the request is a HEAD request because they have // no body. if ($sink !== $stream) { $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); } $this->invokeStats($options, $request, $startTime, $response, null); return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($response); } private function createSink(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $options) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { if (!empty($options['stream'])) { return $stream; } $sink = $options['sink'] ?? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'r+'); return \is_string($sink) ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+') : \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink); } /** * @param resource $stream */ private function checkDecode(array $options, array $headers, $stream) : array { // Automatically decode responses when instructed. if (!empty($options['decode_content'])) { $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers); if (isset($normalizedKeys['content-encoding'])) { $encoding = $headers[$normalizedKeys['content-encoding']]; if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\InflateStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream)); $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; // Remove content-encoding header unset($headers[$normalizedKeys['content-encoding']]); // Fix content-length header if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $length = (int) $stream->getSize(); if ($length === 0) { unset($headers[$normalizedKeys['content-length']]); } else { $headers[$normalizedKeys['content-length']] = [$length]; } } } } } return [$stream, $headers]; } /** * Drains the source stream into the "sink" client option. * * @param string $contentLength Header specifying the amount of * data to read. * * @throws \RuntimeException when the sink option is invalid. */ private function drain(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $sink, string $contentLength) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { // If a content-length header is provided, then stop reading once // that number of bytes has been read. This can prevent infinitely // reading from a stream when dealing with servers that do not honor // Connection: Close headers. \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1); $sink->seek(0); $source->close(); return $sink; } /** * Create a resource and check to ensure it was created successfully * * @param callable $callback Callable that returns stream resource * * @return resource * * @throws \RuntimeException on error */ private function createResource(callable $callback) { $errors = []; \set_error_handler(static function ($_, $msg, $file, $line) use(&$errors) : bool { $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line]; return \true; }); try { $resource = $callback(); } finally { \restore_error_handler(); } if (!$resource) { $message = 'Error creating resource: '; foreach ($errors as $err) { foreach ($err as $key => $value) { $message .= "[{$key}] {$value}" . \PHP_EOL; } } throw new \RuntimeException(\trim($message)); } return $resource; } /** * @return resource */ private function createStream(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) { static $methods; if (!$methods) { $methods = \array_flip(\get_class_methods(__CLASS__)); } if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request); } // HTTP/1.1 streams using the PHP stream wrapper require a // Connection: close header if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) { $request = $request->withHeader('Connection', 'close'); } // Ensure SSL is verified by default if (!isset($options['verify'])) { $options['verify'] = \true; } $params = []; $context = $this->getDefaultContext($request); if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } if (!empty($options)) { foreach ($options as $key => $value) { $method = "add_{$key}"; if (isset($methods[$method])) { $this->{$method}($request, $context, $value, $params); } } } if (isset($options['stream_context'])) { if (!\is_array($options['stream_context'])) { throw new \InvalidArgumentException('stream_context must be an array'); } $context = \array_replace_recursive($context, $options['stream_context']); } // Microsoft NTLM authentication only supported with curl handler if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); } $uri = $this->resolveHost($request, $options); $contextResource = $this->createResource(static function () use($context, $params) { return \stream_context_create($context, $params); }); return $this->createResource(function () use($uri, $contextResource, $context, $options, $request) { $resource = @\fopen((string) $uri, 'r', \false, $contextResource); // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable if (\function_exists('YoastSEO_Vendor\\http_get_last_response_headers')) { /** @var array|null */ $http_response_header = \YoastSEO_Vendor\http_get_last_response_headers(); } $this->lastHeaders = $http_response_header ?? []; if (\false === $resource) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context); } if (isset($options['read_timeout'])) { $readTimeout = $options['read_timeout']; $sec = (int) $readTimeout; $usec = ($readTimeout - $sec) * 100000; \stream_set_timeout($resource, $sec, $usec); } return $resource; }); } private function resolveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $uri = $request->getUri(); if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { if ('v4' === $options['force_ip_resolve']) { $records = \dns_get_record($uri->getHost(), \DNS_A); if (\false === $records || !isset($records[0]['ip'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); } return $uri->withHost($records[0]['ip']); } if ('v6' === $options['force_ip_resolve']) { $records = \dns_get_record($uri->getHost(), \DNS_AAAA); if (\false === $records || !isset($records[0]['ipv6'])) { throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); } return $uri->withHost('[' . $records[0]['ipv6'] . ']'); } } return $uri; } private function getDefaultContext(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : array { $headers = ''; foreach ($request->getHeaders() as $name => $value) { foreach ($value as $val) { $headers .= "{$name}: {$val}\r\n"; } } $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]]; $body = (string) $request->getBody(); if ('' !== $body) { $context['http']['content'] = $body; // Prevent the HTTP handler from adding a Content-Type header. if (!$request->hasHeader('Content-Type')) { $context['http']['header'] .= "Content-Type:\r\n"; } } $context['http']['header'] = \rtrim($context['http']['header']); return $context; } /** * @param mixed $value as passed via Request transfer options. */ private function add_proxy(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { $uri = null; if (!\is_array($value)) { $uri = $value; } else { $scheme = $request->getUri()->getScheme(); if (isset($value[$scheme])) { if (!isset($value['no']) || !\YoastSEO_Vendor\GuzzleHttp\Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { $uri = $value[$scheme]; } } } if (!$uri) { return; } $parsed = $this->parse_proxy($uri); $options['http']['proxy'] = $parsed['proxy']; if ($parsed['auth']) { if (!isset($options['http']['header'])) { $options['http']['header'] = []; } $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; } } /** * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. */ private function parse_proxy(string $url) : array { $parsed = \parse_url($url); if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { if (isset($parsed['host']) && isset($parsed['port'])) { $auth = null; if (isset($parsed['user']) && isset($parsed['pass'])) { $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); } return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null]; } } // Return proxy as-is. return ['proxy' => $url, 'auth' => null]; } /** * @param mixed $value as passed via Request transfer options. */ private function add_timeout(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { if ($value > 0) { $options['http']['timeout'] = $value; } } /** * @param mixed $value as passed via Request transfer options. */ private function add_crypto_method(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT || \defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) { $options['http']['crypto_method'] = $value; return; } throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } /** * @param mixed $value as passed via Request transfer options. */ private function add_verify(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { if ($value === \false) { $options['ssl']['verify_peer'] = \false; $options['ssl']['verify_peer_name'] = \false; return; } if (\is_string($value)) { $options['ssl']['cafile'] = $value; if (!\file_exists($value)) { throw new \RuntimeException("SSL CA bundle not found: {$value}"); } } elseif ($value !== \true) { throw new \InvalidArgumentException('Invalid verify request option'); } $options['ssl']['verify_peer'] = \true; $options['ssl']['verify_peer_name'] = \true; $options['ssl']['allow_self_signed'] = \false; } /** * @param mixed $value as passed via Request transfer options. */ private function add_cert(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { if (\is_array($value)) { $options['ssl']['passphrase'] = $value[1]; $value = $value[0]; } if (!\file_exists($value)) { throw new \RuntimeException("SSL certificate not found: {$value}"); } $options['ssl']['local_cert'] = $value; } /** * @param mixed $value as passed via Request transfer options. */ private function add_progress(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) { if ($code == \STREAM_NOTIFY_PROGRESS) { // The upload progress cannot be determined. Use 0 for cURL compatibility: // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html $value($total, $transferred, 0, 0); } }); } /** * @param mixed $value as passed via Request transfer options. */ private function add_debug(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void { if ($value === \false) { return; } static $map = [\STREAM_NOTIFY_CONNECT => 'CONNECT', \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', \STREAM_NOTIFY_PROGRESS => 'PROGRESS', \STREAM_NOTIFY_FAILURE => 'FAILURE', \STREAM_NOTIFY_COMPLETED => 'COMPLETED', \STREAM_NOTIFY_RESOLVE => 'RESOLVE']; static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; $value = \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($value); $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void { \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); foreach (\array_filter($passed) as $i => $v) { \fwrite($value, $args[$i] . ': "' . $v . '" '); } \fwrite($value, "\n"); }); } private static function addNotification(array &$params, callable $notify) : void { // Wrap the existing function if needed. if (!isset($params['notification'])) { $params['notification'] = $notify; } else { $params['notification'] = self::callArray([$params['notification'], $notify]); } } private static function callArray(array $functions) : callable { return static function (...$args) use($functions) { foreach ($functions as $fn) { $fn(...$args); } }; } } vendor_prefixed/guzzlehttp/guzzle/src/Handler/MockHandler.php 0000644 00000015315 15174712003 0020540 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp\Handler; use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException; use YoastSEO_Vendor\GuzzleHttp\HandlerStack; use YoastSEO_Vendor\GuzzleHttp\Promise as P; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\GuzzleHttp\TransferStats; use YoastSEO_Vendor\GuzzleHttp\Utils; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Handler that returns responses or throw exceptions from a queue. * * @final */ class MockHandler implements \Countable { /** * @var array */ private $queue = []; /** * @var RequestInterface|null */ private $lastRequest; /** * @var array */ private $lastOptions = []; /** * @var callable|null */ private $onFulfilled; /** * @var callable|null */ private $onRejected; /** * Creates a new MockHandler that uses the default handler stack list of * middlewares. * * @param array|null $queue Array of responses, callables, or exceptions. * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ public static function createWithMiddleware(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\HandlerStack { return \YoastSEO_Vendor\GuzzleHttp\HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); } /** * The passed in value must be an array of * {@see ResponseInterface} objects, Exceptions, * callables, or Promises. * * @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array. * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) { $this->onFulfilled = $onFulfilled; $this->onRejected = $onRejected; if ($queue) { // array_values included for BC $this->append(...\array_values($queue)); } } public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if (!$this->queue) { throw new \OutOfBoundsException('Mock queue is empty'); } if (isset($options['delay']) && \is_numeric($options['delay'])) { \usleep((int) $options['delay'] * 1000); } $this->lastRequest = $request; $this->lastOptions = $options; $response = \array_shift($this->queue); if (isset($options['on_headers'])) { if (!\is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } try { $options['on_headers']($response); } catch (\Exception $e) { $msg = 'An error was encountered during the on_headers event'; $response = new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException($msg, $request, $response, $e); } } if (\is_callable($response)) { $response = $response($request, $options); } $response = $response instanceof \Throwable ? \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($response) : \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($response); return $response->then(function (?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $value) use($request, $options) { $this->invokeStats($request, $options, $value); if ($this->onFulfilled) { ($this->onFulfilled)($value); } if ($value !== null && isset($options['sink'])) { $contents = (string) $value->getBody(); $sink = $options['sink']; if (\is_resource($sink)) { \fwrite($sink, $contents); } elseif (\is_string($sink)) { \file_put_contents($sink, $contents); } elseif ($sink instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { $sink->write($contents); } } return $value; }, function ($reason) use($request, $options) { $this->invokeStats($request, $options, null, $reason); if ($this->onRejected) { ($this->onRejected)($reason); } return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($reason); }); } /** * Adds one or more variadic requests, exceptions, callables, or promises * to the queue. * * @param mixed ...$values */ public function append(...$values) : void { foreach ($values as $value) { if ($value instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface || $value instanceof \Throwable || $value instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface || \is_callable($value)) { $this->queue[] = $value; } else { throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found ' . \YoastSEO_Vendor\GuzzleHttp\Utils::describeType($value)); } } } /** * Get the last received request. */ public function getLastRequest() : ?\YoastSEO_Vendor\Psr\Http\Message\RequestInterface { return $this->lastRequest; } /** * Get the last received request options. */ public function getLastOptions() : array { return $this->lastOptions; } /** * Returns the number of remaining items in the queue. */ public function count() : int { return \count($this->queue); } public function reset() : void { $this->queue = []; } /** * @param mixed $reason Promise or reason. */ private function invokeStats(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, $reason = null) : void { if (isset($options['on_stats'])) { $transferTime = $options['transfer_time'] ?? 0; $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, $transferTime, $reason); $options['on_stats']($stats); } } } vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizer.php 0000644 00000001334 15174712003 0017744 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\Psr\Http\Message\MessageInterface; final class BodySummarizer implements \YoastSEO_Vendor\GuzzleHttp\BodySummarizerInterface { /** * @var int|null */ private $truncateAt; public function __construct(?int $truncateAt = null) { $this->truncateAt = $truncateAt; } /** * Returns a summarized message body. */ public function summarize(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : ?string { return $this->truncateAt === null ? \YoastSEO_Vendor\GuzzleHttp\Psr7\Message::bodySummary($message) : \YoastSEO_Vendor\GuzzleHttp\Psr7\Message::bodySummary($message, $this->truncateAt); } } vendor_prefixed/guzzlehttp/guzzle/src/ClientTrait.php 0000644 00000022550 15174712003 0017215 0 ustar 00 <?php namespace YoastSEO_Vendor\GuzzleHttp; use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Client interface for sending HTTP requests. */ trait ClientTrait { /** * Create and send an HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string $method HTTP method. * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public abstract function request(string $method, $uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Create and send an HTTP GET request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function get($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('GET', $uri, $options); } /** * Create and send an HTTP HEAD request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function head($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('HEAD', $uri, $options); } /** * Create and send an HTTP PUT request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function put($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('PUT', $uri, $options); } /** * Create and send an HTTP POST request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function post($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('POST', $uri, $options); } /** * Create and send an HTTP PATCH request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function patch($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('PATCH', $uri, $options); } /** * Create and send an HTTP DELETE request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. * * @throws GuzzleException */ public function delete($uri, array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return $this->request('DELETE', $uri, $options); } /** * Create and send an asynchronous HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string $method HTTP method * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public abstract function requestAsync(string $method, $uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; /** * Create and send an asynchronous HTTP GET request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function getAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('GET', $uri, $options); } /** * Create and send an asynchronous HTTP HEAD request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function headAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('HEAD', $uri, $options); } /** * Create and send an asynchronous HTTP PUT request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function putAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('PUT', $uri, $options); } /** * Create and send an asynchronous HTTP POST request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function postAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('POST', $uri, $options); } /** * Create and send an asynchronous HTTP PATCH request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function patchAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('PATCH', $uri, $options); } /** * Create and send an asynchronous HTTP DELETE request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. */ public function deleteAsync($uri, array $options = []) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->requestAsync('DELETE', $uri, $options); } } vendor_prefixed/guzzlehttp/promises/src/Utils.php 0000644 00000021462 15174712003 0016415 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; final class Utils { /** * Get the global task queue used for promise resolution. * * This task queue MUST be run in an event loop in order for promises to be * settled asynchronously. It will be automatically run when synchronously * waiting on a promise. * * <code> * while ($eventLoop->isRunning()) { * GuzzleHttp\Promise\Utils::queue()->run(); * } * </code> * * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. */ public static function queue(?\YoastSEO_Vendor\GuzzleHttp\Promise\TaskQueueInterface $assign = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\TaskQueueInterface { static $queue; if ($assign) { $queue = $assign; } elseif (!$queue) { $queue = new \YoastSEO_Vendor\GuzzleHttp\Promise\TaskQueue(); } return $queue; } /** * Adds a function to run in the task queue when it is next `run()` and * returns a promise that is fulfilled or rejected with the result. * * @param callable $task Task function to run. */ public static function task(callable $task) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $queue = self::queue(); $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise([$queue, 'run']); $queue->add(function () use($task, $promise) : void { try { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($promise)) { $promise->resolve($task()); } } catch (\Throwable $e) { $promise->reject($e); } }); return $promise; } /** * Synchronously waits on a promise to resolve and returns an inspection * state array. * * Returns a state associative array containing a "state" key mapping to a * valid promise state. If the state of the promise is "fulfilled", the * array will contain a "value" key mapping to the fulfilled value of the * promise. If the promise is rejected, the array will contain a "reason" * key mapping to the rejection reason of the promise. * * @param PromiseInterface $promise Promise or value. */ public static function inspect(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise) : array { try { return ['state' => \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::FULFILLED, 'value' => $promise->wait()]; } catch (\YoastSEO_Vendor\GuzzleHttp\Promise\RejectionException $e) { return ['state' => \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::REJECTED, 'reason' => $e->getReason()]; } catch (\Throwable $e) { return ['state' => \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::REJECTED, 'reason' => $e]; } } /** * Waits on all of the provided promises, but does not unwrap rejected * promises as thrown exception. * * Returns an array of inspection state arrays. * * @see inspect for the inspection state array format. * * @param PromiseInterface[] $promises Traversable of promises to wait upon. */ public static function inspectAll($promises) : array { $results = []; foreach ($promises as $key => $promise) { $results[$key] = self::inspect($promise); } return $results; } /** * Waits on all of the provided promises and returns the fulfilled values. * * Returns an array that contains the value of each promise (in the same * order the promises were provided). An exception is thrown if any of the * promises are rejected. * * @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on. * * @throws \Throwable on error */ public static function unwrap($promises) : array { $results = []; foreach ($promises as $key => $promise) { $results[$key] = $promise->wait(); } return $results; } /** * Given an array of promises, return a promise that is fulfilled when all * the items in the array are fulfilled. * * The promise's fulfillment value is an array with fulfillment values at * respective positions to the original array. If any promise in the array * rejects, the returned promise is rejected with the rejection reason. * * @param mixed $promises Promises or values. * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. */ public static function all($promises, bool $recursive = \false) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $results = []; $promise = \YoastSEO_Vendor\GuzzleHttp\Promise\Each::of($promises, function ($value, $idx) use(&$results) : void { $results[$idx] = $value; }, function ($reason, $idx, \YoastSEO_Vendor\GuzzleHttp\Promise\Promise $aggregate) : void { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($aggregate)) { $aggregate->reject($reason); } })->then(function () use(&$results) { \ksort($results); return $results; }); if (\true === $recursive) { $promise = $promise->then(function ($results) use($recursive, &$promises) { foreach ($promises as $promise) { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($promise)) { return self::all($promises, $recursive); } } return $results; }); } return $promise; } /** * Initiate a competitive race between multiple promises or values (values * will become immediately fulfilled promises). * * When count amount of promises have been fulfilled, the returned promise * is fulfilled with an array that contains the fulfillment values of the * winners in order of resolution. * * This promise is rejected with a {@see AggregateException} if the number * of fulfilled promises is less than the desired $count. * * @param int $count Total number of promises. * @param mixed $promises Promises or values. */ public static function some(int $count, $promises) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $results = []; $rejections = []; return \YoastSEO_Vendor\GuzzleHttp\Promise\Each::of($promises, function ($value, $idx, \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $p) use(&$results, $count) : void { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::settled($p)) { return; } $results[$idx] = $value; if (\count($results) >= $count) { $p->resolve(null); } }, function ($reason) use(&$rejections) : void { $rejections[] = $reason; })->then(function () use(&$results, &$rejections, $count) { if (\count($results) !== $count) { throw new \YoastSEO_Vendor\GuzzleHttp\Promise\AggregateException('Not enough promises to fulfill count', $rejections); } \ksort($results); return \array_values($results); }); } /** * Like some(), with 1 as count. However, if the promise fulfills, the * fulfillment value is not an array of 1 but the value directly. * * @param mixed $promises Promises or values. */ public static function any($promises) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return self::some(1, $promises)->then(function ($values) { return $values[0]; }); } /** * Returns a promise that is fulfilled when all of the provided promises have * been fulfilled or rejected. * * The returned promise is fulfilled with an array of inspection state arrays. * * @see inspect for the inspection state array format. * * @param mixed $promises Promises or values. */ public static function settle($promises) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { $results = []; return \YoastSEO_Vendor\GuzzleHttp\Promise\Each::of($promises, function ($value, $idx) use(&$results) : void { $results[$idx] = ['state' => \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::FULFILLED, 'value' => $value]; }, function ($reason, $idx) use(&$results) : void { $results[$idx] = ['state' => \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::REJECTED, 'reason' => $reason]; })->then(function () use(&$results) { \ksort($results); return $results; }); } } vendor_prefixed/guzzlehttp/promises/src/PromiseInterface.php 0000644 00000005513 15174712003 0020553 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * A promise represents the eventual result of an asynchronous operation. * * The primary way of interacting with a promise is through its then method, * which registers callbacks to receive either a promise’s eventual value or * the reason why the promise cannot be fulfilled. * * @see https://promisesaplus.com/ */ interface PromiseInterface { public const PENDING = 'pending'; public const FULFILLED = 'fulfilled'; public const REJECTED = 'rejected'; /** * Appends fulfillment and rejection handlers to the promise, and returns * a new promise resolving to the return value of the called handler. * * @param callable $onFulfilled Invoked when the promise fulfills. * @param callable $onRejected Invoked when the promise is rejected. */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; /** * Appends a rejection handler callback to the promise, and returns a new * promise resolving to the return value of the callback if it is called, * or to its original fulfillment value if the promise is instead * fulfilled. * * @param callable $onRejected Invoked when the promise is rejected. */ public function otherwise(callable $onRejected) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; /** * Get the state of the promise ("pending", "rejected", or "fulfilled"). * * The three states can be checked against the constants defined on * PromiseInterface: PENDING, FULFILLED, and REJECTED. */ public function getState() : string; /** * Resolve the promise with the given value. * * @param mixed $value * * @throws \RuntimeException if the promise is already resolved. */ public function resolve($value) : void; /** * Reject the promise with the given reason. * * @param mixed $reason * * @throws \RuntimeException if the promise is already resolved. */ public function reject($reason) : void; /** * Cancels the promise if possible. * * @see https://github.com/promises-aplus/cancellation-spec/issues/7 */ public function cancel() : void; /** * Waits until the promise completes if possible. * * Pass $unwrap as true to unwrap the result of the promise, either * returning the resolved value or throwing the rejected exception. * * If the promise cannot be waited on, then the promise will be rejected. * * @return mixed * * @throws \LogicException if the promise has no wait function or if the * promise does not settle after waiting. */ public function wait(bool $unwrap = \true); } vendor_prefixed/guzzlehttp/promises/src/Promise.php 0000644 00000022203 15174712003 0016725 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * Promises/A+ implementation that avoids recursion when possible. * * @see https://promisesaplus.com/ * * @final */ class Promise implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { private $state = self::PENDING; private $result; private $cancelFn; private $waitFn; private $waitList; private $handlers = []; /** * @param callable $waitFn Fn that when invoked resolves the promise. * @param callable $cancelFn Fn that when invoked cancels the promise. */ public function __construct(?callable $waitFn = null, ?callable $cancelFn = null) { $this->waitFn = $waitFn; $this->cancelFn = $cancelFn; } public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if ($this->state === self::PENDING) { $p = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise(null, [$this, 'cancel']); $this->handlers[] = [$p, $onFulfilled, $onRejected]; $p->waitList = $this->waitList; $p->waitList[] = $this; return $p; } // Return a fulfilled promise and immediately invoke any callbacks. if ($this->state === self::FULFILLED) { $promise = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($this->result); return $onFulfilled ? $promise->then($onFulfilled) : $promise; } // It's either cancelled or rejected, so return a rejected promise // and immediately invoke any callbacks. $rejection = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($this->result); return $onRejected ? $rejection->then(null, $onRejected) : $rejection; } public function otherwise(callable $onRejected) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->then(null, $onRejected); } public function wait(bool $unwrap = \true) { $this->waitIfPending(); if ($this->result instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface) { return $this->result->wait($unwrap); } if ($unwrap) { if ($this->state === self::FULFILLED) { return $this->result; } // It's rejected so "unwrap" and throw an exception. throw \YoastSEO_Vendor\GuzzleHttp\Promise\Create::exceptionFor($this->result); } } public function getState() : string { return $this->state; } public function cancel() : void { if ($this->state !== self::PENDING) { return; } $this->waitFn = $this->waitList = null; if ($this->cancelFn) { $fn = $this->cancelFn; $this->cancelFn = null; try { $fn(); } catch (\Throwable $e) { $this->reject($e); } } // Reject the promise only if it wasn't rejected in a then callback. /** @psalm-suppress RedundantCondition */ if ($this->state === self::PENDING) { $this->reject(new \YoastSEO_Vendor\GuzzleHttp\Promise\CancellationException('Promise has been cancelled')); } } public function resolve($value) : void { $this->settle(self::FULFILLED, $value); } public function reject($reason) : void { $this->settle(self::REJECTED, $reason); } private function settle(string $state, $value) : void { if ($this->state !== self::PENDING) { // Ignore calls with the same resolution. if ($state === $this->state && $value === $this->result) { return; } throw $this->state === $state ? new \LogicException("The promise is already {$state}.") : new \LogicException("Cannot change a {$this->state} promise to {$state}"); } if ($value === $this) { throw new \LogicException('Cannot fulfill or reject a promise with itself'); } // Clear out the state of the promise but stash the handlers. $this->state = $state; $this->result = $value; $handlers = $this->handlers; $this->handlers = null; $this->waitList = $this->waitFn = null; $this->cancelFn = null; if (!$handlers) { return; } // If the value was not a settled promise or a thenable, then resolve // it in the task queue using the correct ID. if (!\is_object($value) || !\method_exists($value, 'then')) { $id = $state === self::FULFILLED ? 1 : 2; // It's a success, so resolve the handlers in the queue. \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(static function () use($id, $value, $handlers) : void { foreach ($handlers as $handler) { self::callHandler($id, $value, $handler); } }); } elseif ($value instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\Promise && \YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($value)) { // We can just merge our handlers onto the next promise. $value->handlers = \array_merge($value->handlers, $handlers); } else { // Resolve the handlers when the forwarded promise is resolved. $value->then(static function ($value) use($handlers) : void { foreach ($handlers as $handler) { self::callHandler(1, $value, $handler); } }, static function ($reason) use($handlers) : void { foreach ($handlers as $handler) { self::callHandler(2, $reason, $handler); } }); } } /** * Call a stack of handlers using a specific callback index and value. * * @param int $index 1 (resolve) or 2 (reject). * @param mixed $value Value to pass to the callback. * @param array $handler Array of handler data (promise and callbacks). */ private static function callHandler(int $index, $value, array $handler) : void { /** @var PromiseInterface $promise */ $promise = $handler[0]; // The promise may have been cancelled or resolved before placing // this thunk in the queue. if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::settled($promise)) { return; } try { if (isset($handler[$index])) { /* * If $f throws an exception, then $handler will be in the exception * stack trace. Since $handler contains a reference to the callable * itself we get a circular reference. We clear the $handler * here to avoid that memory leak. */ $f = $handler[$index]; unset($handler); $promise->resolve($f($value)); } elseif ($index === 1) { // Forward resolution values as-is. $promise->resolve($value); } else { // Forward rejections down the chain. $promise->reject($value); } } catch (\Throwable $reason) { $promise->reject($reason); } } private function waitIfPending() : void { if ($this->state !== self::PENDING) { return; } elseif ($this->waitFn) { $this->invokeWaitFn(); } elseif ($this->waitList) { $this->invokeWaitList(); } else { // If there's no wait function, then reject the promise. $this->reject('Cannot wait on a promise that has ' . 'no internal wait function. You must provide a wait ' . 'function when constructing the promise to be able to ' . 'wait on a promise.'); } \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->run(); /** @psalm-suppress RedundantCondition */ if ($this->state === self::PENDING) { $this->reject('Invoking the wait callback did not resolve the promise'); } } private function invokeWaitFn() : void { try { $wfn = $this->waitFn; $this->waitFn = null; $wfn(\true); } catch (\Throwable $reason) { if ($this->state === self::PENDING) { // The promise has not been resolved yet, so reject the promise // with the exception. $this->reject($reason); } else { // The promise was already resolved, so there's a problem in // the application. throw $reason; } } } private function invokeWaitList() : void { $waitList = $this->waitList; $this->waitList = null; foreach ($waitList as $result) { do { $result->waitIfPending(); $result = $result->result; } while ($result instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\Promise); if ($result instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface) { $result->wait(\false); } } } } vendor_prefixed/guzzlehttp/promises/src/TaskQueue.php 0000644 00000004027 15174712003 0017222 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * A task queue that executes tasks in a FIFO order. * * This task queue class is used to settle promises asynchronously and * maintains a constant stack size. You can use the task queue asynchronously * by calling the `run()` function of the global task queue in an event loop. * * GuzzleHttp\Promise\Utils::queue()->run(); * * @final */ class TaskQueue implements \YoastSEO_Vendor\GuzzleHttp\Promise\TaskQueueInterface { private $enableShutdown = \true; private $queue = []; public function __construct(bool $withShutdown = \true) { if ($withShutdown) { \register_shutdown_function(function () : void { if ($this->enableShutdown) { // Only run the tasks if an E_ERROR didn't occur. $err = \error_get_last(); if (!$err || $err['type'] ^ \E_ERROR) { $this->run(); } } }); } } public function isEmpty() : bool { return !$this->queue; } public function add(callable $task) : void { $this->queue[] = $task; } public function run() : void { while ($task = \array_shift($this->queue)) { /** @var callable $task */ $task(); } } /** * The task queue will be run and exhausted by default when the process * exits IFF the exit is not the result of a PHP E_ERROR error. * * You can disable running the automatic shutdown of the queue by calling * this function. If you disable the task queue shutdown process, then you * MUST either run the task queue (as a result of running your event loop * or manually using the run() method) or wait on each outstanding promise. * * Note: This shutdown will occur before any destructors are triggered. */ public function disableShutdown() : void { $this->enableShutdown = \false; } } vendor_prefixed/guzzlehttp/promises/src/TaskQueueInterface.php 0000644 00000000723 15174712003 0021042 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; interface TaskQueueInterface { /** * Returns true if the queue is empty. */ public function isEmpty() : bool; /** * Adds a task to the queue that will be executed the next time run is * called. */ public function add(callable $task) : void; /** * Execute all of the pending task in the queue. */ public function run() : void; } vendor_prefixed/guzzlehttp/promises/src/Is.php 0000644 00000002336 15174712003 0015667 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; final class Is { /** * Returns true if a promise is pending. */ public static function pending(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise) : bool { return $promise->getState() === \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::PENDING; } /** * Returns true if a promise is fulfilled or rejected. */ public static function settled(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise) : bool { return $promise->getState() !== \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::PENDING; } /** * Returns true if a promise is fulfilled. */ public static function fulfilled(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise) : bool { return $promise->getState() === \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::FULFILLED; } /** * Returns true if a promise is rejected. */ public static function rejected(\YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $promise) : bool { return $promise->getState() === \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface::REJECTED; } } vendor_prefixed/guzzlehttp/promises/src/CancellationException.php 0000644 00000000405 15174712003 0021562 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * Exception that is set as the reason for a promise that has been cancelled. */ class CancellationException extends \YoastSEO_Vendor\GuzzleHttp\Promise\RejectionException { } vendor_prefixed/guzzlehttp/promises/src/FulfilledPromise.php 0000644 00000004263 15174712003 0020562 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * A promise that has been fulfilled. * * Thenning off of this promise will invoke the onFulfilled callback * immediately and ignore other callbacks. * * @final */ class FulfilledPromise implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { private $value; /** * @param mixed $value */ public function __construct($value) { if (\is_object($value) && \method_exists($value, 'then')) { throw new \InvalidArgumentException('You cannot create a FulfilledPromise with a promise.'); } $this->value = $value; } public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { // Return itself if there is no onFulfilled function. if (!$onFulfilled) { return $this; } $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue(); $p = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise([$queue, 'run']); $value = $this->value; $queue->add(static function () use($p, $value, $onFulfilled) : void { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($p)) { try { $p->resolve($onFulfilled($value)); } catch (\Throwable $e) { $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->then(null, $onRejected); } public function wait(bool $unwrap = \true) { return $unwrap ? $this->value : null; } public function getState() : string { return self::FULFILLED; } public function resolve($value) : void { if ($value !== $this->value) { throw new \LogicException('Cannot resolve a fulfilled promise'); } } public function reject($reason) : void { throw new \LogicException('Cannot reject a fulfilled promise'); } public function cancel() : void { // pass } } vendor_prefixed/guzzlehttp/promises/src/RejectedPromise.php 0000644 00000004665 15174712003 0020407 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * A promise that has been rejected. * * Thenning off of this promise will invoke the onRejected callback * immediately and ignore other callbacks. * * @final */ class RejectedPromise implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { private $reason; /** * @param mixed $reason */ public function __construct($reason) { if (\is_object($reason) && \method_exists($reason, 'then')) { throw new \InvalidArgumentException('You cannot create a RejectedPromise with a promise.'); } $this->reason = $reason; } public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { // If there's no onRejected callback then just return self. if (!$onRejected) { return $this; } $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue(); $reason = $this->reason; $p = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise([$queue, 'run']); $queue->add(static function () use($p, $reason, $onRejected) : void { if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($p)) { try { // Return a resolved promise if onRejected does not throw. $p->resolve($onRejected($reason)); } catch (\Throwable $e) { // onRejected threw, so return a rejected promise. $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->then(null, $onRejected); } public function wait(bool $unwrap = \true) { if ($unwrap) { throw \YoastSEO_Vendor\GuzzleHttp\Promise\Create::exceptionFor($this->reason); } return null; } public function getState() : string { return self::REJECTED; } public function resolve($value) : void { throw new \LogicException('Cannot resolve a rejected promise'); } public function reject($reason) : void { if ($reason !== $this->reason) { throw new \LogicException('Cannot reject a rejected promise'); } } public function cancel() : void { // pass } } vendor_prefixed/guzzlehttp/promises/src/AggregateException.php 0000644 00000000661 15174712003 0021060 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * Exception thrown when too many errors occur in the some() or any() methods. */ class AggregateException extends \YoastSEO_Vendor\GuzzleHttp\Promise\RejectionException { public function __construct(string $msg, array $reasons) { parent::__construct($reasons, \sprintf('%s; %d rejected promises', $msg, \count($reasons))); } } vendor_prefixed/guzzlehttp/promises/src/PromisorInterface.php 0000644 00000000456 15174712003 0020750 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * Interface used with classes that return a promise. */ interface PromisorInterface { /** * Returns a promise. */ public function promise() : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface; } vendor_prefixed/guzzlehttp/promises/src/EachPromise.php 0000644 00000016566 15174712003 0017525 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * Represents a promise that iterates over many promises and invokes * side-effect functions in the process. * * @final */ class EachPromise implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromisorInterface { private $pending = []; private $nextPendingIndex = 0; /** @var \Iterator|null */ private $iterable; /** @var callable|int|null */ private $concurrency; /** @var callable|null */ private $onFulfilled; /** @var callable|null */ private $onRejected; /** @var Promise|null */ private $aggregate; /** @var bool|null */ private $mutex; /** * Configuration hash can include the following key value pairs: * * - fulfilled: (callable) Invoked when a promise fulfills. The function * is invoked with three arguments: the fulfillment value, the index * position from the iterable list of the promise, and the aggregate * promise that manages all of the promises. The aggregate promise may * be resolved from within the callback to short-circuit the promise. * - rejected: (callable) Invoked when a promise is rejected. The * function is invoked with three arguments: the rejection reason, the * index position from the iterable list of the promise, and the * aggregate promise that manages all of the promises. The aggregate * promise may be resolved from within the callback to short-circuit * the promise. * - concurrency: (integer) Pass this configuration option to limit the * allowed number of outstanding concurrently executing promises, * creating a capped pool of promises. There is no limit by default. * * @param mixed $iterable Promises or values to iterate. * @param array $config Configuration options */ public function __construct($iterable, array $config = []) { $this->iterable = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::iterFor($iterable); if (isset($config['concurrency'])) { $this->concurrency = $config['concurrency']; } if (isset($config['fulfilled'])) { $this->onFulfilled = $config['fulfilled']; } if (isset($config['rejected'])) { $this->onRejected = $config['rejected']; } } /** @psalm-suppress InvalidNullableReturnType */ public function promise() : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if ($this->aggregate) { return $this->aggregate; } try { $this->createPromise(); /** @psalm-assert Promise $this->aggregate */ $this->iterable->rewind(); $this->refillPending(); } catch (\Throwable $e) { $this->aggregate->reject($e); } /** * @psalm-suppress NullableReturnStatement */ return $this->aggregate; } private function createPromise() : void { $this->mutex = \false; $this->aggregate = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise(function () : void { if ($this->checkIfFinished()) { return; } \reset($this->pending); // Consume a potentially fluctuating list of promises while // ensuring that indexes are maintained (precluding array_shift). while ($promise = \current($this->pending)) { \next($this->pending); $promise->wait(); if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::settled($this->aggregate)) { return; } } }); // Clear the references when the promise is resolved. $clearFn = function () : void { $this->iterable = $this->concurrency = $this->pending = null; $this->onFulfilled = $this->onRejected = null; $this->nextPendingIndex = 0; }; $this->aggregate->then($clearFn, $clearFn); } private function refillPending() : void { if (!$this->concurrency) { // Add all pending promises. while ($this->addPending() && $this->advanceIterator()) { } return; } // Add only up to N pending promises. $concurrency = \is_callable($this->concurrency) ? ($this->concurrency)(\count($this->pending)) : $this->concurrency; $concurrency = \max($concurrency - \count($this->pending), 0); // Concurrency may be set to 0 to disallow new promises. if (!$concurrency) { return; } // Add the first pending promise. $this->addPending(); // Note this is special handling for concurrency=1 so that we do // not advance the iterator after adding the first promise. This // helps work around issues with generators that might not have the // next value to yield until promise callbacks are called. while (--$concurrency && $this->advanceIterator() && $this->addPending()) { } } private function addPending() : bool { if (!$this->iterable || !$this->iterable->valid()) { return \false; } $promise = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($this->iterable->current()); $key = $this->iterable->key(); // Iterable keys may not be unique, so we use a counter to // guarantee uniqueness $idx = $this->nextPendingIndex++; $this->pending[$idx] = $promise->then(function ($value) use($idx, $key) : void { if ($this->onFulfilled) { ($this->onFulfilled)($value, $key, $this->aggregate); } $this->step($idx); }, function ($reason) use($idx, $key) : void { if ($this->onRejected) { ($this->onRejected)($reason, $key, $this->aggregate); } $this->step($idx); }); return \true; } private function advanceIterator() : bool { // Place a lock on the iterator so that we ensure to not recurse, // preventing fatal generator errors. if ($this->mutex) { return \false; } $this->mutex = \true; try { $this->iterable->next(); $this->mutex = \false; return \true; } catch (\Throwable $e) { $this->aggregate->reject($e); $this->mutex = \false; return \false; } } private function step(int $idx) : void { // If the promise was already resolved, then ignore this step. if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::settled($this->aggregate)) { return; } unset($this->pending[$idx]); // Only refill pending promises if we are not locked, preventing the // EachPromise to recursively invoke the provided iterator, which // cause a fatal error: "Cannot resume an already running generator" if ($this->advanceIterator() && !$this->checkIfFinished()) { // Add more pending promises if possible. $this->refillPending(); } } private function checkIfFinished() : bool { if (!$this->pending && !$this->iterable->valid()) { // Resolve the promise if there's nothing left to do. $this->aggregate->resolve(null); return \true; } return \false; } } vendor_prefixed/guzzlehttp/promises/src/RejectionException.php 0000644 00000002330 15174712003 0021107 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; /** * A special exception that is thrown when waiting on a rejected promise. * * The reason value is available via the getReason() method. */ class RejectionException extends \RuntimeException { /** @var mixed Rejection reason. */ private $reason; /** * @param mixed $reason Rejection reason. * @param string|null $description Optional description. */ public function __construct($reason, ?string $description = null) { $this->reason = $reason; $message = 'The promise was rejected'; if ($description) { $message .= ' with reason: ' . $description; } elseif (\is_string($reason) || \is_object($reason) && \method_exists($reason, '__toString')) { $message .= ' with reason: ' . $this->reason; } elseif ($reason instanceof \JsonSerializable) { $message .= ' with reason: ' . \json_encode($this->reason, \JSON_PRETTY_PRINT); } parent::__construct($message); } /** * Returns the rejection reason. * * @return mixed */ public function getReason() { return $this->reason; } } vendor_prefixed/guzzlehttp/promises/src/Each.php 0000644 00000005150 15174712003 0016151 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; final class Each { /** * Given an iterator that yields promises or values, returns a promise that * is fulfilled with a null value when the iterator has been consumed or * the aggregate promise has been fulfilled or rejected. * * $onFulfilled is a function that accepts the fulfilled value, iterator * index, and the aggregate promise. The callback can invoke any necessary * side effects and choose to resolve or reject the aggregate if needed. * * $onRejected is a function that accepts the rejection reason, iterator * index, and the aggregate promise. The callback can invoke any necessary * side effects and choose to resolve or reject the aggregate if needed. * * @param mixed $iterable Iterator or array to iterate over. */ public static function of($iterable, ?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return (new \YoastSEO_Vendor\GuzzleHttp\Promise\EachPromise($iterable, ['fulfilled' => $onFulfilled, 'rejected' => $onRejected]))->promise(); } /** * Like of, but only allows a certain number of outstanding promises at any * given time. * * $concurrency may be an integer or a function that accepts the number of * pending promises and returns a numeric concurrency limit value to allow * for dynamic a concurrency size. * * @param mixed $iterable * @param int|callable $concurrency */ public static function ofLimit($iterable, $concurrency, ?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return (new \YoastSEO_Vendor\GuzzleHttp\Promise\EachPromise($iterable, ['fulfilled' => $onFulfilled, 'rejected' => $onRejected, 'concurrency' => $concurrency]))->promise(); } /** * Like limit, but ensures that no promise in the given $iterable argument * is rejected. If any promise is rejected, then the aggregate promise is * rejected with the encountered rejection. * * @param mixed $iterable * @param int|callable $concurrency */ public static function ofLimitAll($iterable, $concurrency, ?callable $onFulfilled = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return self::ofLimit($iterable, $concurrency, $onFulfilled, function ($reason, $idx, \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface $aggregate) : void { $aggregate->reject($reason); }); } } vendor_prefixed/guzzlehttp/promises/src/Coroutine.php 0000644 00000010414 15174712003 0017257 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; use Generator; use Throwable; /** * Creates a promise that is resolved using a generator that yields values or * promises (somewhat similar to C#'s async keyword). * * When called, the Coroutine::of method will start an instance of the generator * and returns a promise that is fulfilled with its final yielded value. * * Control is returned back to the generator when the yielded promise settles. * This can lead to less verbose code when doing lots of sequential async calls * with minimal processing in between. * * use GuzzleHttp\Promise; * * function createPromise($value) { * return new Promise\FulfilledPromise($value); * } * * $promise = Promise\Coroutine::of(function () { * $value = (yield createPromise('a')); * try { * $value = (yield createPromise($value . 'b')); * } catch (\Throwable $e) { * // The promise was rejected. * } * yield $value . 'c'; * }); * * // Outputs "abc" * $promise->then(function ($v) { echo $v; }); * * @param callable $generatorFn Generator function to wrap into a promise. * * @return Promise * * @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration */ final class Coroutine implements \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { /** * @var PromiseInterface|null */ private $currentPromise; /** * @var Generator */ private $generator; /** * @var Promise */ private $result; public function __construct(callable $generatorFn) { $this->generator = $generatorFn(); $this->result = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise(function () : void { while (isset($this->currentPromise)) { $this->currentPromise->wait(); } }); try { $this->nextCoroutine($this->generator->current()); } catch (\Throwable $throwable) { $this->result->reject($throwable); } } /** * Create a new coroutine. */ public static function of(callable $generatorFn) : self { return new self($generatorFn); } public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->result->then($onFulfilled, $onRejected); } public function otherwise(callable $onRejected) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { return $this->result->otherwise($onRejected); } public function wait(bool $unwrap = \true) { return $this->result->wait($unwrap); } public function getState() : string { return $this->result->getState(); } public function resolve($value) : void { $this->result->resolve($value); } public function reject($reason) : void { $this->result->reject($reason); } public function cancel() : void { $this->currentPromise->cancel(); $this->result->cancel(); } private function nextCoroutine($yielded) : void { $this->currentPromise = \YoastSEO_Vendor\GuzzleHttp\Promise\Create::promiseFor($yielded)->then([$this, '_handleSuccess'], [$this, '_handleFailure']); } /** * @internal */ public function _handleSuccess($value) : void { unset($this->currentPromise); try { $next = $this->generator->send($value); if ($this->generator->valid()) { $this->nextCoroutine($next); } else { $this->result->resolve($value); } } catch (\Throwable $throwable) { $this->result->reject($throwable); } } /** * @internal */ public function _handleFailure($reason) : void { unset($this->currentPromise); try { $nextYield = $this->generator->throw(\YoastSEO_Vendor\GuzzleHttp\Promise\Create::exceptionFor($reason)); // The throw was caught, so keep iterating on the coroutine $this->nextCoroutine($nextYield); } catch (\Throwable $throwable) { $this->result->reject($throwable); } } } vendor_prefixed/guzzlehttp/promises/src/Create.php 0000644 00000004412 15174712003 0016514 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Promise; final class Create { /** * Creates a promise for a value if the value is not a promise. * * @param mixed $value Promise or value. */ public static function promiseFor($value) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if ($value instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface) { return $value; } // Return a Guzzle promise that shadows the given promise. if (\is_object($value) && \method_exists($value, 'then')) { $wfn = \method_exists($value, 'wait') ? [$value, 'wait'] : null; $cfn = \method_exists($value, 'cancel') ? [$value, 'cancel'] : null; $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise($wfn, $cfn); $value->then([$promise, 'resolve'], [$promise, 'reject']); return $promise; } return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($value); } /** * Creates a rejected promise for a reason if the reason is not a promise. * If the provided reason is a promise, then it is returned as-is. * * @param mixed $reason Promise or reason. */ public static function rejectionFor($reason) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface { if ($reason instanceof \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface) { return $reason; } return new \YoastSEO_Vendor\GuzzleHttp\Promise\RejectedPromise($reason); } /** * Create an exception for a rejected promise value. * * @param mixed $reason */ public static function exceptionFor($reason) : \Throwable { if ($reason instanceof \Throwable) { return $reason; } return new \YoastSEO_Vendor\GuzzleHttp\Promise\RejectionException($reason); } /** * Returns an iterator for the given value. * * @param mixed $value */ public static function iterFor($value) : \Iterator { if ($value instanceof \Iterator) { return $value; } if (\is_array($value)) { return new \ArrayIterator($value); } return new \ArrayIterator([$value]); } } vendor_prefixed/guzzlehttp/psr7/src/MultipartStream.php 0000644 00000012372 15174712003 0017504 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Stream that when read returns bytes for a streaming multipart or * multipart/form-data stream. */ final class MultipartStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var string */ private $boundary; /** @var StreamInterface */ private $stream; /** * @param array $elements Array of associative arrays, each containing a * required "name" key mapping to the form field, * name, a required "contents" key mapping to a * StreamInterface/resource/string, an optional * "headers" associative array of custom headers, * and an optional "filename" key mapping to a * string to send as the filename in the part. * @param string $boundary You can optionally provide a specific boundary * * @throws \InvalidArgumentException */ public function __construct(array $elements = [], ?string $boundary = null) { $this->boundary = $boundary ?: \bin2hex(\random_bytes(20)); $this->stream = $this->createStream($elements); } public function getBoundary() : string { return $this->boundary; } public function isWritable() : bool { return \false; } /** * Get the headers needed before transferring the content of a POST file * * @param string[] $headers */ private function getHeaders(array $headers) : string { $str = ''; foreach ($headers as $key => $value) { $str .= "{$key}: {$value}\r\n"; } return "--{$this->boundary}\r\n" . \trim($str) . "\r\n\r\n"; } /** * Create the aggregate stream that will be used to upload the POST data */ protected function createStream(array $elements = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\AppendStream(); foreach ($elements as $element) { if (!\is_array($element)) { throw new \UnexpectedValueException('An array is expected'); } $this->addElement($stream, $element); } // Add the trailing boundary with CRLF $stream->addStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor("--{$this->boundary}--\r\n")); return $stream; } private function addElement(\YoastSEO_Vendor\GuzzleHttp\Psr7\AppendStream $stream, array $element) : void { foreach (['contents', 'name'] as $key) { if (!\array_key_exists($key, $element)) { throw new \InvalidArgumentException("A '{$key}' key is required"); } } $element['contents'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($element['contents']); if (empty($element['filename'])) { $uri = $element['contents']->getMetadata('uri'); if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { $element['filename'] = $uri; } } [$body, $headers] = $this->createElement($element['name'], $element['contents'], $element['filename'] ?? null, $element['headers'] ?? []); $stream->addStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($this->getHeaders($headers))); $stream->addStream($body); $stream->addStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor("\r\n")); } /** * @param string[] $headers * * @return array{0: StreamInterface, 1: string[]} */ private function createElement(string $name, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?string $filename, array $headers) : array { // Set a default content-disposition header if one was no provided $disposition = self::getHeader($headers, 'content-disposition'); if (!$disposition) { $headers['Content-Disposition'] = $filename === '0' || $filename ? \sprintf('form-data; name="%s"; filename="%s"', $name, \basename($filename)) : "form-data; name=\"{$name}\""; } // Set a default content-length header if one was no provided $length = self::getHeader($headers, 'content-length'); if (!$length) { if ($length = $stream->getSize()) { $headers['Content-Length'] = (string) $length; } } // Set a default Content-Type if one was not supplied $type = self::getHeader($headers, 'content-type'); if (!$type && ($filename === '0' || $filename)) { $headers['Content-Type'] = \YoastSEO_Vendor\GuzzleHttp\Psr7\MimeType::fromFilename($filename) ?? 'application/octet-stream'; } return [$stream, $headers]; } /** * @param string[] $headers */ private static function getHeader(array $headers, string $key) : ?string { $lowercaseHeader = \strtolower($key); foreach ($headers as $k => $v) { if (\strtolower((string) $k) === $lowercaseHeader) { return $v; } } return null; } } vendor_prefixed/guzzlehttp/psr7/src/HttpFactory.php 0000644 00000007717 15174712003 0016625 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\RequestFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\ServerRequestFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UploadedFileFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface; use YoastSEO_Vendor\Psr\Http\Message\UriFactoryInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Implements all of the PSR-17 interfaces. * * Note: in consuming code it is recommended to require the implemented interfaces * and inject the instance of this class multiple times. */ final class HttpFactory implements \YoastSEO_Vendor\Psr\Http\Message\RequestFactoryInterface, \YoastSEO_Vendor\Psr\Http\Message\ResponseFactoryInterface, \YoastSEO_Vendor\Psr\Http\Message\ServerRequestFactoryInterface, \YoastSEO_Vendor\Psr\Http\Message\StreamFactoryInterface, \YoastSEO_Vendor\Psr\Http\Message\UploadedFileFactoryInterface, \YoastSEO_Vendor\Psr\Http\Message\UriFactoryInterface { public function createUploadedFile(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null) : \YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface { if ($size === null) { $size = $stream->getSize(); } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); } public function createStream(string $content = '') : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($content); } public function createStreamFromFile(string $file, string $mode = 'r') : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { try { $resource = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen($file, $mode); } catch (\RuntimeException $e) { if ('' === $mode || \false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], \true)) { throw new \InvalidArgumentException(\sprintf('Invalid file opening mode "%s"', $mode), 0, $e); } throw $e; } return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($resource); } public function createStreamFromResource($resource) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($resource); } public function createServerRequest(string $method, $uri, array $serverParams = []) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { if (empty($method)) { if (!empty($serverParams['REQUEST_METHOD'])) { $method = $serverParams['REQUEST_METHOD']; } else { throw new \InvalidArgumentException('Cannot determine HTTP method'); } } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\ServerRequest($method, $uri, [], null, '1.1', $serverParams); } public function createResponse(int $code = 200, string $reasonPhrase = '') : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($code, [], null, '1.1', $reasonPhrase); } public function createRequest(string $method, $uri) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($method, $uri); } public function createUri(string $uri = '') : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($uri); } } vendor_prefixed/guzzlehttp/psr7/src/InflateStream.php 0000644 00000003156 15174712003 0017105 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Uses PHP's zlib.inflate filter to inflate zlib (HTTP deflate, RFC1950) or gzipped (RFC1952) content. * * This stream decorator converts the provided stream to a PHP stream resource, * then appends the zlib.inflate filter. The stream is then converted back * to a Guzzle stream resource to be used as a Guzzle stream. * * @see https://datatracker.ietf.org/doc/html/rfc1950 * @see https://datatracker.ietf.org/doc/html/rfc1952 * @see https://www.php.net/manual/en/filters.compression.php */ final class InflateStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var StreamInterface */ private $stream; public function __construct(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream) { $resource = \YoastSEO_Vendor\GuzzleHttp\Psr7\StreamWrapper::getResource($stream); // Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data // See https://www.zlib.net/manual.html#Advanced definition of inflateInit2 // "Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" // Default window size is 15. \stream_filter_append($resource, 'zlib.inflate', \STREAM_FILTER_READ, ['window' => 15 + 32]); $this->stream = $stream->isSeekable() ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource) : new \YoastSEO_Vendor\GuzzleHttp\Psr7\NoSeekStream(new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource)); } } vendor_prefixed/guzzlehttp/psr7/src/Message.php 0000644 00000020640 15174712003 0015730 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\MessageInterface; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; final class Message { /** * Returns the string representation of an HTTP message. * * @param MessageInterface $message Message to convert to a string. */ public static function toString(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : string { if ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\RequestInterface) { $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget()) . ' HTTP/' . $message->getProtocolVersion(); if (!$message->hasHeader('host')) { $msg .= "\r\nHost: " . $message->getUri()->getHost(); } } elseif ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface) { $msg = 'HTTP/' . $message->getProtocolVersion() . ' ' . $message->getStatusCode() . ' ' . $message->getReasonPhrase(); } else { throw new \InvalidArgumentException('Unknown message type'); } foreach ($message->getHeaders() as $name => $values) { if (\is_string($name) && \strtolower($name) === 'set-cookie') { foreach ($values as $value) { $msg .= "\r\n{$name}: " . $value; } } else { $msg .= "\r\n{$name}: " . \implode(', ', $values); } } return "{$msg}\r\n\r\n" . $message->getBody(); } /** * Get a short summary of the message body. * * Will return `null` if the response is not printable. * * @param MessageInterface $message The message to get the body summary * @param int $truncateAt The maximum allowed size of the summary */ public static function bodySummary(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message, int $truncateAt = 120) : ?string { $body = $message->getBody(); if (!$body->isSeekable() || !$body->isReadable()) { return null; } $size = $body->getSize(); if ($size === 0) { return null; } $body->rewind(); $summary = $body->read($truncateAt); $body->rewind(); if ($size > $truncateAt) { $summary .= ' (truncated...)'; } // Matches any printable character, including unicode characters: // letters, marks, numbers, punctuation, spacing, and separators. if (\preg_match('/[^\\pL\\pM\\pN\\pP\\pS\\pZ\\n\\r\\t]/u', $summary) !== 0) { return null; } return $summary; } /** * Attempts to rewind a message body and throws an exception on failure. * * The body of the message will only be rewound if a call to `tell()` * returns a value other than `0`. * * @param MessageInterface $message Message to rewind * * @throws \RuntimeException */ public static function rewindBody(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : void { $body = $message->getBody(); if ($body->tell()) { $body->rewind(); } } /** * Parses an HTTP message into an associative array. * * The array contains the "start-line" key containing the start line of * the message, "headers" key containing an associative array of header * array values, and a "body" key containing the body of the message. * * @param string $message HTTP request or response to parse. */ public static function parseMessage(string $message) : array { if (!$message) { throw new \InvalidArgumentException('Invalid message'); } $message = \ltrim($message, "\r\n"); $messageParts = \preg_split("/\r?\n\r?\n/", $message, 2); if ($messageParts === \false || \count($messageParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); } [$rawHeaders, $body] = $messageParts; $rawHeaders .= "\r\n"; // Put back the delimiter we split previously $headerParts = \preg_split("/\r?\n/", $rawHeaders, 2); if ($headerParts === \false || \count($headerParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing status line'); } [$startLine, $rawHeaders] = $headerParts; if (\preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 $rawHeaders = \preg_replace(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); } /** @var array[] $headerLines */ $count = \preg_match_all(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, \PREG_SET_ORDER); // If these aren't the same, then one line didn't match and there's an invalid header. if ($count !== \substr_count($rawHeaders, "\n")) { // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 if (\preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); } throw new \InvalidArgumentException('Invalid header syntax'); } $headers = []; foreach ($headerLines as $headerLine) { $headers[$headerLine[1]][] = $headerLine[2]; } return ['start-line' => $startLine, 'headers' => $headers, 'body' => $body]; } /** * Constructs a URI for an HTTP request message. * * @param string $path Path from the start-line * @param array $headers Array of headers (each value an array). */ public static function parseRequestUri(string $path, array $headers) : string { $hostKey = \array_filter(\array_keys($headers), function ($k) { // Numeric array keys are converted to int by PHP. $k = (string) $k; return \strtolower($k) === 'host'; }); // If no host is found, then a full URI cannot be constructed. if (!$hostKey) { return $path; } $host = $headers[\reset($hostKey)][0]; $scheme = \substr($host, -4) === ':443' ? 'https' : 'http'; return $scheme . '://' . $host . '/' . \ltrim($path, '/'); } /** * Parses a request message string into a request object. * * @param string $message Request message string. */ public static function parseRequest(string $message) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { $data = self::parseMessage($message); $matches = []; if (!\preg_match('/^[\\S]+\\s+([a-zA-Z]+:\\/\\/|\\/).*/', $data['start-line'], $matches)) { throw new \InvalidArgumentException('Invalid request string'); } $parts = \explode(' ', $data['start-line'], 3); $version = isset($parts[2]) ? \explode('/', $parts[2])[1] : '1.1'; $request = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($parts[0], $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], $data['headers'], $data['body'], $version); return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); } /** * Parses a response message string into a response object. * * @param string $message Response message string. */ public static function parseResponse(string $message) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { $data = self::parseMessage($message); // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 // the space between status-code and reason-phrase is required. But // browsers accept responses without space and reason as well. if (!\preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); } $parts = \explode(' ', $data['start-line'], 3); return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response((int) $parts[1], $data['headers'], $data['body'], \explode('/', $parts[0])[1], $parts[2] ?? null); } } vendor_prefixed/guzzlehttp/psr7/src/BufferStream.php 0000644 00000006321 15174712003 0016731 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Provides a buffer stream that can be written to to fill a buffer, and read * from to remove bytes from the buffer. * * This stream returns a "hwm" metadata value that tells upstream consumers * what the configured high water mark of the stream is, or the maximum * preferred size of the buffer. */ final class BufferStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { /** @var int */ private $hwm; /** @var string */ private $buffer = ''; /** * @param int $hwm High water mark, representing the preferred maximum * buffer size. If the size of the buffer exceeds the high * water mark, then calls to write will continue to succeed * but will return 0 to inform writers to slow down * until the buffer has been drained by reading from it. */ public function __construct(int $hwm = 16384) { $this->hwm = $hwm; } public function __toString() : string { return $this->getContents(); } public function getContents() : string { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } public function close() : void { $this->buffer = ''; } public function detach() { $this->close(); return null; } public function getSize() : ?int { return \strlen($this->buffer); } public function isReadable() : bool { return \true; } public function isWritable() : bool { return \true; } public function isSeekable() : bool { return \false; } public function rewind() : void { $this->seek(0); } public function seek($offset, $whence = \SEEK_SET) : void { throw new \RuntimeException('Cannot seek a BufferStream'); } public function eof() : bool { return \strlen($this->buffer) === 0; } public function tell() : int { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } /** * Reads data from the buffer. */ public function read($length) : string { $currentLength = \strlen($this->buffer); if ($length >= $currentLength) { // No need to slice the buffer because we don't have enough data. $result = $this->buffer; $this->buffer = ''; } else { // Slice up the result to provide a subset of the buffer. $result = \substr($this->buffer, 0, $length); $this->buffer = \substr($this->buffer, $length); } return $result; } /** * Writes data to the buffer. */ public function write($string) : int { $this->buffer .= $string; if (\strlen($this->buffer) >= $this->hwm) { return 0; } return \strlen($string); } /** * @return mixed */ public function getMetadata($key = null) { if ($key === 'hwm') { return $this->hwm; } return $key ? null : []; } } vendor_prefixed/guzzlehttp/psr7/src/Utils.php 0000644 00000037623 15174712003 0015455 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; final class Utils { /** * Remove the items given by the keys, case insensitively from the data. * * @param (string|int)[] $keys */ public static function caselessRemove(array $keys, array $data) : array { $result = []; foreach ($keys as &$key) { $key = \strtolower((string) $key); } foreach ($data as $k => $v) { if (!\in_array(\strtolower((string) $k), $keys)) { $result[$k] = $v; } } return $result; } /** * Copy the contents of a stream into another stream until the given number * of bytes have been read. * * @param StreamInterface $source Stream to read from * @param StreamInterface $dest Stream to write to * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @throws \RuntimeException on error. */ public static function copyToStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, int $maxLen = -1) : void { $bufferSize = 8192; if ($maxLen === -1) { while (!$source->eof()) { if (!$dest->write($source->read($bufferSize))) { break; } } } else { $remaining = $maxLen; while ($remaining > 0 && !$source->eof()) { $buf = $source->read(\min($bufferSize, $remaining)); $len = \strlen($buf); if (!$len) { break; } $remaining -= $len; $dest->write($buf); } } } /** * Copy the contents of a stream into a string until the given number of * bytes have been read. * * @param StreamInterface $stream Stream to read * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @throws \RuntimeException on error. */ public static function copyToString(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $maxLen = -1) : string { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); if ($buf === '') { break; } $buffer .= $buf; } return $buffer; } $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); if ($buf === '') { break; } $buffer .= $buf; $len = \strlen($buffer); } return $buffer; } /** * Calculate a hash of a stream. * * This method reads the entire stream to calculate a rolling hash, based * on PHP's `hash_init` functions. * * @param StreamInterface $stream Stream to calculate the hash for * @param string $algo Hash algorithm (e.g. md5, crc32, etc) * @param bool $rawOutput Whether or not to use raw output * * @throws \RuntimeException on error. */ public static function hash(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, string $algo, bool $rawOutput = \false) : string { $pos = $stream->tell(); if ($pos > 0) { $stream->rewind(); } $ctx = \hash_init($algo); while (!$stream->eof()) { \hash_update($ctx, $stream->read(1048576)); } $out = \hash_final($ctx, $rawOutput); $stream->seek($pos); return $out; } /** * Clone and modify a request with the given changes. * * This method is useful for reducing the number of clones needed to mutate * a message. * * The changes can be one of: * - method: (string) Changes the HTTP method. * - set_headers: (array) Sets the given headers. * - remove_headers: (array) Remove the given headers. * - body: (mixed) Sets the given body. * - uri: (UriInterface) Set the URI. * - query: (string) Set the query string value of the URI. * - version: (string) Set the protocol version. * * @param RequestInterface $request Request to clone and modify. * @param array $changes Changes to apply. */ public static function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $changes) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { if (!$changes) { return $request; } $headers = $request->getHeaders(); if (!isset($changes['uri'])) { $uri = $request->getUri(); } else { // Remove the host header if one is on the URI if ($host = $changes['uri']->getHost()) { $changes['set_headers']['Host'] = $host; if ($port = $changes['uri']->getPort()) { $standardPorts = ['http' => 80, 'https' => 443]; $scheme = $changes['uri']->getScheme(); if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { $changes['set_headers']['Host'] .= ':' . $port; } } } $uri = $changes['uri']; } if (!empty($changes['remove_headers'])) { $headers = self::caselessRemove($changes['remove_headers'], $headers); } if (!empty($changes['set_headers'])) { $headers = self::caselessRemove(\array_keys($changes['set_headers']), $headers); $headers = $changes['set_headers'] + $headers; } if (isset($changes['query'])) { $uri = $uri->withQuery($changes['query']); } if ($request instanceof \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface) { $new = (new \YoastSEO_Vendor\GuzzleHttp\Psr7\ServerRequest($changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion(), $request->getServerParams()))->withParsedBody($request->getParsedBody())->withQueryParams($request->getQueryParams())->withCookieParams($request->getCookieParams())->withUploadedFiles($request->getUploadedFiles()); foreach ($request->getAttributes() as $key => $value) { $new = $new->withAttribute($key, $value); } return $new; } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion()); } /** * Read a line from the stream up to the maximum allowed buffer length. * * @param StreamInterface $stream Stream to read from * @param int|null $maxLength Maximum buffer length */ public static function readLine(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $maxLength = null) : string { $buffer = ''; $size = 0; while (!$stream->eof()) { if ('' === ($byte = $stream->read(1))) { return $buffer; } $buffer .= $byte; // Break when a new line is found or the max length - 1 is reached if ($byte === "\n" || ++$size === $maxLength - 1) { break; } } return $buffer; } /** * Redact the password in the user info part of a URI. */ public static function redactUserInfo(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $userInfo = $uri->getUserInfo(); if (\false !== ($pos = \strpos($userInfo, ':'))) { return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***'); } return $uri; } /** * Create a new stream based on the input type. * * Options is an associative array that can contain the following keys: * - metadata: Array of custom metadata. * - size: Size of the stream. * * This method accepts the following `$resource` types: * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. * - `string`: Creates a stream object that uses the given string as the contents. * - `resource`: Creates a stream object that wraps the given PHP stream resource. * - `Iterator`: If the provided value implements `Iterator`, then a read-only * stream object will be created that wraps the given iterable. Each time the * stream is read from, data from the iterator will fill a buffer and will be * continuously called until the buffer is equal to the requested read size. * Subsequent read calls will first read from the buffer and then call `next` * on the underlying iterator until it is exhausted. * - `object` with `__toString()`: If the object has the `__toString()` method, * the object will be cast to a string and then a stream will be returned that * uses the string value. * - `NULL`: When `null` is passed, an empty stream object is returned. * - `callable` When a callable is passed, a read-only stream object will be * created that invokes the given callable. The callable is invoked with the * number of suggested bytes to read. The callable can return any number of * bytes, but MUST return `false` when there is no more data to return. The * stream object that wraps the callable will invoke the callable until the * number of requested bytes are available. Any additional bytes will be * buffered and used in subsequent reads. * * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data * @param array{size?: int, metadata?: array} $options Additional options * * @throws \InvalidArgumentException if the $resource arg is not valid. */ public static function streamFor($resource = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { if (\is_scalar($resource)) { $stream = self::tryFopen('php://temp', 'r+'); if ($resource !== '') { \fwrite($stream, (string) $resource); \fseek($stream, 0); } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($stream, $options); } switch (\gettype($resource)) { case 'resource': /* * The 'php://input' is a special stream with quirks and inconsistencies. * We avoid using that stream by reading it into php://temp */ /** @var resource $resource */ if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { $stream = self::tryFopen('php://temp', 'w+'); \stream_copy_to_stream($resource, $stream); \fseek($stream, 0); $resource = $stream; } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource, $options); case 'object': /** @var object $resource */ if ($resource instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream(function () use($resource) { if (!$resource->valid()) { return \false; } $result = $resource->current(); $resource->next(); return $result; }, $options); } elseif (\method_exists($resource, '__toString')) { return self::streamFor((string) $resource, $options); } break; case 'NULL': return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream(self::tryFopen('php://temp', 'r+'), $options); } if (\is_callable($resource)) { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream($resource, $options); } throw new \InvalidArgumentException('Invalid resource type: ' . \gettype($resource)); } /** * Safely opens a PHP stream resource using a filename. * * When fopen fails, PHP normally raises a warning. This function adds an * error handler that checks for errors and throws an exception instead. * * @param string $filename File to open * @param string $mode Mode used to open the file * * @return resource * * @throws \RuntimeException if the file cannot be opened */ public static function tryFopen(string $filename, string $mode) { $ex = null; \set_error_handler(static function (int $errno, string $errstr) use($filename, $mode, &$ex) : bool { $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $errstr)); return \true; }); try { /** @var resource $handle */ $handle = \fopen($filename, $mode); } catch (\Throwable $e) { $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage()), 0, $e); } \restore_error_handler(); if ($ex) { /** @var \RuntimeException $ex */ throw $ex; } return $handle; } /** * Safely gets the contents of a given stream. * * When stream_get_contents fails, PHP normally raises a warning. This * function adds an error handler that checks for errors and throws an * exception instead. * * @param resource $stream * * @throws \RuntimeException if the stream cannot be read */ public static function tryGetContents($stream) : string { $ex = null; \set_error_handler(static function (int $errno, string $errstr) use(&$ex) : bool { $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $errstr)); return \true; }); try { /** @var string|false $contents */ $contents = \stream_get_contents($stream); if ($contents === \false) { $ex = new \RuntimeException('Unable to read stream contents'); } } catch (\Throwable $e) { $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e); } \restore_error_handler(); if ($ex) { /** @var \RuntimeException $ex */ throw $ex; } return $contents; } /** * Returns a UriInterface for the given value. * * This function accepts a string or UriInterface and returns a * UriInterface for the given value. If the value is already a * UriInterface, it is returned as-is. * * @param string|UriInterface $uri * * @throws \InvalidArgumentException */ public static function uriFor($uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ($uri instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) { return $uri; } if (\is_string($uri)) { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); } } vendor_prefixed/guzzlehttp/psr7/src/Exception/MalformedUriException.php 0000644 00000000405 15174712003 0022544 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7\Exception; use InvalidArgumentException; /** * Exception thrown if a URI cannot be parsed because it's malformed. */ class MalformedUriException extends \InvalidArgumentException { } vendor_prefixed/guzzlehttp/psr7/src/DroppingStream.php 0000644 00000002413 15174712003 0017300 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Stream decorator that begins dropping data once the size of the underlying * stream becomes too full. */ final class DroppingStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var int */ private $maxLength; /** @var StreamInterface */ private $stream; /** * @param StreamInterface $stream Underlying stream to decorate. * @param int $maxLength Maximum size before dropping data. */ public function __construct(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $maxLength) { $this->stream = $stream; $this->maxLength = $maxLength; } public function write($string) : int { $diff = $this->maxLength - $this->stream->getSize(); // Begin returning 0 when the underlying stream is too large. if ($diff <= 0) { return 0; } // Write the stream or a subset of the stream if needed. if (\strlen($string) < $diff) { return $this->stream->write($string); } return $this->stream->write(\substr($string, 0, $diff)); } } vendor_prefixed/guzzlehttp/psr7/src/UriResolver.php 0000644 00000021311 15174712003 0016621 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Resolves a URI reference in the context of a base URI and the opposite way. * * @author Tobias Schultze * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5 */ final class UriResolver { /** * Removes dot segments from a path and returns the new path. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 */ public static function removeDotSegments(string $path) : string { if ($path === '' || $path === '/') { return $path; } $results = []; $segments = \explode('/', $path); foreach ($segments as $segment) { if ($segment === '..') { \array_pop($results); } elseif ($segment !== '.') { $results[] = $segment; } } $newPath = \implode('/', $results); if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) { // Re-add the leading slash if necessary for cases like "/.." $newPath = '/' . $newPath; } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) { // Add the trailing slash if necessary // If newPath is not empty, then $segment must be set and is the last segment from the foreach $newPath .= '/'; } return $newPath; } /** * Converts the relative URI into a new URI that is resolved against the base URI. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2 */ public static function resolve(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $base, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $rel) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ((string) $rel === '') { // we can simply return the same base URI instance for this same-document reference return $base; } if ($rel->getScheme() != '') { return $rel->withPath(self::removeDotSegments($rel->getPath())); } if ($rel->getAuthority() != '') { $targetAuthority = $rel->getAuthority(); $targetPath = self::removeDotSegments($rel->getPath()); $targetQuery = $rel->getQuery(); } else { $targetAuthority = $base->getAuthority(); if ($rel->getPath() === '') { $targetPath = $base->getPath(); $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); } else { if ($rel->getPath()[0] === '/') { $targetPath = $rel->getPath(); } else { if ($targetAuthority != '' && $base->getPath() === '') { $targetPath = '/' . $rel->getPath(); } else { $lastSlashPos = \strrpos($base->getPath(), '/'); if ($lastSlashPos === \false) { $targetPath = $rel->getPath(); } else { $targetPath = \substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); } } } $targetPath = self::removeDotSegments($targetPath); $targetQuery = $rel->getQuery(); } } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri(\YoastSEO_Vendor\GuzzleHttp\Psr7\Uri::composeComponents($base->getScheme(), $targetAuthority, $targetPath, $targetQuery, $rel->getFragment())); } /** * Returns the target URI as a relative reference from the base URI. * * This method is the counterpart to resolve(): * * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) * * One use-case is to use the current request URI as base URI and then generate relative links in your documents * to reduce the document size or offer self-contained downloadable document archives. * * $base = new Uri('http://example.com/a/b/'); * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. * * This method also accepts a target that is already relative and will try to relativize it further. Only a * relative-path reference will be returned as-is. * * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well */ public static function relativize(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $base, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $target) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ($target->getScheme() !== '' && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')) { return $target; } if (\YoastSEO_Vendor\GuzzleHttp\Psr7\Uri::isRelativePathReference($target)) { // As the target is already highly relative we return it as-is. It would be possible to resolve // the target with `$target = self::resolve($base, $target);` and then try make it more relative // by removing a duplicate query. But let's not do that automatically. return $target; } if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { return $target->withScheme(''); } // We must remove the path before removing the authority because if the path starts with two slashes, the URI // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also // invalid. $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); if ($base->getPath() !== $target->getPath()) { return $emptyPathUri->withPath(self::getRelativePath($base, $target)); } if ($base->getQuery() === $target->getQuery()) { // Only the target fragment is left. And it must be returned even if base and target fragment are the same. return $emptyPathUri->withQuery(''); } // If the base URI has a query but the target has none, we cannot return an empty path reference as it would // inherit the base query component when resolving. if ($target->getQuery() === '') { $segments = \explode('/', $target->getPath()); /** @var string $lastSegment */ $lastSegment = \end($segments); return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); } return $emptyPathUri; } private static function getRelativePath(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $base, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $target) : string { $sourceSegments = \explode('/', $base->getPath()); $targetSegments = \explode('/', $target->getPath()); \array_pop($sourceSegments); $targetLastSegment = \array_pop($targetSegments); foreach ($sourceSegments as $i => $segment) { if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { unset($sourceSegments[$i], $targetSegments[$i]); } else { break; } } $targetSegments[] = $targetLastSegment; $relativePath = \str_repeat('../', \count($sourceSegments)) . \implode('/', $targetSegments); // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. if ('' === $relativePath || \false !== \strpos(\explode('/', $relativePath, 2)[0], ':')) { $relativePath = "./{$relativePath}"; } elseif ('/' === $relativePath[0]) { if ($base->getAuthority() != '' && $base->getPath() === '') { // In this case an extra slash is added by resolve() automatically. So we must not add one here. $relativePath = ".{$relativePath}"; } else { $relativePath = "./{$relativePath}"; } } return $relativePath; } private function __construct() { // cannot be instantiated } } vendor_prefixed/guzzlehttp/psr7/src/UriNormalizer.php 0000644 00000020702 15174712003 0017145 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Provides methods to normalize and compare URIs. * * @author Tobias Schultze * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6 */ final class UriNormalizer { /** * Default normalizations which only include the ones that preserve semantics. */ public const PRESERVING_NORMALIZATIONS = self::CAPITALIZE_PERCENT_ENCODING | self::DECODE_UNRESERVED_CHARACTERS | self::CONVERT_EMPTY_PATH | self::REMOVE_DEFAULT_HOST | self::REMOVE_DEFAULT_PORT | self::REMOVE_DOT_SEGMENTS; /** * All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized. * * Example: http://example.org/a%c2%b1b → http://example.org/a%C2%B1b */ public const CAPITALIZE_PERCENT_ENCODING = 1; /** * Decodes percent-encoded octets of unreserved characters. * * For consistency, percent-encoded octets in the ranges of ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39), * hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should not be created by URI producers and, * when found in a URI, should be decoded to their corresponding unreserved characters by URI normalizers. * * Example: http://example.org/%7Eusern%61me/ → http://example.org/~username/ */ public const DECODE_UNRESERVED_CHARACTERS = 2; /** * Converts the empty path to "/" for http and https URIs. * * Example: http://example.org → http://example.org/ */ public const CONVERT_EMPTY_PATH = 4; /** * Removes the default host of the given URI scheme from the URI. * * Only the "file" scheme defines the default host "localhost". * All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` * are equivalent according to RFC 3986. The first format is not accepted * by PHPs stream functions and thus already normalized implicitly to the * second format in the Uri class. See `GuzzleHttp\Psr7\Uri::composeComponents`. * * Example: file://localhost/myfile → file:///myfile */ public const REMOVE_DEFAULT_HOST = 8; /** * Removes the default port of the given URI scheme from the URI. * * Example: http://example.org:80/ → http://example.org/ */ public const REMOVE_DEFAULT_PORT = 16; /** * Removes unnecessary dot-segments. * * Dot-segments in relative-path references are not removed as it would * change the semantics of the URI reference. * * Example: http://example.org/../a/b/../c/./d.html → http://example.org/a/c/d.html */ public const REMOVE_DOT_SEGMENTS = 32; /** * Paths which include two or more adjacent slashes are converted to one. * * Webservers usually ignore duplicate slashes and treat those URIs equivalent. * But in theory those URIs do not need to be equivalent. So this normalization * may change the semantics. Encoded slashes (%2F) are not removed. * * Example: http://example.org//foo///bar.html → http://example.org/foo/bar.html */ public const REMOVE_DUPLICATE_SLASHES = 64; /** * Sort query parameters with their values in alphabetical order. * * However, the order of parameters in a URI may be significant (this is not defined by the standard). * So this normalization is not safe and may change the semantics of the URI. * * Example: ?lang=en&article=fred → ?article=fred&lang=en * * Note: The sorting is neither locale nor Unicode aware (the URI query does not get decoded at all) as the * purpose is to be able to compare URIs in a reproducible way, not to have the params sorted perfectly. */ public const SORT_QUERY_PARAMETERS = 128; /** * Returns a normalized URI. * * The scheme and host component are already normalized to lowercase per PSR-7 UriInterface. * This methods adds additional normalizations that can be configured with the $flags parameter. * * PSR-7 UriInterface cannot distinguish between an empty component and a missing component as * getQuery(), getFragment() etc. always return a string. This means the URIs "/?#" and "/" are * treated equivalent which is not necessarily true according to RFC 3986. But that difference * is highly uncommon in reality. So this potential normalization is implied in PSR-7 as well. * * @param UriInterface $uri The URI to normalize * @param int $flags A bitmask of normalizations to apply, see constants * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 */ public static function normalize(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, int $flags = self::PRESERVING_NORMALIZATIONS) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { if ($flags & self::CAPITALIZE_PERCENT_ENCODING) { $uri = self::capitalizePercentEncoding($uri); } if ($flags & self::DECODE_UNRESERVED_CHARACTERS) { $uri = self::decodeUnreservedCharacters($uri); } if ($flags & self::CONVERT_EMPTY_PATH && $uri->getPath() === '' && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) { $uri = $uri->withPath('/'); } if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { $uri = $uri->withHost(''); } if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri::isDefaultPort($uri)) { $uri = $uri->withPort(null); } if ($flags & self::REMOVE_DOT_SEGMENTS && !\YoastSEO_Vendor\GuzzleHttp\Psr7\Uri::isRelativePathReference($uri)) { $uri = $uri->withPath(\YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::removeDotSegments($uri->getPath())); } if ($flags & self::REMOVE_DUPLICATE_SLASHES) { $uri = $uri->withPath(\preg_replace('#//++#', '/', $uri->getPath())); } if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { $queryKeyValues = \explode('&', $uri->getQuery()); \sort($queryKeyValues); $uri = $uri->withQuery(\implode('&', $queryKeyValues)); } return $uri; } /** * Whether two URIs can be considered equivalent. * * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be * resolved against the same base URI. If this is not the case, determination of equivalence or difference of * relative references does not mean anything. * * @param UriInterface $uri1 An URI to compare * @param UriInterface $uri2 An URI to compare * @param int $normalizations A bitmask of normalizations to apply, see constants * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.1 */ public static function isEquivalent(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri1, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS) : bool { return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); } private static function capitalizePercentEncoding(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $regex = '/(?:%[A-Fa-f0-9]{2})++/'; $callback = function (array $match) : string { return \strtoupper($match[0]); }; return $uri->withPath(\preg_replace_callback($regex, $callback, $uri->getPath()))->withQuery(\preg_replace_callback($regex, $callback, $uri->getQuery())); } private static function decodeUnreservedCharacters(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; $callback = function (array $match) : string { return \rawurldecode($match[0]); }; return $uri->withPath(\preg_replace_callback($regex, $callback, $uri->getPath()))->withQuery(\preg_replace_callback($regex, $callback, $uri->getQuery())); } private function __construct() { // cannot be instantiated } } vendor_prefixed/guzzlehttp/psr7/src/NoSeekStream.php 0000644 00000001116 15174712003 0016701 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Stream decorator that prevents a stream from being seeked. */ final class NoSeekStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var StreamInterface */ private $stream; public function seek($offset, $whence = \SEEK_SET) : void { throw new \RuntimeException('Cannot seek a NoSeekStream'); } public function isSeekable() : bool { return \false; } } vendor_prefixed/guzzlehttp/psr7/src/ServerRequest.php 0000644 00000023235 15174712003 0017166 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use InvalidArgumentException; use YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Server-side HTTP request * * Extends the Request definition to add methods for accessing incoming data, * specifically server parameters, cookies, matched path parameters, query * string arguments, body parameters, and upload file information. * * "Attributes" are discovered via decomposing the request (and usually * specifically the URI path), and typically will be injected by the application. * * Requests are considered immutable; all methods that might change state are * implemented such that they retain the internal state of the current * message and return a new instance that contains the changed state. */ class ServerRequest extends \YoastSEO_Vendor\GuzzleHttp\Psr7\Request implements \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { /** * @var array */ private $attributes = []; /** * @var array */ private $cookieParams = []; /** * @var array|object|null */ private $parsedBody; /** * @var array */ private $queryParams = []; /** * @var array */ private $serverParams; /** * @var array */ private $uploadedFiles = []; /** * @param string $method HTTP method * @param string|UriInterface $uri URI * @param (string|string[])[] $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version * @param array $serverParams Typically the $_SERVER superglobal */ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = []) { $this->serverParams = $serverParams; parent::__construct($method, $uri, $headers, $body, $version); } /** * Return an UploadedFile instance array. * * @param array $files An array which respect $_FILES structure * * @throws InvalidArgumentException for unrecognized values */ public static function normalizeFiles(array $files) : array { $normalized = []; foreach ($files as $key => $value) { if ($value instanceof \YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface) { $normalized[$key] = $value; } elseif (\is_array($value) && isset($value['tmp_name'])) { $normalized[$key] = self::createUploadedFileFromSpec($value); } elseif (\is_array($value)) { $normalized[$key] = self::normalizeFiles($value); continue; } else { throw new \InvalidArgumentException('Invalid value in files specification'); } } return $normalized; } /** * Create and return an UploadedFile instance from a $_FILES specification. * * If the specification represents an array of values, this method will * delegate to normalizeNestedFileSpec() and return that return value. * * @param array $value $_FILES struct * * @return UploadedFileInterface|UploadedFileInterface[] */ private static function createUploadedFileFromSpec(array $value) { if (\is_array($value['tmp_name'])) { return self::normalizeNestedFileSpec($value); } return new \YoastSEO_Vendor\GuzzleHttp\Psr7\UploadedFile($value['tmp_name'], (int) $value['size'], (int) $value['error'], $value['name'], $value['type']); } /** * Normalize an array of file specifications. * * Loops through all nested files and returns a normalized array of * UploadedFileInterface instances. * * @return UploadedFileInterface[] */ private static function normalizeNestedFileSpec(array $files = []) : array { $normalizedFiles = []; foreach (\array_keys($files['tmp_name']) as $key) { $spec = ['tmp_name' => $files['tmp_name'][$key], 'size' => $files['size'][$key] ?? null, 'error' => $files['error'][$key] ?? null, 'name' => $files['name'][$key] ?? null, 'type' => $files['type'][$key] ?? null]; $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); } return $normalizedFiles; } /** * Return a ServerRequest populated with superglobals: * $_GET * $_POST * $_COOKIE * $_FILES * $_SERVER */ public static function fromGlobals() : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $headers = \getallheaders(); $uri = self::getUriFromGlobals(); $body = new \YoastSEO_Vendor\GuzzleHttp\Psr7\CachingStream(new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream('php://input', 'r+')); $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; $serverRequest = new \YoastSEO_Vendor\GuzzleHttp\Psr7\ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); return $serverRequest->withCookieParams($_COOKIE)->withQueryParams($_GET)->withParsedBody($_POST)->withUploadedFiles(self::normalizeFiles($_FILES)); } private static function extractHostAndPortFromAuthority(string $authority) : array { $uri = 'http://' . $authority; $parts = \parse_url($uri); if (\false === $parts) { return [null, null]; } $host = $parts['host'] ?? null; $port = $parts['port'] ?? null; return [$host, $port]; } /** * Get a Uri populated with values from $_SERVER. */ public static function getUriFromGlobals() : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $uri = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri(''); $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); $hasPort = \false; if (isset($_SERVER['HTTP_HOST'])) { [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); if ($host !== null) { $uri = $uri->withHost($host); } if ($port !== null) { $hasPort = \true; $uri = $uri->withPort($port); } } elseif (isset($_SERVER['SERVER_NAME'])) { $uri = $uri->withHost($_SERVER['SERVER_NAME']); } elseif (isset($_SERVER['SERVER_ADDR'])) { $uri = $uri->withHost($_SERVER['SERVER_ADDR']); } if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { $uri = $uri->withPort($_SERVER['SERVER_PORT']); } $hasQuery = \false; if (isset($_SERVER['REQUEST_URI'])) { $requestUriParts = \explode('?', $_SERVER['REQUEST_URI'], 2); $uri = $uri->withPath($requestUriParts[0]); if (isset($requestUriParts[1])) { $hasQuery = \true; $uri = $uri->withQuery($requestUriParts[1]); } } if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { $uri = $uri->withQuery($_SERVER['QUERY_STRING']); } return $uri; } public function getServerParams() : array { return $this->serverParams; } public function getUploadedFiles() : array { return $this->uploadedFiles; } public function withUploadedFiles(array $uploadedFiles) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $new = clone $this; $new->uploadedFiles = $uploadedFiles; return $new; } public function getCookieParams() : array { return $this->cookieParams; } public function withCookieParams(array $cookies) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $new = clone $this; $new->cookieParams = $cookies; return $new; } public function getQueryParams() : array { return $this->queryParams; } public function withQueryParams(array $query) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $new = clone $this; $new->queryParams = $query; return $new; } /** * @return array|object|null */ public function getParsedBody() { return $this->parsedBody; } public function withParsedBody($data) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $new = clone $this; $new->parsedBody = $data; return $new; } public function getAttributes() : array { return $this->attributes; } /** * @return mixed */ public function getAttribute($attribute, $default = null) { if (\false === \array_key_exists($attribute, $this->attributes)) { return $default; } return $this->attributes[$attribute]; } public function withAttribute($attribute, $value) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { $new = clone $this; $new->attributes[$attribute] = $value; return $new; } public function withoutAttribute($attribute) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface { if (\false === \array_key_exists($attribute, $this->attributes)) { return $this; } $new = clone $this; unset($new->attributes[$attribute]); return $new; } } vendor_prefixed/guzzlehttp/psr7/src/StreamWrapper.php 0000644 00000010306 15174712003 0017136 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Converts Guzzle streams into PHP stream resources. * * @see https://www.php.net/streamwrapper */ final class StreamWrapper { /** @var resource */ public $context; /** @var StreamInterface */ private $stream; /** @var string r, r+, or w */ private $mode; /** * Returns a resource representing the stream. * * @param StreamInterface $stream The stream to get a resource for * * @return resource * * @throws \InvalidArgumentException if stream is not readable or writable */ public static function getResource(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream) { self::register(); if ($stream->isReadable()) { $mode = $stream->isWritable() ? 'r+' : 'r'; } elseif ($stream->isWritable()) { $mode = 'w'; } else { throw new \InvalidArgumentException('The stream must be readable, ' . 'writable, or both.'); } return \fopen('guzzle://stream', $mode, \false, self::createStreamContext($stream)); } /** * Creates a stream context that can be used to open a stream as a php stream resource. * * @return resource */ public static function createStreamContext(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream) { return \stream_context_create(['guzzle' => ['stream' => $stream]]); } /** * Registers the stream wrapper if needed */ public static function register() : void { if (!\in_array('guzzle', \stream_get_wrappers())) { \stream_wrapper_register('guzzle', __CLASS__); } } public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null) : bool { $options = \stream_context_get_options($this->context); if (!isset($options['guzzle']['stream'])) { return \false; } $this->mode = $mode; $this->stream = $options['guzzle']['stream']; return \true; } public function stream_read(int $count) : string { return $this->stream->read($count); } public function stream_write(string $data) : int { return $this->stream->write($data); } public function stream_tell() : int { return $this->stream->tell(); } public function stream_eof() : bool { return $this->stream->eof(); } public function stream_seek(int $offset, int $whence) : bool { $this->stream->seek($offset, $whence); return \true; } /** * @return resource|false */ public function stream_cast(int $cast_as) { $stream = clone $this->stream; $resource = $stream->detach(); return $resource ?? \false; } /** * @return array{ * dev: int, * ino: int, * mode: int, * nlink: int, * uid: int, * gid: int, * rdev: int, * size: int, * atime: int, * mtime: int, * ctime: int, * blksize: int, * blocks: int * }|false */ public function stream_stat() { if ($this->stream->getSize() === null) { return \false; } static $modeMap = ['r' => 33060, 'rb' => 33060, 'r+' => 33206, 'w' => 33188, 'wb' => 33188]; return ['dev' => 0, 'ino' => 0, 'mode' => $modeMap[$this->mode], 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => $this->stream->getSize() ?: 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0]; } /** * @return array{ * dev: int, * ino: int, * mode: int, * nlink: int, * uid: int, * gid: int, * rdev: int, * size: int, * atime: int, * mtime: int, * ctime: int, * blksize: int, * blocks: int * } */ public function url_stat(string $path, int $flags) : array { return ['dev' => 0, 'ino' => 0, 'mode' => 0, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0]; } } vendor_prefixed/guzzlehttp/psr7/src/Response.php 0000644 00000010647 15174712003 0016150 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * PSR-7 response implementation. */ class Response implements \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { use MessageTrait; /** Map of standard HTTP status code/reason phrases */ private const PHRASES = [100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required']; /** @var string */ private $reasonPhrase; /** @var int */ private $statusCode; /** * @param int $status Status code * @param (string|string[])[] $headers Response headers * @param string|resource|StreamInterface|null $body Response body * @param string $version Protocol version * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) */ public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null) { $this->assertStatusCodeRange($status); $this->statusCode = $status; if ($body !== '' && $body !== null) { $this->stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($body); } $this->setHeaders($headers); if ($reason == '' && isset(self::PHRASES[$this->statusCode])) { $this->reasonPhrase = self::PHRASES[$this->statusCode]; } else { $this->reasonPhrase = (string) $reason; } $this->protocol = $version; } public function getStatusCode() : int { return $this->statusCode; } public function getReasonPhrase() : string { return $this->reasonPhrase; } public function withStatus($code, $reasonPhrase = '') : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface { $this->assertStatusCodeIsInteger($code); $code = (int) $code; $this->assertStatusCodeRange($code); $new = clone $this; $new->statusCode = $code; if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) { $reasonPhrase = self::PHRASES[$new->statusCode]; } $new->reasonPhrase = (string) $reasonPhrase; return $new; } /** * @param mixed $statusCode */ private function assertStatusCodeIsInteger($statusCode) : void { if (\filter_var($statusCode, \FILTER_VALIDATE_INT) === \false) { throw new \InvalidArgumentException('Status code must be an integer value.'); } } private function assertStatusCodeRange(int $statusCode) : void { if ($statusCode < 100 || $statusCode >= 600) { throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); } } } vendor_prefixed/guzzlehttp/psr7/src/LimitStream.php 0000644 00000010316 15174712003 0016575 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Decorator used to return only a subset of a stream. */ final class LimitStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var int Offset to start reading from */ private $offset; /** @var int Limit the number of bytes that can be read */ private $limit; /** @var StreamInterface */ private $stream; /** * @param StreamInterface $stream Stream to wrap * @param int $limit Total number of bytes to allow to be read * from the stream. Pass -1 for no limit. * @param int $offset Position to seek to before reading (only * works on seekable streams). */ public function __construct(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $limit = -1, int $offset = 0) { $this->stream = $stream; $this->setLimit($limit); $this->setOffset($offset); } public function eof() : bool { // Always return true if the underlying stream is EOF if ($this->stream->eof()) { return \true; } // No limit and the underlying stream is not at EOF if ($this->limit === -1) { return \false; } return $this->stream->tell() >= $this->offset + $this->limit; } /** * Returns the size of the limited subset of data */ public function getSize() : ?int { if (null === ($length = $this->stream->getSize())) { return null; } elseif ($this->limit === -1) { return $length - $this->offset; } else { return \min($this->limit, $length - $this->offset); } } /** * Allow for a bounded seek on the read limited stream */ public function seek($offset, $whence = \SEEK_SET) : void { if ($whence !== \SEEK_SET || $offset < 0) { throw new \RuntimeException(\sprintf('Cannot seek to offset %s with whence %s', $offset, $whence)); } $offset += $this->offset; if ($this->limit !== -1) { if ($offset > $this->offset + $this->limit) { $offset = $this->offset + $this->limit; } } $this->stream->seek($offset); } /** * Give a relative tell() */ public function tell() : int { return $this->stream->tell() - $this->offset; } /** * Set the offset to start limiting from * * @param int $offset Offset to seek to and begin byte limiting from * * @throws \RuntimeException if the stream cannot be seeked. */ public function setOffset(int $offset) : void { $current = $this->stream->tell(); if ($current !== $offset) { // If the stream cannot seek to the offset position, then read to it if ($this->stream->isSeekable()) { $this->stream->seek($offset); } elseif ($current > $offset) { throw new \RuntimeException("Could not seek to stream offset {$offset}"); } else { $this->stream->read($offset - $current); } } $this->offset = $offset; } /** * Set the limit of bytes that the decorator allows to be read from the * stream. * * @param int $limit Number of bytes to allow to be read from the stream. * Use -1 for no limit. */ public function setLimit(int $limit) : void { $this->limit = $limit; } public function read($length) : string { if ($this->limit === -1) { return $this->stream->read($length); } // Check if the current position is less than the total allowed // bytes + original offset $remaining = $this->offset + $this->limit - $this->stream->tell(); if ($remaining > 0) { // Only return the amount of requested data, ensuring that the byte // limit is not exceeded return $this->stream->read(\min($remaining, $length)); } return ''; } } vendor_prefixed/guzzlehttp/psr7/src/Uri.php 0000644 00000053367 15174712003 0015117 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * PSR-7 URI implementation. * * @author Michael Dowling * @author Tobias Schultze * @author Matthew Weier O'Phinney */ class Uri implements \YoastSEO_Vendor\Psr\Http\Message\UriInterface, \JsonSerializable { /** * Absolute http and https URIs require a host per RFC 7230 Section 2.7 * but in generic URIs the host can be empty. So for http(s) URIs * we apply this default host when no host is given yet to form a * valid URI. */ private const HTTP_DEFAULT_HOST = 'localhost'; private const DEFAULT_PORTS = ['http' => 80, 'https' => 443, 'ftp' => 21, 'gopher' => 70, 'nntp' => 119, 'news' => 119, 'telnet' => 23, 'tn3270' => 23, 'imap' => 143, 'pop' => 110, 'ldap' => 389]; /** * Unreserved characters for use in a regex. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 */ private const CHAR_UNRESERVED = 'a-zA-Z0-9_\\-\\.~'; /** * Sub-delims for use in a regex. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 */ private const CHAR_SUB_DELIMS = '!\\$&\'\\(\\)\\*\\+,;='; private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26']; /** @var string Uri scheme. */ private $scheme = ''; /** @var string Uri user info. */ private $userInfo = ''; /** @var string Uri host. */ private $host = ''; /** @var int|null Uri port. */ private $port; /** @var string Uri path. */ private $path = ''; /** @var string Uri query string. */ private $query = ''; /** @var string Uri fragment. */ private $fragment = ''; /** @var string|null String representation */ private $composedComponents; public function __construct(string $uri = '') { if ($uri !== '') { $parts = self::parse($uri); if ($parts === \false) { throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException("Unable to parse URI: {$uri}"); } $this->applyParts($parts); } } /** * UTF-8 aware \parse_url() replacement. * * The internal function produces broken output for non ASCII domain names * (IDN) when used with locales other than "C". * * On the other hand, cURL understands IDN correctly only when UTF-8 locale * is configured ("C.UTF-8", "en_US.UTF-8", etc.). * * @see https://bugs.php.net/bug.php?id=52923 * @see https://www.php.net/manual/en/function.parse-url.php#114817 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING * * @return array|false */ private static function parse(string $url) { // If IPv6 $prefix = ''; if (\preg_match('%^(.*://\\[[0-9:a-fA-F]+\\])(.*?)$%', $url, $matches)) { /** @var array{0:string, 1:string, 2:string} $matches */ $prefix = $matches[1]; $url = $matches[2]; } /** @var string */ $encodedUrl = \preg_replace_callback('%[^:/@?&=#]+%usD', static function ($matches) { return \urlencode($matches[0]); }, $url); $result = \parse_url($prefix . $encodedUrl); if ($result === \false) { return \false; } return \array_map('urldecode', $result); } public function __toString() : string { if ($this->composedComponents === null) { $this->composedComponents = self::composeComponents($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); } return $this->composedComponents; } /** * Composes a URI reference string from its various components. * * Usually this method does not need to be called manually but instead is used indirectly via * `Psr\Http\Message\UriInterface::__toString`. * * PSR-7 UriInterface treats an empty component the same as a missing component as * getQuery(), getFragment() etc. always return a string. This explains the slight * difference to RFC 3986 Section 5.3. * * Another adjustment is that the authority separator is added even when the authority is missing/empty * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to * that format). * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3 */ public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment) : string { $uri = ''; // weak type checks to also accept null until we can add scalar type hints if ($scheme != '') { $uri .= $scheme . ':'; } if ($authority != '' || $scheme === 'file') { $uri .= '//' . $authority; } if ($authority != '' && $path != '' && $path[0] != '/') { $path = '/' . $path; } $uri .= $path; if ($query != '') { $uri .= '?' . $query; } if ($fragment != '') { $uri .= '#' . $fragment; } return $uri; } /** * Whether the URI has the default port of the current scheme. * * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used * independently of the implementation. */ public static function isDefaultPort(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool { return $uri->getPort() === null || isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]; } /** * Whether the URI is absolute, i.e. it has a scheme. * * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative * to another URI, the base URI. Relative references can be divided into several forms: * - network-path references, e.g. '//example.com/path' * - absolute-path references, e.g. '/path' * - relative-path references, e.g. 'subpath' * * @see Uri::isNetworkPathReference * @see Uri::isAbsolutePathReference * @see Uri::isRelativePathReference * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4 */ public static function isAbsolute(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool { return $uri->getScheme() !== ''; } /** * Whether the URI is a network-path reference. * * A relative reference that begins with two slash characters is termed an network-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isNetworkPathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool { return $uri->getScheme() === '' && $uri->getAuthority() !== ''; } /** * Whether the URI is a absolute-path reference. * * A relative reference that begins with a single slash character is termed an absolute-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isAbsolutePathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/'; } /** * Whether the URI is a relative-path reference. * * A relative reference that does not begin with a slash character is termed a relative-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isRelativePathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); } /** * Whether the URI is a same-document reference. * * A same-document reference refers to a URI that is, aside from its fragment * component, identical to the base URI. When no base URI is given, only an empty * URI reference (apart from its fragment) is considered a same-document reference. * * @param UriInterface $uri The URI to check * @param UriInterface|null $base An optional base URI to compare against * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4 */ public static function isSameDocumentReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, ?\YoastSEO_Vendor\Psr\Http\Message\UriInterface $base = null) : bool { if ($base !== null) { $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve($base, $uri); return $uri->getScheme() === $base->getScheme() && $uri->getAuthority() === $base->getAuthority() && $uri->getPath() === $base->getPath() && $uri->getQuery() === $base->getQuery(); } return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; } /** * Creates a new URI with a specific query string value removed. * * Any existing query string values that exactly match the provided key are * removed. * * @param UriInterface $uri URI to use as a base. * @param string $key Query string key to remove. */ public static function withoutQueryValue(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, string $key) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $result = self::getFilteredQueryString($uri, [$key]); return $uri->withQuery(\implode('&', $result)); } /** * Creates a new URI with a specific query string value. * * Any existing query string values that exactly match the provided key are * removed and replaced with the given key value pair. * * A value of null will set the query string key without a value, e.g. "key" * instead of "key=value". * * @param UriInterface $uri URI to use as a base. * @param string $key Key to set. * @param string|null $value Value to set */ public static function withQueryValue(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, string $key, ?string $value) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $result = self::getFilteredQueryString($uri, [$key]); $result[] = self::generateQueryString($key, $value); return $uri->withQuery(\implode('&', $result)); } /** * Creates a new URI with multiple specific query string values. * * It has the same behavior as withQueryValue() but for an associative array of key => value. * * @param UriInterface $uri URI to use as a base. * @param (string|null)[] $keyValueArray Associative array of key and values */ public static function withQueryValues(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $keyValueArray) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $result = self::getFilteredQueryString($uri, \array_keys($keyValueArray)); foreach ($keyValueArray as $key => $value) { $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null); } return $uri->withQuery(\implode('&', $result)); } /** * Creates a URI from a hash of `parse_url` components. * * @see https://www.php.net/manual/en/function.parse-url.php * * @throws MalformedUriException If the components do not form a valid URI. */ public static function fromParts(array $parts) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $uri = new self(); $uri->applyParts($parts); $uri->validateState(); return $uri; } public function getScheme() : string { return $this->scheme; } public function getAuthority() : string { $authority = $this->host; if ($this->userInfo !== '') { $authority = $this->userInfo . '@' . $authority; } if ($this->port !== null) { $authority .= ':' . $this->port; } return $authority; } public function getUserInfo() : string { return $this->userInfo; } public function getHost() : string { return $this->host; } public function getPort() : ?int { return $this->port; } public function getPath() : string { return $this->path; } public function getQuery() : string { return $this->query; } public function getFragment() : string { return $this->fragment; } public function withScheme($scheme) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $scheme = $this->filterScheme($scheme); if ($this->scheme === $scheme) { return $this; } $new = clone $this; $new->scheme = $scheme; $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withUserInfo($user, $password = null) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $info = $this->filterUserInfoComponent($user); if ($password !== null) { $info .= ':' . $this->filterUserInfoComponent($password); } if ($this->userInfo === $info) { return $this; } $new = clone $this; $new->userInfo = $info; $new->composedComponents = null; $new->validateState(); return $new; } public function withHost($host) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $host = $this->filterHost($host); if ($this->host === $host) { return $this; } $new = clone $this; $new->host = $host; $new->composedComponents = null; $new->validateState(); return $new; } public function withPort($port) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $port = $this->filterPort($port); if ($this->port === $port) { return $this; } $new = clone $this; $new->port = $port; $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withPath($path) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $path = $this->filterPath($path); if ($this->path === $path) { return $this; } $new = clone $this; $new->path = $path; $new->composedComponents = null; $new->validateState(); return $new; } public function withQuery($query) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $query = $this->filterQueryAndFragment($query); if ($this->query === $query) { return $this; } $new = clone $this; $new->query = $query; $new->composedComponents = null; return $new; } public function withFragment($fragment) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { $fragment = $this->filterQueryAndFragment($fragment); if ($this->fragment === $fragment) { return $this; } $new = clone $this; $new->fragment = $fragment; $new->composedComponents = null; return $new; } public function jsonSerialize() : string { return $this->__toString(); } /** * Apply parse_url parts to a URI. * * @param array $parts Array of parse_url parts to apply. */ private function applyParts(array $parts) : void { $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : ''; $this->userInfo = isset($parts['user']) ? $this->filterUserInfoComponent($parts['user']) : ''; $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']); } $this->removeDefaultPort(); } /** * @param mixed $scheme * * @throws \InvalidArgumentException If the scheme is invalid. */ private function filterScheme($scheme) : string { if (!\is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); } return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } /** * @param mixed $component * * @throws \InvalidArgumentException If the user info is invalid. */ private function filterUserInfoComponent($component) : string { if (!\is_string($component)) { throw new \InvalidArgumentException('User info must be a string'); } return \preg_replace_callback('/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $component); } /** * @param mixed $host * * @throws \InvalidArgumentException If the host is invalid. */ private function filterHost($host) : string { if (!\is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); } return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } /** * @param mixed $port * * @throws \InvalidArgumentException If the port is invalid. */ private function filterPort($port) : ?int { if ($port === null) { return null; } $port = (int) $port; if (0 > $port || 0xffff < $port) { throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return $port; } /** * @param (string|int)[] $keys * * @return string[] */ private static function getFilteredQueryString(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $keys) : array { $current = $uri->getQuery(); if ($current === '') { return []; } $decodedKeys = \array_map(function ($k) : string { return \rawurldecode((string) $k); }, $keys); return \array_filter(\explode('&', $current), function ($part) use($decodedKeys) { return !\in_array(\rawurldecode(\explode('=', $part)[0]), $decodedKeys, \true); }); } private static function generateQueryString(string $key, ?string $value) : string { // Query string separators ("=", "&") within the key or value need to be encoded // (while preventing double-encoding) before setting the query string. All other // chars that need percent-encoding will be encoded by withQuery(). $queryString = \strtr($key, self::QUERY_SEPARATORS_REPLACEMENT); if ($value !== null) { $queryString .= '=' . \strtr($value, self::QUERY_SEPARATORS_REPLACEMENT); } return $queryString; } private function removeDefaultPort() : void { if ($this->port !== null && self::isDefaultPort($this)) { $this->port = null; } } /** * Filters the path of a URI * * @param mixed $path * * @throws \InvalidArgumentException If the path is invalid. */ private function filterPath($path) : string { if (!\is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path); } /** * Filters the query string or fragment of a URI. * * @param mixed $str * * @throws \InvalidArgumentException If the query or fragment is invalid. */ private function filterQueryAndFragment($str) : string { if (!\is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/\\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str); } private function rawurlencodeMatchZero(array $match) : string { return \rawurlencode($match[0]); } private function validateState() : void { if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { $this->host = self::HTTP_DEFAULT_HOST; } if ($this->getAuthority() === '') { if (0 === \strpos($this->path, '//')) { throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException('The path of a URI without an authority must not start with two slashes "//"'); } if ($this->scheme === '' && \false !== \strpos(\explode('/', $this->path, 2)[0], ':')) { throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon'); } } } } vendor_prefixed/guzzlehttp/psr7/src/StreamDecoratorTrait.php 0000644 00000006565 15174712003 0020460 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Stream decorator trait * * @property StreamInterface $stream */ trait StreamDecoratorTrait { /** * @param StreamInterface $stream Stream to decorate */ public function __construct(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream) { $this->stream = $stream; } /** * Magic method used to create a new stream if streams are not added in * the constructor of a decorator (e.g., LazyOpenStream). * * @return StreamInterface */ public function __get(string $name) { if ($name === 'stream') { $this->stream = $this->createStream(); return $this->stream; } throw new \UnexpectedValueException("{$name} not found on class"); } public function __toString() : string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } \trigger_error(\sprintf('%s::__toString exception: %s', self::class, (string) $e), \E_USER_ERROR); return ''; } } public function getContents() : string { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToString($this); } /** * Allow decorators to implement custom methods * * @return mixed */ public function __call(string $method, array $args) { /** @var callable $callable */ $callable = [$this->stream, $method]; $result = $callable(...$args); // Always return the wrapped object if the result is a return $this return $result === $this->stream ? $this : $result; } public function close() : void { $this->stream->close(); } /** * @return mixed */ public function getMetadata($key = null) { return $this->stream->getMetadata($key); } public function detach() { return $this->stream->detach(); } public function getSize() : ?int { return $this->stream->getSize(); } public function eof() : bool { return $this->stream->eof(); } public function tell() : int { return $this->stream->tell(); } public function isReadable() : bool { return $this->stream->isReadable(); } public function isWritable() : bool { return $this->stream->isWritable(); } public function isSeekable() : bool { return $this->stream->isSeekable(); } public function rewind() : void { $this->seek(0); } public function seek($offset, $whence = \SEEK_SET) : void { $this->stream->seek($offset, $whence); } public function read($length) : string { return $this->stream->read($length); } public function write($string) : int { return $this->stream->write($string); } /** * Implement in subclasses to dynamically create streams when requested. * * @throws \BadMethodCallException */ protected function createStream() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { throw new \BadMethodCallException('Not implemented'); } } vendor_prefixed/guzzlehttp/psr7/src/MimeType.php 0000644 00000130251 15174712003 0016075 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; final class MimeType { private const MIME_TYPES = ['1km' => 'application/vnd.1000minds.decision-model+xml', '3dml' => 'text/vnd.in3d.3dml', '3ds' => 'image/x-3ds', '3g2' => 'video/3gpp2', '3gp' => 'video/3gp', '3gpp' => 'video/3gpp', '3mf' => 'model/3mf', '7z' => 'application/x-7z-compressed', '7zip' => 'application/x-7z-compressed', '123' => 'application/vnd.lotus-1-2-3', 'aab' => 'application/x-authorware-bin', 'aac' => 'audio/aac', 'aam' => 'application/x-authorware-map', 'aas' => 'application/x-authorware-seg', 'abw' => 'application/x-abiword', 'ac' => 'application/vnd.nokia.n-gage.ac+xml', 'ac3' => 'audio/ac3', 'acc' => 'application/vnd.americandynamics.acc', 'ace' => 'application/x-ace-compressed', 'acu' => 'application/vnd.acucobol', 'acutc' => 'application/vnd.acucorp', 'adp' => 'audio/adpcm', 'adts' => 'audio/aac', 'aep' => 'application/vnd.audiograph', 'afm' => 'application/x-font-type1', 'afp' => 'application/vnd.ibm.modcap', 'age' => 'application/vnd.age', 'ahead' => 'application/vnd.ahead.space', 'ai' => 'application/pdf', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'air' => 'application/vnd.adobe.air-application-installer-package+zip', 'ait' => 'application/vnd.dvb.ait', 'ami' => 'application/vnd.amiga.ami', 'aml' => 'application/automationml-aml+xml', 'amlx' => 'application/automationml-amlx+zip', 'amr' => 'audio/amr', 'apk' => 'application/vnd.android.package-archive', 'apng' => 'image/apng', 'appcache' => 'text/cache-manifest', 'appinstaller' => 'application/appinstaller', 'application' => 'application/x-ms-application', 'appx' => 'application/appx', 'appxbundle' => 'application/appxbundle', 'apr' => 'application/vnd.lotus-approach', 'arc' => 'application/x-freearc', 'arj' => 'application/x-arj', 'asc' => 'application/pgp-signature', 'asf' => 'video/x-ms-asf', 'asm' => 'text/x-asm', 'aso' => 'application/vnd.accpac.simply.aso', 'asx' => 'video/x-ms-asf', 'atc' => 'application/vnd.acucorp', 'atom' => 'application/atom+xml', 'atomcat' => 'application/atomcat+xml', 'atomdeleted' => 'application/atomdeleted+xml', 'atomsvc' => 'application/atomsvc+xml', 'atx' => 'application/vnd.antix.game-component', 'au' => 'audio/x-au', 'avci' => 'image/avci', 'avcs' => 'image/avcs', 'avi' => 'video/x-msvideo', 'avif' => 'image/avif', 'aw' => 'application/applixware', 'azf' => 'application/vnd.airzip.filesecure.azf', 'azs' => 'application/vnd.airzip.filesecure.azs', 'azv' => 'image/vnd.airzip.accelerator.azv', 'azw' => 'application/vnd.amazon.ebook', 'b16' => 'image/vnd.pco.b16', 'bat' => 'application/x-msdownload', 'bcpio' => 'application/x-bcpio', 'bdf' => 'application/x-font-bdf', 'bdm' => 'application/vnd.syncml.dm+wbxml', 'bdoc' => 'application/x-bdoc', 'bed' => 'application/vnd.realvnc.bed', 'bh2' => 'application/vnd.fujitsu.oasysprs', 'bin' => 'application/octet-stream', 'blb' => 'application/x-blorb', 'blorb' => 'application/x-blorb', 'bmi' => 'application/vnd.bmi', 'bmml' => 'application/vnd.balsamiq.bmml+xml', 'bmp' => 'image/bmp', 'book' => 'application/vnd.framemaker', 'box' => 'application/vnd.previewsystems.box', 'boz' => 'application/x-bzip2', 'bpk' => 'application/octet-stream', 'bpmn' => 'application/octet-stream', 'bsp' => 'model/vnd.valve.source.compiled-map', 'btf' => 'image/prs.btif', 'btif' => 'image/prs.btif', 'buffer' => 'application/octet-stream', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'c' => 'text/x-c', 'c4d' => 'application/vnd.clonk.c4group', 'c4f' => 'application/vnd.clonk.c4group', 'c4g' => 'application/vnd.clonk.c4group', 'c4p' => 'application/vnd.clonk.c4group', 'c4u' => 'application/vnd.clonk.c4group', 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', 'cab' => 'application/vnd.ms-cab-compressed', 'caf' => 'audio/x-caf', 'cap' => 'application/vnd.tcpdump.pcap', 'car' => 'application/vnd.curl.car', 'cat' => 'application/vnd.ms-pki.seccat', 'cb7' => 'application/x-cbr', 'cba' => 'application/x-cbr', 'cbr' => 'application/x-cbr', 'cbt' => 'application/x-cbr', 'cbz' => 'application/x-cbr', 'cc' => 'text/x-c', 'cco' => 'application/x-cocoa', 'cct' => 'application/x-director', 'ccxml' => 'application/ccxml+xml', 'cdbcmsg' => 'application/vnd.contact.cmsg', 'cdf' => 'application/x-netcdf', 'cdfx' => 'application/cdfx+xml', 'cdkey' => 'application/vnd.mediastation.cdkey', 'cdmia' => 'application/cdmi-capability', 'cdmic' => 'application/cdmi-container', 'cdmid' => 'application/cdmi-domain', 'cdmio' => 'application/cdmi-object', 'cdmiq' => 'application/cdmi-queue', 'cdr' => 'application/cdr', 'cdx' => 'chemical/x-cdx', 'cdxml' => 'application/vnd.chemdraw+xml', 'cdy' => 'application/vnd.cinderella', 'cer' => 'application/pkix-cert', 'cfs' => 'application/x-cfs-compressed', 'cgm' => 'image/cgm', 'chat' => 'application/x-chat', 'chm' => 'application/vnd.ms-htmlhelp', 'chrt' => 'application/vnd.kde.kchart', 'cif' => 'chemical/x-cif', 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', 'cil' => 'application/vnd.ms-artgalry', 'cjs' => 'application/node', 'cla' => 'application/vnd.claymore', 'class' => 'application/octet-stream', 'cld' => 'model/vnd.cld', 'clkk' => 'application/vnd.crick.clicker.keyboard', 'clkp' => 'application/vnd.crick.clicker.palette', 'clkt' => 'application/vnd.crick.clicker.template', 'clkw' => 'application/vnd.crick.clicker.wordbank', 'clkx' => 'application/vnd.crick.clicker', 'clp' => 'application/x-msclip', 'cmc' => 'application/vnd.cosmocaller', 'cmdf' => 'chemical/x-cmdf', 'cml' => 'chemical/x-cml', 'cmp' => 'application/vnd.yellowriver-custom-menu', 'cmx' => 'image/x-cmx', 'cod' => 'application/vnd.rim.cod', 'coffee' => 'text/coffeescript', 'com' => 'application/x-msdownload', 'conf' => 'text/plain', 'cpio' => 'application/x-cpio', 'cpl' => 'application/cpl+xml', 'cpp' => 'text/x-c', 'cpt' => 'application/mac-compactpro', 'crd' => 'application/x-mscardfile', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'crx' => 'application/x-chrome-extension', 'cryptonote' => 'application/vnd.rig.cryptonote', 'csh' => 'application/x-csh', 'csl' => 'application/vnd.citationstyles.style+xml', 'csml' => 'chemical/x-csml', 'csp' => 'application/vnd.commonspace', 'csr' => 'application/octet-stream', 'css' => 'text/css', 'cst' => 'application/x-director', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'curl' => 'text/vnd.curl', 'cwl' => 'application/cwl', 'cww' => 'application/prs.cww', 'cxt' => 'application/x-director', 'cxx' => 'text/x-c', 'dae' => 'model/vnd.collada+xml', 'daf' => 'application/vnd.mobius.daf', 'dart' => 'application/vnd.dart', 'dataless' => 'application/vnd.fdsn.seed', 'davmount' => 'application/davmount+xml', 'dbf' => 'application/vnd.dbf', 'dbk' => 'application/docbook+xml', 'dcr' => 'application/x-director', 'dcurl' => 'text/vnd.curl.dcurl', 'dd2' => 'application/vnd.oma.dd2+xml', 'ddd' => 'application/vnd.fujixerox.ddd', 'ddf' => 'application/vnd.syncml.dmddf+xml', 'dds' => 'image/vnd.ms-dds', 'deb' => 'application/x-debian-package', 'def' => 'text/plain', 'deploy' => 'application/octet-stream', 'der' => 'application/x-x509-ca-cert', 'dfac' => 'application/vnd.dreamfactory', 'dgc' => 'application/x-dgc-compressed', 'dib' => 'image/bmp', 'dic' => 'text/x-c', 'dir' => 'application/x-director', 'dis' => 'application/vnd.mobius.dis', 'disposition-notification' => 'message/disposition-notification', 'dist' => 'application/octet-stream', 'distz' => 'application/octet-stream', 'djv' => 'image/vnd.djvu', 'djvu' => 'image/vnd.djvu', 'dll' => 'application/octet-stream', 'dmg' => 'application/x-apple-diskimage', 'dmn' => 'application/octet-stream', 'dmp' => 'application/vnd.tcpdump.pcap', 'dms' => 'application/octet-stream', 'dna' => 'application/vnd.dna', 'doc' => 'application/msword', 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dot' => 'application/msword', 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dp' => 'application/vnd.osgi.dp', 'dpg' => 'application/vnd.dpgraph', 'dpx' => 'image/dpx', 'dra' => 'audio/vnd.dra', 'drle' => 'image/dicom-rle', 'dsc' => 'text/prs.lines.tag', 'dssc' => 'application/dssc+der', 'dtb' => 'application/x-dtbook+xml', 'dtd' => 'application/xml-dtd', 'dts' => 'audio/vnd.dts', 'dtshd' => 'audio/vnd.dts.hd', 'dump' => 'application/octet-stream', 'dvb' => 'video/vnd.dvb.file', 'dvi' => 'application/x-dvi', 'dwd' => 'application/atsc-dwd+xml', 'dwf' => 'model/vnd.dwf', 'dwg' => 'image/vnd.dwg', 'dxf' => 'image/vnd.dxf', 'dxp' => 'application/vnd.spotfire.dxp', 'dxr' => 'application/x-director', 'ear' => 'application/java-archive', 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', 'ecma' => 'application/ecmascript', 'edm' => 'application/vnd.novadigm.edm', 'edx' => 'application/vnd.novadigm.edx', 'efif' => 'application/vnd.picsel', 'ei6' => 'application/vnd.pg.osasli', 'elc' => 'application/octet-stream', 'emf' => 'image/emf', 'eml' => 'message/rfc822', 'emma' => 'application/emma+xml', 'emotionml' => 'application/emotionml+xml', 'emz' => 'application/x-msmetafile', 'eol' => 'audio/vnd.digital-winds', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'es3' => 'application/vnd.eszigno3+xml', 'esa' => 'application/vnd.osgi.subsystem', 'esf' => 'application/vnd.epson.esf', 'et3' => 'application/vnd.eszigno3+xml', 'etx' => 'text/x-setext', 'eva' => 'application/x-eva', 'evy' => 'application/x-envoy', 'exe' => 'application/octet-stream', 'exi' => 'application/exi', 'exp' => 'application/express', 'exr' => 'image/aces', 'ext' => 'application/vnd.novadigm.ext', 'ez' => 'application/andrew-inset', 'ez2' => 'application/vnd.ezpix-album', 'ez3' => 'application/vnd.ezpix-package', 'f' => 'text/x-fortran', 'f4v' => 'video/mp4', 'f77' => 'text/x-fortran', 'f90' => 'text/x-fortran', 'fbs' => 'image/vnd.fastbidsheet', 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', 'fcs' => 'application/vnd.isac.fcs', 'fdf' => 'application/vnd.fdf', 'fdt' => 'application/fdt+xml', 'fe_launch' => 'application/vnd.denovo.fcselayout-link', 'fg5' => 'application/vnd.fujitsu.oasysgp', 'fgd' => 'application/x-director', 'fh' => 'image/x-freehand', 'fh4' => 'image/x-freehand', 'fh5' => 'image/x-freehand', 'fh7' => 'image/x-freehand', 'fhc' => 'image/x-freehand', 'fig' => 'application/x-xfig', 'fits' => 'image/fits', 'flac' => 'audio/x-flac', 'fli' => 'video/x-fli', 'flo' => 'application/vnd.micrografx.flo', 'flv' => 'video/x-flv', 'flw' => 'application/vnd.kde.kivio', 'flx' => 'text/vnd.fmi.flexstor', 'fly' => 'text/vnd.fly', 'fm' => 'application/vnd.framemaker', 'fnc' => 'application/vnd.frogans.fnc', 'fo' => 'application/vnd.software602.filler.form+xml', 'for' => 'text/x-fortran', 'fpx' => 'image/vnd.fpx', 'frame' => 'application/vnd.framemaker', 'fsc' => 'application/vnd.fsc.weblaunch', 'fst' => 'image/vnd.fst', 'ftc' => 'application/vnd.fluxtime.clip', 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', 'fvt' => 'video/vnd.fvt', 'fxp' => 'application/vnd.adobe.fxp', 'fxpl' => 'application/vnd.adobe.fxp', 'fzs' => 'application/vnd.fuzzysheet', 'g2w' => 'application/vnd.geoplan', 'g3' => 'image/g3fax', 'g3w' => 'application/vnd.geospace', 'gac' => 'application/vnd.groove-account', 'gam' => 'application/x-tads', 'gbr' => 'application/rpki-ghostbusters', 'gca' => 'application/x-gca-compressed', 'gdl' => 'model/vnd.gdl', 'gdoc' => 'application/vnd.google-apps.document', 'ged' => 'text/vnd.familysearch.gedcom', 'geo' => 'application/vnd.dynageo', 'geojson' => 'application/geo+json', 'gex' => 'application/vnd.geometry-explorer', 'ggb' => 'application/vnd.geogebra.file', 'ggt' => 'application/vnd.geogebra.tool', 'ghf' => 'application/vnd.groove-help', 'gif' => 'image/gif', 'gim' => 'application/vnd.groove-identity-message', 'glb' => 'model/gltf-binary', 'gltf' => 'model/gltf+json', 'gml' => 'application/gml+xml', 'gmx' => 'application/vnd.gmx', 'gnumeric' => 'application/x-gnumeric', 'gpg' => 'application/gpg-keys', 'gph' => 'application/vnd.flographit', 'gpx' => 'application/gpx+xml', 'gqf' => 'application/vnd.grafeq', 'gqs' => 'application/vnd.grafeq', 'gram' => 'application/srgs', 'gramps' => 'application/x-gramps-xml', 'gre' => 'application/vnd.geometry-explorer', 'grv' => 'application/vnd.groove-injector', 'grxml' => 'application/srgs+xml', 'gsf' => 'application/x-font-ghostscript', 'gsheet' => 'application/vnd.google-apps.spreadsheet', 'gslides' => 'application/vnd.google-apps.presentation', 'gtar' => 'application/x-gtar', 'gtm' => 'application/vnd.groove-tool-message', 'gtw' => 'model/vnd.gtw', 'gv' => 'text/vnd.graphviz', 'gxf' => 'application/gxf', 'gxt' => 'application/vnd.geonext', 'gz' => 'application/gzip', 'gzip' => 'application/gzip', 'h' => 'text/x-c', 'h261' => 'video/h261', 'h263' => 'video/h263', 'h264' => 'video/h264', 'hal' => 'application/vnd.hal+xml', 'hbci' => 'application/vnd.hbci', 'hbs' => 'text/x-handlebars-template', 'hdd' => 'application/x-virtualbox-hdd', 'hdf' => 'application/x-hdf', 'heic' => 'image/heic', 'heics' => 'image/heic-sequence', 'heif' => 'image/heif', 'heifs' => 'image/heif-sequence', 'hej2' => 'image/hej2k', 'held' => 'application/atsc-held+xml', 'hh' => 'text/x-c', 'hjson' => 'application/hjson', 'hlp' => 'application/winhlp', 'hpgl' => 'application/vnd.hp-hpgl', 'hpid' => 'application/vnd.hp-hpid', 'hps' => 'application/vnd.hp-hps', 'hqx' => 'application/mac-binhex40', 'hsj2' => 'image/hsj2', 'htc' => 'text/x-component', 'htke' => 'application/vnd.kenameaapp', 'htm' => 'text/html', 'html' => 'text/html', 'hvd' => 'application/vnd.yamaha.hv-dic', 'hvp' => 'application/vnd.yamaha.hv-voice', 'hvs' => 'application/vnd.yamaha.hv-script', 'i2g' => 'application/vnd.intergeo', 'icc' => 'application/vnd.iccprofile', 'ice' => 'x-conference/x-cooltalk', 'icm' => 'application/vnd.iccprofile', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ief' => 'image/ief', 'ifb' => 'text/calendar', 'ifm' => 'application/vnd.shana.informed.formdata', 'iges' => 'model/iges', 'igl' => 'application/vnd.igloader', 'igm' => 'application/vnd.insors.igm', 'igs' => 'model/iges', 'igx' => 'application/vnd.micrografx.igx', 'iif' => 'application/vnd.shana.informed.interchange', 'img' => 'application/octet-stream', 'imp' => 'application/vnd.accpac.simply.imp', 'ims' => 'application/vnd.ms-ims', 'in' => 'text/plain', 'ini' => 'text/plain', 'ink' => 'application/inkml+xml', 'inkml' => 'application/inkml+xml', 'install' => 'application/x-install-instructions', 'iota' => 'application/vnd.astraea-software.iota', 'ipfix' => 'application/ipfix', 'ipk' => 'application/vnd.shana.informed.package', 'irm' => 'application/vnd.ibm.rights-management', 'irp' => 'application/vnd.irepository.package+xml', 'iso' => 'application/x-iso9660-image', 'itp' => 'application/vnd.shana.informed.formtemplate', 'its' => 'application/its+xml', 'ivp' => 'application/vnd.immervision-ivp', 'ivu' => 'application/vnd.immervision-ivu', 'jad' => 'text/vnd.sun.j2me.app-descriptor', 'jade' => 'text/jade', 'jam' => 'application/vnd.jam', 'jar' => 'application/java-archive', 'jardiff' => 'application/x-java-archive-diff', 'java' => 'text/x-java-source', 'jhc' => 'image/jphc', 'jisp' => 'application/vnd.jisp', 'jls' => 'image/jls', 'jlt' => 'application/vnd.hp-jlyt', 'jng' => 'image/x-jng', 'jnlp' => 'application/x-java-jnlp-file', 'joda' => 'application/vnd.joost.joda-archive', 'jp2' => 'image/jp2', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpf' => 'image/jpx', 'jpg' => 'image/jpeg', 'jpg2' => 'image/jp2', 'jpgm' => 'video/jpm', 'jpgv' => 'video/jpeg', 'jph' => 'image/jph', 'jpm' => 'video/jpm', 'jpx' => 'image/jpx', 'js' => 'application/javascript', 'json' => 'application/json', 'json5' => 'application/json5', 'jsonld' => 'application/ld+json', 'jsonml' => 'application/jsonml+json', 'jsx' => 'text/jsx', 'jt' => 'model/jt', 'jxr' => 'image/jxr', 'jxra' => 'image/jxra', 'jxrs' => 'image/jxrs', 'jxs' => 'image/jxs', 'jxsc' => 'image/jxsc', 'jxsi' => 'image/jxsi', 'jxss' => 'image/jxss', 'kar' => 'audio/midi', 'karbon' => 'application/vnd.kde.karbon', 'kdb' => 'application/octet-stream', 'kdbx' => 'application/x-keepass2', 'key' => 'application/x-iwork-keynote-sffkey', 'kfo' => 'application/vnd.kde.kformula', 'kia' => 'application/vnd.kidspiration', 'kml' => 'application/vnd.google-earth.kml+xml', 'kmz' => 'application/vnd.google-earth.kmz', 'kne' => 'application/vnd.kinar', 'knp' => 'application/vnd.kinar', 'kon' => 'application/vnd.kde.kontour', 'kpr' => 'application/vnd.kde.kpresenter', 'kpt' => 'application/vnd.kde.kpresenter', 'kpxx' => 'application/vnd.ds-keypoint', 'ksp' => 'application/vnd.kde.kspread', 'ktr' => 'application/vnd.kahootz', 'ktx' => 'image/ktx', 'ktx2' => 'image/ktx2', 'ktz' => 'application/vnd.kahootz', 'kwd' => 'application/vnd.kde.kword', 'kwt' => 'application/vnd.kde.kword', 'lasxml' => 'application/vnd.las.las+xml', 'latex' => 'application/x-latex', 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', 'les' => 'application/vnd.hhe.lesson-player', 'less' => 'text/less', 'lgr' => 'application/lgr+xml', 'lha' => 'application/octet-stream', 'link66' => 'application/vnd.route66.link66+xml', 'list' => 'text/plain', 'list3820' => 'application/vnd.ibm.modcap', 'listafp' => 'application/vnd.ibm.modcap', 'litcoffee' => 'text/coffeescript', 'lnk' => 'application/x-ms-shortcut', 'log' => 'text/plain', 'lostxml' => 'application/lost+xml', 'lrf' => 'application/octet-stream', 'lrm' => 'application/vnd.ms-lrm', 'ltf' => 'application/vnd.frogans.ltf', 'lua' => 'text/x-lua', 'luac' => 'application/x-lua-bytecode', 'lvp' => 'audio/vnd.lucent.voice', 'lwp' => 'application/vnd.lotus-wordpro', 'lzh' => 'application/octet-stream', 'm1v' => 'video/mpeg', 'm2a' => 'audio/mpeg', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'text/plain', 'm3u8' => 'application/vnd.apple.mpegurl', 'm4a' => 'audio/x-m4a', 'm4p' => 'application/mp4', 'm4s' => 'video/iso.segment', 'm4u' => 'application/vnd.mpegurl', 'm4v' => 'video/x-m4v', 'm13' => 'application/x-msmediaview', 'm14' => 'application/x-msmediaview', 'm21' => 'application/mp21', 'ma' => 'application/mathematica', 'mads' => 'application/mads+xml', 'maei' => 'application/mmt-aei+xml', 'mag' => 'application/vnd.ecowin.chart', 'maker' => 'application/vnd.framemaker', 'man' => 'text/troff', 'manifest' => 'text/cache-manifest', 'map' => 'application/json', 'mar' => 'application/octet-stream', 'markdown' => 'text/markdown', 'mathml' => 'application/mathml+xml', 'mb' => 'application/mathematica', 'mbk' => 'application/vnd.mobius.mbk', 'mbox' => 'application/mbox', 'mc1' => 'application/vnd.medcalcdata', 'mcd' => 'application/vnd.mcd', 'mcurl' => 'text/vnd.curl.mcurl', 'md' => 'text/markdown', 'mdb' => 'application/x-msaccess', 'mdi' => 'image/vnd.ms-modi', 'mdx' => 'text/mdx', 'me' => 'text/troff', 'mesh' => 'model/mesh', 'meta4' => 'application/metalink4+xml', 'metalink' => 'application/metalink+xml', 'mets' => 'application/mets+xml', 'mfm' => 'application/vnd.mfmp', 'mft' => 'application/rpki-manifest', 'mgp' => 'application/vnd.osgeo.mapguide.package', 'mgz' => 'application/vnd.proteus.magazine', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mie' => 'application/x-mie', 'mif' => 'application/vnd.mif', 'mime' => 'message/rfc822', 'mj2' => 'video/mj2', 'mjp2' => 'video/mj2', 'mjs' => 'text/javascript', 'mk3d' => 'video/x-matroska', 'mka' => 'audio/x-matroska', 'mkd' => 'text/x-markdown', 'mks' => 'video/x-matroska', 'mkv' => 'video/x-matroska', 'mlp' => 'application/vnd.dolby.mlp', 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', 'mmf' => 'application/vnd.smaf', 'mml' => 'text/mathml', 'mmr' => 'image/vnd.fujixerox.edmics-mmr', 'mng' => 'video/x-mng', 'mny' => 'application/x-msmoney', 'mobi' => 'application/x-mobipocket-ebook', 'mods' => 'application/mods+xml', 'mov' => 'video/quicktime', 'movie' => 'video/x-sgi-movie', 'mp2' => 'audio/mpeg', 'mp2a' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4s' => 'application/mp4', 'mp4v' => 'video/mp4', 'mp21' => 'application/mp21', 'mpc' => 'application/vnd.mophun.certificate', 'mpd' => 'application/dash+xml', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpf' => 'application/media-policy-dataset+xml', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'mpga' => 'audio/mpeg', 'mpkg' => 'application/vnd.apple.installer+xml', 'mpm' => 'application/vnd.blueice.multipass', 'mpn' => 'application/vnd.mophun.application', 'mpp' => 'application/vnd.ms-project', 'mpt' => 'application/vnd.ms-project', 'mpy' => 'application/vnd.ibm.minipay', 'mqy' => 'application/vnd.mobius.mqy', 'mrc' => 'application/marc', 'mrcx' => 'application/marcxml+xml', 'ms' => 'text/troff', 'mscml' => 'application/mediaservercontrol+xml', 'mseed' => 'application/vnd.fdsn.mseed', 'mseq' => 'application/vnd.mseq', 'msf' => 'application/vnd.epson.msf', 'msg' => 'application/vnd.ms-outlook', 'msh' => 'model/mesh', 'msi' => 'application/x-msdownload', 'msix' => 'application/msix', 'msixbundle' => 'application/msixbundle', 'msl' => 'application/vnd.mobius.msl', 'msm' => 'application/octet-stream', 'msp' => 'application/octet-stream', 'msty' => 'application/vnd.muvee.style', 'mtl' => 'model/mtl', 'mts' => 'model/vnd.mts', 'mus' => 'application/vnd.musician', 'musd' => 'application/mmt-usd+xml', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', 'mvt' => 'application/vnd.mapbox-vector-tile', 'mwf' => 'application/vnd.mfer', 'mxf' => 'application/mxf', 'mxl' => 'application/vnd.recordare.musicxml', 'mxmf' => 'audio/mobile-xmf', 'mxml' => 'application/xv+xml', 'mxs' => 'application/vnd.triscape.mxs', 'mxu' => 'video/vnd.mpegurl', 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', 'n3' => 'text/n3', 'nb' => 'application/mathematica', 'nbp' => 'application/vnd.wolfram.player', 'nc' => 'application/x-netcdf', 'ncx' => 'application/x-dtbncx+xml', 'nfo' => 'text/x-nfo', 'ngdat' => 'application/vnd.nokia.n-gage.data', 'nitf' => 'application/vnd.nitf', 'nlu' => 'application/vnd.neurolanguage.nlu', 'nml' => 'application/vnd.enliven', 'nnd' => 'application/vnd.noblenet-directory', 'nns' => 'application/vnd.noblenet-sealer', 'nnw' => 'application/vnd.noblenet-web', 'npx' => 'image/vnd.net-fpx', 'nq' => 'application/n-quads', 'nsc' => 'application/x-conference', 'nsf' => 'application/vnd.lotus-notes', 'nt' => 'application/n-triples', 'ntf' => 'application/vnd.nitf', 'numbers' => 'application/x-iwork-numbers-sffnumbers', 'nzb' => 'application/x-nzb', 'oa2' => 'application/vnd.fujitsu.oasys2', 'oa3' => 'application/vnd.fujitsu.oasys3', 'oas' => 'application/vnd.fujitsu.oasys', 'obd' => 'application/x-msbinder', 'obgx' => 'application/vnd.openblox.game+xml', 'obj' => 'model/obj', 'oda' => 'application/oda', 'odb' => 'application/vnd.oasis.opendocument.database', 'odc' => 'application/vnd.oasis.opendocument.chart', 'odf' => 'application/vnd.oasis.opendocument.formula', 'odft' => 'application/vnd.oasis.opendocument.formula-template', 'odg' => 'application/vnd.oasis.opendocument.graphics', 'odi' => 'application/vnd.oasis.opendocument.image', 'odm' => 'application/vnd.oasis.opendocument.text-master', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odt' => 'application/vnd.oasis.opendocument.text', 'oga' => 'audio/ogg', 'ogex' => 'model/vnd.opengex', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'omdoc' => 'application/omdoc+xml', 'onepkg' => 'application/onenote', 'onetmp' => 'application/onenote', 'onetoc' => 'application/onenote', 'onetoc2' => 'application/onenote', 'opf' => 'application/oebps-package+xml', 'opml' => 'text/x-opml', 'oprc' => 'application/vnd.palm', 'opus' => 'audio/ogg', 'org' => 'text/x-org', 'osf' => 'application/vnd.yamaha.openscoreformat', 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 'osm' => 'application/vnd.openstreetmap.data+xml', 'otc' => 'application/vnd.oasis.opendocument.chart-template', 'otf' => 'font/otf', 'otg' => 'application/vnd.oasis.opendocument.graphics-template', 'oth' => 'application/vnd.oasis.opendocument.text-web', 'oti' => 'application/vnd.oasis.opendocument.image-template', 'otp' => 'application/vnd.oasis.opendocument.presentation-template', 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', 'ott' => 'application/vnd.oasis.opendocument.text-template', 'ova' => 'application/x-virtualbox-ova', 'ovf' => 'application/x-virtualbox-ovf', 'owl' => 'application/rdf+xml', 'oxps' => 'application/oxps', 'oxt' => 'application/vnd.openofficeorg.extension', 'p' => 'text/x-pascal', 'p7a' => 'application/x-pkcs7-signature', 'p7b' => 'application/x-pkcs7-certificates', 'p7c' => 'application/pkcs7-mime', 'p7m' => 'application/pkcs7-mime', 'p7r' => 'application/x-pkcs7-certreqresp', 'p7s' => 'application/pkcs7-signature', 'p8' => 'application/pkcs8', 'p10' => 'application/x-pkcs10', 'p12' => 'application/x-pkcs12', 'pac' => 'application/x-ns-proxy-autoconfig', 'pages' => 'application/x-iwork-pages-sffpages', 'pas' => 'text/x-pascal', 'paw' => 'application/vnd.pawaafile', 'pbd' => 'application/vnd.powerbuilder6', 'pbm' => 'image/x-portable-bitmap', 'pcap' => 'application/vnd.tcpdump.pcap', 'pcf' => 'application/x-font-pcf', 'pcl' => 'application/vnd.hp-pcl', 'pclxl' => 'application/vnd.hp-pclxl', 'pct' => 'image/x-pict', 'pcurl' => 'application/vnd.curl.pcurl', 'pcx' => 'image/x-pcx', 'pdb' => 'application/x-pilot', 'pde' => 'text/x-processing', 'pdf' => 'application/pdf', 'pem' => 'application/x-x509-user-cert', 'pfa' => 'application/x-font-type1', 'pfb' => 'application/x-font-type1', 'pfm' => 'application/x-font-type1', 'pfr' => 'application/font-tdpfr', 'pfx' => 'application/x-pkcs12', 'pgm' => 'image/x-portable-graymap', 'pgn' => 'application/x-chess-pgn', 'pgp' => 'application/pgp', 'phar' => 'application/octet-stream', 'php' => 'application/x-httpd-php', 'php3' => 'application/x-httpd-php', 'php4' => 'application/x-httpd-php', 'phps' => 'application/x-httpd-php-source', 'phtml' => 'application/x-httpd-php', 'pic' => 'image/x-pict', 'pkg' => 'application/octet-stream', 'pki' => 'application/pkixcmp', 'pkipath' => 'application/pkix-pkipath', 'pkpass' => 'application/vnd.apple.pkpass', 'pl' => 'application/x-perl', 'plb' => 'application/vnd.3gpp.pic-bw-large', 'plc' => 'application/vnd.mobius.plc', 'plf' => 'application/vnd.pocketlearn', 'pls' => 'application/pls+xml', 'pm' => 'application/x-perl', 'pml' => 'application/vnd.ctc-posml', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'portpkg' => 'application/vnd.macports.portpkg', 'pot' => 'application/vnd.ms-powerpoint', 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ppa' => 'application/vnd.ms-powerpoint', 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'ppd' => 'application/vnd.cups-ppd', 'ppm' => 'image/x-portable-pixmap', 'pps' => 'application/vnd.ms-powerpoint', 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'ppt' => 'application/powerpoint', 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pqa' => 'application/vnd.palm', 'prc' => 'model/prc', 'pre' => 'application/vnd.lotus-freelance', 'prf' => 'application/pics-rules', 'provx' => 'application/provenance+xml', 'ps' => 'application/postscript', 'psb' => 'application/vnd.3gpp.pic-bw-small', 'psd' => 'application/x-photoshop', 'psf' => 'application/x-font-linux-psf', 'pskcxml' => 'application/pskc+xml', 'pti' => 'image/prs.pti', 'ptid' => 'application/vnd.pvi.ptid1', 'pub' => 'application/x-mspublisher', 'pvb' => 'application/vnd.3gpp.pic-bw-var', 'pwn' => 'application/vnd.3m.post-it-notes', 'pya' => 'audio/vnd.ms-playready.media.pya', 'pyo' => 'model/vnd.pytha.pyox', 'pyox' => 'model/vnd.pytha.pyox', 'pyv' => 'video/vnd.ms-playready.media.pyv', 'qam' => 'application/vnd.epson.quickanime', 'qbo' => 'application/vnd.intu.qbo', 'qfx' => 'application/vnd.intu.qfx', 'qps' => 'application/vnd.publishare-delta-tree', 'qt' => 'video/quicktime', 'qwd' => 'application/vnd.quark.quarkxpress', 'qwt' => 'application/vnd.quark.quarkxpress', 'qxb' => 'application/vnd.quark.quarkxpress', 'qxd' => 'application/vnd.quark.quarkxpress', 'qxl' => 'application/vnd.quark.quarkxpress', 'qxt' => 'application/vnd.quark.quarkxpress', 'ra' => 'audio/x-realaudio', 'ram' => 'audio/x-pn-realaudio', 'raml' => 'application/raml+yaml', 'rapd' => 'application/route-apd+xml', 'rar' => 'application/x-rar', 'ras' => 'image/x-cmu-raster', 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', 'rdf' => 'application/rdf+xml', 'rdz' => 'application/vnd.data-vision.rdz', 'relo' => 'application/p2p-overlay+xml', 'rep' => 'application/vnd.businessobjects', 'res' => 'application/x-dtbresource+xml', 'rgb' => 'image/x-rgb', 'rif' => 'application/reginfo+xml', 'rip' => 'audio/vnd.rip', 'ris' => 'application/x-research-info-systems', 'rl' => 'application/resource-lists+xml', 'rlc' => 'image/vnd.fujixerox.edmics-rlc', 'rld' => 'application/resource-lists-diff+xml', 'rm' => 'audio/x-pn-realaudio', 'rmi' => 'audio/midi', 'rmp' => 'audio/x-pn-realaudio-plugin', 'rms' => 'application/vnd.jcp.javame.midlet-rms', 'rmvb' => 'application/vnd.rn-realmedia-vbr', 'rnc' => 'application/relax-ng-compact-syntax', 'rng' => 'application/xml', 'roa' => 'application/rpki-roa', 'roff' => 'text/troff', 'rp9' => 'application/vnd.cloanto.rp9', 'rpm' => 'audio/x-pn-realaudio-plugin', 'rpss' => 'application/vnd.nokia.radio-presets', 'rpst' => 'application/vnd.nokia.radio-preset', 'rq' => 'application/sparql-query', 'rs' => 'application/rls-services+xml', 'rsa' => 'application/x-pkcs7', 'rsat' => 'application/atsc-rsat+xml', 'rsd' => 'application/rsd+xml', 'rsheet' => 'application/urc-ressheet+xml', 'rss' => 'application/rss+xml', 'rtf' => 'text/rtf', 'rtx' => 'text/richtext', 'run' => 'application/x-makeself', 'rusd' => 'application/route-usd+xml', 'rv' => 'video/vnd.rn-realvideo', 's' => 'text/x-asm', 's3m' => 'audio/s3m', 'saf' => 'application/vnd.yamaha.smaf-audio', 'sass' => 'text/x-sass', 'sbml' => 'application/sbml+xml', 'sc' => 'application/vnd.ibm.secure-container', 'scd' => 'application/x-msschedule', 'scm' => 'application/vnd.lotus-screencam', 'scq' => 'application/scvp-cv-request', 'scs' => 'application/scvp-cv-response', 'scss' => 'text/x-scss', 'scurl' => 'text/vnd.curl.scurl', 'sda' => 'application/vnd.stardivision.draw', 'sdc' => 'application/vnd.stardivision.calc', 'sdd' => 'application/vnd.stardivision.impress', 'sdkd' => 'application/vnd.solent.sdkm+xml', 'sdkm' => 'application/vnd.solent.sdkm+xml', 'sdp' => 'application/sdp', 'sdw' => 'application/vnd.stardivision.writer', 'sea' => 'application/octet-stream', 'see' => 'application/vnd.seemail', 'seed' => 'application/vnd.fdsn.seed', 'sema' => 'application/vnd.sema', 'semd' => 'application/vnd.semd', 'semf' => 'application/vnd.semf', 'senmlx' => 'application/senml+xml', 'sensmlx' => 'application/sensml+xml', 'ser' => 'application/java-serialized-object', 'setpay' => 'application/set-payment-initiation', 'setreg' => 'application/set-registration-initiation', 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', 'sfs' => 'application/vnd.spotfire.sfs', 'sfv' => 'text/x-sfv', 'sgi' => 'image/sgi', 'sgl' => 'application/vnd.stardivision.writer-global', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'sh' => 'application/x-sh', 'shar' => 'application/x-shar', 'shex' => 'text/shex', 'shf' => 'application/shf+xml', 'shtml' => 'text/html', 'sid' => 'image/x-mrsid-image', 'sieve' => 'application/sieve', 'sig' => 'application/pgp-signature', 'sil' => 'audio/silk', 'silo' => 'model/mesh', 'sis' => 'application/vnd.symbian.install', 'sisx' => 'application/vnd.symbian.install', 'sit' => 'application/x-stuffit', 'sitx' => 'application/x-stuffitx', 'siv' => 'application/sieve', 'skd' => 'application/vnd.koan', 'skm' => 'application/vnd.koan', 'skp' => 'application/vnd.koan', 'skt' => 'application/vnd.koan', 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', 'slim' => 'text/slim', 'slm' => 'text/slim', 'sls' => 'application/route-s-tsid+xml', 'slt' => 'application/vnd.epson.salt', 'sm' => 'application/vnd.stepmania.stepchart', 'smf' => 'application/vnd.stardivision.math', 'smi' => 'application/smil', 'smil' => 'application/smil', 'smv' => 'video/x-smv', 'smzip' => 'application/vnd.stepmania.package', 'snd' => 'audio/basic', 'snf' => 'application/x-font-snf', 'so' => 'application/octet-stream', 'spc' => 'application/x-pkcs7-certificates', 'spdx' => 'text/spdx', 'spf' => 'application/vnd.yamaha.smaf-phrase', 'spl' => 'application/x-futuresplash', 'spot' => 'text/vnd.in3d.spot', 'spp' => 'application/scvp-vp-response', 'spq' => 'application/scvp-vp-request', 'spx' => 'audio/ogg', 'sql' => 'application/x-sql', 'src' => 'application/x-wais-source', 'srt' => 'application/x-subrip', 'sru' => 'application/sru+xml', 'srx' => 'application/sparql-results+xml', 'ssdl' => 'application/ssdl+xml', 'sse' => 'application/vnd.kodak-descriptor', 'ssf' => 'application/vnd.epson.ssf', 'ssml' => 'application/ssml+xml', 'sst' => 'application/octet-stream', 'st' => 'application/vnd.sailingtracker.track', 'stc' => 'application/vnd.sun.xml.calc.template', 'std' => 'application/vnd.sun.xml.draw.template', 'step' => 'application/STEP', 'stf' => 'application/vnd.wt.stf', 'sti' => 'application/vnd.sun.xml.impress.template', 'stk' => 'application/hyperstudio', 'stl' => 'model/stl', 'stp' => 'application/STEP', 'stpx' => 'model/step+xml', 'stpxz' => 'model/step-xml+zip', 'stpz' => 'model/step+zip', 'str' => 'application/vnd.pg.format', 'stw' => 'application/vnd.sun.xml.writer.template', 'styl' => 'text/stylus', 'stylus' => 'text/stylus', 'sub' => 'text/vnd.dvb.subtitle', 'sus' => 'application/vnd.sus-calendar', 'susp' => 'application/vnd.sus-calendar', 'sv4cpio' => 'application/x-sv4cpio', 'sv4crc' => 'application/x-sv4crc', 'svc' => 'application/vnd.dvb.service', 'svd' => 'application/vnd.svd', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', 'swa' => 'application/x-director', 'swf' => 'application/x-shockwave-flash', 'swi' => 'application/vnd.aristanetworks.swi', 'swidtag' => 'application/swid+xml', 'sxc' => 'application/vnd.sun.xml.calc', 'sxd' => 'application/vnd.sun.xml.draw', 'sxg' => 'application/vnd.sun.xml.writer.global', 'sxi' => 'application/vnd.sun.xml.impress', 'sxm' => 'application/vnd.sun.xml.math', 'sxw' => 'application/vnd.sun.xml.writer', 't' => 'text/troff', 't3' => 'application/x-t3vm-image', 't38' => 'image/t38', 'taglet' => 'application/vnd.mynfc', 'tao' => 'application/vnd.tao.intent-module-archive', 'tap' => 'image/vnd.tencent.tap', 'tar' => 'application/x-tar', 'tcap' => 'application/vnd.3gpp2.tcap', 'tcl' => 'application/x-tcl', 'td' => 'application/urc-targetdesc+xml', 'teacher' => 'application/vnd.smart.teacher', 'tei' => 'application/tei+xml', 'teicorpus' => 'application/tei+xml', 'tex' => 'application/x-tex', 'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'text' => 'text/plain', 'tfi' => 'application/thraud+xml', 'tfm' => 'application/x-tex-tfm', 'tfx' => 'image/tiff-fx', 'tga' => 'image/x-tga', 'tgz' => 'application/x-tar', 'thmx' => 'application/vnd.ms-officetheme', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'tk' => 'application/x-tcl', 'tmo' => 'application/vnd.tmobile-livetv', 'toml' => 'application/toml', 'torrent' => 'application/x-bittorrent', 'tpl' => 'application/vnd.groove-tool-template', 'tpt' => 'application/vnd.trid.tpt', 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trig' => 'application/trig', 'trm' => 'application/x-msterminal', 'ts' => 'video/mp2t', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'font/collection', 'ttf' => 'font/ttf', 'ttl' => 'text/turtle', 'ttml' => 'application/ttml+xml', 'twd' => 'application/vnd.simtech-mindmapper', 'twds' => 'application/vnd.simtech-mindmapper', 'txd' => 'application/vnd.genomatix.tuxedo', 'txf' => 'application/vnd.mobius.txf', 'txt' => 'text/plain', 'u3d' => 'model/u3d', 'u8dsn' => 'message/global-delivery-status', 'u8hdr' => 'message/global-headers', 'u8mdn' => 'message/global-disposition-notification', 'u8msg' => 'message/global', 'u32' => 'application/x-authorware-bin', 'ubj' => 'application/ubjson', 'udeb' => 'application/x-debian-package', 'ufd' => 'application/vnd.ufdl', 'ufdl' => 'application/vnd.ufdl', 'ulx' => 'application/x-glulx', 'umj' => 'application/vnd.umajin', 'unityweb' => 'application/vnd.unity', 'uo' => 'application/vnd.uoml+xml', 'uoml' => 'application/vnd.uoml+xml', 'uri' => 'text/uri-list', 'uris' => 'text/uri-list', 'urls' => 'text/uri-list', 'usda' => 'model/vnd.usda', 'usdz' => 'model/vnd.usdz+zip', 'ustar' => 'application/x-ustar', 'utz' => 'application/vnd.uiq.theme', 'uu' => 'text/x-uuencode', 'uva' => 'audio/vnd.dece.audio', 'uvd' => 'application/vnd.dece.data', 'uvf' => 'application/vnd.dece.data', 'uvg' => 'image/vnd.dece.graphic', 'uvh' => 'video/vnd.dece.hd', 'uvi' => 'image/vnd.dece.graphic', 'uvm' => 'video/vnd.dece.mobile', 'uvp' => 'video/vnd.dece.pd', 'uvs' => 'video/vnd.dece.sd', 'uvt' => 'application/vnd.dece.ttml+xml', 'uvu' => 'video/vnd.uvvu.mp4', 'uvv' => 'video/vnd.dece.video', 'uvva' => 'audio/vnd.dece.audio', 'uvvd' => 'application/vnd.dece.data', 'uvvf' => 'application/vnd.dece.data', 'uvvg' => 'image/vnd.dece.graphic', 'uvvh' => 'video/vnd.dece.hd', 'uvvi' => 'image/vnd.dece.graphic', 'uvvm' => 'video/vnd.dece.mobile', 'uvvp' => 'video/vnd.dece.pd', 'uvvs' => 'video/vnd.dece.sd', 'uvvt' => 'application/vnd.dece.ttml+xml', 'uvvu' => 'video/vnd.uvvu.mp4', 'uvvv' => 'video/vnd.dece.video', 'uvvx' => 'application/vnd.dece.unspecified', 'uvvz' => 'application/vnd.dece.zip', 'uvx' => 'application/vnd.dece.unspecified', 'uvz' => 'application/vnd.dece.zip', 'vbox' => 'application/x-virtualbox-vbox', 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', 'vcard' => 'text/vcard', 'vcd' => 'application/x-cdlink', 'vcf' => 'text/x-vcard', 'vcg' => 'application/vnd.groove-vcard', 'vcs' => 'text/x-vcalendar', 'vcx' => 'application/vnd.vcx', 'vdi' => 'application/x-virtualbox-vdi', 'vds' => 'model/vnd.sap.vds', 'vhd' => 'application/x-virtualbox-vhd', 'vis' => 'application/vnd.visionary', 'viv' => 'video/vnd.vivo', 'vlc' => 'application/videolan', 'vmdk' => 'application/x-virtualbox-vmdk', 'vob' => 'video/x-ms-vob', 'vor' => 'application/vnd.stardivision.writer', 'vox' => 'application/x-authorware-bin', 'vrml' => 'model/vrml', 'vsd' => 'application/vnd.visio', 'vsf' => 'application/vnd.vsf', 'vss' => 'application/vnd.visio', 'vst' => 'application/vnd.visio', 'vsw' => 'application/vnd.visio', 'vtf' => 'image/vnd.valve.source.texture', 'vtt' => 'text/vtt', 'vtu' => 'model/vnd.vtu', 'vxml' => 'application/voicexml+xml', 'w3d' => 'application/x-director', 'wad' => 'application/x-doom', 'wadl' => 'application/vnd.sun.wadl+xml', 'war' => 'application/java-archive', 'wasm' => 'application/wasm', 'wav' => 'audio/x-wav', 'wax' => 'audio/x-ms-wax', 'wbmp' => 'image/vnd.wap.wbmp', 'wbs' => 'application/vnd.criticaltools.wbs+xml', 'wbxml' => 'application/wbxml', 'wcm' => 'application/vnd.ms-works', 'wdb' => 'application/vnd.ms-works', 'wdp' => 'image/vnd.ms-photo', 'weba' => 'audio/webm', 'webapp' => 'application/x-web-app-manifest+json', 'webm' => 'video/webm', 'webmanifest' => 'application/manifest+json', 'webp' => 'image/webp', 'wg' => 'application/vnd.pmi.widget', 'wgsl' => 'text/wgsl', 'wgt' => 'application/widget', 'wif' => 'application/watcherinfo+xml', 'wks' => 'application/vnd.ms-works', 'wm' => 'video/x-ms-wm', 'wma' => 'audio/x-ms-wma', 'wmd' => 'application/x-ms-wmd', 'wmf' => 'image/wmf', 'wml' => 'text/vnd.wap.wml', 'wmlc' => 'application/wmlc', 'wmls' => 'text/vnd.wap.wmlscript', 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'wmv' => 'video/x-ms-wmv', 'wmx' => 'video/x-ms-wmx', 'wmz' => 'application/x-msmetafile', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'word' => 'application/msword', 'wpd' => 'application/vnd.wordperfect', 'wpl' => 'application/vnd.ms-wpl', 'wps' => 'application/vnd.ms-works', 'wqd' => 'application/vnd.wqd', 'wri' => 'application/x-mswrite', 'wrl' => 'model/vrml', 'wsc' => 'message/vnd.wfa.wsc', 'wsdl' => 'application/wsdl+xml', 'wspolicy' => 'application/wspolicy+xml', 'wtb' => 'application/vnd.webturbo', 'wvx' => 'video/x-ms-wvx', 'x3d' => 'model/x3d+xml', 'x3db' => 'model/x3d+fastinfoset', 'x3dbz' => 'model/x3d+binary', 'x3dv' => 'model/x3d-vrml', 'x3dvz' => 'model/x3d+vrml', 'x3dz' => 'model/x3d+xml', 'x32' => 'application/x-authorware-bin', 'x_b' => 'model/vnd.parasolid.transmit.binary', 'x_t' => 'model/vnd.parasolid.transmit.text', 'xaml' => 'application/xaml+xml', 'xap' => 'application/x-silverlight-app', 'xar' => 'application/vnd.xara', 'xav' => 'application/xcap-att+xml', 'xbap' => 'application/x-ms-xbap', 'xbd' => 'application/vnd.fujixerox.docuworks.binder', 'xbm' => 'image/x-xbitmap', 'xca' => 'application/xcap-caps+xml', 'xcs' => 'application/calendar+xml', 'xdf' => 'application/xcap-diff+xml', 'xdm' => 'application/vnd.syncml.dm+xml', 'xdp' => 'application/vnd.adobe.xdp+xml', 'xdssc' => 'application/dssc+xml', 'xdw' => 'application/vnd.fujixerox.docuworks', 'xel' => 'application/xcap-el+xml', 'xenc' => 'application/xenc+xml', 'xer' => 'application/patch-ops-error+xml', 'xfdf' => 'application/xfdf', 'xfdl' => 'application/vnd.xfdl', 'xht' => 'application/xhtml+xml', 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', 'xhtml' => 'application/xhtml+xml', 'xhvml' => 'application/xv+xml', 'xif' => 'image/vnd.xiff', 'xl' => 'application/excel', 'xla' => 'application/vnd.ms-excel', 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', 'xlc' => 'application/vnd.ms-excel', 'xlf' => 'application/xliff+xml', 'xlm' => 'application/vnd.ms-excel', 'xls' => 'application/vnd.ms-excel', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlt' => 'application/vnd.ms-excel', 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xlw' => 'application/vnd.ms-excel', 'xm' => 'audio/xm', 'xml' => 'application/xml', 'xns' => 'application/xcap-ns+xml', 'xo' => 'application/vnd.olpc-sugar', 'xop' => 'application/xop+xml', 'xpi' => 'application/x-xpinstall', 'xpl' => 'application/xproc+xml', 'xpm' => 'image/x-xpixmap', 'xpr' => 'application/vnd.is-xpr', 'xps' => 'application/vnd.ms-xpsdocument', 'xpw' => 'application/vnd.intercon.formnet', 'xpx' => 'application/vnd.intercon.formnet', 'xsd' => 'application/xml', 'xsf' => 'application/prs.xsf+xml', 'xsl' => 'application/xml', 'xslt' => 'application/xslt+xml', 'xsm' => 'application/vnd.syncml+xml', 'xspf' => 'application/xspf+xml', 'xul' => 'application/vnd.mozilla.xul+xml', 'xvm' => 'application/xv+xml', 'xvml' => 'application/xv+xml', 'xwd' => 'image/x-xwindowdump', 'xyz' => 'chemical/x-xyz', 'xz' => 'application/x-xz', 'yaml' => 'text/yaml', 'yang' => 'application/yang', 'yin' => 'application/yin+xml', 'yml' => 'text/yaml', 'ymp' => 'text/x-suse-ymp', 'z' => 'application/x-compress', 'z1' => 'application/x-zmachine', 'z2' => 'application/x-zmachine', 'z3' => 'application/x-zmachine', 'z4' => 'application/x-zmachine', 'z5' => 'application/x-zmachine', 'z6' => 'application/x-zmachine', 'z7' => 'application/x-zmachine', 'z8' => 'application/x-zmachine', 'zaz' => 'application/vnd.zzazz.deck+xml', 'zip' => 'application/zip', 'zir' => 'application/vnd.zul', 'zirz' => 'application/vnd.zul', 'zmm' => 'application/vnd.handheld-entertainment+xml', 'zsh' => 'text/x-scriptzsh']; /** * Determines the mimetype of a file by looking at its extension. * * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ public static function fromFilename(string $filename) : ?string { return self::fromExtension(\pathinfo($filename, \PATHINFO_EXTENSION)); } /** * Maps a file extensions to a mimetype. * * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ public static function fromExtension(string $extension) : ?string { return self::MIME_TYPES[\strtolower($extension)] ?? null; } } vendor_prefixed/guzzlehttp/psr7/src/LazyOpenStream.php 0000644 00000002340 15174712003 0017256 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Lazily reads or writes to a file that is opened only after an IO operation * take place on the stream. */ final class LazyOpenStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var string */ private $filename; /** @var string */ private $mode; /** * @var StreamInterface */ private $stream; /** * @param string $filename File to lazily open * @param string $mode fopen mode to use when opening the stream */ public function __construct(string $filename, string $mode) { $this->filename = $filename; $this->mode = $mode; // unsetting the property forces the first access to go through // __get(). unset($this->stream); } /** * Creates the underlying stream lazily when required. */ protected function createStream() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen($this->filename, $this->mode)); } } vendor_prefixed/guzzlehttp/psr7/src/UploadedFile.php 0000644 00000012032 15174712003 0016675 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use InvalidArgumentException; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface; use RuntimeException; class UploadedFile implements \YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface { private const ERROR_MAP = [\UPLOAD_ERR_OK => 'UPLOAD_ERR_OK', \UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE', \UPLOAD_ERR_FORM_SIZE => 'UPLOAD_ERR_FORM_SIZE', \UPLOAD_ERR_PARTIAL => 'UPLOAD_ERR_PARTIAL', \UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE', \UPLOAD_ERR_NO_TMP_DIR => 'UPLOAD_ERR_NO_TMP_DIR', \UPLOAD_ERR_CANT_WRITE => 'UPLOAD_ERR_CANT_WRITE', \UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION']; /** * @var string|null */ private $clientFilename; /** * @var string|null */ private $clientMediaType; /** * @var int */ private $error; /** * @var string|null */ private $file; /** * @var bool */ private $moved = \false; /** * @var int|null */ private $size; /** * @var StreamInterface|null */ private $stream; /** * @param StreamInterface|string|resource $streamOrFile */ public function __construct($streamOrFile, ?int $size, int $errorStatus, ?string $clientFilename = null, ?string $clientMediaType = null) { $this->setError($errorStatus); $this->size = $size; $this->clientFilename = $clientFilename; $this->clientMediaType = $clientMediaType; if ($this->isOk()) { $this->setStreamOrFile($streamOrFile); } } /** * Depending on the value set file or stream variable * * @param StreamInterface|string|resource $streamOrFile * * @throws InvalidArgumentException */ private function setStreamOrFile($streamOrFile) : void { if (\is_string($streamOrFile)) { $this->file = $streamOrFile; } elseif (\is_resource($streamOrFile)) { $this->stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($streamOrFile); } elseif ($streamOrFile instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { $this->stream = $streamOrFile; } else { throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile'); } } /** * @throws InvalidArgumentException */ private function setError(int $error) : void { if (!isset(\YoastSEO_Vendor\GuzzleHttp\Psr7\UploadedFile::ERROR_MAP[$error])) { throw new \InvalidArgumentException('Invalid error status for UploadedFile'); } $this->error = $error; } private static function isStringNotEmpty($param) : bool { return \is_string($param) && \false === empty($param); } /** * Return true if there is no upload error */ private function isOk() : bool { return $this->error === \UPLOAD_ERR_OK; } public function isMoved() : bool { return $this->moved; } /** * @throws RuntimeException if is moved or not ok */ private function validateActive() : void { if (\false === $this->isOk()) { throw new \RuntimeException(\sprintf('Cannot retrieve stream due to upload error (%s)', self::ERROR_MAP[$this->error])); } if ($this->isMoved()) { throw new \RuntimeException('Cannot retrieve stream after it has already been moved'); } } public function getStream() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { $this->validateActive(); if ($this->stream instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { return $this->stream; } /** @var string $file */ $file = $this->file; return new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($file, 'r+'); } public function moveTo($targetPath) : void { $this->validateActive(); if (\false === self::isStringNotEmpty($targetPath)) { throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); } if ($this->file) { $this->moved = \PHP_SAPI === 'cli' ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath); } else { \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($this->getStream(), new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($targetPath, 'w')); $this->moved = \true; } if (\false === $this->moved) { throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath)); } } public function getSize() : ?int { return $this->size; } public function getError() : int { return $this->error; } public function getClientFilename() : ?string { return $this->clientFilename; } public function getClientMediaType() : ?string { return $this->clientMediaType; } } vendor_prefixed/guzzlehttp/psr7/src/Request.php 0000644 00000010143 15174712003 0015771 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use InvalidArgumentException; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * PSR-7 request implementation. */ class Request implements \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { use MessageTrait; /** @var string */ private $method; /** @var string|null */ private $requestTarget; /** @var UriInterface */ private $uri; /** * @param string $method HTTP method * @param string|UriInterface $uri URI * @param (string|string[])[] $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version */ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1') { $this->assertMethod($method); if (!$uri instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) { $uri = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($uri); } $this->method = \strtoupper($method); $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; if (!isset($this->headerNames['host'])) { $this->updateHostFromUri(); } if ($body !== '' && $body !== null) { $this->stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($body); } } public function getRequestTarget() : string { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); if ($target === '') { $target = '/'; } if ($this->uri->getQuery() != '') { $target .= '?' . $this->uri->getQuery(); } return $target; } public function withRequestTarget($requestTarget) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { if (\preg_match('#\\s#', $requestTarget)) { throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); } $new = clone $this; $new->requestTarget = $requestTarget; return $new; } public function getMethod() : string { return $this->method; } public function withMethod($method) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { $this->assertMethod($method); $new = clone $this; $new->method = \strtoupper($method); return $new; } public function getUri() : \YoastSEO_Vendor\Psr\Http\Message\UriInterface { return $this->uri; } public function withUri(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, $preserveHost = \false) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { if ($uri === $this->uri) { return $this; } $new = clone $this; $new->uri = $uri; if (!$preserveHost || !isset($this->headerNames['host'])) { $new->updateHostFromUri(); } return $new; } private function updateHostFromUri() : void { $host = $this->uri->getHost(); if ($host == '') { return; } if (($port = $this->uri->getPort()) !== null) { $host .= ':' . $port; } if (isset($this->headerNames['host'])) { $header = $this->headerNames['host']; } else { $header = 'Host'; $this->headerNames['host'] = 'Host'; } // Ensure Host is the first header. // See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 $this->headers = [$header => [$host]] + $this->headers; } /** * @param mixed $method */ private function assertMethod($method) : void { if (!\is_string($method) || $method === '') { throw new \InvalidArgumentException('Method must be a non-empty string.'); } } } vendor_prefixed/guzzlehttp/psr7/src/CachingStream.php 0000644 00000011304 15174712003 0017051 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Stream decorator that can cache previously read bytes from a sequentially * read stream. */ final class CachingStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { use StreamDecoratorTrait; /** @var StreamInterface Stream being wrapped */ private $remoteStream; /** @var int Number of bytes to skip reading due to a write on the buffer */ private $skipReadBytes = 0; /** * @var StreamInterface */ private $stream; /** * We will treat the buffer object as the body of the stream * * @param StreamInterface $stream Stream to cache. The cursor is assumed to be at the beginning of the stream. * @param StreamInterface $target Optionally specify where data is cached */ public function __construct(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $target = null) { $this->remoteStream = $stream; $this->stream = $target ?: new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'r+')); } public function getSize() : ?int { $remoteSize = $this->remoteStream->getSize(); if (null === $remoteSize) { return null; } return \max($this->stream->getSize(), $remoteSize); } public function rewind() : void { $this->seek(0); } public function seek($offset, $whence = \SEEK_SET) : void { if ($whence === \SEEK_SET) { $byte = $offset; } elseif ($whence === \SEEK_CUR) { $byte = $offset + $this->tell(); } elseif ($whence === \SEEK_END) { $size = $this->remoteStream->getSize(); if ($size === null) { $size = $this->cacheEntireStream(); } $byte = $size + $offset; } else { throw new \InvalidArgumentException('Invalid whence'); } $diff = $byte - $this->stream->getSize(); if ($diff > 0) { // Read the remoteStream until we have read in at least the amount // of bytes requested, or we reach the end of the file. while ($diff > 0 && !$this->remoteStream->eof()) { $this->read($diff); $diff = $byte - $this->stream->getSize(); } } else { // We can just do a normal seek since we've already seen this byte. $this->stream->seek($byte); } } public function read($length) : string { // Perform a regular read on any previously read data from the buffer $data = $this->stream->read($length); $remaining = $length - \strlen($data); // More data was requested so read from the remote stream if ($remaining) { // If data was written to the buffer in a position that would have // been filled from the remote stream, then we must skip bytes on // the remote stream to emulate overwriting bytes from that // position. This mimics the behavior of other PHP stream wrappers. $remoteData = $this->remoteStream->read($remaining + $this->skipReadBytes); if ($this->skipReadBytes) { $len = \strlen($remoteData); $remoteData = \substr($remoteData, $this->skipReadBytes); $this->skipReadBytes = \max(0, $this->skipReadBytes - $len); } $data .= $remoteData; $this->stream->write($remoteData); } return $data; } public function write($string) : int { // When appending to the end of the currently read stream, you'll want // to skip bytes from being read from the remote stream to emulate // other stream wrappers. Basically replacing bytes of data of a fixed // length. $overflow = \strlen($string) + $this->tell() - $this->remoteStream->tell(); if ($overflow > 0) { $this->skipReadBytes += $overflow; } return $this->stream->write($string); } public function eof() : bool { return $this->stream->eof() && $this->remoteStream->eof(); } /** * Close both the remote stream and buffer stream */ public function close() : void { $this->remoteStream->close(); $this->stream->close(); } private function cacheEntireStream() : int { $target = new \YoastSEO_Vendor\GuzzleHttp\Psr7\FnStream(['write' => 'strlen']); \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($this, $target); return $this->tell(); } } vendor_prefixed/guzzlehttp/psr7/src/Query.php 0000644 00000010023 15174712003 0015443 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; final class Query { /** * Parse a query string into an associative array. * * If multiple values are found for the same key, the value of that key * value pair will become an array. This function does not parse nested * PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2` * will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`. * * @param string $str Query string to parse * @param int|bool $urlEncoding How the query string is encoded */ public static function parse(string $str, $urlEncoding = \true) : array { $result = []; if ($str === '') { return $result; } if ($urlEncoding === \true) { $decoder = function ($value) { return \rawurldecode(\str_replace('+', ' ', (string) $value)); }; } elseif ($urlEncoding === \PHP_QUERY_RFC3986) { $decoder = 'rawurldecode'; } elseif ($urlEncoding === \PHP_QUERY_RFC1738) { $decoder = 'urldecode'; } else { $decoder = function ($str) { return $str; }; } foreach (\explode('&', $str) as $kvp) { $parts = \explode('=', $kvp, 2); $key = $decoder($parts[0]); $value = isset($parts[1]) ? $decoder($parts[1]) : null; if (!\array_key_exists($key, $result)) { $result[$key] = $value; } else { if (!\is_array($result[$key])) { $result[$key] = [$result[$key]]; } $result[$key][] = $value; } } return $result; } /** * Build a query string from an array of key value pairs. * * This function can use the return value of `parse()` to build a query * string. This function does not modify the provided keys when an array is * encountered (like `http_build_query()` would). * * @param array $params Query string parameters. * @param int|false $encoding Set to false to not encode, * PHP_QUERY_RFC3986 to encode using * RFC3986, or PHP_QUERY_RFC1738 to * encode using RFC1738. * @param bool $treatBoolsAsInts Set to true to encode as 0/1, and * false as false/true. */ public static function build(array $params, $encoding = \PHP_QUERY_RFC3986, bool $treatBoolsAsInts = \true) : string { if (!$params) { return ''; } if ($encoding === \false) { $encoder = function (string $str) : string { return $str; }; } elseif ($encoding === \PHP_QUERY_RFC3986) { $encoder = 'rawurlencode'; } elseif ($encoding === \PHP_QUERY_RFC1738) { $encoder = 'urlencode'; } else { throw new \InvalidArgumentException('Invalid type'); } $castBool = $treatBoolsAsInts ? static function ($v) { return (int) $v; } : static function ($v) { return $v ? 'true' : 'false'; }; $qs = ''; foreach ($params as $k => $v) { $k = $encoder((string) $k); if (!\is_array($v)) { $qs .= $k; $v = \is_bool($v) ? $castBool($v) : $v; if ($v !== null) { $qs .= '=' . $encoder((string) $v); } $qs .= '&'; } else { foreach ($v as $vv) { $qs .= $k; $vv = \is_bool($vv) ? $castBool($vv) : $vv; if ($vv !== null) { $qs .= '=' . $encoder((string) $vv); } $qs .= '&'; } } } return $qs ? (string) \substr($qs, 0, -1) : ''; } } vendor_prefixed/guzzlehttp/psr7/src/AppendStream.php 0000644 00000013614 15174712003 0016732 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Reads from multiple streams, one after the other. * * This is a read-only stream decorator. */ final class AppendStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { /** @var StreamInterface[] Streams being decorated */ private $streams = []; /** @var bool */ private $seekable = \true; /** @var int */ private $current = 0; /** @var int */ private $pos = 0; /** * @param StreamInterface[] $streams Streams to decorate. Each stream must * be readable. */ public function __construct(array $streams = []) { foreach ($streams as $stream) { $this->addStream($stream); } } public function __toString() : string { try { $this->rewind(); return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } \trigger_error(\sprintf('%s::__toString exception: %s', self::class, (string) $e), \E_USER_ERROR); return ''; } } /** * Add a stream to the AppendStream * * @param StreamInterface $stream Stream to append. Must be readable. * * @throws \InvalidArgumentException if the stream is not readable */ public function addStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream) : void { if (!$stream->isReadable()) { throw new \InvalidArgumentException('Each stream must be readable'); } // The stream is only seekable if all streams are seekable if (!$stream->isSeekable()) { $this->seekable = \false; } $this->streams[] = $stream; } public function getContents() : string { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToString($this); } /** * Closes each attached stream. */ public function close() : void { $this->pos = $this->current = 0; $this->seekable = \true; foreach ($this->streams as $stream) { $stream->close(); } $this->streams = []; } /** * Detaches each attached stream. * * Returns null as it's not clear which underlying stream resource to return. */ public function detach() { $this->pos = $this->current = 0; $this->seekable = \true; foreach ($this->streams as $stream) { $stream->detach(); } $this->streams = []; return null; } public function tell() : int { return $this->pos; } /** * Tries to calculate the size by adding the size of each stream. * * If any of the streams do not return a valid number, then the size of the * append stream cannot be determined and null is returned. */ public function getSize() : ?int { $size = 0; foreach ($this->streams as $stream) { $s = $stream->getSize(); if ($s === null) { return null; } $size += $s; } return $size; } public function eof() : bool { return !$this->streams || $this->current >= \count($this->streams) - 1 && $this->streams[$this->current]->eof(); } public function rewind() : void { $this->seek(0); } /** * Attempts to seek to the given position. Only supports SEEK_SET. */ public function seek($offset, $whence = \SEEK_SET) : void { if (!$this->seekable) { throw new \RuntimeException('This AppendStream is not seekable'); } elseif ($whence !== \SEEK_SET) { throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); } $this->pos = $this->current = 0; // Rewind each stream foreach ($this->streams as $i => $stream) { try { $stream->rewind(); } catch (\Exception $e) { throw new \RuntimeException('Unable to seek stream ' . $i . ' of the AppendStream', 0, $e); } } // Seek to the actual position by reading from each stream while ($this->pos < $offset && !$this->eof()) { $result = $this->read(\min(8096, $offset - $this->pos)); if ($result === '') { break; } } } /** * Reads from all of the appended streams until the length is met or EOF. */ public function read($length) : string { $buffer = ''; $total = \count($this->streams) - 1; $remaining = $length; $progressToNext = \false; while ($remaining > 0) { // Progress to the next stream if needed. if ($progressToNext || $this->streams[$this->current]->eof()) { $progressToNext = \false; if ($this->current === $total) { break; } ++$this->current; } $result = $this->streams[$this->current]->read($remaining); if ($result === '') { $progressToNext = \true; continue; } $buffer .= $result; $remaining = $length - \strlen($buffer); } $this->pos += \strlen($buffer); return $buffer; } public function isReadable() : bool { return \true; } public function isWritable() : bool { return \false; } public function isSeekable() : bool { return $this->seekable; } public function write($string) : int { throw new \RuntimeException('Cannot write to an AppendStream'); } /** * @return mixed */ public function getMetadata($key = null) { return $key ? null : []; } } vendor_prefixed/guzzlehttp/psr7/src/MessageTrait.php 0000644 00000017131 15174712003 0016735 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\MessageInterface; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Trait implementing functionality common to requests and responses. */ trait MessageTrait { /** @var string[][] Map of all registered headers, as original name => array of values */ private $headers = []; /** @var string[] Map of lowercase header name => original name at registration */ private $headerNames = []; /** @var string */ private $protocol = '1.1'; /** @var StreamInterface|null */ private $stream; public function getProtocolVersion() : string { return $this->protocol; } public function withProtocolVersion($version) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { if ($this->protocol === $version) { return $this; } $new = clone $this; $new->protocol = $version; return $new; } public function getHeaders() : array { return $this->headers; } public function hasHeader($header) : bool { return isset($this->headerNames[\strtolower($header)]); } public function getHeader($header) : array { $header = \strtolower($header); if (!isset($this->headerNames[$header])) { return []; } $header = $this->headerNames[$header]; return $this->headers[$header]; } public function getHeaderLine($header) : string { return \implode(', ', $this->getHeader($header)); } public function withHeader($header, $value) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = \strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { unset($new->headers[$new->headerNames[$normalized]]); } $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; return $new; } public function withAddedHeader($header, $value) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = \strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $new->headers[$header] = \array_merge($this->headers[$header], $value); } else { $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } return $new; } public function withoutHeader($header) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { $normalized = \strtolower($header); if (!isset($this->headerNames[$normalized])) { return $this; } $header = $this->headerNames[$normalized]; $new = clone $this; unset($new->headers[$header], $new->headerNames[$normalized]); return $new; } public function getBody() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { if (!$this->stream) { $this->stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor(''); } return $this->stream; } public function withBody(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $body) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { if ($body === $this->stream) { return $this; } $new = clone $this; $new->stream = $body; return $new; } /** * @param (string|string[])[] $headers */ private function setHeaders(array $headers) : void { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { // Numeric array keys are converted to int by PHP. $header = (string) $header; $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = \strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = \array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; } } } /** * @param mixed $value * * @return string[] */ private function normalizeHeaderValue($value) : array { if (!\is_array($value)) { return $this->trimAndValidateHeaderValues([$value]); } return $this->trimAndValidateHeaderValues($value); } /** * Trims whitespace from the header values. * * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. * * header-field = field-name ":" OWS field-value OWS * OWS = *( SP / HTAB ) * * @param mixed[] $values Header values * * @return string[] Trimmed header values * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 */ private function trimAndValidateHeaderValues(array $values) : array { return \array_map(function ($value) { if (!\is_scalar($value) && null !== $value) { throw new \InvalidArgumentException(\sprintf('Header value must be scalar or null but %s provided.', \is_object($value) ? \get_class($value) : \gettype($value))); } $trimmed = \trim((string) $value, " \t"); $this->assertValue($trimmed); return $trimmed; }, \array_values($values)); } /** * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 * * @param mixed $header */ private function assertHeader($header) : void { if (!\is_string($header)) { throw new \InvalidArgumentException(\sprintf('Header name must be a string but %s provided.', \is_object($header) ? \get_class($header) : \gettype($header))); } if (!\preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) { throw new \InvalidArgumentException(\sprintf('"%s" is not valid header name.', $header)); } } /** * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 * * field-value = *( field-content / obs-fold ) * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] * field-vchar = VCHAR / obs-text * VCHAR = %x21-7E * obs-text = %x80-FF * obs-fold = CRLF 1*( SP / HTAB ) */ private function assertValue(string $value) : void { // The regular expression intentionally does not support the obs-fold production, because as // per RFC 7230#3.2.4: // // A sender MUST NOT generate a message that includes // line folding (i.e., that has any field-value that contains a match to // the obs-fold rule) unless the message is intended for packaging // within the message/http media type. // // Clients must not send a request with line folding and a server sending folded headers is // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting // folding is not likely to break any legitimate use case. if (!\preg_match('/^[\\x20\\x09\\x21-\\x7E\\x80-\\xFF]*$/D', $value)) { throw new \InvalidArgumentException(\sprintf('"%s" is not valid header value.', $value)); } } } vendor_prefixed/guzzlehttp/psr7/src/PumpStream.php 0000644 00000011166 15174712003 0016444 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Provides a read only stream that pumps data from a PHP callable. * * When invoking the provided callable, the PumpStream will pass the amount of * data requested to read to the callable. The callable can choose to ignore * this value and return fewer or more bytes than requested. Any extra data * returned by the provided callable is buffered internally until drained using * the read() function of the PumpStream. The provided callable MUST return * false when there is no more data to read. */ final class PumpStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { /** @var callable(int): (string|false|null)|null */ private $source; /** @var int|null */ private $size; /** @var int */ private $tellPos = 0; /** @var array */ private $metadata; /** @var BufferStream */ private $buffer; /** * @param callable(int): (string|false|null) $source Source of the stream data. The callable MAY * accept an integer argument used to control the * amount of data to return. The callable MUST * return a string when called, or false|null on error * or EOF. * @param array{size?: int, metadata?: array} $options Stream options: * - metadata: Hash of metadata to use with stream. * - size: Size of the stream, if known. */ public function __construct(callable $source, array $options = []) { $this->source = $source; $this->size = $options['size'] ?? null; $this->metadata = $options['metadata'] ?? []; $this->buffer = new \YoastSEO_Vendor\GuzzleHttp\Psr7\BufferStream(); } public function __toString() : string { try { return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToString($this); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } \trigger_error(\sprintf('%s::__toString exception: %s', self::class, (string) $e), \E_USER_ERROR); return ''; } } public function close() : void { $this->detach(); } public function detach() { $this->tellPos = 0; $this->source = null; return null; } public function getSize() : ?int { return $this->size; } public function tell() : int { return $this->tellPos; } public function eof() : bool { return $this->source === null; } public function isSeekable() : bool { return \false; } public function rewind() : void { $this->seek(0); } public function seek($offset, $whence = \SEEK_SET) : void { throw new \RuntimeException('Cannot seek a PumpStream'); } public function isWritable() : bool { return \false; } public function write($string) : int { throw new \RuntimeException('Cannot write to a PumpStream'); } public function isReadable() : bool { return \true; } public function read($length) : string { $data = $this->buffer->read($length); $readLen = \strlen($data); $this->tellPos += $readLen; $remaining = $length - $readLen; if ($remaining) { $this->pump($remaining); $data .= $this->buffer->read($remaining); $this->tellPos += \strlen($data) - $readLen; } return $data; } public function getContents() : string { $result = ''; while (!$this->eof()) { $result .= $this->read(1000000); } return $result; } /** * @return mixed */ public function getMetadata($key = null) { if (!$key) { return $this->metadata; } return $this->metadata[$key] ?? null; } private function pump(int $length) : void { if ($this->source !== null) { do { $data = ($this->source)($length); if ($data === \false || $data === null) { $this->source = null; return; } $this->buffer->write($data); $length -= \strlen($data); } while ($length > 0); } } } vendor_prefixed/guzzlehttp/psr7/src/Header.php 0000644 00000007561 15174712003 0015543 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; final class Header { /** * Parse an array of header values containing ";" separated data into an * array of associative arrays representing the header key value pair data * of the header. When a parameter does not contain a value, but just * contains a key, this function will inject a key with a '' string value. * * @param string|array $header Header to parse into components. */ public static function parse($header) : array { static $trimmed = "\"' \n\t\r"; $params = $matches = []; foreach ((array) $header as $value) { foreach (self::splitList($value) as $val) { $part = []; foreach (\preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) ?: [] as $kvp) { if (\preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { $m = $matches[0]; if (isset($m[1])) { $part[\trim($m[0], $trimmed)] = \trim($m[1], $trimmed); } else { $part[] = \trim($m[0], $trimmed); } } } if ($part) { $params[] = $part; } } } return $params; } /** * Converts an array of header values that may contain comma separated * headers into an array of headers with no comma separated values. * * @param string|array $header Header to normalize. * * @deprecated Use self::splitList() instead. */ public static function normalize($header) : array { $result = []; foreach ((array) $header as $value) { foreach (self::splitList($value) as $parsed) { $result[] = $parsed; } } return $result; } /** * Splits a HTTP header defined to contain a comma-separated list into * each individual value. Empty values will be removed. * * Example headers include 'accept', 'cache-control' and 'if-none-match'. * * This method must not be used to parse headers that are not defined as * a list, such as 'user-agent' or 'set-cookie'. * * @param string|string[] $values Header value as returned by MessageInterface::getHeader() * * @return string[] */ public static function splitList($values) : array { if (!\is_array($values)) { $values = [$values]; } $result = []; foreach ($values as $value) { if (!\is_string($value)) { throw new \TypeError('$header must either be a string or an array containing strings.'); } $v = ''; $isQuoted = \false; $isEscaped = \false; for ($i = 0, $max = \strlen($value); $i < $max; ++$i) { if ($isEscaped) { $v .= $value[$i]; $isEscaped = \false; continue; } if (!$isQuoted && $value[$i] === ',') { $v = \trim($v); if ($v !== '') { $result[] = $v; } $v = ''; continue; } if ($isQuoted && $value[$i] === '\\') { $isEscaped = \true; $v .= $value[$i]; continue; } if ($value[$i] === '"') { $isQuoted = !$isQuoted; $v .= $value[$i]; continue; } $v .= $value[$i]; } $v = \trim($v); if ($v !== '') { $result[] = $v; } } return $result; } } vendor_prefixed/guzzlehttp/psr7/src/Rfc7230.php 0000644 00000001232 15174712003 0015366 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; /** * @internal */ final class Rfc7230 { /** * Header related regular expressions (based on amphp/http package) * * Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons. * * @see https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15 * * @license https://github.com/amphp/http/blob/v1.0.1/LICENSE */ public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\\]?={}\x01- ]++):[ \t]*+((?:[ \t]*+[!-~\x80-\xff]++)*+)[ \t]*+\r?\n)m"; public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; } vendor_prefixed/guzzlehttp/psr7/src/FnStream.php 0000644 00000010416 15174712003 0016063 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Compose stream implementations based on a hash of functions. * * Allows for easy testing and extension of a provided stream without needing * to create a concrete class for a simple extension point. */ #[\AllowDynamicProperties] final class FnStream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { private const SLOTS = ['__toString', 'close', 'detach', 'rewind', 'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write', 'isReadable', 'read', 'getContents', 'getMetadata']; /** @var array<string, callable> */ private $methods; /** * @param array<string, callable> $methods Hash of method name to a callable. */ public function __construct(array $methods) { $this->methods = $methods; // Create the functions on the class foreach ($methods as $name => $fn) { $this->{'_fn_' . $name} = $fn; } } /** * Lazily determine which methods are not implemented. * * @throws \BadMethodCallException */ public function __get(string $name) : void { throw new \BadMethodCallException(\str_replace('_fn_', '', $name) . '() is not implemented in the FnStream'); } /** * The close method is called on the underlying stream only if possible. */ public function __destruct() { if (isset($this->_fn_close)) { ($this->_fn_close)(); } } /** * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. * * @throws \LogicException */ public function __wakeup() : void { throw new \LogicException('FnStream should never be unserialized'); } /** * Adds custom functionality to an underlying stream by intercepting * specific method calls. * * @param StreamInterface $stream Stream to decorate * @param array<string, callable> $methods Hash of method name to a closure * * @return FnStream */ public static function decorate(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $methods) { // If any of the required methods were not provided, then simply // proxy to the decorated stream. foreach (\array_diff(self::SLOTS, \array_keys($methods)) as $diff) { /** @var callable $callable */ $callable = [$stream, $diff]; $methods[$diff] = $callable; } return new self($methods); } public function __toString() : string { try { /** @var string */ return ($this->_fn___toString)(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } \trigger_error(\sprintf('%s::__toString exception: %s', self::class, (string) $e), \E_USER_ERROR); return ''; } } public function close() : void { ($this->_fn_close)(); } public function detach() { return ($this->_fn_detach)(); } public function getSize() : ?int { return ($this->_fn_getSize)(); } public function tell() : int { return ($this->_fn_tell)(); } public function eof() : bool { return ($this->_fn_eof)(); } public function isSeekable() : bool { return ($this->_fn_isSeekable)(); } public function rewind() : void { ($this->_fn_rewind)(); } public function seek($offset, $whence = \SEEK_SET) : void { ($this->_fn_seek)($offset, $whence); } public function isWritable() : bool { return ($this->_fn_isWritable)(); } public function write($string) : int { return ($this->_fn_write)($string); } public function isReadable() : bool { return ($this->_fn_isReadable)(); } public function read($length) : string { return ($this->_fn_read)($length); } public function getContents() : string { return ($this->_fn_getContents)(); } /** * @return mixed */ public function getMetadata($key = null) { return ($this->_fn_getMetadata)($key); } } vendor_prefixed/guzzlehttp/psr7/src/UriComparator.php 0000644 00000002402 15174712003 0017127 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Provides methods to determine if a modified URL should be considered cross-origin. * * @author Graham Campbell */ final class UriComparator { /** * Determines if a modified URL should be considered cross-origin with * respect to an original URL. */ public static function isCrossOrigin(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $original, \YoastSEO_Vendor\Psr\Http\Message\UriInterface $modified) : bool { if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) { return \true; } if ($original->getScheme() !== $modified->getScheme()) { return \true; } if (self::computePort($original) !== self::computePort($modified)) { return \true; } return \false; } private static function computePort(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : int { $port = $uri->getPort(); if (null !== $port) { return $port; } return 'https' === $uri->getScheme() ? 443 : 80; } private function __construct() { // cannot be instantiated } } vendor_prefixed/guzzlehttp/psr7/src/Stream.php 0000644 00000016466 15174712003 0015612 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\GuzzleHttp\Psr7; use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * PHP stream implementation. */ class Stream implements \YoastSEO_Vendor\Psr\Http\Message\StreamInterface { /** * @see https://www.php.net/manual/en/function.fopen.php * @see https://www.php.net/manual/en/function.gzopen.php */ private const READABLE_MODES = '/r|a\\+|ab\\+|w\\+|wb\\+|x\\+|xb\\+|c\\+|cb\\+/'; private const WRITABLE_MODES = '/a|w|r\\+|rb\\+|rw|x|c/'; /** @var resource */ private $stream; /** @var int|null */ private $size; /** @var bool */ private $seekable; /** @var bool */ private $readable; /** @var bool */ private $writable; /** @var string|null */ private $uri; /** @var mixed[] */ private $customMetadata; /** * This constructor accepts an associative array of options. * * - size: (int) If a read stream would otherwise have an indeterminate * size, but the size is known due to foreknowledge, then you can * provide that size, in bytes. * - metadata: (array) Any additional metadata to return when the metadata * of the stream is accessed. * * @param resource $stream Stream resource to wrap. * @param array{size?: int, metadata?: array} $options Associative array of options. * * @throws \InvalidArgumentException if the stream is not a stream resource */ public function __construct($stream, array $options = []) { if (!\is_resource($stream)) { throw new \InvalidArgumentException('Stream must be a resource'); } if (isset($options['size'])) { $this->size = $options['size']; } $this->customMetadata = $options['metadata'] ?? []; $this->stream = $stream; $meta = \stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; $this->readable = (bool) \preg_match(self::READABLE_MODES, $meta['mode']); $this->writable = (bool) \preg_match(self::WRITABLE_MODES, $meta['mode']); $this->uri = $this->getMetadata('uri'); } /** * Closes the stream when the destructed */ public function __destruct() { $this->close(); } public function __toString() : string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } \trigger_error(\sprintf('%s::__toString exception: %s', self::class, (string) $e), \E_USER_ERROR); return ''; } } public function getContents() : string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryGetContents($this->stream); } public function close() : void { if (isset($this->stream)) { if (\is_resource($this->stream)) { \fclose($this->stream); } $this->detach(); } } public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->size = $this->uri = null; $this->readable = $this->writable = $this->seekable = \false; return $result; } public function getSize() : ?int { if ($this->size !== null) { return $this->size; } if (!isset($this->stream)) { return null; } // Clear the stat cache if the stream has a URI if ($this->uri) { \clearstatcache(\true, $this->uri); } $stats = \fstat($this->stream); if (\is_array($stats) && isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } return null; } public function isReadable() : bool { return $this->readable; } public function isWritable() : bool { return $this->writable; } public function isSeekable() : bool { return $this->seekable; } public function eof() : bool { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } return \feof($this->stream); } public function tell() : int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $result = \ftell($this->stream); if ($result === \false) { throw new \RuntimeException('Unable to determine stream position'); } return $result; } public function rewind() : void { $this->seek(0); } public function seek($offset, $whence = \SEEK_SET) : void { $whence = (int) $whence; if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } if (\fseek($this->stream, $offset, $whence) === -1) { throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, \true)); } } public function read($length) : string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } if ($length < 0) { throw new \RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } try { $string = \fread($this->stream, $length); } catch (\Exception $e) { throw new \RuntimeException('Unable to read from stream', 0, $e); } if (\false === $string) { throw new \RuntimeException('Unable to read from stream'); } return $string; } public function write($string) : int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } // We can't know the size after writing anything $this->size = null; $result = \fwrite($this->stream, $string); if ($result === \false) { throw new \RuntimeException('Unable to write to stream'); } return $result; } /** * @return mixed */ public function getMetadata($key = null) { if (!isset($this->stream)) { return $key ? null : []; } elseif (!$key) { return $this->customMetadata + \stream_get_meta_data($this->stream); } elseif (isset($this->customMetadata[$key])) { return $this->customMetadata[$key]; } $meta = \stream_get_meta_data($this->stream); return $meta[$key] ?? null; } } vendor_prefixed/symfony/deprecation-contracts/function.php 0000644 00000001760 15174712003 0020270 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trigger_deprecation')) { /** * Triggers a silenced deprecation notice. * * @param string $package The name of the Composer package that is triggering the deprecation * @param string $version The version of the package that introduced the deprecation * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @author Nicolas Grekas <p@tchwork.com> */ function trigger_deprecation(string $package, string $version, string $message, ...$args): void { @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); } } vendor_prefixed/symfony/service-contracts/ResetInterface.php 0000644 00000001766 15174712003 0020517 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Contracts\Service; /** * Provides a way to reset an object to its initial state. * * When calling the "reset()" method on an object, it should be put back to its * initial state. This usually means clearing any internal buffers and forwarding * the call to internal dependencies. All properties of the object should be put * back to the same state it had when it was first ready to use. * * This method could be called, for example, to recycle objects that are used as * services, so that they can be used to handle several requests in the same * process loop (note that we advise making your services stateless instead of * implementing this interface when possible.) */ interface ResetInterface { public function reset(); } vendor_prefixed/symfony/service-contracts/ServiceProviderInterface.php 0000644 00000002370 15174712003 0022540 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Contracts\Service; use YoastSEO_Vendor\Psr\Container\ContainerInterface; /** * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. * * @author Nicolas Grekas <p@tchwork.com> * @author Mateusz Sip <mateusz.sip@gmail.com> */ interface ServiceProviderInterface extends \YoastSEO_Vendor\Psr\Container\ContainerInterface { /** * Returns an associative array of service types keyed by the identifiers provided by the current container. * * Examples: * * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface * * ['foo' => '?'] means the container provides service name "foo" of unspecified type * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null * * @return string[] The provided service types, keyed by service names */ public function getProvidedServices() : array; } vendor_prefixed/symfony/service-contracts/ServiceLocatorTrait.php 0000644 00000007672 15174712003 0021546 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Contracts\Service; use YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface; use YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface; // Help opcache.preload discover always-needed symbols \class_exists(\YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface::class); \class_exists(\YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface::class); /** * A trait to help implement ServiceProviderInterface. * * @author Robin Chalas <robin.chalas@gmail.com> * @author Nicolas Grekas <p@tchwork.com> */ trait ServiceLocatorTrait { private $factories; private $loading = []; private $providedTypes; /** * @param callable[] $factories */ public function __construct(array $factories) { $this->factories = $factories; } /** * {@inheritdoc} * * @return bool */ public function has(string $id) { return isset($this->factories[$id]); } /** * {@inheritdoc} * * @return mixed */ public function get(string $id) { if (!isset($this->factories[$id])) { throw $this->createNotFoundException($id); } if (isset($this->loading[$id])) { $ids = \array_values($this->loading); $ids = \array_slice($this->loading, \array_search($id, $ids)); $ids[] = $id; throw $this->createCircularReferenceException($id, $ids); } $this->loading[$id] = $id; try { return $this->factories[$id]($this); } finally { unset($this->loading[$id]); } } /** * {@inheritdoc} */ public function getProvidedServices() : array { if (null === $this->providedTypes) { $this->providedTypes = []; foreach ($this->factories as $name => $factory) { if (!\is_callable($factory)) { $this->providedTypes[$name] = '?'; } else { $type = (new \ReflectionFunction($factory))->getReturnType(); $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '') . ($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; } } } return $this->providedTypes; } private function createNotFoundException(string $id) : \YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface { if (!($alternatives = \array_keys($this->factories))) { $message = 'is empty...'; } else { $last = \array_pop($alternatives); if ($alternatives) { $message = \sprintf('only knows about the "%s" and "%s" services.', \implode('", "', $alternatives), $last); } else { $message = \sprintf('only knows about the "%s" service.', $last); } } if ($this->loading) { $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', \end($this->loading), $id, $message); } else { $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message); } return new class($message) extends \InvalidArgumentException implements \YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface { }; } private function createCircularReferenceException(string $id, array $path) : \YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface { return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, \implode(' -> ', $path))) extends \RuntimeException implements \YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface { }; } } vendor_prefixed/symfony/dependency-injection/Container.php 0000644 00000036077 15174712003 0020201 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use YoastSEO_Vendor\Symfony\Contracts\Service\ResetInterface; // Help opcache.preload discover always-needed symbols \class_exists(\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\RewindableGenerator::class); \class_exists(\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\ServiceLocator::class); /** * Container is a dependency injection container. * * It gives access to object instances (services). * Services and parameters are simple key/pair stores. * The container can have four possible behaviors when a service * does not exist (or is not initialized for the last case): * * * EXCEPTION_ON_INVALID_REFERENCE: Throws an exception at compilation time (the default) * * NULL_ON_INVALID_REFERENCE: Returns null * * IGNORE_ON_INVALID_REFERENCE: Ignores the wrapping command asking for the reference * (for instance, ignore a setter if the service does not exist) * * IGNORE_ON_UNINITIALIZED_REFERENCE: Ignores/returns null for uninitialized services or invalid references * * RUNTIME_EXCEPTION_ON_INVALID_REFERENCE: Throws an exception at runtime * * @author Fabien Potencier <fabien@symfony.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class Container implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface, \YoastSEO_Vendor\Symfony\Contracts\Service\ResetInterface { protected $parameterBag; protected $services = []; protected $privates = []; protected $fileMap = []; protected $methodMap = []; protected $factories = []; protected $aliases = []; protected $loading = []; protected $resolving = []; protected $syntheticIds = []; private $envCache = []; private $compiled = \false; private $getEnv; public function __construct(?\YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface $parameterBag = null) { $this->parameterBag = $parameterBag ?? new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag(); } /** * Compiles the container. * * This method does two things: * * * Parameter values are resolved; * * The parameter bag is frozen. */ public function compile() { $this->parameterBag->resolve(); $this->parameterBag = new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag($this->parameterBag->all()); $this->compiled = \true; } /** * Returns true if the container is compiled. * * @return bool */ public function isCompiled() { return $this->compiled; } /** * Gets the service container parameter bag. * * @return ParameterBagInterface */ public function getParameterBag() { return $this->parameterBag; } /** * Gets a parameter. * * @return array|bool|string|int|float|\UnitEnum|null * * @throws InvalidArgumentException if the parameter is not defined */ public function getParameter(string $name) { return $this->parameterBag->get($name); } /** * @return bool */ public function hasParameter(string $name) { return $this->parameterBag->has($name); } /** * Sets a parameter. * * @param string $name The parameter name * @param array|bool|string|int|float|\UnitEnum|null $value The parameter value */ public function setParameter(string $name, $value) { $this->parameterBag->set($name, $value); } /** * Sets a service. * * Setting a synthetic service to null resets it: has() returns false and get() * behaves in the same way as if the service was never created. */ public function set(string $id, ?object $service) { // Runs the internal initializer; used by the dumped container to include always-needed files if (isset($this->privates['service_container']) && $this->privates['service_container'] instanceof \Closure) { $initialize = $this->privates['service_container']; unset($this->privates['service_container']); $initialize(); } if ('service_container' === $id) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('You cannot set service "service_container".'); } if (!(isset($this->fileMap[$id]) || isset($this->methodMap[$id]))) { if (isset($this->syntheticIds[$id]) || !isset($this->getRemovedIds()[$id])) { // no-op } elseif (null === $service) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException(\sprintf('The "%s" service is private, you cannot unset it.', $id)); } else { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException(\sprintf('The "%s" service is private, you cannot replace it.', $id)); } } elseif (isset($this->services[$id])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException(\sprintf('The "%s" service is already initialized, you cannot replace it.', $id)); } if (isset($this->aliases[$id])) { unset($this->aliases[$id]); } if (null === $service) { unset($this->services[$id]); return; } $this->services[$id] = $service; } /** * Returns true if the given service is defined. * * @param string $id The service identifier * * @return bool */ public function has(string $id) { if (isset($this->aliases[$id])) { $id = $this->aliases[$id]; } if (isset($this->services[$id])) { return \true; } if ('service_container' === $id) { return \true; } return isset($this->fileMap[$id]) || isset($this->methodMap[$id]); } /** * Gets a service. * * @return object|null * * @throws ServiceCircularReferenceException When a circular reference is detected * @throws ServiceNotFoundException When the service is not defined * @throws \Exception if an exception has been thrown when the service has been resolved * * @see Reference */ public function get(string $id, int $invalidBehavior = 1) { return $this->services[$id] ?? $this->services[$id = $this->aliases[$id] ?? $id] ?? ('service_container' === $id ? $this : ($this->factories[$id] ?? [$this, 'make'])($id, $invalidBehavior)); } /** * Creates a service. * * As a separate method to allow "get()" to use the really fast `??` operator. */ private function make(string $id, int $invalidBehavior) { if (isset($this->loading[$id])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException($id, \array_merge(\array_keys($this->loading), [$id])); } $this->loading[$id] = \true; try { if (isset($this->fileMap[$id])) { return 4 === $invalidBehavior ? null : $this->load($this->fileMap[$id]); } elseif (isset($this->methodMap[$id])) { return 4 === $invalidBehavior ? null : $this->{$this->methodMap[$id]}(); } } catch (\Exception $e) { unset($this->services[$id]); throw $e; } finally { unset($this->loading[$id]); } if (1 === $invalidBehavior) { if (!$id) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id); } if (isset($this->syntheticIds[$id])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, null, null, [], \sprintf('The "%s" service is synthetic, it needs to be set at boot time before it can be used.', $id)); } if (isset($this->getRemovedIds()[$id])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, null, null, [], \sprintf('The "%s" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.', $id)); } $alternatives = []; foreach ($this->getServiceIds() as $knownId) { if ('' === $knownId || '.' === $knownId[0]) { continue; } $lev = \levenshtein($id, $knownId); if ($lev <= \strlen($id) / 3 || \str_contains($knownId, $id)) { $alternatives[] = $knownId; } } throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, null, null, $alternatives); } return null; } /** * Returns true if the given service has actually been initialized. * * @return bool */ public function initialized(string $id) { if (isset($this->aliases[$id])) { $id = $this->aliases[$id]; } if ('service_container' === $id) { return \false; } return isset($this->services[$id]); } /** * {@inheritdoc} */ public function reset() { $services = $this->services + $this->privates; foreach ($services as $service) { try { if ($service instanceof \YoastSEO_Vendor\Symfony\Contracts\Service\ResetInterface) { $service->reset(); } } catch (\Throwable $e) { continue; } } $this->services = $this->factories = $this->privates = []; } /** * Gets all service ids. * * @return string[] */ public function getServiceIds() { return \array_map('strval', \array_unique(\array_merge(['service_container'], \array_keys($this->fileMap), \array_keys($this->methodMap), \array_keys($this->aliases), \array_keys($this->services)))); } /** * Gets service ids that existed at compile time. * * @return array */ public function getRemovedIds() { return []; } /** * Camelizes a string. * * @return string */ public static function camelize(string $id) { return \strtr(\ucwords(\strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']); } /** * A string to underscore. * * @return string */ public static function underscore(string $id) { return \strtolower(\preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], \str_replace('_', '.', $id))); } /** * Creates a service by requiring its factory file. */ protected function load(string $file) { return require $file; } /** * Fetches a variable from the environment. * * @return mixed * * @throws EnvNotFoundException When the environment variable is not found and has no default value */ protected function getEnv(string $name) { if (isset($this->resolving[$envName = "env({$name})"])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException(\array_keys($this->resolving)); } if (isset($this->envCache[$name]) || \array_key_exists($name, $this->envCache)) { return $this->envCache[$name]; } if (!$this->has($id = 'container.env_var_processors_locator')) { $this->set($id, new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ServiceLocator([])); } if (!$this->getEnv) { $this->getEnv = \Closure::fromCallable([$this, 'getEnv']); } $processors = $this->get($id); if (\false !== ($i = \strpos($name, ':'))) { $prefix = \substr($name, 0, $i); $localName = \substr($name, 1 + $i); } else { $prefix = 'string'; $localName = $name; } $processor = $processors->has($prefix) ? $processors->get($prefix) : new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\EnvVarProcessor($this); if (\false === $i) { $prefix = ''; } $this->resolving[$envName] = \true; try { return $this->envCache[$name] = $processor->getEnv($prefix, $localName, $this->getEnv); } finally { unset($this->resolving[$envName]); } } /** * @param string|false $registry * @param string|bool $load * * @return mixed * * @internal */ protected final function getService($registry, string $id, ?string $method, $load) { if ('service_container' === $id) { return $this; } if (\is_string($load)) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException($load); } if (null === $method) { return \false !== $registry ? $this->{$registry}[$id] ?? null : null; } if (\false !== $registry) { return $this->{$registry}[$id] ?? ($this->{$registry}[$id] = $load ? $this->load($method) : $this->{$method}()); } if (!$load) { return $this->{$method}(); } return ($factory = $this->factories[$id] ?? $this->factories['service_container'][$id] ?? null) ? $factory() : $this->load($method); } private function __clone() { } } vendor_prefixed/symfony/dependency-injection/Exception/EnvNotFoundException.php 0000644 00000001066 15174712003 0024267 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * This exception is thrown when an environment variable is not found. * * @author Nicolas Grekas <p@tchwork.com> */ class EnvNotFoundException extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException { } vendor_prefixed/symfony/dependency-injection/Exception/ExceptionInterface.php 0000644 00000001237 15174712003 0023762 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; use YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface; /** * Base ExceptionInterface for Dependency Injection component. * * @author Fabien Potencier <fabien@symfony.com> * @author Bulat Shakirzyanov <bulat@theopenskyproject.com> */ interface ExceptionInterface extends \YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface, \Throwable { } vendor_prefixed/symfony/dependency-injection/Exception/ServiceCircularReferenceException.php 0000644 00000002111 15174712003 0026756 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * This exception is thrown when a circular reference is detected. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class ServiceCircularReferenceException extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException { private $serviceId; private $path; public function __construct(string $serviceId, array $path, ?\Throwable $previous = null) { parent::__construct(\sprintf('Circular reference detected for service "%s", path: "%s".', $serviceId, \implode(' -> ', $path)), 0, $previous); $this->serviceId = $serviceId; $this->path = $path; } public function getServiceId() { return $this->serviceId; } public function getPath() { return $this->path; } } vendor_prefixed/symfony/dependency-injection/Exception/ServiceNotFoundException.php 0000644 00000003606 15174712003 0025141 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; use YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface; /** * This exception is thrown when a non-existent service is requested. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class ServiceNotFoundException extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException implements \YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface { private $id; private $sourceId; private $alternatives; public function __construct(string $id, ?string $sourceId = null, ?\Throwable $previous = null, array $alternatives = [], ?string $msg = null) { if (null !== $msg) { // no-op } elseif (null === $sourceId) { $msg = \sprintf('You have requested a non-existent service "%s".', $id); } else { $msg = \sprintf('The service "%s" has a dependency on a non-existent service "%s".', $sourceId, $id); } if ($alternatives) { if (1 == \count($alternatives)) { $msg .= ' Did you mean this: "'; } else { $msg .= ' Did you mean one of these: "'; } $msg .= \implode('", "', $alternatives) . '"?'; } parent::__construct($msg, 0, $previous); $this->id = $id; $this->sourceId = $sourceId; $this->alternatives = $alternatives; } public function getId() { return $this->id; } public function getSourceId() { return $this->sourceId; } public function getAlternatives() { return $this->alternatives; } } vendor_prefixed/symfony/dependency-injection/Exception/ParameterCircularReferenceException.php 0000644 00000001766 15174712003 0027315 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * This exception is thrown when a circular reference in a parameter is detected. * * @author Fabien Potencier <fabien@symfony.com> */ class ParameterCircularReferenceException extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException { private $parameters; public function __construct(array $parameters, ?\Throwable $previous = null) { parent::__construct(\sprintf('Circular reference detected for parameter "%s" ("%s" > "%s").', $parameters[0], \implode('" > "', $parameters), $parameters[0]), 0, $previous); $this->parameters = $parameters; } public function getParameters() { return $this->parameters; } } vendor_prefixed/symfony/dependency-injection/Exception/LogicException.php 0000644 00000001014 15174712003 0023110 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * Base LogicException for Dependency Injection component. */ class LogicException extends \LogicException implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ExceptionInterface { } vendor_prefixed/symfony/dependency-injection/Exception/RuntimeException.php 0000644 00000001113 15174712003 0023476 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * Base RuntimeException for Dependency Injection component. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ class RuntimeException extends \RuntimeException implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ExceptionInterface { } vendor_prefixed/symfony/dependency-injection/Exception/InvalidArgumentException.php 0000644 00000001151 15174712003 0025146 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception; /** * Base InvalidArgumentException for Dependency Injection component. * * @author Bulat Shakirzyanov <bulat@theopenskyproject.com> */ class InvalidArgumentException extends \InvalidArgumentException implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ExceptionInterface { } vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocatorArgument.php 0000644 00000002404 15174712003 0024453 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Reference; /** * Represents a closure acting as a service locator. * * @author Nicolas Grekas <p@tchwork.com> */ class ServiceLocatorArgument implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\ArgumentInterface { use ReferenceSetArgumentTrait; private $taggedIteratorArgument; /** * @param Reference[]|TaggedIteratorArgument $values */ public function __construct($values = []) { if ($values instanceof \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument) { $this->taggedIteratorArgument = $values; $this->values = []; } else { $this->setValues($values); } } public function getTaggedIteratorArgument() : ?\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument { return $this->taggedIteratorArgument; } } vendor_prefixed/symfony/dependency-injection/Argument/RewindableGenerator.php 0000644 00000001640 15174712003 0023750 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument; /** * @internal */ class RewindableGenerator implements \IteratorAggregate, \Countable { private $generator; private $count; /** * @param int|callable $count */ public function __construct(callable $generator, $count) { $this->generator = $generator; $this->count = $count; } public function getIterator() : \Traversable { $g = $this->generator; return $g(); } public function count() : int { if (\is_callable($count = $this->count)) { $this->count = $count(); } return $this->count; } } vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocator.php 0000644 00000002556 15174712003 0022760 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\Argument; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ServiceLocator as BaseServiceLocator; /** * @author Nicolas Grekas <p@tchwork.com> * * @internal */ class ServiceLocator extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ServiceLocator { private $factory; private $serviceMap; private $serviceTypes; public function __construct(\Closure $factory, array $serviceMap, ?array $serviceTypes = null) { $this->factory = $factory; $this->serviceMap = $serviceMap; $this->serviceTypes = $serviceTypes; parent::__construct($serviceMap); } /** * {@inheritdoc} * * @return mixed */ public function get(string $id) { return isset($this->serviceMap[$id]) ? ($this->factory)(...$this->serviceMap[$id]) : parent::get($id); } /** * {@inheritdoc} */ public function getProvidedServices() : array { return $this->serviceTypes ?? ($this->serviceTypes = \array_map(function () { return '?'; }, $this->serviceMap)); } } vendor_prefixed/symfony/dependency-injection/ContainerInterface.php 0000644 00000005104 15174712003 0022005 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection; use YoastSEO_Vendor\Psr\Container\ContainerInterface as PsrContainerInterface; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; /** * ContainerInterface is the interface implemented by service container classes. * * @author Fabien Potencier <fabien@symfony.com> * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ interface ContainerInterface extends \YoastSEO_Vendor\Psr\Container\ContainerInterface { public const RUNTIME_EXCEPTION_ON_INVALID_REFERENCE = 0; public const EXCEPTION_ON_INVALID_REFERENCE = 1; public const NULL_ON_INVALID_REFERENCE = 2; public const IGNORE_ON_INVALID_REFERENCE = 3; public const IGNORE_ON_UNINITIALIZED_REFERENCE = 4; /** * Sets a service. */ public function set(string $id, ?object $service); /** * Gets a service. * * @param string $id The service identifier * @param int $invalidBehavior The behavior when the service does not exist * * @return object|null * * @throws ServiceCircularReferenceException When a circular reference is detected * @throws ServiceNotFoundException When the service is not defined * * @see Reference */ public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE); /** * @return bool */ public function has(string $id); /** * Check for whether or not a service has been initialized. * * @return bool */ public function initialized(string $id); /** * @return array|bool|string|int|float|\UnitEnum|null * * @throws InvalidArgumentException if the parameter is not defined */ public function getParameter(string $name); /** * @return bool */ public function hasParameter(string $name); /** * Sets a parameter. * * @param string $name The parameter name * @param array|bool|string|int|float|\UnitEnum|null $value The parameter value */ public function setParameter(string $name, $value); } vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBagInterface.php 0000644 00000005075 15174712003 0024756 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; /** * ParameterBagInterface is the interface implemented by objects that manage service container parameters. * * @author Fabien Potencier <fabien@symfony.com> */ interface ParameterBagInterface { /** * Clears all parameters. * * @throws LogicException if the ParameterBagInterface cannot be cleared */ public function clear(); /** * Adds parameters to the service container parameters. * * @throws LogicException if the parameter cannot be added */ public function add(array $parameters); /** * Gets the service container parameters. * * @return array */ public function all(); /** * Gets a service container parameter. * * @return array|bool|string|int|float|\UnitEnum|null * * @throws ParameterNotFoundException if the parameter is not defined */ public function get(string $name); /** * Removes a parameter. */ public function remove(string $name); /** * Sets a service container parameter. * * @param array|bool|string|int|float|\UnitEnum|null $value The parameter value * * @throws LogicException if the parameter cannot be set */ public function set(string $name, $value); /** * Returns true if a parameter name is defined. * * @return bool */ public function has(string $name); /** * Replaces parameter placeholders (%name%) by their values for all parameters. */ public function resolve(); /** * Replaces parameter placeholders (%name%) by their values. * * @param mixed $value A value * * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist */ public function resolveValue($value); /** * Escape parameter placeholders %. * * @param mixed $value * * @return mixed */ public function escapeValue($value); /** * Unescape parameter placeholders %. * * @param mixed $value * * @return mixed */ public function unescapeValue($value); } vendor_prefixed/symfony/dependency-injection/ParameterBag/FrozenParameterBag.php 0000644 00000003651 15174712003 0024317 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException; /** * Holds read-only parameters. * * @author Fabien Potencier <fabien@symfony.com> */ class FrozenParameterBag extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\ParameterBag { /** * For performance reasons, the constructor assumes that * all keys are already lowercased. * * This is always the case when used internally. * * @param array $parameters An array of parameters */ public function __construct(array $parameters = []) { $this->parameters = $parameters; $this->resolved = \true; } /** * {@inheritdoc} */ public function clear() { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException('Impossible to call clear() on a frozen ParameterBag.'); } /** * {@inheritdoc} */ public function add(array $parameters) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException('Impossible to call add() on a frozen ParameterBag.'); } /** * {@inheritdoc} */ public function set(string $name, $value) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException('Impossible to call set() on a frozen ParameterBag.'); } /** * {@inheritdoc} */ public function remove(string $name) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\LogicException('Impossible to call remove() on a frozen ParameterBag.'); } } vendor_prefixed/symfony/dependency-injection/ParameterBag/EnvPlaceholderParameterBag.php 0000644 00000012452 15174712003 0025746 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException; /** * @author Nicolas Grekas <p@tchwork.com> */ class EnvPlaceholderParameterBag extends \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\ParameterBag { private $envPlaceholderUniquePrefix; private $envPlaceholders = []; private $unusedEnvPlaceholders = []; private $providedTypes = []; private static $counter = 0; /** * {@inheritdoc} */ public function get(string $name) { if (\str_starts_with($name, 'env(') && \str_ends_with($name, ')') && 'env()' !== $name) { $env = \substr($name, 4, -1); if (isset($this->envPlaceholders[$env])) { foreach ($this->envPlaceholders[$env] as $placeholder) { return $placeholder; // return first result } } if (isset($this->unusedEnvPlaceholders[$env])) { foreach ($this->unusedEnvPlaceholders[$env] as $placeholder) { return $placeholder; // return first result } } if (!\preg_match('/^(?:[-.\\w]*+:)*+\\w++$/', $env)) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException(\sprintf('Invalid %s name: only "word" characters are allowed.', $name)); } if ($this->has($name) && null !== ($defaultValue = parent::get($name)) && !\is_string($defaultValue)) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException(\sprintf('The default value of an env() parameter must be a string or null, but "%s" given to "%s".', \get_debug_type($defaultValue), $name)); } $uniqueName = \md5($name . '_' . self::$counter++); $placeholder = \sprintf('%s_%s_%s', $this->getEnvPlaceholderUniquePrefix(), \strtr($env, ':-.', '___'), $uniqueName); $this->envPlaceholders[$env][$placeholder] = $placeholder; return $placeholder; } return parent::get($name); } /** * Gets the common env placeholder prefix for env vars created by this bag. */ public function getEnvPlaceholderUniquePrefix() : string { if (null === $this->envPlaceholderUniquePrefix) { $reproducibleEntropy = \unserialize(\serialize($this->parameters)); \array_walk_recursive($reproducibleEntropy, function (&$v) { $v = null; }); $this->envPlaceholderUniquePrefix = 'env_' . \substr(\md5(\serialize($reproducibleEntropy)), -16); } return $this->envPlaceholderUniquePrefix; } /** * Returns the map of env vars used in the resolved parameter values to their placeholders. * * @return string[][] A map of env var names to their placeholders */ public function getEnvPlaceholders() { return $this->envPlaceholders; } public function getUnusedEnvPlaceholders() : array { return $this->unusedEnvPlaceholders; } public function clearUnusedEnvPlaceholders() { $this->unusedEnvPlaceholders = []; } /** * Merges the env placeholders of another EnvPlaceholderParameterBag. */ public function mergeEnvPlaceholders(self $bag) { if ($newPlaceholders = $bag->getEnvPlaceholders()) { $this->envPlaceholders += $newPlaceholders; foreach ($newPlaceholders as $env => $placeholders) { $this->envPlaceholders[$env] += $placeholders; } } if ($newUnusedPlaceholders = $bag->getUnusedEnvPlaceholders()) { $this->unusedEnvPlaceholders += $newUnusedPlaceholders; foreach ($newUnusedPlaceholders as $env => $placeholders) { $this->unusedEnvPlaceholders[$env] += $placeholders; } } } /** * Maps env prefixes to their corresponding PHP types. */ public function setProvidedTypes(array $providedTypes) { $this->providedTypes = $providedTypes; } /** * Gets the PHP types corresponding to env() parameter prefixes. * * @return string[][] */ public function getProvidedTypes() { return $this->providedTypes; } /** * {@inheritdoc} */ public function resolve() { if ($this->resolved) { return; } parent::resolve(); foreach ($this->envPlaceholders as $env => $placeholders) { if ($this->has($name = "env({$env})") && null !== ($default = $this->parameters[$name]) && !\is_string($default)) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException(\sprintf('The default value of env parameter "%s" must be a string or null, "%s" given.', $env, \get_debug_type($default))); } } } } vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBag.php 0000644 00000020111 15174712003 0023121 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException; /** * Holds parameters. * * @author Fabien Potencier <fabien@symfony.com> */ class ParameterBag implements \YoastSEO_Vendor\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface { protected $parameters = []; protected $resolved = \false; public function __construct(array $parameters = []) { $this->add($parameters); } /** * {@inheritdoc} */ public function clear() { $this->parameters = []; } /** * {@inheritdoc} */ public function add(array $parameters) { foreach ($parameters as $key => $value) { $this->set($key, $value); } } /** * {@inheritdoc} */ public function all() { return $this->parameters; } /** * {@inheritdoc} */ public function get(string $name) { if (!\array_key_exists($name, $this->parameters)) { if (!$name) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException($name); } $alternatives = []; foreach ($this->parameters as $key => $parameterValue) { $lev = \levenshtein($name, $key); if ($lev <= \strlen($name) / 3 || \str_contains($key, $name)) { $alternatives[] = $key; } } $nonNestedAlternative = null; if (!\count($alternatives) && \str_contains($name, '.')) { $namePartsLength = \array_map('strlen', \explode('.', $name)); $key = \substr($name, 0, -1 * (1 + \array_pop($namePartsLength))); while (\count($namePartsLength)) { if ($this->has($key)) { if (\is_array($this->get($key))) { $nonNestedAlternative = $key; } break; } $key = \substr($key, 0, -1 * (1 + \array_pop($namePartsLength))); } } throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException($name, null, null, null, $alternatives, $nonNestedAlternative); } return $this->parameters[$name]; } /** * {@inheritdoc} */ public function set(string $name, $value) { $this->parameters[$name] = $value; } /** * {@inheritdoc} */ public function has(string $name) { return \array_key_exists($name, $this->parameters); } /** * {@inheritdoc} */ public function remove(string $name) { unset($this->parameters[$name]); } /** * {@inheritdoc} */ public function resolve() { if ($this->resolved) { return; } $parameters = []; foreach ($this->parameters as $key => $value) { try { $value = $this->resolveValue($value); $parameters[$key] = $this->unescapeValue($value); } catch (\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException $e) { $e->setSourceKey($key); throw $e; } } $this->parameters = $parameters; $this->resolved = \true; } /** * Replaces parameter placeholders (%name%) by their values. * * @param mixed $value A value * @param array $resolving An array of keys that are being resolved (used internally to detect circular references) * * @return mixed * * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist * @throws ParameterCircularReferenceException if a circular reference if detected * @throws RuntimeException when a given parameter has a type problem */ public function resolveValue($value, array $resolving = []) { if (\is_array($value)) { $args = []; foreach ($value as $k => $v) { $args[\is_string($k) ? $this->resolveValue($k, $resolving) : $k] = $this->resolveValue($v, $resolving); } return $args; } if (!\is_string($value) || 2 > \strlen($value)) { return $value; } return $this->resolveString($value, $resolving); } /** * Resolves parameters inside a string. * * @param array $resolving An array of keys that are being resolved (used internally to detect circular references) * * @return mixed * * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist * @throws ParameterCircularReferenceException if a circular reference if detected * @throws RuntimeException when a given parameter has a type problem */ public function resolveString(string $value, array $resolving = []) { // we do this to deal with non string values (Boolean, integer, ...) // as the preg_replace_callback throw an exception when trying // a non-string in a parameter value if (\preg_match('/^%([^%\\s]+)%$/', $value, $match)) { $key = $match[1]; if (isset($resolving[$key])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException(\array_keys($resolving)); } $resolving[$key] = \true; return $this->resolved ? $this->get($key) : $this->resolveValue($this->get($key), $resolving); } return \preg_replace_callback('/%%|%([^%\\s]+)%/', function ($match) use($resolving, $value) { // skip %% if (!isset($match[1])) { return '%%'; } $key = $match[1]; if (isset($resolving[$key])) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException(\array_keys($resolving)); } $resolved = $this->get($key); if (!\is_string($resolved) && !\is_numeric($resolved)) { throw new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException(\sprintf('A string value must be composed of strings and/or numbers, but found parameter "%s" of type "%s" inside string value "%s".', $key, \get_debug_type($resolved), $value)); } $resolved = (string) $resolved; $resolving[$key] = \true; return $this->isResolved() ? $resolved : $this->resolveString($resolved, $resolving); }, $value); } public function isResolved() { return $this->resolved; } /** * {@inheritdoc} */ public function escapeValue($value) { if (\is_string($value)) { return \str_replace('%', '%%', $value); } if (\is_array($value)) { $result = []; foreach ($value as $k => $v) { $result[$k] = $this->escapeValue($v); } return $result; } return $value; } /** * {@inheritdoc} */ public function unescapeValue($value) { if (\is_string($value)) { return \str_replace('%%', '%', $value); } if (\is_array($value)) { $result = []; foreach ($value as $k => $v) { $result[$k] = $this->unescapeValue($v); } return $result; } return $value; } } vendor_prefixed/symfony/dependency-injection/ServiceLocator.php 0000644 00000013527 15174712003 0021176 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace YoastSEO_Vendor\Symfony\Component\DependencyInjection; use YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface; use YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use YoastSEO_Vendor\Symfony\Contracts\Service\ServiceLocatorTrait; use YoastSEO_Vendor\Symfony\Contracts\Service\ServiceProviderInterface; use YoastSEO_Vendor\Symfony\Contracts\Service\ServiceSubscriberInterface; /** * @author Robin Chalas <robin.chalas@gmail.com> * @author Nicolas Grekas <p@tchwork.com> */ class ServiceLocator implements \YoastSEO_Vendor\Symfony\Contracts\Service\ServiceProviderInterface { use ServiceLocatorTrait { get as private doGet; } private $externalId; private $container; /** * {@inheritdoc} * * @return mixed */ public function get(string $id) { if (!$this->externalId) { return $this->doGet($id); } try { return $this->doGet($id); } catch (\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\RuntimeException $e) { $what = \sprintf('service "%s" required by "%s"', $id, $this->externalId); $message = \preg_replace('/service "\\.service_locator\\.[^"]++"/', $what, $e->getMessage()); if ($e->getMessage() === $message) { $message = \sprintf('Cannot resolve %s: %s', $what, $message); } $r = new \ReflectionProperty($e, 'message'); $r->setAccessible(\true); $r->setValue($e, $message); throw $e; } } public function __invoke(string $id) { return isset($this->factories[$id]) ? $this->get($id) : null; } /** * @internal * * @return static */ public function withContext(string $externalId, \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Container $container) : self { $locator = clone $this; $locator->externalId = $externalId; $locator->container = $container; return $locator; } private function createNotFoundException(string $id) : \YoastSEO_Vendor\Psr\Container\NotFoundExceptionInterface { if ($this->loading) { $msg = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', \end($this->loading), $id, $this->formatAlternatives()); return new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, \end($this->loading) ?: null, null, [], $msg); } $class = \debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 4); $class = isset($class[3]['object']) ? \get_class($class[3]['object']) : null; $externalId = $this->externalId ?: $class; $msg = []; $msg[] = \sprintf('Service "%s" not found:', $id); if (!$this->container) { $class = null; } elseif ($this->container->has($id) || isset($this->container->getRemovedIds()[$id])) { $msg[] = 'even though it exists in the app\'s container,'; } else { try { $this->container->get($id); $class = null; } catch (\YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException $e) { if ($e->getAlternatives()) { $msg[] = \sprintf('did you mean %s? Anyway,', $this->formatAlternatives($e->getAlternatives(), 'or')); } else { $class = null; } } } if ($externalId) { $msg[] = \sprintf('the container inside "%s" is a smaller service locator that %s', $externalId, $this->formatAlternatives()); } else { $msg[] = \sprintf('the current service locator %s', $this->formatAlternatives()); } if (!$class) { // no-op } elseif (\is_subclass_of($class, \YoastSEO_Vendor\Symfony\Contracts\Service\ServiceSubscriberInterface::class)) { $msg[] = \sprintf('Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "%s::getSubscribedServices()".', \preg_replace('/([^\\\\]++\\\\)++/', '', $class)); } else { $msg[] = 'Try using dependency injection instead.'; } return new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, \end($this->loading) ?: null, null, [], \implode(' ', $msg)); } private function createCircularReferenceException(string $id, array $path) : \YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface { return new \YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException($id, $path); } private function formatAlternatives(?array $alternatives = null, string $separator = 'and') : string { $format = '"%s"%s'; if (null === $alternatives) { if (!($alternatives = \array_keys($this->factories))) { return 'is empty...'; } $format = \sprintf('only knows about the %s service%s.', $format, 1 < \count($alternatives) ? 's' : ''); } $last = \array_pop($alternatives); return \sprintf($format, $alternatives ? \implode('", "', $alternatives) : $last, $alternatives ? \sprintf(' %s "%s"', $separator, $last) : ''); } } vendor_prefixed/psr/http-message/src/ServerRequestInterface.php 0000644 00000024467 15174712003 0021111 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Representation of an incoming, server-side HTTP request. * * Per the HTTP specification, this interface includes properties for * each of the following: * * - Protocol version * - HTTP method * - URI * - Headers * - Message body * * Additionally, it encapsulates all data as it has arrived to the * application from the CGI and/or PHP environment, including: * * - The values represented in $_SERVER. * - Any cookies provided (generally via $_COOKIE) * - Query string arguments (generally via $_GET, or as parsed via parse_str()) * - Upload files, if any (as represented by $_FILES) * - Deserialized body parameters (generally from $_POST) * * $_SERVER values MUST be treated as immutable, as they represent application * state at the time of request; as such, no methods are provided to allow * modification of those values. The other values provide such methods, as they * can be restored from $_SERVER or the request body, and may need treatment * during the application (e.g., body parameters may be deserialized based on * content type). * * Additionally, this interface recognizes the utility of introspecting a * request to derive and match additional parameters (e.g., via URI path * matching, decrypting cookie values, deserializing non-form-encoded body * content, matching authorization headers to users, etc). These parameters * are stored in an "attributes" property. * * Requests are considered immutable; all methods that might change state MUST * be implemented such that they retain the internal state of the current * message and return an instance that contains the changed state. */ interface ServerRequestInterface extends \YoastSEO_Vendor\Psr\Http\Message\RequestInterface { /** * Retrieve server parameters. * * Retrieves data related to the incoming request environment, * typically derived from PHP's $_SERVER superglobal. The data IS NOT * REQUIRED to originate from $_SERVER. * * @return array */ public function getServerParams() : array; /** * Retrieve cookies. * * Retrieves cookies sent by the client to the server. * * The data MUST be compatible with the structure of the $_COOKIE * superglobal. * * @return array */ public function getCookieParams() : array; /** * Return an instance with the specified cookies. * * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST * be compatible with the structure of $_COOKIE. Typically, this data will * be injected at instantiation. * * This method MUST NOT update the related Cookie header of the request * instance, nor related values in the server params. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated cookie values. * * @param array $cookies Array of key/value pairs representing cookies. * @return static */ public function withCookieParams(array $cookies) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; /** * Retrieve query string arguments. * * Retrieves the deserialized query string arguments, if any. * * Note: the query params might not be in sync with the URI or server * params. If you need to ensure you are only getting the original * values, you may need to parse the query string from `getUri()->getQuery()` * or from the `QUERY_STRING` server param. * * @return array */ public function getQueryParams() : array; /** * Return an instance with the specified query string arguments. * * These values SHOULD remain immutable over the course of the incoming * request. They MAY be injected during instantiation, such as from PHP's * $_GET superglobal, or MAY be derived from some other value such as the * URI. In cases where the arguments are parsed from the URI, the data * MUST be compatible with what PHP's parse_str() would return for * purposes of how duplicate query parameters are handled, and how nested * sets are handled. * * Setting query string arguments MUST NOT change the URI stored by the * request, nor the values in the server params. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated query string arguments. * * @param array $query Array of query string arguments, typically from * $_GET. * @return static */ public function withQueryParams(array $query) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; /** * Retrieve normalized file upload data. * * This method returns upload metadata in a normalized tree, with each leaf * an instance of Psr\Http\Message\UploadedFileInterface. * * These values MAY be prepared from $_FILES or the message body during * instantiation, or MAY be injected via withUploadedFiles(). * * @return array An array tree of UploadedFileInterface instances; an empty * array MUST be returned if no data is present. */ public function getUploadedFiles() : array; /** * Create a new instance with the specified uploaded files. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param array $uploadedFiles An array tree of UploadedFileInterface instances. * @return static * @throws \InvalidArgumentException if an invalid structure is provided. */ public function withUploadedFiles(array $uploadedFiles) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; /** * Retrieve any parameters provided in the request body. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, this method MUST * return the contents of $_POST. * * Otherwise, this method may return any results of deserializing * the request body content; as parsing returns structured content, the * potential types MUST be arrays or objects only. A null value indicates * the absence of body content. * * @return null|array|object The deserialized body parameters, if any. * These will typically be an array or object. */ public function getParsedBody(); /** * Return an instance with the specified body parameters. * * These MAY be injected during instantiation. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, use this method * ONLY to inject the contents of $_POST. * * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of * deserializing the request body content. Deserialization/parsing returns * structured data, and, as such, this method ONLY accepts arrays or objects, * or a null value if nothing was available to parse. * * As an example, if content negotiation determines that the request data * is a JSON payload, this method could be used to create a request * instance with the deserialized parameters. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param null|array|object $data The deserialized body data. This will * typically be in an array or object. * @return static * @throws \InvalidArgumentException if an unsupported argument type is * provided. */ public function withParsedBody($data) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; /** * Retrieve attributes derived from the request. * * The request "attributes" may be used to allow injection of any * parameters derived from the request: e.g., the results of path * match operations; the results of decrypting cookies; the results of * deserializing non-form-encoded message bodies; etc. Attributes * will be application and request specific, and CAN be mutable. * * @return array Attributes derived from the request. */ public function getAttributes() : array; /** * Retrieve a single derived request attribute. * * Retrieves a single derived request attribute as described in * getAttributes(). If the attribute has not been previously set, returns * the default value as provided. * * This method obviates the need for a hasAttribute() method, as it allows * specifying a default value to return if the attribute is not found. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $default Default value to return if the attribute does not exist. * @return mixed */ public function getAttribute(string $name, $default = null); /** * Return an instance with the specified derived request attribute. * * This method allows setting a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated attribute. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $value The value of the attribute. * @return static */ public function withAttribute(string $name, $value) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; /** * Return an instance that removes the specified derived request attribute. * * This method allows removing a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the attribute. * * @see getAttributes() * @param string $name The attribute name. * @return static */ public function withoutAttribute(string $name) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; } vendor_prefixed/psr/http-message/src/UploadedFileInterface.php 0000644 00000011257 15174712003 0020620 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Value object representing a file uploaded through an HTTP request. * * Instances of this interface are considered immutable; all methods that * might change state MUST be implemented such that they retain the internal * state of the current instance and return an instance that contains the * changed state. */ interface UploadedFileInterface { /** * Retrieve a stream representing the uploaded file. * * This method MUST return a StreamInterface instance, representing the * uploaded file. The purpose of this method is to allow utilizing native PHP * stream functionality to manipulate the file upload, such as * stream_copy_to_stream() (though the result will need to be decorated in a * native PHP stream wrapper to work with such functions). * * If the moveTo() method has been called previously, this method MUST raise * an exception. * * @return StreamInterface Stream representation of the uploaded file. * @throws \RuntimeException in cases when no stream is available or can be * created. */ public function getStream() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Move the uploaded file to a new location. * * Use this method as an alternative to move_uploaded_file(). This method is * guaranteed to work in both SAPI and non-SAPI environments. * Implementations must determine which environment they are in, and use the * appropriate method (move_uploaded_file(), rename(), or a stream * operation) to perform the operation. * * $targetPath may be an absolute path, or a relative path. If it is a * relative path, resolution should be the same as used by PHP's rename() * function. * * The original file or stream MUST be removed on completion. * * If this method is called more than once, any subsequent calls MUST raise * an exception. * * When used in an SAPI environment where $_FILES is populated, when writing * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be * used to ensure permissions and upload status are verified correctly. * * If you wish to move to a stream, use getStream(), as SAPI operations * cannot guarantee writing to stream destinations. * * @see http://php.net/is_uploaded_file * @see http://php.net/move_uploaded_file * @param string $targetPath Path to which to move the uploaded file. * @throws \InvalidArgumentException if the $targetPath specified is invalid. * @throws \RuntimeException on any error during the move operation, or on * the second or subsequent call to the method. */ public function moveTo(string $targetPath) : void; /** * Retrieve the file size. * * Implementations SHOULD return the value stored in the "size" key of * the file in the $_FILES array if available, as PHP calculates this based * on the actual size transmitted. * * @return int|null The file size in bytes or null if unknown. */ public function getSize() : ?int; /** * Retrieve the error associated with the uploaded file. * * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. * * If the file was uploaded successfully, this method MUST return * UPLOAD_ERR_OK. * * Implementations SHOULD return the value stored in the "error" key of * the file in the $_FILES array. * * @see http://php.net/manual/en/features.file-upload.errors.php * @return int One of PHP's UPLOAD_ERR_XXX constants. */ public function getError() : int; /** * Retrieve the filename sent by the client. * * Do not trust the value returned by this method. A client could send * a malicious filename with the intention to corrupt or hack your * application. * * Implementations SHOULD return the value stored in the "name" key of * the file in the $_FILES array. * * @return string|null The filename sent by the client or null if none * was provided. */ public function getClientFilename() : ?string; /** * Retrieve the media type sent by the client. * * Do not trust the value returned by this method. A client could send * a malicious media type with the intention to corrupt or hack your * application. * * Implementations SHOULD return the value stored in the "type" key of * the file in the $_FILES array. * * @return string|null The media type sent by the client or null if none * was provided. */ public function getClientMediaType() : ?string; } vendor_prefixed/psr/http-message/src/StreamInterface.php 0000644 00000011414 15174712003 0017511 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Describes a data stream. * * Typically, an instance will wrap a PHP stream; this interface provides * a wrapper around the most common operations, including serialization of * the entire stream to a string. */ interface StreamInterface { /** * Reads all data from the stream into a string, from the beginning to end. * * This method MUST attempt to seek to the beginning of the stream before * reading data and read the stream until the end is reached. * * Warning: This could attempt to load a large amount of data into memory. * * This method MUST NOT raise an exception in order to conform with PHP's * string casting operations. * * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring * @return string */ public function __toString() : string; /** * Closes the stream and any underlying resources. * * @return void */ public function close() : void; /** * Separates any underlying resources from the stream. * * After the stream has been detached, the stream is in an unusable state. * * @return resource|null Underlying PHP stream, if any */ public function detach(); /** * Get the size of the stream if known. * * @return int|null Returns the size in bytes if known, or null if unknown. */ public function getSize() : ?int; /** * Returns the current position of the file read/write pointer * * @return int Position of the file pointer * @throws \RuntimeException on error. */ public function tell() : int; /** * Returns true if the stream is at the end of the stream. * * @return bool */ public function eof() : bool; /** * Returns whether or not the stream is seekable. * * @return bool */ public function isSeekable() : bool; /** * Seek to a position in the stream. * * @link http://www.php.net/manual/en/function.fseek.php * @param int $offset Stream offset * @param int $whence Specifies how the cursor position will be calculated * based on the seek offset. Valid values are identical to the built-in * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to * offset bytes SEEK_CUR: Set position to current location plus offset * SEEK_END: Set position to end-of-stream plus offset. * @throws \RuntimeException on failure. */ public function seek(int $offset, int $whence = \SEEK_SET) : void; /** * Seek to the beginning of the stream. * * If the stream is not seekable, this method will raise an exception; * otherwise, it will perform a seek(0). * * @see seek() * @link http://www.php.net/manual/en/function.fseek.php * @throws \RuntimeException on failure. */ public function rewind() : void; /** * Returns whether or not the stream is writable. * * @return bool */ public function isWritable() : bool; /** * Write data to the stream. * * @param string $string The string that is to be written. * @return int Returns the number of bytes written to the stream. * @throws \RuntimeException on failure. */ public function write(string $string) : int; /** * Returns whether or not the stream is readable. * * @return bool */ public function isReadable() : bool; /** * Read data from the stream. * * @param int $length Read up to $length bytes from the object and return * them. Fewer than $length bytes may be returned if underlying stream * call returns fewer bytes. * @return string Returns the data read from the stream, or an empty string * if no bytes are available. * @throws \RuntimeException if an error occurs. */ public function read(int $length) : string; /** * Returns the remaining contents in a string * * @return string * @throws \RuntimeException if unable to read or an error occurs while * reading. */ public function getContents() : string; /** * Get stream metadata as an associative array or retrieve a specific key. * * The keys returned are identical to the keys returned from PHP's * stream_get_meta_data() function. * * @link http://php.net/manual/en/function.stream-get-meta-data.php * @param string|null $key Specific metadata to retrieve. * @return array|mixed|null Returns an associative array if no key is * provided. Returns a specific key value if a key is provided and the * value is found, or null if the key is not found. */ public function getMetadata(?string $key = null); } vendor_prefixed/psr/http-message/src/MessageInterface.php 0000644 00000016275 15174712003 0017654 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * HTTP messages consist of requests from a client to a server and responses * from a server to a client. This interface defines the methods common to * each. * * Messages are considered immutable; all methods that might change state MUST * be implemented such that they retain the internal state of the current * message and return an instance that contains the changed state. * * @link http://www.ietf.org/rfc/rfc7230.txt * @link http://www.ietf.org/rfc/rfc7231.txt */ interface MessageInterface { /** * Retrieves the HTTP protocol version as a string. * * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). * * @return string HTTP protocol version. */ public function getProtocolVersion() : string; /** * Return an instance with the specified HTTP protocol version. * * The version string MUST contain only the HTTP version number (e.g., * "1.1", "1.0"). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new protocol version. * * @param string $version HTTP protocol version * @return static */ public function withProtocolVersion(string $version) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface; /** * Retrieves all message header values. * * The keys represent the header name as it will be sent over the wire, and * each value is an array of strings associated with the header. * * // Represent the headers as a string * foreach ($message->getHeaders() as $name => $values) { * echo $name . ": " . implode(", ", $values); * } * * // Emit headers iteratively: * foreach ($message->getHeaders() as $name => $values) { * foreach ($values as $value) { * header(sprintf('%s: %s', $name, $value), false); * } * } * * While header names are not case-sensitive, getHeaders() will preserve the * exact case in which headers were originally specified. * * @return string[][] Returns an associative array of the message's headers. Each * key MUST be a header name, and each value MUST be an array of strings * for that header. */ public function getHeaders() : array; /** * Checks if a header exists by the given case-insensitive name. * * @param string $name Case-insensitive header field name. * @return bool Returns true if any header names match the given header * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ public function hasHeader(string $name) : bool; /** * Retrieves a message header value by the given case-insensitive name. * * This method returns an array of all the header values of the given * case-insensitive header name. * * If the header does not appear in the message, this method MUST return an * empty array. * * @param string $name Case-insensitive header field name. * @return string[] An array of string values as provided for the given * header. If the header does not appear in the message, this method MUST * return an empty array. */ public function getHeader(string $name) : array; /** * Retrieves a comma-separated string of the values for a single header. * * This method returns all of the header values of the given * case-insensitive header name as a string concatenated together using * a comma. * * NOTE: Not all header values may be appropriately represented using * comma concatenation. For such headers, use getHeader() instead * and supply your own delimiter when concatenating. * * If the header does not appear in the message, this method MUST return * an empty string. * * @param string $name Case-insensitive header field name. * @return string A string of values as provided for the given header * concatenated together using a comma. If the header does not appear in * the message, this method MUST return an empty string. */ public function getHeaderLine(string $name) : string; /** * Return an instance with the provided value replacing the specified header. * * While header names are case-insensitive, the casing of the header will * be preserved by this function, and returned from getHeaders(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new and/or updated header and value. * * @param string $name Case-insensitive header field name. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withHeader(string $name, $value) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface; /** * Return an instance with the specified header appended with the given value. * * Existing values for the specified header will be maintained. The new * value(s) will be appended to the existing list. If the header did not * exist previously, it will be added. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new header and/or value. * * @param string $name Case-insensitive header field name to add. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withAddedHeader(string $name, $value) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface; /** * Return an instance without the specified header. * * Header resolution MUST be done without case-sensitivity. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the named header. * * @param string $name Case-insensitive header field name to remove. * @return static */ public function withoutHeader(string $name) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface; /** * Gets the body of the message. * * @return StreamInterface Returns the body as a stream. */ public function getBody() : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Return an instance with the specified message body. * * The body MUST be a StreamInterface object. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return a new instance that has the * new body stream. * * @param StreamInterface $body Body. * @return static * @throws \InvalidArgumentException When the body is not valid. */ public function withBody(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $body) : \YoastSEO_Vendor\Psr\Http\Message\MessageInterface; } vendor_prefixed/psr/http-message/src/ResponseInterface.php 0000644 00000005237 15174712003 0020062 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Representation of an outgoing, server-side response. * * Per the HTTP specification, this interface includes properties for * each of the following: * * - Protocol version * - Status code and reason phrase * - Headers * - Message body * * Responses are considered immutable; all methods that might change state MUST * be implemented such that they retain the internal state of the current * message and return an instance that contains the changed state. */ interface ResponseInterface extends \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { /** * Gets the response status code. * * The status code is a 3-digit integer result code of the server's attempt * to understand and satisfy the request. * * @return int Status code. */ public function getStatusCode() : int; /** * Return an instance with the specified status code and, optionally, reason phrase. * * If no reason phrase is specified, implementations MAY choose to default * to the RFC 7231 or IANA recommended reason phrase for the response's * status code. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated status and reason phrase. * * @link http://tools.ietf.org/html/rfc7231#section-6 * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml * @param int $code The 3-digit integer result code to set. * @param string $reasonPhrase The reason phrase to use with the * provided status code; if none is provided, implementations MAY * use the defaults as suggested in the HTTP specification. * @return static * @throws \InvalidArgumentException For invalid status code arguments. */ public function withStatus(int $code, string $reasonPhrase = '') : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Gets the response reason phrase associated with the status code. * * Because a reason phrase is not a required element in a response * status line, the reason phrase value MAY be null. Implementations MAY * choose to return the default RFC 7231 recommended reason phrase (or those * listed in the IANA HTTP Status Code Registry) for the response's * status code. * * @link http://tools.ietf.org/html/rfc7231#section-6 * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml * @return string Reason phrase; must return an empty string if none present. */ public function getReasonPhrase() : string; } vendor_prefixed/psr/http-message/src/UriInterface.php 0000644 00000031436 15174712003 0017023 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Value object representing a URI. * * This interface is meant to represent URIs according to RFC 3986 and to * provide methods for most common operations. Additional functionality for * working with URIs can be provided on top of the interface or externally. * Its primary use is for HTTP requests, but may also be used in other * contexts. * * Instances of this interface are considered immutable; all methods that * might change state MUST be implemented such that they retain the internal * state of the current instance and return an instance that contains the * changed state. * * Typically the Host header will be also be present in the request message. * For server-side requests, the scheme will typically be discoverable in the * server parameters. * * @link http://tools.ietf.org/html/rfc3986 (the URI specification) */ interface UriInterface { /** * Retrieve the scheme component of the URI. * * If no scheme is present, this method MUST return an empty string. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.1. * * The trailing ":" character is not part of the scheme and MUST NOT be * added. * * @see https://tools.ietf.org/html/rfc3986#section-3.1 * @return string The URI scheme. */ public function getScheme() : string; /** * Retrieve the authority component of the URI. * * If no authority information is present, this method MUST return an empty * string. * * The authority syntax of the URI is: * * <pre> * [user-info@]host[:port] * </pre> * * If the port component is not set or is the standard port for the current * scheme, it SHOULD NOT be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 * @return string The URI authority, in "[user-info@]host[:port]" format. */ public function getAuthority() : string; /** * Retrieve the user information component of the URI. * * If no user information is present, this method MUST return an empty * string. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and MUST * NOT be added. * * @return string The URI user information, in "username[:password]" format. */ public function getUserInfo() : string; /** * Retrieve the host component of the URI. * * If no host is present, this method MUST return an empty string. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 * @return string The URI host. */ public function getHost() : string; /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. * * @return null|int The URI port. */ public function getPort() : ?int; /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 * @return string The URI path. */ public function getPath() : string; /** * Retrieve the query string of the URI. * * If no query string is present, this method MUST return an empty string. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 * @return string The URI query string. */ public function getQuery() : string; /** * Retrieve the fragment component of the URI. * * If no fragment is present, this method MUST return an empty string. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 * @return string The URI fragment. */ public function getFragment() : string; /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * Implementations MUST support the schemes "http" and "https" case * insensitively, and MAY accommodate other schemes if required. * * An empty scheme is equivalent to removing the scheme. * * @param string $scheme The scheme to use with the new instance. * @return static A new instance with the specified scheme. * @throws \InvalidArgumentException for invalid or unsupported schemes. */ public function withScheme(string $scheme) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; an empty string for the user is equivalent to removing user * information. * * @param string $user The user name to use for authority. * @param null|string $password The password associated with $user. * @return static A new instance with the specified user information. */ public function withUserInfo(string $user, ?string $password = null) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * An empty host value is equivalent to removing the host. * * @param string $host The hostname to use with the new instance. * @return static A new instance with the specified host. * @throws \InvalidArgumentException for invalid hostnames. */ public function withHost(string $host) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * Implementations MUST raise an exception for ports outside the * established TCP and UDP port ranges. * * A null value provided for the port is equivalent to removing the port * information. * * @param null|int $port The port to use with the new instance; a null value * removes the port information. * @return static A new instance with the specified port. * @throws \InvalidArgumentException for invalid ports. */ public function withPort(?int $port) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * If the path is intended to be domain-relative rather than path relative then * it must begin with a slash ("/"). Paths not starting with a slash ("/") * are assumed to be relative to some base path known to the application or * consumer. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @param string $path The path to use with the new instance. * @return static A new instance with the specified path. * @throws \InvalidArgumentException for invalid paths. */ public function withPath(string $path) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * An empty query string value is equivalent to removing the query string. * * @param string $query The query string to use with the new instance. * @return static A new instance with the specified query string. * @throws \InvalidArgumentException for invalid query strings. */ public function withQuery(string $query) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * An empty fragment value is equivalent to removing the fragment. * * @param string $fragment The fragment to use with the new instance. * @return static A new instance with the specified fragment. */ public function withFragment(string $fragment) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Return the string representation as a URI reference. * * Depending on which components of the URI are present, the resulting * string is either a full URI or relative reference according to RFC 3986, * Section 4.1. The method concatenates the various components of the URI, * using the appropriate delimiters: * * - If a scheme is present, it MUST be suffixed by ":". * - If an authority is present, it MUST be prefixed by "//". * - The path can be concatenated without delimiters. But there are two * cases where the path has to be adjusted to make the URI reference * valid as PHP does not allow to throw an exception in __toString(): * - If the path is rootless and an authority is present, the path MUST * be prefixed by "/". * - If the path is starting with more than one "/" and no authority is * present, the starting slashes MUST be reduced to one. * - If a query is present, it MUST be prefixed by "?". * - If a fragment is present, it MUST be prefixed by "#". * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @return string */ public function __toString() : string; } vendor_prefixed/psr/http-message/src/RequestInterface.php 0000644 00000012024 15174712003 0017704 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; /** * Representation of an outgoing, client-side request. * * Per the HTTP specification, this interface includes properties for * each of the following: * * - Protocol version * - HTTP method * - URI * - Headers * - Message body * * During construction, implementations MUST attempt to set the Host header from * a provided URI if no Host header is provided. * * Requests are considered immutable; all methods that might change state MUST * be implemented such that they retain the internal state of the current * message and return an instance that contains the changed state. */ interface RequestInterface extends \YoastSEO_Vendor\Psr\Http\Message\MessageInterface { /** * Retrieves the message's request target. * * Retrieves the message's request-target either as it will appear (for * clients), as it appeared at request (for servers), or as it was * specified for the instance (see withRequestTarget()). * * In most cases, this will be the origin-form of the composed URI, * unless a value was provided to the concrete implementation (see * withRequestTarget() below). * * If no URI is available, and no request-target has been specifically * provided, this method MUST return the string "/". * * @return string */ public function getRequestTarget() : string; /** * Return an instance with the specific request-target. * * If the request needs a non-origin-form request-target — e.g., for * specifying an absolute-form, authority-form, or asterisk-form — * this method may be used to create an instance with the specified * request-target, verbatim. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * changed request target. * * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various * request-target forms allowed in request messages) * @param string $requestTarget * @return static */ public function withRequestTarget(string $requestTarget) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Retrieves the HTTP method of the request. * * @return string Returns the request method. */ public function getMethod() : string; /** * Return an instance with the provided HTTP method. * * While HTTP method names are typically all uppercase characters, HTTP * method names are case-sensitive and thus implementations SHOULD NOT * modify the given string. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * changed request method. * * @param string $method Case-sensitive method. * @return static * @throws \InvalidArgumentException for invalid HTTP methods. */ public function withMethod(string $method) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Retrieves the URI instance. * * This method MUST return a UriInterface instance. * * @link http://tools.ietf.org/html/rfc3986#section-4.3 * @return UriInterface Returns a UriInterface instance * representing the URI of the request. */ public function getUri() : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; /** * Returns an instance with the provided URI. * * This method MUST update the Host header of the returned request by * default if the URI contains a host component. If the URI does not * contain a host component, any pre-existing Host header MUST be carried * over to the returned request. * * You can opt-in to preserving the original state of the Host header by * setting `$preserveHost` to `true`. When `$preserveHost` is set to * `true`, this method interacts with the Host header in the following ways: * * - If the Host header is missing or empty, and the new URI contains * a host component, this method MUST update the Host header in the returned * request. * - If the Host header is missing or empty, and the new URI does not contain a * host component, this method MUST NOT update the Host header in the returned * request. * - If a Host header is present and non-empty, this method MUST NOT update * the Host header in the returned request. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new UriInterface instance. * * @link http://tools.ietf.org/html/rfc3986#section-4.3 * @param UriInterface $uri New request URI to use. * @param bool $preserveHost Preserve the original state of the Host header. * @return static */ public function withUri(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, bool $preserveHost = \false) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; } vendor_prefixed/psr/container/src/ContainerInterface.php 0000644 00000002037 15174712003 0017562 0 ustar 00 <?php declare (strict_types=1); namespace YoastSEO_Vendor\Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get(string $id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has(string $id); } vendor_prefixed/psr/container/src/NotFoundExceptionInterface.php 0000644 00000000315 15174712003 0021250 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends \YoastSEO_Vendor\Psr\Container\ContainerExceptionInterface { } vendor_prefixed/psr/container/src/ContainerExceptionInterface.php 0000644 00000000246 15174712003 0021441 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Container; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface { } vendor_prefixed/psr/http-client/src/RequestExceptionInterface.php 0000644 00000001255 15174712003 0021421 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Client; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Exception for when a request failed. * * Examples: * - Request is invalid (e.g. method is missing) * - Runtime request errors (e.g. the body stream is not seekable) */ interface RequestExceptionInterface extends \YoastSEO_Vendor\Psr\Http\Client\ClientExceptionInterface { /** * Returns the request. * * The request object MAY be a different object from the one passed to ClientInterface::sendRequest() * * @return RequestInterface */ public function getRequest() : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; } vendor_prefixed/psr/http-client/src/ClientInterface.php 0000644 00000001150 15174712003 0017322 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Client; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; interface ClientInterface { /** * Sends a PSR-7 request and returns a PSR-7 response. * * @param RequestInterface $request * * @return ResponseInterface * * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request. */ public function sendRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; } vendor_prefixed/psr/http-client/src/NetworkExceptionInterface.php 0000644 00000001365 15174712003 0021424 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Client; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; /** * Thrown when the request cannot be completed because of network issues. * * There is no response object as this exception is thrown when no response has been received. * * Example: the target host name can not be resolved or the connection failed. */ interface NetworkExceptionInterface extends \YoastSEO_Vendor\Psr\Http\Client\ClientExceptionInterface { /** * Returns the request. * * The request object MAY be a different object from the one passed to ClientInterface::sendRequest() * * @return RequestInterface */ public function getRequest() : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; } vendor_prefixed/psr/http-client/src/ClientExceptionInterface.php 0000644 00000000273 15174712003 0021206 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Client; /** * Every HTTP client related exception MUST implement this interface. */ interface ClientExceptionInterface extends \Throwable { } vendor_prefixed/psr/log/Psr/Log/LoggerTrait.php 0000644 00000007047 15174712003 0015545 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * This is a simple Logger trait that classes unable to extend AbstractLogger * (because they extend another class, etc) can include. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ trait LoggerTrait { /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public abstract function log($level, $message, array $context = array()); } vendor_prefixed/psr/log/Psr/Log/LoggerInterface.php 0000644 00000006062 15174712003 0016356 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * Describes a logger instance. * * The message MUST be a string or object implementing __toString(). * * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data. The only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md * for the full interface specification. */ interface LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()); /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()); /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()); /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()); /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()); /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()); /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()); /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()); /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param mixed[] $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()); } vendor_prefixed/psr/log/Psr/Log/AbstractLogger.php 0000644 00000006412 15174712003 0016220 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * This is a simple Logger implementation that other Loggers can inherit from. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ abstract class AbstractLogger implements \YoastSEO_Vendor\Psr\Log\LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()) { $this->log(\YoastSEO_Vendor\Psr\Log\LogLevel::DEBUG, $message, $context); } } vendor_prefixed/psr/log/Psr/Log/NullLogger.php 0000644 00000001354 15174712003 0015367 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * This Logger can be used to avoid conditional log calls. * * Logging should always be optional, and if no logger is provided to your * library creating a NullLogger instance to have something to throw logs at * is a good way to avoid littering your code with `if ($this->logger) { }` * blocks. */ class NullLogger extends \YoastSEO_Vendor\Psr\Log\AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()) { // noop } } vendor_prefixed/psr/log/Psr/Log/LoggerAwareTrait.php 0000644 00000000672 15174712003 0016522 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * Basic Implementation of LoggerAwareInterface. */ trait LoggerAwareTrait { /** * The logger instance. * * @var LoggerInterface|null */ protected $logger; /** * Sets a logger. * * @param LoggerInterface $logger */ public function setLogger(\YoastSEO_Vendor\Psr\Log\LoggerInterface $logger) { $this->logger = $logger; } } vendor_prefixed/psr/log/Psr/Log/LogLevel.php 0000644 00000000511 15174712003 0015020 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * Describes log levels. */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; } vendor_prefixed/psr/log/Psr/Log/InvalidArgumentException.php 0000644 00000000160 15174712003 0020257 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; class InvalidArgumentException extends \InvalidArgumentException { } vendor_prefixed/psr/log/Psr/Log/LoggerAwareInterface.php 0000644 00000000522 15174712003 0017331 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Log; /** * Describes a logger-aware instance. */ interface LoggerAwareInterface { /** * Sets a logger instance on the object. * * @param LoggerInterface $logger * * @return void */ public function setLogger(\YoastSEO_Vendor\Psr\Log\LoggerInterface $logger); } vendor_prefixed/psr/http-factory/src/UriFactoryInterface.php 0000644 00000000570 15174712003 0020371 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface UriFactoryInterface { /** * Create a new URI. * * @param string $uri * * @return UriInterface * * @throws \InvalidArgumentException If the given URI cannot be parsed. */ public function createUri(string $uri = '') : \YoastSEO_Vendor\Psr\Http\Message\UriInterface; } vendor_prefixed/psr/http-factory/src/RequestFactoryInterface.php 0000644 00000001046 15174712003 0021261 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface RequestFactoryInterface { /** * Create a new request. * * @param string $method The HTTP method associated with the request. * @param UriInterface|string $uri The URI associated with the request. If * the value is a string, the factory MUST create a UriInterface * instance based on it. * * @return RequestInterface */ public function createRequest(string $method, $uri) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface; } vendor_prefixed/psr/http-factory/src/ServerRequestFactoryInterface.php 0000644 00000001722 15174712003 0022451 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface ServerRequestFactoryInterface { /** * Create a new server request. * * Note that server-params are taken precisely as given - no parsing/processing * of the given values is performed, and, in particular, no attempt is made to * determine the HTTP method or URI, which must be provided explicitly. * * @param string $method The HTTP method associated with the request. * @param UriInterface|string $uri The URI associated with the request. If * the value is a string, the factory MUST create a UriInterface * instance based on it. * @param array $serverParams Array of SAPI parameters with which to seed * the generated request instance. * * @return ServerRequestInterface */ public function createServerRequest(string $method, $uri, array $serverParams = []) : \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; } vendor_prefixed/psr/http-factory/src/ResponseFactoryInterface.php 0000644 00000001125 15174712003 0021425 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface ResponseFactoryInterface { /** * Create a new response. * * @param int $code HTTP status code; defaults to 200 * @param string $reasonPhrase Reason phrase to associate with status code * in generated response; if none is provided implementations MAY use * the defaults as suggested in the HTTP specification. * * @return ResponseInterface */ public function createResponse(int $code = 200, string $reasonPhrase = '') : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; } vendor_prefixed/psr/http-factory/src/StreamFactoryInterface.php 0000644 00000003001 15174712003 0021055 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface StreamFactoryInterface { /** * Create a new stream from a string. * * The stream SHOULD be created with a temporary resource. * * @param string $content String content with which to populate the stream. * * @return StreamInterface */ public function createStream(string $content = '') : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Create a stream from an existing file. * * The file MUST be opened using the given mode, which may be any mode * supported by the `fopen` function. * * The `$filename` MAY be any string supported by `fopen()`. * * @param string $filename Filename or stream URI to use as basis of stream. * @param string $mode Mode with which to open the underlying filename/stream. * * @return StreamInterface * @throws \RuntimeException If the file cannot be opened. * @throws \InvalidArgumentException If the mode is invalid. */ public function createStreamFromFile(string $filename, string $mode = 'r') : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface; /** * Create a new stream from an existing resource. * * The stream MUST be readable and may be writable. * * @param resource $resource PHP resource to use as basis of stream. * * @return StreamInterface */ public function createStreamFromResource($resource) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface; } vendor_prefixed/psr/http-factory/src/UploadedFileFactoryInterface.php 0000644 00000002217 15174712003 0022167 0 ustar 00 <?php namespace YoastSEO_Vendor\Psr\Http\Message; interface UploadedFileFactoryInterface { /** * Create a new uploaded file. * * If a size is not provided it will be determined by checking the size of * the file. * * @see http://php.net/manual/features.file-upload.post-method.php * @see http://php.net/manual/features.file-upload.errors.php * * @param StreamInterface $stream Underlying stream representing the * uploaded file content. * @param int|null $size in bytes * @param int $error PHP file upload error * @param string|null $clientFilename Filename as provided by the client, if any. * @param string|null $clientMediaType Media type as provided by the client, if any. * * @return UploadedFileInterface * * @throws \InvalidArgumentException If the file resource is not readable. */ public function createUploadedFile(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null) : \YoastSEO_Vendor\Psr\Http\Message\UploadedFileInterface; } vendor_prefixed/league/oauth2-client/src/Token/AccessToken.php 0000644 00000014152 15174712003 0020434 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Token; use InvalidArgumentException; use RuntimeException; /** * Represents an access token. * * @link http://tools.ietf.org/html/rfc6749#section-1.4 Access Token (RFC 6749, §1.4) */ class AccessToken implements \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface, \YoastSEO_Vendor\League\OAuth2\Client\Token\ResourceOwnerAccessTokenInterface, \YoastSEO_Vendor\League\OAuth2\Client\Token\SettableRefreshTokenInterface { /** * @var string */ protected $accessToken; /** * @var int */ protected $expires; /** * @var string */ protected $refreshToken; /** * @var string */ protected $resourceOwnerId; /** * @var array */ protected $values = []; /** * @var int */ private static $timeNow; /** * Set the time now. This should only be used for testing purposes. * * @param int $timeNow the time in seconds since epoch * @return void */ public static function setTimeNow($timeNow) { self::$timeNow = $timeNow; } /** * Reset the time now if it was set for test purposes. * * @return void */ public static function resetTimeNow() { self::$timeNow = null; } /** * @return int */ public function getTimeNow() { return self::$timeNow ? self::$timeNow : \time(); } /** * Constructs an access token. * * @param array $options An array of options returned by the service provider * in the access token request. The `access_token` option is required. * @throws InvalidArgumentException if `access_token` is not provided in `$options`. */ public function __construct(array $options = []) { if (empty($options['access_token'])) { throw new \InvalidArgumentException('Required option not passed: "access_token"'); } $this->accessToken = $options['access_token']; if (!empty($options['resource_owner_id'])) { $this->resourceOwnerId = $options['resource_owner_id']; } if (!empty($options['refresh_token'])) { $this->refreshToken = $options['refresh_token']; } // We need to know when the token expires. Show preference to // 'expires_in' since it is defined in RFC6749 Section 5.1. // Defer to 'expires' if it is provided instead. if (isset($options['expires_in'])) { if (!\is_numeric($options['expires_in'])) { throw new \InvalidArgumentException('expires_in value must be an integer'); } $this->expires = $options['expires_in'] != 0 ? $this->getTimeNow() + $options['expires_in'] : 0; } elseif (!empty($options['expires'])) { // Some providers supply the seconds until expiration rather than // the exact timestamp. Take a best guess at which we received. $expires = (int) $options['expires']; if (!$this->isExpirationTimestamp($expires)) { $expires += $this->getTimeNow(); } $this->expires = $expires; } // Capture any additional values that might exist in the token but are // not part of the standard response. Vendors will sometimes pass // additional user data this way. $this->values = \array_diff_key($options, \array_flip(['access_token', 'resource_owner_id', 'refresh_token', 'expires_in', 'expires'])); } /** * Check if a value is an expiration timestamp or second value. * * @param integer $value * @return bool */ protected function isExpirationTimestamp($value) { // If the given value is larger than the original OAuth 2 draft date, // assume that it is meant to be a (possible expired) timestamp. $oauth2InceptionDate = 1349067600; // 2012-10-01 return $value > $oauth2InceptionDate; } /** * @inheritdoc */ public function getToken() { return $this->accessToken; } /** * @inheritdoc */ public function getRefreshToken() { return $this->refreshToken; } /** * @inheritdoc */ public function setRefreshToken($refreshToken) { $this->refreshToken = $refreshToken; } /** * @inheritdoc */ public function getExpires() { return $this->expires; } /** * @inheritdoc */ public function getResourceOwnerId() { return $this->resourceOwnerId; } /** * @inheritdoc */ public function hasExpired() { $expires = $this->getExpires(); if (empty($expires)) { throw new \RuntimeException('"expires" is not set on the token'); } return $expires < $this->getTimeNow(); } /** * @inheritdoc */ public function getValues() { return $this->values; } /** * @inheritdoc */ public function __toString() { return (string) $this->getToken(); } /** * @inheritdoc */ public function jsonSerialize() { $parameters = $this->values; if ($this->accessToken) { $parameters['access_token'] = $this->accessToken; } if ($this->refreshToken) { $parameters['refresh_token'] = $this->refreshToken; } if ($this->expires) { $parameters['expires'] = $this->expires; } if ($this->resourceOwnerId) { $parameters['resource_owner_id'] = $this->resourceOwnerId; } return $parameters; } } vendor_prefixed/league/oauth2-client/src/Token/SettableRefreshTokenInterface.php 0000644 00000001462 15174712003 0024136 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Token; interface SettableRefreshTokenInterface { /** * Sets or replaces the refresh token with the provided refresh token. * * @param string $refreshToken * @return void */ public function setRefreshToken($refreshToken); } vendor_prefixed/league/oauth2-client/src/Token/AccessTokenInterface.php 0000644 00000003464 15174712003 0022261 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Token; use JsonSerializable; use ReturnTypeWillChange; use RuntimeException; interface AccessTokenInterface extends \JsonSerializable { /** * Returns the access token string of this instance. * * @return string */ public function getToken(); /** * Returns the refresh token, if defined. * * @return string|null */ public function getRefreshToken(); /** * Returns the expiration timestamp in seconds, if defined. * * @return integer|null */ public function getExpires(); /** * Checks if this token has expired. * * @return boolean true if the token has expired, false otherwise. * @throws RuntimeException if 'expires' is not set on the token. */ public function hasExpired(); /** * Returns additional vendor values stored in the token. * * @return array */ public function getValues(); /** * Returns a string representation of the access token * * @return string */ public function __toString(); /** * Returns an array of parameters to serialize when this is serialized with * json_encode(). * * @return array */ #[ReturnTypeWillChange] public function jsonSerialize(); } vendor_prefixed/league/oauth2-client/src/Token/ResourceOwnerAccessTokenInterface.php 0000644 00000001510 15174712003 0024772 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Token; interface ResourceOwnerAccessTokenInterface extends \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface { /** * Returns the resource owner identifier, if defined. * * @return string|null */ public function getResourceOwnerId(); } vendor_prefixed/league/oauth2-client/src/Grant/GrantFactory.php 0000644 00000005364 15174712003 0020635 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; use YoastSEO_Vendor\League\OAuth2\Client\Grant\Exception\InvalidGrantException; /** * Represents a factory used when retrieving an authorization grant type. */ class GrantFactory { /** * @var array */ protected $registry = []; /** * Defines a grant singleton in the registry. * * @param string $name * @param AbstractGrant $grant * @return self */ public function setGrant($name, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant $grant) { $this->registry[$name] = $grant; return $this; } /** * Returns a grant singleton by name. * * If the grant has not be registered, a default grant will be loaded. * * @param string $name * @return AbstractGrant */ public function getGrant($name) { if (empty($this->registry[$name])) { $this->registerDefaultGrant($name); } return $this->registry[$name]; } /** * Registers a default grant singleton by name. * * @param string $name * @return self */ protected function registerDefaultGrant($name) { // PascalCase the grant. E.g: 'authorization_code' becomes 'AuthorizationCode' $class = \str_replace(' ', '', \ucwords(\str_replace(['-', '_'], ' ', $name))); $class = 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\' . $class; $this->checkGrant($class); return $this->setGrant($name, new $class()); } /** * Determines if a variable is a valid grant. * * @param mixed $class * @return boolean */ public function isGrant($class) { return \is_subclass_of($class, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant::class); } /** * Checks if a variable is a valid grant. * * @throws InvalidGrantException * @param mixed $class * @return void */ public function checkGrant($class) { if (!$this->isGrant($class)) { throw new \YoastSEO_Vendor\League\OAuth2\Client\Grant\Exception\InvalidGrantException(\sprintf('Grant "%s" must extend AbstractGrant', \is_object($class) ? \get_class($class) : $class)); } } } vendor_prefixed/league/oauth2-client/src/Grant/RefreshToken.php 0000644 00000002032 15174712003 0020616 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; /** * Represents a refresh token grant. * * @link http://tools.ietf.org/html/rfc6749#section-6 Refreshing an Access Token (RFC 6749, §6) */ class RefreshToken extends \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant { /** * @inheritdoc */ protected function getName() { return 'refresh_token'; } /** * @inheritdoc */ protected function getRequiredRequestParameters() { return ['refresh_token']; } } vendor_prefixed/league/oauth2-client/src/Grant/Exception/InvalidGrantException.php 0000644 00000001444 15174712003 0024424 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant\Exception; use InvalidArgumentException; /** * Exception thrown if the grant does not extend from AbstractGrant. * * @see League\OAuth2\Client\Grant\AbstractGrant */ class InvalidGrantException extends \InvalidArgumentException { } vendor_prefixed/league/oauth2-client/src/Grant/AbstractGrant.php 0000644 00000004676 15174712003 0020776 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; use YoastSEO_Vendor\League\OAuth2\Client\Tool\RequiredParameterTrait; /** * Represents a type of authorization grant. * * An authorization grant is a credential representing the resource * owner's authorization (to access its protected resources) used by the * client to obtain an access token. OAuth 2.0 defines four * grant types -- authorization code, implicit, resource owner password * credentials, and client credentials -- as well as an extensibility * mechanism for defining additional types. * * @link http://tools.ietf.org/html/rfc6749#section-1.3 Authorization Grant (RFC 6749, §1.3) */ abstract class AbstractGrant { use RequiredParameterTrait; /** * Returns the name of this grant, eg. 'grant_name', which is used as the * grant type when encoding URL query parameters. * * @return string */ protected abstract function getName(); /** * Returns a list of all required request parameters. * * @return array */ protected abstract function getRequiredRequestParameters(); /** * Returns this grant's name as its string representation. This allows for * string interpolation when building URL query parameters. * * @return string */ public function __toString() { return $this->getName(); } /** * Prepares an access token request's parameters by checking that all * required parameters are set, then merging with any given defaults. * * @param array $defaults * @param array $options * @return array */ public function prepareRequestParameters(array $defaults, array $options) { $defaults['grant_type'] = $this->getName(); $required = $this->getRequiredRequestParameters(); $provided = \array_merge($defaults, $options); $this->checkRequiredParameters($required, $provided); return $provided; } } vendor_prefixed/league/oauth2-client/src/Grant/ClientCredentials.php 0000644 00000002032 15174712003 0021613 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; /** * Represents a client credentials grant. * * @link http://tools.ietf.org/html/rfc6749#section-1.3.4 Client Credentials (RFC 6749, §1.3.4) */ class ClientCredentials extends \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant { /** * @inheritdoc */ protected function getName() { return 'client_credentials'; } /** * @inheritdoc */ protected function getRequiredRequestParameters() { return []; } } vendor_prefixed/league/oauth2-client/src/Grant/AuthorizationCode.php 0000644 00000002041 15174712003 0021652 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; /** * Represents an authorization code grant. * * @link http://tools.ietf.org/html/rfc6749#section-1.3.1 Authorization Code (RFC 6749, §1.3.1) */ class AuthorizationCode extends \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant { /** * @inheritdoc */ protected function getName() { return 'authorization_code'; } /** * @inheritdoc */ protected function getRequiredRequestParameters() { return ['code']; } } vendor_prefixed/league/oauth2-client/src/Grant/Password.php 0000644 00000002077 15174712003 0020032 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Grant; /** * Represents a resource owner password credentials grant. * * @link http://tools.ietf.org/html/rfc6749#section-1.3.3 Resource Owner Password Credentials (RFC 6749, §1.3.3) */ class Password extends \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant { /** * @inheritdoc */ protected function getName() { return 'password'; } /** * @inheritdoc */ protected function getRequiredRequestParameters() { return ['username', 'password']; } } vendor_prefixed/league/oauth2-client/src/Provider/Exception/IdentityProviderException.php 0000644 00000002273 15174712003 0026066 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception; /** * Exception thrown if the provider response contains errors. */ class IdentityProviderException extends \Exception { /** * @var mixed */ protected $response; /** * @param string $message * @param int $code * @param mixed $response The response body */ public function __construct($message, $code, $response) { $this->response = $response; parent::__construct($message, $code); } /** * Returns the exception's response body. * * @return mixed */ public function getResponseBody() { return $this->response; } } vendor_prefixed/league/oauth2-client/src/Provider/AbstractProvider.php 0000644 00000066003 15174712003 0022224 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Provider; use YoastSEO_Vendor\GuzzleHttp\Client as HttpClient; use YoastSEO_Vendor\GuzzleHttp\ClientInterface as HttpClientInterface; use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException; use YoastSEO_Vendor\GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant; use YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory; use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface; use YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider; use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface; use YoastSEO_Vendor\League\OAuth2\Client\Tool\ArrayAccessorTrait; use YoastSEO_Vendor\League\OAuth2\Client\Tool\GuardedPropertyTrait; use YoastSEO_Vendor\League\OAuth2\Client\Tool\QueryBuilderTrait; use YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; use UnexpectedValueException; /** * Represents a service provider (authorization server). * * @link http://tools.ietf.org/html/rfc6749#section-1.1 Roles (RFC 6749, §1.1) */ abstract class AbstractProvider { use ArrayAccessorTrait; use GuardedPropertyTrait; use QueryBuilderTrait; /** * @var string|null Key used in a token response to identify the resource owner. */ const ACCESS_TOKEN_RESOURCE_OWNER_ID = null; /** * @var string HTTP method used to fetch access tokens. */ const METHOD_GET = 'GET'; /** * @var string HTTP method used to fetch access tokens. */ const METHOD_POST = 'POST'; /** * @var string PKCE method used to fetch authorization token. * The PKCE code challenge will be hashed with sha256 (recommended). */ const PKCE_METHOD_S256 = 'S256'; /** * @var string PKCE method used to fetch authorization token. * The PKCE code challenge will be sent as plain text, this is NOT recommended. * Only use `plain` if no other option is possible. */ const PKCE_METHOD_PLAIN = 'plain'; /** * @var string */ protected $clientId; /** * @var string */ protected $clientSecret; /** * @var string */ protected $redirectUri; /** * @var string */ protected $state; /** * @var string|null */ protected $pkceCode = null; /** * @var GrantFactory */ protected $grantFactory; /** * @var RequestFactory */ protected $requestFactory; /** * @var HttpClientInterface */ protected $httpClient; /** * @var OptionProviderInterface */ protected $optionProvider; /** * Constructs an OAuth 2.0 service provider. * * @param array $options An array of options to set on this provider. * Options include `clientId`, `clientSecret`, `redirectUri`, and `state`. * Individual providers may introduce more options, as needed. * @param array $collaborators An array of collaborators that may be used to * override this provider's default behavior. Collaborators include * `grantFactory`, `requestFactory`, and `httpClient`. * Individual providers may introduce more collaborators, as needed. */ public function __construct(array $options = [], array $collaborators = []) { // We'll let the GuardedPropertyTrait handle mass assignment of incoming // options, skipping any blacklisted properties defined in the provider $this->fillProperties($options); if (empty($collaborators['grantFactory'])) { $collaborators['grantFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory(); } $this->setGrantFactory($collaborators['grantFactory']); if (empty($collaborators['requestFactory'])) { $collaborators['requestFactory'] = new \YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory(); } $this->setRequestFactory($collaborators['requestFactory']); if (empty($collaborators['httpClient'])) { $client_options = $this->getAllowedClientOptions($options); $collaborators['httpClient'] = new \YoastSEO_Vendor\GuzzleHttp\Client(\array_intersect_key($options, \array_flip($client_options))); } $this->setHttpClient($collaborators['httpClient']); if (empty($collaborators['optionProvider'])) { $collaborators['optionProvider'] = new \YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider(); } $this->setOptionProvider($collaborators['optionProvider']); } /** * Returns the list of options that can be passed to the HttpClient * * @param array $options An array of options to set on this provider. * Options include `clientId`, `clientSecret`, `redirectUri`, and `state`. * Individual providers may introduce more options, as needed. * @return array The options to pass to the HttpClient constructor */ protected function getAllowedClientOptions(array $options) { $client_options = ['timeout', 'proxy']; // Only allow turning off ssl verification if it's for a proxy if (!empty($options['proxy'])) { $client_options[] = 'verify'; } return $client_options; } /** * Sets the grant factory instance. * * @param GrantFactory $factory * @return self */ public function setGrantFactory(\YoastSEO_Vendor\League\OAuth2\Client\Grant\GrantFactory $factory) { $this->grantFactory = $factory; return $this; } /** * Returns the current grant factory instance. * * @return GrantFactory */ public function getGrantFactory() { return $this->grantFactory; } /** * Sets the request factory instance. * * @param RequestFactory $factory * @return self */ public function setRequestFactory(\YoastSEO_Vendor\League\OAuth2\Client\Tool\RequestFactory $factory) { $this->requestFactory = $factory; return $this; } /** * Returns the request factory instance. * * @return RequestFactory */ public function getRequestFactory() { return $this->requestFactory; } /** * Sets the HTTP client instance. * * @param HttpClientInterface $client * @return self */ public function setHttpClient(\YoastSEO_Vendor\GuzzleHttp\ClientInterface $client) { $this->httpClient = $client; return $this; } /** * Returns the HTTP client instance. * * @return HttpClientInterface */ public function getHttpClient() { return $this->httpClient; } /** * Sets the option provider instance. * * @param OptionProviderInterface $provider * @return self */ public function setOptionProvider(\YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface $provider) { $this->optionProvider = $provider; return $this; } /** * Returns the option provider instance. * * @return OptionProviderInterface */ public function getOptionProvider() { return $this->optionProvider; } /** * Returns the current value of the state parameter. * * This can be accessed by the redirect handler during authorization. * * @return string */ public function getState() { return $this->state; } /** * Set the value of the pkceCode parameter. * * When using PKCE this should be set before requesting an access token. * * @param string $pkceCode * @return self */ public function setPkceCode($pkceCode) { $this->pkceCode = $pkceCode; return $this; } /** * Returns the current value of the pkceCode parameter. * * This can be accessed by the redirect handler during authorization. * * @return string|null */ public function getPkceCode() { return $this->pkceCode; } /** * Returns the base URL for authorizing a client. * * Eg. https://oauth.service.com/authorize * * @return string */ public abstract function getBaseAuthorizationUrl(); /** * Returns the base URL for requesting an access token. * * Eg. https://oauth.service.com/token * * @param array $params * @return string */ public abstract function getBaseAccessTokenUrl(array $params); /** * Returns the URL for requesting the resource owner's details. * * @param AccessToken $token * @return string */ public abstract function getResourceOwnerDetailsUrl(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token); /** * Returns a new random string to use as the state parameter in an * authorization flow. * * @param int $length Length of the random string to be generated. * @return string */ protected function getRandomState($length = 32) { // Converting bytes to hex will always double length. Hence, we can reduce // the amount of bytes by half to produce the correct length. return \bin2hex(\random_bytes($length / 2)); } /** * Returns a new random string to use as PKCE code_verifier and * hashed as code_challenge parameters in an authorization flow. * Must be between 43 and 128 characters long. * * @param int $length Length of the random string to be generated. * @return string */ protected function getRandomPkceCode($length = 64) { return \substr(\strtr(\base64_encode(\random_bytes($length)), '+/', '-_'), 0, $length); } /** * Returns the default scopes used by this provider. * * This should only be the scopes that are required to request the details * of the resource owner, rather than all the available scopes. * * @return array */ protected abstract function getDefaultScopes(); /** * Returns the string that should be used to separate scopes when building * the URL for requesting an access token. * * @return string Scope separator, defaults to ',' */ protected function getScopeSeparator() { return ','; } /** * @return string|null */ protected function getPkceMethod() { return null; } /** * Returns authorization parameters based on provided options. * * @param array $options * @return array Authorization parameters * @throws InvalidArgumentException */ protected function getAuthorizationParameters(array $options) { if (empty($options['state'])) { $options['state'] = $this->getRandomState(); } if (empty($options['scope'])) { $options['scope'] = $this->getDefaultScopes(); } $options += ['response_type' => 'code', 'approval_prompt' => 'auto']; if (\is_array($options['scope'])) { $separator = $this->getScopeSeparator(); $options['scope'] = \implode($separator, $options['scope']); } // Store the state as it may need to be accessed later on. $this->state = $options['state']; $pkceMethod = $this->getPkceMethod(); if (!empty($pkceMethod)) { $this->pkceCode = $this->getRandomPkceCode(); if ($pkceMethod === static::PKCE_METHOD_S256) { $options['code_challenge'] = \trim(\strtr(\base64_encode(\hash('sha256', $this->pkceCode, \true)), '+/', '-_'), '='); } elseif ($pkceMethod === static::PKCE_METHOD_PLAIN) { $options['code_challenge'] = $this->pkceCode; } else { throw new \InvalidArgumentException('Unknown PKCE method "' . $pkceMethod . '".'); } $options['code_challenge_method'] = $pkceMethod; } // Business code layer might set a different redirect_uri parameter // depending on the context, leave it as-is if (!isset($options['redirect_uri'])) { $options['redirect_uri'] = $this->redirectUri; } $options['client_id'] = $this->clientId; return $options; } /** * Builds the authorization URL's query string. * * @param array $params Query parameters * @return string Query string */ protected function getAuthorizationQuery(array $params) { return $this->buildQueryString($params); } /** * Builds the authorization URL. * * @param array $options * @return string Authorization URL * @throws InvalidArgumentException */ public function getAuthorizationUrl(array $options = []) { $base = $this->getBaseAuthorizationUrl(); $params = $this->getAuthorizationParameters($options); $query = $this->getAuthorizationQuery($params); return $this->appendQuery($base, $query); } /** * Redirects the client for authorization. * * @param array $options * @param callable|null $redirectHandler * @return mixed * @throws InvalidArgumentException */ public function authorize(array $options = [], ?callable $redirectHandler = null) { $url = $this->getAuthorizationUrl($options); if ($redirectHandler) { return $redirectHandler($url, $this); } // @codeCoverageIgnoreStart \header('Location: ' . $url); exit; // @codeCoverageIgnoreEnd } /** * Appends a query string to a URL. * * @param string $url The URL to append the query to * @param string $query The HTTP query string * @return string The resulting URL */ protected function appendQuery($url, $query) { $query = \trim($query, '?&'); if ($query) { $glue = \strstr($url, '?') === \false ? '?' : '&'; return $url . $glue . $query; } return $url; } /** * Returns the method to use when requesting an access token. * * @return string HTTP method */ protected function getAccessTokenMethod() { return self::METHOD_POST; } /** * Returns the key used in the access token response to identify the resource owner. * * @return string|null Resource owner identifier key */ protected function getAccessTokenResourceOwnerId() { return static::ACCESS_TOKEN_RESOURCE_OWNER_ID; } /** * Builds the access token URL's query string. * * @param array $params Query parameters * @return string Query string */ protected function getAccessTokenQuery(array $params) { return $this->buildQueryString($params); } /** * Checks that a provided grant is valid, or attempts to produce one if the * provided grant is a string. * * @param AbstractGrant|string $grant * @return AbstractGrant */ protected function verifyGrant($grant) { if (\is_string($grant)) { return $this->grantFactory->getGrant($grant); } $this->grantFactory->checkGrant($grant); return $grant; } /** * Returns the full URL to use when requesting an access token. * * @param array $params Query parameters * @return string */ protected function getAccessTokenUrl(array $params) { $url = $this->getBaseAccessTokenUrl($params); if ($this->getAccessTokenMethod() === self::METHOD_GET) { $query = $this->getAccessTokenQuery($params); return $this->appendQuery($url, $query); } return $url; } /** * Returns a prepared request for requesting an access token. * * @param array $params Query string parameters * @return RequestInterface */ protected function getAccessTokenRequest(array $params) { $method = $this->getAccessTokenMethod(); $url = $this->getAccessTokenUrl($params); $options = $this->optionProvider->getAccessTokenOptions($this->getAccessTokenMethod(), $params); return $this->getRequest($method, $url, $options); } /** * Requests an access token using a specified grant and option set. * * @param mixed $grant * @param array<string, mixed> $options * @return AccessTokenInterface * @throws IdentityProviderException * @throws UnexpectedValueException * @throws GuzzleException */ public function getAccessToken($grant, array $options = []) { $grant = $this->verifyGrant($grant); if (isset($options['scope']) && \is_array($options['scope'])) { $separator = $this->getScopeSeparator(); $options['scope'] = \implode($separator, $options['scope']); } $params = ['client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'redirect_uri' => $this->redirectUri]; if (!empty($this->pkceCode)) { $params['code_verifier'] = $this->pkceCode; } $params = $grant->prepareRequestParameters($params, $options); $request = $this->getAccessTokenRequest($params); $response = $this->getParsedResponse($request); if (\false === \is_array($response)) { throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.'); } $prepared = $this->prepareAccessTokenResponse($response); $token = $this->createAccessToken($prepared, $grant); return $token; } /** * Returns a PSR-7 request instance that is not authenticated. * * @param string $method * @param string $url * @param array $options * @return RequestInterface */ public function getRequest($method, $url, array $options = []) { return $this->createRequest($method, $url, null, $options); } /** * Returns an authenticated PSR-7 request instance. * * @param string $method * @param string $url * @param AccessTokenInterface|string|null $token * @param array $options Any of "headers", "body", and "protocolVersion". * @return RequestInterface */ public function getAuthenticatedRequest($method, $url, $token, array $options = []) { return $this->createRequest($method, $url, $token, $options); } /** * Creates a PSR-7 request instance. * * @param string $method * @param string $url * @param AccessTokenInterface|string|null $token * @param array $options * @return RequestInterface */ protected function createRequest($method, $url, $token, array $options) { $defaults = ['headers' => $this->getHeaders($token)]; $options = \array_merge_recursive($defaults, $options); $factory = $this->getRequestFactory(); return $factory->getRequestWithOptions($method, $url, $options); } /** * Sends a request instance and returns a response instance. * * WARNING: This method does not attempt to catch exceptions caused by HTTP * errors! It is recommended to wrap this method in a try/catch block. * * @param RequestInterface $request * @return ResponseInterface * @throws GuzzleException */ public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { return $this->getHttpClient()->send($request); } /** * Sends a request and returns the parsed response. * * @param RequestInterface $request * @return mixed * @throws IdentityProviderException * @throws UnexpectedValueException * @throws GuzzleException */ public function getParsedResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { try { $response = $this->getResponse($request); } catch (\YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException $e) { $response = $e->getResponse(); } $parsed = $this->parseResponse($response); $this->checkResponse($response, $parsed); return $parsed; } /** * Attempts to parse a JSON response. * * @param string $content JSON content from response body * @return array Parsed JSON data * @throws UnexpectedValueException if the content could not be parsed */ protected function parseJson($content) { $content = \json_decode($content, \true); if (\json_last_error() !== \JSON_ERROR_NONE) { throw new \UnexpectedValueException(\sprintf("Failed to parse JSON response: %s", \json_last_error_msg())); } return $content; } /** * Returns the content type header of a response. * * @param ResponseInterface $response * @return string Semi-colon separated join of content-type headers. */ protected function getContentType(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { return \implode(';', $response->getHeader('content-type')); } /** * Parses the response according to its content-type header. * * @throws UnexpectedValueException * @param ResponseInterface $response * @return array */ protected function parseResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { $content = (string) $response->getBody(); $type = $this->getContentType($response); if (\strpos($type, 'urlencoded') !== \false) { \parse_str($content, $parsed); return $parsed; } // Attempt to parse the string as JSON regardless of content type, // since some providers use non-standard content types. Only throw an // exception if the JSON could not be parsed when it was expected to. try { return $this->parseJson($content); } catch (\UnexpectedValueException $e) { if (\strpos($type, 'json') !== \false) { throw $e; } if ($response->getStatusCode() == 500) { throw new \UnexpectedValueException('An OAuth server error was encountered that did not contain a JSON body', 0, $e); } return $content; } } /** * Checks a provider response for errors. * * @throws IdentityProviderException * @param ResponseInterface $response * @param array|string $data Parsed response data * @return void */ protected abstract function checkResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, $data); /** * Prepares an parsed access token response for a grant. * * Custom mapping of expiration, etc should be done here. Always call the * parent method when overloading this method. * * @param array<string, mixed> $result * @return array */ protected function prepareAccessTokenResponse(array $result) { if ($this->getAccessTokenResourceOwnerId() !== null) { $result['resource_owner_id'] = $this->getValueByKey($result, $this->getAccessTokenResourceOwnerId()); } return $result; } /** * Creates an access token from a response. * * The grant that was used to fetch the response can be used to provide * additional context. * * @param array $response * @param AbstractGrant $grant * @return AccessTokenInterface */ protected function createAccessToken(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Grant\AbstractGrant $grant) { return new \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken($response); } /** * Generates a resource owner object from a successful resource owner * details request. * * @param array $response * @param AccessToken $token * @return ResourceOwnerInterface */ protected abstract function createResourceOwner(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token); /** * Requests and returns the resource owner of given access token. * * @param AccessToken $token * @return ResourceOwnerInterface * @throws IdentityProviderException * @throws UnexpectedValueException * @throws GuzzleException */ public function getResourceOwner(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { $response = $this->fetchResourceOwnerDetails($token); return $this->createResourceOwner($response, $token); } /** * Requests resource owner details. * * @param AccessToken $token * @return mixed * @throws IdentityProviderException * @throws UnexpectedValueException * @throws GuzzleException */ protected function fetchResourceOwnerDetails(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { $url = $this->getResourceOwnerDetailsUrl($token); $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); $response = $this->getParsedResponse($request); if (\false === \is_array($response)) { throw new \UnexpectedValueException('Invalid response received from Authorization Server. Expected JSON.'); } return $response; } /** * Returns the default headers used by this provider. * * Typically this is used to set 'Accept' or 'Content-Type' headers. * * @return array */ protected function getDefaultHeaders() { return []; } /** * Returns the authorization headers used by this provider. * * Typically this is "Bearer" or "MAC". For more information see: * http://tools.ietf.org/html/rfc6749#section-7.1 * * No default is provided, providers must overload this method to activate * authorization headers. * * @param mixed|null $token Either a string or an access token instance * @return array */ protected function getAuthorizationHeaders($token = null) { return []; } /** * Returns all headers used by this provider for a request. * * The request will be authenticated if an access token is provided. * * @param mixed|null $token object or string * @return array */ public function getHeaders($token = null) { if ($token) { return \array_merge($this->getDefaultHeaders(), $this->getAuthorizationHeaders($token)); } return $this->getDefaultHeaders(); } } vendor_prefixed/league/oauth2-client/src/Provider/ResourceOwnerInterface.php 0000644 00000002002 15174712003 0023356 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Provider; /** * Classes implementing `ResourceOwnerInterface` may be used to represent * the resource owner authenticated with a service provider. */ interface ResourceOwnerInterface { /** * Returns the identifier of the authorized resource owner. * * @return mixed */ public function getId(); /** * Return all of the owner details available as an array. * * @return array */ public function toArray(); } vendor_prefixed/league/oauth2-client/src/Provider/GenericResourceOwner.php 0000644 00000002750 15174712003 0023044 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Provider; /** * Represents a generic resource owner for use with the GenericProvider. */ class GenericResourceOwner implements \YoastSEO_Vendor\League\OAuth2\Client\Provider\ResourceOwnerInterface { /** * @var array */ protected $response; /** * @var string */ protected $resourceOwnerId; /** * @param array $response * @param string $resourceOwnerId */ public function __construct(array $response, $resourceOwnerId) { $this->response = $response; $this->resourceOwnerId = $resourceOwnerId; } /** * Returns the identifier of the authorized resource owner. * * @return mixed */ public function getId() { return $this->response[$this->resourceOwnerId]; } /** * Returns the raw resource owner response. * * @return array */ public function toArray() { return $this->response; } } vendor_prefixed/league/oauth2-client/src/Provider/GenericProvider.php 0000644 00000013404 15174712003 0022032 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Provider; use InvalidArgumentException; use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken; use YoastSEO_Vendor\League\OAuth2\Client\Tool\BearerAuthorizationTrait; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; /** * Represents a generic service provider that may be used to interact with any * OAuth 2.0 service provider, using Bearer token authentication. */ class GenericProvider extends \YoastSEO_Vendor\League\OAuth2\Client\Provider\AbstractProvider { use BearerAuthorizationTrait; /** * @var string */ private $urlAuthorize; /** * @var string */ private $urlAccessToken; /** * @var string */ private $urlResourceOwnerDetails; /** * @var string */ private $accessTokenMethod; /** * @var string */ private $accessTokenResourceOwnerId; /** * @var array|null */ private $scopes = null; /** * @var string */ private $scopeSeparator; /** * @var string */ private $responseError = 'error'; /** * @var string */ private $responseCode; /** * @var string */ private $responseResourceOwnerId = 'id'; /** * @var string|null */ private $pkceMethod = null; /** * @param array $options * @param array $collaborators */ public function __construct(array $options = [], array $collaborators = []) { $this->assertRequiredOptions($options); $possible = $this->getConfigurableOptions(); $configured = \array_intersect_key($options, \array_flip($possible)); foreach ($configured as $key => $value) { $this->{$key} = $value; } // Remove all options that are only used locally $options = \array_diff_key($options, $configured); parent::__construct($options, $collaborators); } /** * Returns all options that can be configured. * * @return array */ protected function getConfigurableOptions() { return \array_merge($this->getRequiredOptions(), ['accessTokenMethod', 'accessTokenResourceOwnerId', 'scopeSeparator', 'responseError', 'responseCode', 'responseResourceOwnerId', 'scopes', 'pkceMethod']); } /** * Returns all options that are required. * * @return array */ protected function getRequiredOptions() { return ['urlAuthorize', 'urlAccessToken', 'urlResourceOwnerDetails']; } /** * Verifies that all required options have been passed. * * @param array $options * @return void * @throws InvalidArgumentException */ private function assertRequiredOptions(array $options) { $missing = \array_diff_key(\array_flip($this->getRequiredOptions()), $options); if (!empty($missing)) { throw new \InvalidArgumentException('Required options not defined: ' . \implode(', ', \array_keys($missing))); } } /** * @inheritdoc */ public function getBaseAuthorizationUrl() { return $this->urlAuthorize; } /** * @inheritdoc */ public function getBaseAccessTokenUrl(array $params) { return $this->urlAccessToken; } /** * @inheritdoc */ public function getResourceOwnerDetailsUrl(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { return $this->urlResourceOwnerDetails; } /** * @inheritdoc */ public function getDefaultScopes() { return $this->scopes; } /** * @inheritdoc */ protected function getAccessTokenMethod() { return $this->accessTokenMethod ?: parent::getAccessTokenMethod(); } /** * @inheritdoc */ protected function getAccessTokenResourceOwnerId() { return $this->accessTokenResourceOwnerId ?: parent::getAccessTokenResourceOwnerId(); } /** * @inheritdoc */ protected function getScopeSeparator() { return $this->scopeSeparator ?: parent::getScopeSeparator(); } /** * @inheritdoc */ protected function getPkceMethod() { return $this->pkceMethod ?: parent::getPkceMethod(); } /** * @inheritdoc */ protected function checkResponse(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response, $data) { if (!empty($data[$this->responseError])) { $error = $data[$this->responseError]; if (!\is_string($error)) { $error = \var_export($error, \true); } $code = $this->responseCode && !empty($data[$this->responseCode]) ? $data[$this->responseCode] : 0; if (!\is_int($code)) { $code = \intval($code); } throw new \YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException($error, $code, $data); } } /** * @inheritdoc */ protected function createResourceOwner(array $response, \YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token) { return new \YoastSEO_Vendor\League\OAuth2\Client\Provider\GenericResourceOwner($response, $this->responseResourceOwnerId); } } vendor_prefixed/league/oauth2-client/src/Tool/RequestFactory.php 0000644 00000004262 15174712003 0021050 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; use YoastSEO_Vendor\GuzzleHttp\Psr7\Request; /** * Used to produce PSR-7 Request instances. * * @link https://github.com/guzzle/guzzle/pull/1101 */ class RequestFactory { /** * Creates a PSR-7 Request instance. * * @param null|string $method HTTP method for the request. * @param null|string $uri URI for the request. * @param array $headers Headers for the message. * @param string|resource|StreamInterface $body Message body. * @param string $version HTTP protocol version. * * @return Request */ public function getRequest($method, $uri, array $headers = [], $body = null, $version = '1.1') { return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($method, $uri, $headers, $body, $version); } /** * Parses simplified options. * * @param array $options Simplified options. * * @return array Extended options for use with getRequest. */ protected function parseOptions(array $options) { // Should match default values for getRequest $defaults = ['headers' => [], 'body' => null, 'version' => '1.1']; return \array_merge($defaults, $options); } /** * Creates a request using a simplified array of options. * * @param null|string $method * @param null|string $uri * @param array $options * * @return Request */ public function getRequestWithOptions($method, $uri, array $options = []) { $options = $this->parseOptions($options); return $this->getRequest($method, $uri, $options['headers'], $options['body'], $options['version']); } } vendor_prefixed/league/oauth2-client/src/Tool/ArrayAccessorTrait.php 0000644 00000002662 15174712003 0021637 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; /** * Provides generic array navigation tools. */ trait ArrayAccessorTrait { /** * Returns a value by key using dot notation. * * @param array $data * @param string $key * @param mixed|null $default * @return mixed */ private function getValueByKey(array $data, $key, $default = null) { if (!\is_string($key) || empty($key) || !\count($data)) { return $default; } if (\strpos($key, '.') !== \false) { $keys = \explode('.', $key); foreach ($keys as $innerKey) { if (!\is_array($data) || !\array_key_exists($innerKey, $data)) { return $default; } $data = $data[$innerKey]; } return $data; } return \array_key_exists($key, $data) ? $data[$key] : $default; } } vendor_prefixed/league/oauth2-client/src/Tool/MacAuthorizationTrait.php 0000644 00000005133 15174712003 0022353 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface; /** * Enables `MAC` header authorization for providers. * * @link http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-05 Message Authentication Code (MAC) Tokens */ trait MacAuthorizationTrait { /** * Returns the id of this token for MAC generation. * * @param AccessToken $token * @return string */ protected abstract function getTokenId(\YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken $token); /** * Returns the MAC signature for the current request. * * @param string $id * @param integer $ts * @param string $nonce * @return string */ protected abstract function getMacSignature($id, $ts, $nonce); /** * Returns a new random string to use as the state parameter in an * authorization flow. * * @param int $length Length of the random string to be generated. * @return string */ protected abstract function getRandomState($length = 32); /** * Returns the authorization headers for the 'mac' grant. * * @param AccessTokenInterface|string|null $token Either a string or an access token instance * @return array * @codeCoverageIgnore * * @todo This is currently untested and provided only as an example. If you * complete the implementation, please create a pull request for * https://github.com/thephpleague/oauth2-client */ protected function getAuthorizationHeaders($token = null) { if ($token === null) { return []; } $ts = \time(); $id = $this->getTokenId($token); $nonce = $this->getRandomState(16); $mac = $this->getMacSignature($id, $ts, $nonce); $parts = []; foreach (\compact('id', 'ts', 'nonce', 'mac') as $key => $value) { $parts[] = \sprintf('%s="%s"', $key, $value); } return ['Authorization' => 'MAC ' . \implode(', ', $parts)]; } } vendor_prefixed/league/oauth2-client/src/Tool/BearerAuthorizationTrait.php 0000644 00000002173 15174712003 0023054 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface; /** * Enables `Bearer` header authorization for providers. * * @link http://tools.ietf.org/html/rfc6750 Bearer Token Usage (RFC 6750) */ trait BearerAuthorizationTrait { /** * Returns authorization headers for the 'bearer' grant. * * @param AccessTokenInterface|string|null $token Either a string or an access token instance * @return array */ protected function getAuthorizationHeaders($token = null) { return ['Authorization' => 'Bearer ' . $token]; } } vendor_prefixed/league/oauth2-client/src/Tool/QueryBuilderTrait.php 0000644 00000001630 15174712003 0021504 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; /** * Provides a standard way to generate query strings. */ trait QueryBuilderTrait { /** * Build a query string from an array. * * @param array $params * * @return string */ protected function buildQueryString(array $params) { return \http_build_query($params, '', '&', \PHP_QUERY_RFC3986); } } vendor_prefixed/league/oauth2-client/src/Tool/RequiredParameterTrait.php 0000644 00000002740 15174712003 0022514 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; use BadMethodCallException; /** * Provides functionality to check for required parameters. */ trait RequiredParameterTrait { /** * Checks for a required parameter in a hash. * * @throws BadMethodCallException * @param string $name * @param array $params * @return void */ private function checkRequiredParameter($name, array $params) { if (!isset($params[$name])) { throw new \BadMethodCallException(\sprintf('Required parameter not passed: "%s"', $name)); } } /** * Checks for multiple required parameters in a hash. * * @throws InvalidArgumentException * @param array $names * @param array $params * @return void */ private function checkRequiredParameters(array $names, array $params) { foreach ($names as $name) { $this->checkRequiredParameter($name, $params); } } } vendor_prefixed/league/oauth2-client/src/Tool/ProviderRedirectTrait.php 0000644 00000006472 15174712003 0022355 0 ustar 00 <?php namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException; use YoastSEO_Vendor\GuzzleHttp\Psr7\Uri; use InvalidArgumentException; use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface; trait ProviderRedirectTrait { /** * Maximum number of times to follow provider initiated redirects * * @var integer */ protected $redirectLimit = 2; /** * Retrieves a response for a given request and retrieves subsequent * responses, with authorization headers, if a redirect is detected. * * @param RequestInterface $request * @return ResponseInterface * @throws BadResponseException */ protected function followRequestRedirects(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { $response = null; $attempts = 0; while ($attempts < $this->redirectLimit) { $attempts++; $response = $this->getHttpClient()->send($request, ['allow_redirects' => \false]); if ($this->isRedirect($response)) { $redirectUrl = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($response->getHeader('Location')[0]); $request = $request->withUri($redirectUrl); } else { break; } } return $response; } /** * Returns the HTTP client instance. * * @return GuzzleHttp\ClientInterface */ public abstract function getHttpClient(); /** * Retrieves current redirect limit. * * @return integer */ public function getRedirectLimit() { return $this->redirectLimit; } /** * Determines if a given response is a redirect. * * @param ResponseInterface $response * * @return boolean */ protected function isRedirect(\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response) { $statusCode = $response->getStatusCode(); return $statusCode > 300 && $statusCode < 400 && $response->hasHeader('Location'); } /** * Sends a request instance and returns a response instance. * * WARNING: This method does not attempt to catch exceptions caused by HTTP * errors! It is recommended to wrap this method in a try/catch block. * * @param RequestInterface $request * @return ResponseInterface */ public function getResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) { try { $response = $this->followRequestRedirects($request); } catch (\YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException $e) { $response = $e->getResponse(); } return $response; } /** * Updates the redirect limit. * * @param integer $limit * @return League\OAuth2\Client\Provider\AbstractProvider * @throws InvalidArgumentException */ public function setRedirectLimit($limit) { if (!\is_int($limit)) { throw new \InvalidArgumentException('redirectLimit must be an integer.'); } if ($limit < 1) { throw new \InvalidArgumentException('redirectLimit must be greater than or equal to one.'); } $this->redirectLimit = $limit; return $this; } } vendor_prefixed/league/oauth2-client/src/Tool/GuardedPropertyTrait.php 0000644 00000003432 15174712003 0022212 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\Tool; /** * Provides support for blacklisting explicit properties from the * mass assignment behavior. */ trait GuardedPropertyTrait { /** * The properties that aren't mass assignable. * * @var array */ protected $guarded = []; /** * Attempts to mass assign the given options to explicitly defined properties, * skipping over any properties that are defined in the guarded array. * * @param array $options * @return mixed */ protected function fillProperties(array $options = []) { if (isset($options['guarded'])) { unset($options['guarded']); } foreach ($options as $option => $value) { if (\property_exists($this, $option) && !$this->isGuarded($option)) { $this->{$option} = $value; } } } /** * Returns current guarded properties. * * @return array */ public function getGuarded() { return $this->guarded; } /** * Determines if the given property is guarded. * * @param string $property * @return bool */ public function isGuarded($property) { return \in_array($property, $this->getGuarded()); } } vendor_prefixed/league/oauth2-client/src/OptionProvider/HttpBasicAuthOptionProvider.php 0000644 00000002752 15174712003 0025547 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\OptionProvider; use InvalidArgumentException; /** * Add http basic auth into access token request options * @link https://tools.ietf.org/html/rfc6749#section-2.3.1 */ class HttpBasicAuthOptionProvider extends \YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\PostAuthOptionProvider { /** * @inheritdoc */ public function getAccessTokenOptions($method, array $params) { if (empty($params['client_id']) || empty($params['client_secret'])) { throw new \InvalidArgumentException('clientId and clientSecret are required for http basic auth'); } $encodedCredentials = \base64_encode(\sprintf('%s:%s', $params['client_id'], $params['client_secret'])); unset($params['client_id'], $params['client_secret']); $options = parent::getAccessTokenOptions($method, $params); $options['headers']['Authorization'] = 'Basic ' . $encodedCredentials; return $options; } } vendor_prefixed/league/oauth2-client/src/OptionProvider/PostAuthOptionProvider.php 0000644 00000003041 15174712003 0024603 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\OptionProvider; use YoastSEO_Vendor\League\OAuth2\Client\Provider\AbstractProvider; use YoastSEO_Vendor\League\OAuth2\Client\Tool\QueryBuilderTrait; /** * Provide options for access token */ class PostAuthOptionProvider implements \YoastSEO_Vendor\League\OAuth2\Client\OptionProvider\OptionProviderInterface { use QueryBuilderTrait; /** * @inheritdoc */ public function getAccessTokenOptions($method, array $params) { $options = ['headers' => ['content-type' => 'application/x-www-form-urlencoded']]; if ($method === \YoastSEO_Vendor\League\OAuth2\Client\Provider\AbstractProvider::METHOD_POST) { $options['body'] = $this->getAccessTokenBody($params); } return $options; } /** * Returns the request body for requesting an access token. * * @param array $params * @return string */ protected function getAccessTokenBody(array $params) { return $this->buildQueryString($params); } } vendor_prefixed/league/oauth2-client/src/OptionProvider/OptionProviderInterface.php 0000644 00000001613 15174712003 0024737 0 ustar 00 <?php /** * This file is part of the league/oauth2-client library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Alex Bilbie <hello@alexbilbie.com> * @license http://opensource.org/licenses/MIT MIT * @link http://thephpleague.com/oauth2-client/ Documentation * @link https://packagist.org/packages/league/oauth2-client Packagist * @link https://github.com/thephpleague/oauth2-client GitHub */ namespace YoastSEO_Vendor\League\OAuth2\Client\OptionProvider; /** * Interface for access token options provider */ interface OptionProviderInterface { /** * Builds request options used for requesting an access token. * * @param string $method * @param array $params * @return array */ public function getAccessTokenOptions($method, array $params); } vendor/composer/autoload_namespaces.php 0000644 00000000213 15174712003 0014404 0 ustar 00 <?php // autoload_namespaces.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( ); vendor/composer/platform_check.php 0000644 00000001625 15174712003 0013366 0 ustar 00 <?php // platform_check.php @generated by Composer $issues = array(); if (!(PHP_VERSION_ID >= 70400)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); } elseif (!headers_sent()) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } throw new \RuntimeException( 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } vendor/composer/autoload_psr4.php 0000644 00000000351 15174712003 0013160 0 ustar 00 <?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'), ); vendor/composer/LICENSE 0000644 00000002056 15174712003 0010700 0 ustar 00 Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vendor/composer/installed.php 0000644 00000002171 15174712003 0012361 0 ustar 00 <?php return array( 'root' => array( 'name' => 'yoast/wordpress-seo', 'pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => '23edb9bc80a412593153cb438b601481d1cdbf95', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false, ), 'versions' => array( 'composer/installers' => array( 'pretty_version' => 'v2.3.0', 'version' => '2.3.0.0', 'reference' => '12fb2dfe5e16183de69e784a7b84046c43d97e8e', 'type' => 'composer-plugin', 'install_path' => __DIR__ . '/./installers', 'aliases' => array(), 'dev_requirement' => false, ), 'yoast/wordpress-seo' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => '23edb9bc80a412593153cb438b601481d1cdbf95', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), ), ); vendor/composer/autoload_static.php 0000644 00000652036 15174712003 0013574 0 ustar 00 <?php // autoload_static.php @generated by Composer namespace Composer\Autoload; class ComposerStaticInit39a23b5c2ed5051c90d7939162f27c90 { public static $prefixLengthsPsr4 = array ( 'C' => array ( 'Composer\\Installers\\' => 20, ), ); public static $prefixDirsPsr4 = array ( 'Composer\\Installers\\' => array ( 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers', ), ); public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\Installers\\AglInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AglInstaller.php', 'Composer\\Installers\\AkauntingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AkauntingInstaller.php', 'Composer\\Installers\\AnnotateCmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', 'Composer\\Installers\\AsgardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', 'Composer\\Installers\\AttogramInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', 'Composer\\Installers\\BaseInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BaseInstaller.php', 'Composer\\Installers\\BitrixInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', 'Composer\\Installers\\BonefishInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', 'Composer\\Installers\\BotbleInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BotbleInstaller.php', 'Composer\\Installers\\CakePHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', 'Composer\\Installers\\ChefInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ChefInstaller.php', 'Composer\\Installers\\CiviCrmInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', 'Composer\\Installers\\ClanCatsFrameworkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', 'Composer\\Installers\\CockpitInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', 'Composer\\Installers\\CodeIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', 'Composer\\Installers\\Concrete5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', 'Composer\\Installers\\ConcreteCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ConcreteCMSInstaller.php', 'Composer\\Installers\\CroogoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', 'Composer\\Installers\\DecibelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', 'Composer\\Installers\\DframeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DframeInstaller.php', 'Composer\\Installers\\DokuWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', 'Composer\\Installers\\DolibarrInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', 'Composer\\Installers\\DrupalInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', 'Composer\\Installers\\ElggInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ElggInstaller.php', 'Composer\\Installers\\EliasisInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', 'Composer\\Installers\\ExpressionEngineInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', 'Composer\\Installers\\EzPlatformInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', 'Composer\\Installers\\ForkCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ForkCMSInstaller.php', 'Composer\\Installers\\FuelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelInstaller.php', 'Composer\\Installers\\FuelphpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', 'Composer\\Installers\\GravInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/GravInstaller.php', 'Composer\\Installers\\HuradInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/HuradInstaller.php', 'Composer\\Installers\\ImageCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', 'Composer\\Installers\\Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Installer.php', 'Composer\\Installers\\ItopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ItopInstaller.php', 'Composer\\Installers\\KanboardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', 'Composer\\Installers\\KnownInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KnownInstaller.php', 'Composer\\Installers\\KodiCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', 'Composer\\Installers\\KohanaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', 'Composer\\Installers\\LanManagementSystemInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', 'Composer\\Installers\\LaravelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', 'Composer\\Installers\\LavaLiteInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', 'Composer\\Installers\\LithiumInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', 'Composer\\Installers\\MODULEWorkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', 'Composer\\Installers\\MODXEvoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', 'Composer\\Installers\\MagentoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', 'Composer\\Installers\\MajimaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', 'Composer\\Installers\\MakoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MakoInstaller.php', 'Composer\\Installers\\MantisBTInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', 'Composer\\Installers\\MatomoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MatomoInstaller.php', 'Composer\\Installers\\MauticInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MauticInstaller.php', 'Composer\\Installers\\MayaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MayaInstaller.php', 'Composer\\Installers\\MediaWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', 'Composer\\Installers\\MiaoxingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', 'Composer\\Installers\\MicroweberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', 'Composer\\Installers\\ModxInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ModxInstaller.php', 'Composer\\Installers\\MoodleInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', 'Composer\\Installers\\OctoberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', 'Composer\\Installers\\OntoWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', 'Composer\\Installers\\OsclassInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', 'Composer\\Installers\\OxidInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OxidInstaller.php', 'Composer\\Installers\\PPIInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PPIInstaller.php', 'Composer\\Installers\\PantheonInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', 'Composer\\Installers\\PhiftyInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', 'Composer\\Installers\\PhpBBInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', 'Composer\\Installers\\PiwikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', 'Composer\\Installers\\PlentymarketsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', 'Composer\\Installers\\Plugin' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Plugin.php', 'Composer\\Installers\\PortoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PortoInstaller.php', 'Composer\\Installers\\PrestashopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', 'Composer\\Installers\\ProcessWireInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', 'Composer\\Installers\\PuppetInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', 'Composer\\Installers\\PxcmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', 'Composer\\Installers\\RadPHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', 'Composer\\Installers\\ReIndexInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', 'Composer\\Installers\\Redaxo5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', 'Composer\\Installers\\RedaxoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', 'Composer\\Installers\\RoundcubeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', 'Composer\\Installers\\SMFInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SMFInstaller.php', 'Composer\\Installers\\ShopwareInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', 'Composer\\Installers\\SilverStripeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', 'Composer\\Installers\\SiteDirectInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', 'Composer\\Installers\\StarbugInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', 'Composer\\Installers\\SyDESInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', 'Composer\\Installers\\SyliusInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', 'Composer\\Installers\\TaoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TaoInstaller.php', 'Composer\\Installers\\TastyIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', 'Composer\\Installers\\TheliaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', 'Composer\\Installers\\TuskInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TuskInstaller.php', 'Composer\\Installers\\UserFrostingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', 'Composer\\Installers\\VanillaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', 'Composer\\Installers\\VgmcpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', 'Composer\\Installers\\WHMCSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', 'Composer\\Installers\\WinterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WinterInstaller.php', 'Composer\\Installers\\WolfCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', 'Composer\\Installers\\WordPressInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', 'Composer\\Installers\\YawikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/YawikInstaller.php', 'Composer\\Installers\\ZendInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZendInstaller.php', 'Composer\\Installers\\ZikulaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', 'WPSEO_Abstract_Capability_Manager' => __DIR__ . '/../..' . '/admin/capabilities/class-abstract-capability-manager.php', 'WPSEO_Abstract_Metabox_Tab_With_Sections' => __DIR__ . '/../..' . '/admin/metabox/class-abstract-sectioned-metabox-tab.php', 'WPSEO_Abstract_Post_Filter' => __DIR__ . '/../..' . '/admin/filters/class-abstract-post-filter.php', 'WPSEO_Abstract_Role_Manager' => __DIR__ . '/../..' . '/admin/roles/class-abstract-role-manager.php', 'WPSEO_Addon_Manager' => __DIR__ . '/../..' . '/inc/class-addon-manager.php', 'WPSEO_Admin' => __DIR__ . '/../..' . '/admin/class-admin.php', 'WPSEO_Admin_Asset' => __DIR__ . '/../..' . '/admin/class-asset.php', 'WPSEO_Admin_Asset_Analysis_Worker_Location' => __DIR__ . '/../..' . '/admin/class-admin-asset-analysis-worker-location.php', 'WPSEO_Admin_Asset_Dev_Server_Location' => __DIR__ . '/../..' . '/admin/class-admin-asset-dev-server-location.php', 'WPSEO_Admin_Asset_Location' => __DIR__ . '/../..' . '/admin/class-admin-asset-location.php', 'WPSEO_Admin_Asset_Manager' => __DIR__ . '/../..' . '/admin/class-admin-asset-manager.php', 'WPSEO_Admin_Asset_SEO_Location' => __DIR__ . '/../..' . '/admin/class-admin-asset-seo-location.php', 'WPSEO_Admin_Bar_Menu' => __DIR__ . '/../..' . '/inc/class-wpseo-admin-bar-menu.php', 'WPSEO_Admin_Editor_Specific_Replace_Vars' => __DIR__ . '/../..' . '/admin/class-admin-editor-specific-replace-vars.php', 'WPSEO_Admin_Gutenberg_Compatibility_Notification' => __DIR__ . '/../..' . '/admin/class-admin-gutenberg-compatibility-notification.php', 'WPSEO_Admin_Help_Panel' => __DIR__ . '/../..' . '/admin/class-admin-help-panel.php', 'WPSEO_Admin_Init' => __DIR__ . '/../..' . '/admin/class-admin-init.php', 'WPSEO_Admin_Menu' => __DIR__ . '/../..' . '/admin/menu/class-admin-menu.php', 'WPSEO_Admin_Pages' => __DIR__ . '/../..' . '/admin/class-config.php', 'WPSEO_Admin_Recommended_Replace_Vars' => __DIR__ . '/../..' . '/admin/class-admin-recommended-replace-vars.php', 'WPSEO_Admin_Settings_Changed_Listener' => __DIR__ . '/../..' . '/admin/admin-settings-changed-listener.php', 'WPSEO_Admin_User_Profile' => __DIR__ . '/../..' . '/admin/class-admin-user-profile.php', 'WPSEO_Admin_Utils' => __DIR__ . '/../..' . '/admin/class-admin-utils.php', 'WPSEO_Author_Sitemap_Provider' => __DIR__ . '/../..' . '/inc/sitemaps/class-author-sitemap-provider.php', 'WPSEO_Base_Menu' => __DIR__ . '/../..' . '/admin/menu/class-base-menu.php', 'WPSEO_Breadcrumbs' => __DIR__ . '/../..' . '/src/deprecated/frontend/breadcrumbs.php', 'WPSEO_Bulk_Description_List_Table' => __DIR__ . '/../..' . '/admin/class-bulk-description-editor-list-table.php', 'WPSEO_Bulk_List_Table' => __DIR__ . '/../..' . '/admin/class-bulk-editor-list-table.php', 'WPSEO_Bulk_Title_Editor_List_Table' => __DIR__ . '/../..' . '/admin/class-bulk-title-editor-list-table.php', 'WPSEO_Capability_Manager' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-manager.php', 'WPSEO_Capability_Manager_Factory' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-manager-factory.php', 'WPSEO_Capability_Manager_Integration' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-manager-integration.php', 'WPSEO_Capability_Manager_VIP' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-manager-vip.php', 'WPSEO_Capability_Manager_WP' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-manager-wp.php', 'WPSEO_Capability_Utils' => __DIR__ . '/../..' . '/admin/capabilities/class-capability-utils.php', 'WPSEO_Collection' => __DIR__ . '/../..' . '/admin/interface-collection.php', 'WPSEO_Collector' => __DIR__ . '/../..' . '/admin/class-collector.php', 'WPSEO_Content_Images' => __DIR__ . '/../..' . '/inc/class-wpseo-content-images.php', 'WPSEO_Cornerstone_Filter' => __DIR__ . '/../..' . '/admin/filters/class-cornerstone-filter.php', 'WPSEO_Custom_Fields' => __DIR__ . '/../..' . '/inc/class-wpseo-custom-fields.php', 'WPSEO_Custom_Taxonomies' => __DIR__ . '/../..' . '/inc/class-wpseo-custom-taxonomies.php', 'WPSEO_Database_Proxy' => __DIR__ . '/../..' . '/admin/class-database-proxy.php', 'WPSEO_Date_Helper' => __DIR__ . '/../..' . '/inc/date-helper.php', 'WPSEO_Dismissible_Notification' => __DIR__ . '/../..' . '/admin/notifiers/dismissible-notification.php', 'WPSEO_Endpoint' => __DIR__ . '/../..' . '/admin/endpoints/class-endpoint.php', 'WPSEO_Endpoint_File_Size' => __DIR__ . '/../..' . '/admin/endpoints/class-endpoint-file-size.php', 'WPSEO_Endpoint_Statistics' => __DIR__ . '/../..' . '/admin/endpoints/class-endpoint-statistics.php', 'WPSEO_Export' => __DIR__ . '/../..' . '/admin/class-export.php', 'WPSEO_Expose_Shortlinks' => __DIR__ . '/../..' . '/admin/class-expose-shortlinks.php', 'WPSEO_File_Size_Exception' => __DIR__ . '/../..' . '/admin/exceptions/class-file-size-exception.php', 'WPSEO_File_Size_Service' => __DIR__ . '/../..' . '/admin/services/class-file-size.php', 'WPSEO_Frontend' => __DIR__ . '/../..' . '/src/deprecated/frontend/frontend.php', 'WPSEO_GSC' => __DIR__ . '/../..' . '/admin/google_search_console/class-gsc.php', 'WPSEO_Gutenberg_Compatibility' => __DIR__ . '/../..' . '/admin/class-gutenberg-compatibility.php', 'WPSEO_Image_Utils' => __DIR__ . '/../..' . '/inc/class-wpseo-image-utils.php', 'WPSEO_Import_AIOSEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-aioseo.php', 'WPSEO_Import_AIOSEO_V4' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-aioseo-v4.php', 'WPSEO_Import_Greg_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-greg-high-performance-seo.php', 'WPSEO_Import_HeadSpace' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-headspace.php', 'WPSEO_Import_Jetpack_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-jetpack.php', 'WPSEO_Import_Platinum_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-platinum-seo-pack.php', 'WPSEO_Import_Plugin' => __DIR__ . '/../..' . '/admin/import/class-import-plugin.php', 'WPSEO_Import_Plugins_Detector' => __DIR__ . '/../..' . '/admin/import/class-import-detector.php', 'WPSEO_Import_Premium_SEO_Pack' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-premium-seo-pack.php', 'WPSEO_Import_RankMath' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-rankmath.php', 'WPSEO_Import_SEOPressor' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-seopressor.php', 'WPSEO_Import_SEO_Framework' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-seo-framework.php', 'WPSEO_Import_Settings' => __DIR__ . '/../..' . '/admin/import/class-import-settings.php', 'WPSEO_Import_Smartcrawl_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-smartcrawl.php', 'WPSEO_Import_Squirrly' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-squirrly.php', 'WPSEO_Import_Status' => __DIR__ . '/../..' . '/admin/import/class-import-status.php', 'WPSEO_Import_Ultimate_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-ultimate-seo.php', 'WPSEO_Import_WPSEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-wpseo.php', 'WPSEO_Import_WP_Meta_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-wp-meta-seo.php', 'WPSEO_Import_WooThemes_SEO' => __DIR__ . '/../..' . '/admin/import/plugins/class-import-woothemes-seo.php', 'WPSEO_Installable' => __DIR__ . '/../..' . '/admin/interface-installable.php', 'WPSEO_Installation' => __DIR__ . '/../..' . '/inc/class-wpseo-installation.php', 'WPSEO_Language_Utils' => __DIR__ . '/../..' . '/inc/language-utils.php', 'WPSEO_Listener' => __DIR__ . '/../..' . '/admin/listeners/class-listener.php', 'WPSEO_Menu' => __DIR__ . '/../..' . '/admin/menu/class-menu.php', 'WPSEO_Meta' => __DIR__ . '/../..' . '/inc/class-wpseo-meta.php', 'WPSEO_Meta_Columns' => __DIR__ . '/../..' . '/admin/class-meta-columns.php', 'WPSEO_Metabox' => __DIR__ . '/../..' . '/admin/metabox/class-metabox.php', 'WPSEO_Metabox_Analysis' => __DIR__ . '/../..' . '/admin/metabox/interface-metabox-analysis.php', 'WPSEO_Metabox_Analysis_Inclusive_Language' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-analysis-inclusive-language.php', 'WPSEO_Metabox_Analysis_Readability' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-analysis-readability.php', 'WPSEO_Metabox_Analysis_SEO' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-analysis-seo.php', 'WPSEO_Metabox_Collapsible' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-collapsible.php', 'WPSEO_Metabox_Collapsibles_Sections' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-collapsibles-section.php', 'WPSEO_Metabox_Editor' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-editor.php', 'WPSEO_Metabox_Form_Tab' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-form-tab.php', 'WPSEO_Metabox_Formatter' => __DIR__ . '/../..' . '/admin/formatter/class-metabox-formatter.php', 'WPSEO_Metabox_Formatter_Interface' => __DIR__ . '/../..' . '/admin/formatter/interface-metabox-formatter.php', 'WPSEO_Metabox_Null_Tab' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-null-tab.php', 'WPSEO_Metabox_Section' => __DIR__ . '/../..' . '/admin/metabox/interface-metabox-section.php', 'WPSEO_Metabox_Section_Additional' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-section-additional.php', 'WPSEO_Metabox_Section_Inclusive_Language' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-section-inclusive-language.php', 'WPSEO_Metabox_Section_React' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-section-react.php', 'WPSEO_Metabox_Section_Readability' => __DIR__ . '/../..' . '/admin/metabox/class-metabox-section-readability.php', 'WPSEO_Metabox_Tab' => __DIR__ . '/../..' . '/admin/metabox/interface-metabox-tab.php', 'WPSEO_MyYoast_Api_Request' => __DIR__ . '/../..' . '/inc/class-my-yoast-api-request.php', 'WPSEO_MyYoast_Bad_Request_Exception' => __DIR__ . '/../..' . '/inc/exceptions/class-myyoast-bad-request-exception.php', 'WPSEO_MyYoast_Invalid_JSON_Exception' => __DIR__ . '/../..' . '/inc/exceptions/class-myyoast-invalid-json-exception.php', 'WPSEO_MyYoast_Proxy' => __DIR__ . '/../..' . '/admin/class-my-yoast-proxy.php', 'WPSEO_Network_Admin_Menu' => __DIR__ . '/../..' . '/admin/menu/class-network-admin-menu.php', 'WPSEO_Notification_Handler' => __DIR__ . '/../..' . '/admin/notifiers/interface-notification-handler.php', 'WPSEO_Option' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option.php', 'WPSEO_Option_Llmstxt' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-llmstxt.php', 'WPSEO_Option_MS' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-ms.php', 'WPSEO_Option_Social' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-social.php', 'WPSEO_Option_Tab' => __DIR__ . '/../..' . '/admin/class-option-tab.php', 'WPSEO_Option_Tabs' => __DIR__ . '/../..' . '/admin/class-option-tabs.php', 'WPSEO_Option_Tabs_Formatter' => __DIR__ . '/../..' . '/admin/class-option-tabs-formatter.php', 'WPSEO_Option_Titles' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-titles.php', 'WPSEO_Option_Tracking_Only' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-tracking-only.php', 'WPSEO_Option_Wpseo' => __DIR__ . '/../..' . '/inc/options/class-wpseo-option-wpseo.php', 'WPSEO_Options' => __DIR__ . '/../..' . '/inc/options/class-wpseo-options.php', 'WPSEO_Paper_Presenter' => __DIR__ . '/../..' . '/admin/class-paper-presenter.php', 'WPSEO_Plugin_Availability' => __DIR__ . '/../..' . '/admin/class-plugin-availability.php', 'WPSEO_Plugin_Conflict' => __DIR__ . '/../..' . '/admin/class-plugin-conflict.php', 'WPSEO_Plugin_Importer' => __DIR__ . '/../..' . '/admin/import/plugins/class-abstract-plugin-importer.php', 'WPSEO_Plugin_Importers' => __DIR__ . '/../..' . '/admin/import/plugins/class-importers.php', 'WPSEO_Post_Metabox_Formatter' => __DIR__ . '/../..' . '/admin/formatter/class-post-metabox-formatter.php', 'WPSEO_Post_Type' => __DIR__ . '/../..' . '/inc/class-post-type.php', 'WPSEO_Post_Type_Sitemap_Provider' => __DIR__ . '/../..' . '/inc/sitemaps/class-post-type-sitemap-provider.php', 'WPSEO_Premium_Popup' => __DIR__ . '/../..' . '/admin/class-premium-popup.php', 'WPSEO_Premium_Upsell_Admin_Block' => __DIR__ . '/../..' . '/admin/class-premium-upsell-admin-block.php', 'WPSEO_Primary_Term' => __DIR__ . '/../..' . '/inc/class-wpseo-primary-term.php', 'WPSEO_Primary_Term_Admin' => __DIR__ . '/../..' . '/admin/class-primary-term-admin.php', 'WPSEO_Product_Upsell_Notice' => __DIR__ . '/../..' . '/admin/class-product-upsell-notice.php', 'WPSEO_Rank' => __DIR__ . '/../..' . '/inc/class-wpseo-rank.php', 'WPSEO_Register_Capabilities' => __DIR__ . '/../..' . '/admin/capabilities/class-register-capabilities.php', 'WPSEO_Register_Roles' => __DIR__ . '/../..' . '/admin/roles/class-register-roles.php', 'WPSEO_Remote_Request' => __DIR__ . '/../..' . '/admin/class-remote-request.php', 'WPSEO_Replace_Vars' => __DIR__ . '/../..' . '/inc/class-wpseo-replace-vars.php', 'WPSEO_Replacement_Variable' => __DIR__ . '/../..' . '/inc/class-wpseo-replacement-variable.php', 'WPSEO_Replacevar_Editor' => __DIR__ . '/../..' . '/admin/menu/class-replacevar-editor.php', 'WPSEO_Replacevar_Field' => __DIR__ . '/../..' . '/admin/menu/class-replacevar-field.php', 'WPSEO_Rewrite' => __DIR__ . '/../..' . '/inc/class-rewrite.php', 'WPSEO_Role_Manager' => __DIR__ . '/../..' . '/admin/roles/class-role-manager.php', 'WPSEO_Role_Manager_Factory' => __DIR__ . '/../..' . '/admin/roles/class-role-manager-factory.php', 'WPSEO_Role_Manager_WP' => __DIR__ . '/../..' . '/admin/roles/class-role-manager-wp.php', 'WPSEO_Schema_Person_Upgrade_Notification' => __DIR__ . '/../..' . '/admin/class-schema-person-upgrade-notification.php', 'WPSEO_Shortcode_Filter' => __DIR__ . '/../..' . '/admin/ajax/class-shortcode-filter.php', 'WPSEO_Shortlinker' => __DIR__ . '/../..' . '/inc/class-wpseo-shortlinker.php', 'WPSEO_Sitemap_Cache_Data' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemap-cache-data.php', 'WPSEO_Sitemap_Cache_Data_Interface' => __DIR__ . '/../..' . '/inc/sitemaps/interface-sitemap-cache-data.php', 'WPSEO_Sitemap_Image_Parser' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemap-image-parser.php', 'WPSEO_Sitemap_Provider' => __DIR__ . '/../..' . '/inc/sitemaps/interface-sitemap-provider.php', 'WPSEO_Sitemaps' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps.php', 'WPSEO_Sitemaps_Admin' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps-admin.php', 'WPSEO_Sitemaps_Cache' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps-cache.php', 'WPSEO_Sitemaps_Cache_Validator' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps-cache-validator.php', 'WPSEO_Sitemaps_Renderer' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps-renderer.php', 'WPSEO_Sitemaps_Router' => __DIR__ . '/../..' . '/inc/sitemaps/class-sitemaps-router.php', 'WPSEO_Slug_Change_Watcher' => __DIR__ . '/../..' . '/admin/watchers/class-slug-change-watcher.php', 'WPSEO_Statistic_Integration' => __DIR__ . '/../..' . '/admin/statistics/class-statistics-integration.php', 'WPSEO_Statistics' => __DIR__ . '/../..' . '/inc/class-wpseo-statistics.php', 'WPSEO_Statistics_Service' => __DIR__ . '/../..' . '/admin/statistics/class-statistics-service.php', 'WPSEO_Submenu_Capability_Normalize' => __DIR__ . '/../..' . '/admin/menu/class-submenu-capability-normalize.php', 'WPSEO_Suggested_Plugins' => __DIR__ . '/../..' . '/admin/class-suggested-plugins.php', 'WPSEO_Taxonomy' => __DIR__ . '/../..' . '/admin/taxonomy/class-taxonomy.php', 'WPSEO_Taxonomy_Columns' => __DIR__ . '/../..' . '/admin/taxonomy/class-taxonomy-columns.php', 'WPSEO_Taxonomy_Fields' => __DIR__ . '/../..' . '/admin/taxonomy/class-taxonomy-fields.php', 'WPSEO_Taxonomy_Fields_Presenter' => __DIR__ . '/../..' . '/admin/taxonomy/class-taxonomy-fields-presenter.php', 'WPSEO_Taxonomy_Meta' => __DIR__ . '/../..' . '/inc/options/class-wpseo-taxonomy-meta.php', 'WPSEO_Taxonomy_Metabox' => __DIR__ . '/../..' . '/admin/taxonomy/class-taxonomy-metabox.php', 'WPSEO_Taxonomy_Sitemap_Provider' => __DIR__ . '/../..' . '/inc/sitemaps/class-taxonomy-sitemap-provider.php', 'WPSEO_Term_Metabox_Formatter' => __DIR__ . '/../..' . '/admin/formatter/class-term-metabox-formatter.php', 'WPSEO_Tracking' => __DIR__ . '/../..' . '/admin/tracking/class-tracking.php', 'WPSEO_Tracking_Addon_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-addon-data.php', 'WPSEO_Tracking_Default_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-default-data.php', 'WPSEO_Tracking_Plugin_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-plugin-data.php', 'WPSEO_Tracking_Server_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-server-data.php', 'WPSEO_Tracking_Settings_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-settings-data.php', 'WPSEO_Tracking_Theme_Data' => __DIR__ . '/../..' . '/admin/tracking/class-tracking-theme-data.php', 'WPSEO_Upgrade' => __DIR__ . '/../..' . '/inc/class-upgrade.php', 'WPSEO_Upgrade_History' => __DIR__ . '/../..' . '/inc/class-upgrade-history.php', 'WPSEO_Utils' => __DIR__ . '/../..' . '/inc/class-wpseo-utils.php', 'WPSEO_WordPress_AJAX_Integration' => __DIR__ . '/../..' . '/inc/interface-wpseo-wordpress-ajax-integration.php', 'WPSEO_WordPress_Integration' => __DIR__ . '/../..' . '/inc/interface-wpseo-wordpress-integration.php', 'WPSEO_Yoast_Columns' => __DIR__ . '/../..' . '/admin/class-yoast-columns.php', 'Wincher_Dashboard_Widget' => __DIR__ . '/../..' . '/admin/class-wincher-dashboard-widget.php', 'YoastSEO_Vendor\\GuzzleHttp\\BodySummarizer' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizer.php', 'YoastSEO_Vendor\\GuzzleHttp\\BodySummarizerInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizerInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Client' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Client.php', 'YoastSEO_Vendor\\GuzzleHttp\\ClientInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/ClientInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\ClientTrait' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/ClientTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\CookieJar' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\CookieJarInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\FileCookieJar' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\SessionCookieJar' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\SetCookie' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SetCookie.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\BadResponseException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/BadResponseException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ClientException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ClientException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ConnectException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ConnectException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\GuzzleException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/GuzzleException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\InvalidArgumentException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\RequestException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/RequestException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ServerException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ServerException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\TooManyRedirectsException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\TransferException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/TransferException.php', 'YoastSEO_Vendor\\GuzzleHttp\\HandlerStack' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/HandlerStack.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlFactory' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlHandler' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlMultiHandler' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\EasyHandle' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/EasyHandle.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\HeaderProcessor' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\MockHandler' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/MockHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\Proxy' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/Proxy.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\StreamHandler' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\MessageFormatter' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatter.php', 'YoastSEO_Vendor\\GuzzleHttp\\MessageFormatterInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatterInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Middleware' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Middleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\Pool' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Pool.php', 'YoastSEO_Vendor\\GuzzleHttp\\PrepareBodyMiddleware' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\AggregateException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/AggregateException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\CancellationException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/CancellationException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Coroutine' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Coroutine.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Create' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Create.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Each' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Each.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\EachPromise' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/EachPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\FulfilledPromise' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/FulfilledPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Is' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Is.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Promise' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Promise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\PromiseInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/PromiseInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\PromisorInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/PromisorInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\RejectedPromise' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/RejectedPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\RejectionException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/RejectionException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\TaskQueue' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/TaskQueue.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\TaskQueueInterface' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/TaskQueueInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Utils' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/promises/src/Utils.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\AppendStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/AppendStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\BufferStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/BufferStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\CachingStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/CachingStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\DroppingStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/DroppingStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Exception\\MalformedUriException' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Exception/MalformedUriException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\FnStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/FnStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Header' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Header.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\HttpFactory' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/HttpFactory.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\InflateStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/InflateStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\LazyOpenStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/LazyOpenStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\LimitStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/LimitStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Message' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Message.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MessageTrait' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/MessageTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MimeType' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/MimeType.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MultipartStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/MultipartStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\NoSeekStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/NoSeekStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\PumpStream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/PumpStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Query' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Query.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Request' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Request.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Response' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Response.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Rfc7230' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Rfc7230.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\ServerRequest' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/ServerRequest.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Stream' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Stream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\StreamDecoratorTrait' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/StreamDecoratorTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\StreamWrapper' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/StreamWrapper.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UploadedFile' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/UploadedFile.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Uri' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Uri.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriComparator' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/UriComparator.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriNormalizer' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/UriNormalizer.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriResolver' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/UriResolver.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Utils' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/psr7/src/Utils.php', 'YoastSEO_Vendor\\GuzzleHttp\\RedirectMiddleware' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/RedirectMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\RequestOptions' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/RequestOptions.php', 'YoastSEO_Vendor\\GuzzleHttp\\RetryMiddleware' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/RetryMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\TransferStats' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/TransferStats.php', 'YoastSEO_Vendor\\GuzzleHttp\\Utils' => __DIR__ . '/../..' . '/vendor_prefixed/guzzlehttp/guzzle/src/Utils.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\AbstractGrant' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/AbstractGrant.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\AuthorizationCode' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/AuthorizationCode.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\ClientCredentials' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/ClientCredentials.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\Exception\\InvalidGrantException' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/Exception/InvalidGrantException.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\GrantFactory' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/GrantFactory.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\Password' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/Password.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\RefreshToken' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Grant/RefreshToken.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\HttpBasicAuthOptionProvider' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/HttpBasicAuthOptionProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\OptionProviderInterface' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/OptionProviderInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\PostAuthOptionProvider' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/PostAuthOptionProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\AbstractProvider' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Provider/AbstractProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\Exception\\IdentityProviderException' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Provider/Exception/IdentityProviderException.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\GenericProvider' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Provider/GenericProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\GenericResourceOwner' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Provider/GenericResourceOwner.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\ResourceOwnerInterface' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Provider/ResourceOwnerInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\AccessToken' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Token/AccessToken.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\AccessTokenInterface' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Token/AccessTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\ResourceOwnerAccessTokenInterface' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Token/ResourceOwnerAccessTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\SettableRefreshTokenInterface' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Token/SettableRefreshTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\ArrayAccessorTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/ArrayAccessorTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\BearerAuthorizationTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/BearerAuthorizationTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\GuardedPropertyTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/GuardedPropertyTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\MacAuthorizationTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/MacAuthorizationTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\ProviderRedirectTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/ProviderRedirectTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\QueryBuilderTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/QueryBuilderTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\RequestFactory' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/RequestFactory.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\RequiredParameterTrait' => __DIR__ . '/../..' . '/vendor_prefixed/league/oauth2-client/src/Tool/RequiredParameterTrait.php', 'YoastSEO_Vendor\\Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/container/src/ContainerExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Container\\ContainerInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/container/src/ContainerInterface.php', 'YoastSEO_Vendor\\Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/container/src/NotFoundExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\ClientExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-client/src/ClientExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\ClientInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-client/src/ClientInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\NetworkExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-client/src/NetworkExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\RequestExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-client/src/RequestExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\MessageInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/MessageInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\RequestFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/RequestFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\RequestInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/RequestInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ResponseFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/ResponseFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ResponseInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/ResponseInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ServerRequestFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/ServerRequestFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ServerRequestInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/ServerRequestInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\StreamFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/StreamFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\StreamInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/StreamInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UploadedFileFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/UploadedFileFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UploadedFileInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/UploadedFileInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UriFactoryInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-factory/src/UriFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UriInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/http-message/src/UriInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\AbstractLogger' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/AbstractLogger.php', 'YoastSEO_Vendor\\Psr\\Log\\InvalidArgumentException' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/InvalidArgumentException.php', 'YoastSEO_Vendor\\Psr\\Log\\LogLevel' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/LogLevel.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/LoggerAwareInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/LoggerAwareTrait.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerInterface' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/LoggerInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerTrait' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/LoggerTrait.php', 'YoastSEO_Vendor\\Psr\\Log\\NullLogger' => __DIR__ . '/../..' . '/vendor_prefixed/psr/log/Psr/Log/NullLogger.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\RewindableGenerator' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Argument/RewindableGenerator.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\ServiceLocator' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocator.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\ServiceLocatorArgument' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocatorArgument.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Container' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Container.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ContainerInterface' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ContainerInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\EnvNotFoundException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/EnvNotFoundException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ExceptionInterface' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/ExceptionInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/InvalidArgumentException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\LogicException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/LogicException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ParameterCircularReferenceException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/ParameterCircularReferenceException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\RuntimeException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/RuntimeException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ServiceCircularReferenceException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/ServiceCircularReferenceException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ServiceNotFoundException' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/Exception/ServiceNotFoundException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\EnvPlaceholderParameterBag' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/EnvPlaceholderParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\FrozenParameterBag' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/FrozenParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBag' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBagInterface' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBagInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ServiceLocator' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/dependency-injection/ServiceLocator.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ResetInterface' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/service-contracts/ResetInterface.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ServiceLocatorTrait' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/service-contracts/ServiceLocatorTrait.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ServiceProviderInterface' => __DIR__ . '/../..' . '/vendor_prefixed/symfony/service-contracts/ServiceProviderInterface.php', 'Yoast\\WP\\Lib\\Abstract_Main' => __DIR__ . '/../..' . '/lib/abstract-main.php', 'Yoast\\WP\\Lib\\Dependency_Injection\\Container_Registry' => __DIR__ . '/../..' . '/lib/dependency-injection/container-registry.php', 'Yoast\\WP\\Lib\\Migrations\\Adapter' => __DIR__ . '/../..' . '/lib/migrations/adapter.php', 'Yoast\\WP\\Lib\\Migrations\\Column' => __DIR__ . '/../..' . '/lib/migrations/column.php', 'Yoast\\WP\\Lib\\Migrations\\Constants' => __DIR__ . '/../..' . '/lib/migrations/constants.php', 'Yoast\\WP\\Lib\\Migrations\\Migration' => __DIR__ . '/../..' . '/lib/migrations/migration.php', 'Yoast\\WP\\Lib\\Migrations\\Table' => __DIR__ . '/../..' . '/lib/migrations/table.php', 'Yoast\\WP\\Lib\\Model' => __DIR__ . '/../..' . '/lib/model.php', 'Yoast\\WP\\Lib\\ORM' => __DIR__ . '/../..' . '/lib/orm.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Generator_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/application/code-generator-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler' => __DIR__ . '/../..' . '/src/ai-authorization/application/code-verifier-handler.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/application/code-verifier-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager' => __DIR__ . '/../..' . '/src/ai-authorization/application/token-manager.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/application/token-manager-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Code_Verifier' => __DIR__ . '/../..' . '/src/ai-authorization/domain/code-verifier.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Token' => __DIR__ . '/../..' . '/src/ai-authorization/domain/token.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/access-token-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/access-token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Generator' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/code-generator.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/code-verifier-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/code-verifier-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/refresh-token-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/refresh-token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Token_User_Meta_Repository_Interface' => __DIR__ . '/../..' . '/src/ai-authorization/infrastructure/token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Abstract_Callback_Route' => __DIR__ . '/../..' . '/src/ai-authorization/user-interface/abstract-callback-route.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Callback_Route' => __DIR__ . '/../..' . '/src/ai-authorization/user-interface/callback-route.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Refresh_Callback_Route' => __DIR__ . '/../..' . '/src/ai-authorization/user-interface/refresh-callback-route.php', 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler' => __DIR__ . '/../..' . '/src/ai-consent/application/consent-handler.php', 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler_Interface' => __DIR__ . '/../..' . '/src/ai-consent/application/consent-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Consent\\Domain\\Endpoint\\Endpoint_Interface' => __DIR__ . '/../..' . '/src/ai-consent/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\AI_Consent\\Infrastructure\\Endpoints\\Consent_Endpoint' => __DIR__ . '/../..' . '/src/ai-consent/infrastructure/endpoints/consent-endpoint.php', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Ai_Consent_Integration' => __DIR__ . '/../..' . '/src/ai-consent/user-interface/ai-consent-integration.php', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Consent_Route' => __DIR__ . '/../..' . '/src/ai-consent/user-interface/consent-route.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler' => __DIR__ . '/../..' . '/src/ai-free-sparks/application/free-sparks-handler.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler_Interface' => __DIR__ . '/../..' . '/src/ai-free-sparks/application/free-sparks-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Infrastructure\\Endpoints\\Free_Sparks_Endpoint' => __DIR__ . '/../..' . '/src/ai-free-sparks/infrastructure/endpoints/free-sparks-endpoint.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\User_Interface\\Free_Sparks_Route' => __DIR__ . '/../..' . '/src/ai-free-sparks/user-interface/free-sparks-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\Application\\Suggestions_Provider' => __DIR__ . '/../..' . '/src/ai-generator/application/suggestions-provider.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Endpoint\\Endpoint_Interface' => __DIR__ . '/../..' . '/src/ai-generator/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Endpoint\\Endpoint_List' => __DIR__ . '/../..' . '/src/ai-generator/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestion' => __DIR__ . '/../..' . '/src/ai-generator/domain/suggestion.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestions_Bucket' => __DIR__ . '/../..' . '/src/ai-generator/domain/suggestions-bucket.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\URLs_Interface' => __DIR__ . '/../..' . '/src/ai-generator/domain/urls-interface.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Suggestions_Endpoint' => __DIR__ . '/../..' . '/src/ai-generator/infrastructure/endpoints/get-suggestions-endpoint.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Usage_Endpoint' => __DIR__ . '/../..' . '/src/ai-generator/infrastructure/endpoints/get-usage-endpoint.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\WordPress_URLs' => __DIR__ . '/../..' . '/src/ai-generator/infrastructure/wordpress-urls.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Ai_Generator_Integration' => __DIR__ . '/../..' . '/src/ai-generator/user-interface/ai-generator-integration.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Bust_Subscription_Cache_Route' => __DIR__ . '/../..' . '/src/ai-generator/user-interface/bust-subscription-cache-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Suggestions_Route' => __DIR__ . '/../..' . '/src/ai-generator/user-interface/get-suggestions-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Usage_Route' => __DIR__ . '/../..' . '/src/ai-generator/user-interface/get-usage-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Route_Permission_Trait' => __DIR__ . '/../..' . '/src/ai-generator/user-interface/route-permission-trait.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler' => __DIR__ . '/../..' . '/src/ai-http-request/application/request-handler.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler_Interface' => __DIR__ . '/../..' . '/src/ai-http-request/application/request-handler-interface.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser' => __DIR__ . '/../..' . '/src/ai-http-request/application/response-parser.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser_Interface' => __DIR__ . '/../..' . '/src/ai-http-request/application/response-parser-interface.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Bad_Request_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/bad-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Forbidden_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/forbidden-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Internal_Server_Error_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/internal-server-error-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Not_Found_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/not-found-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Payment_Required_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/payment-required-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Remote_Request_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/remote-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Request_Timeout_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/request-timeout-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Service_Unavailable_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/service-unavailable-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Too_Many_Requests_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/too-many-requests-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Unauthorized_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/unauthorized-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\WP_Request_Exception' => __DIR__ . '/../..' . '/src/ai-http-request/domain/exceptions/wp-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Request' => __DIR__ . '/../..' . '/src/ai-http-request/domain/request.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Response' => __DIR__ . '/../..' . '/src/ai-http-request/domain/response.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client' => __DIR__ . '/../..' . '/src/ai-http-request/infrastructure/api-client.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client_Interface' => __DIR__ . '/../..' . '/src/ai-http-request/infrastructure/api-client-interface.php', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Activate_Action' => __DIR__ . '/../..' . '/src/actions/addon-installation/addon-activate-action.php', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Install_Action' => __DIR__ . '/../..' . '/src/actions/addon-installation/addon-install-action.php', 'Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action' => __DIR__ . '/../..' . '/src/actions/alert-dismissal-action.php', 'Yoast\\WP\\SEO\\Actions\\Configuration\\First_Time_Configuration_Action' => __DIR__ . '/../..' . '/src/actions/configuration/first-time-configuration-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Abstract_Aioseo_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/abstract-aioseo-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Abstract_Aioseo_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/abstract-aioseo-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-cleanup-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-custom-archive-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-default-archive-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-general-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-posts-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-taxonomy-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action' => __DIR__ . '/../..' . '/src/actions/importing/aioseo/aioseo-validate-data-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action' => __DIR__ . '/../..' . '/src/actions/importing/deactivate-conflicting-plugins-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Importing_Action_Interface' => __DIR__ . '/../..' . '/src/actions/importing/importing-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Importing_Indexation_Action_Interface' => __DIR__ . '/../..' . '/src/actions/importing/importing-indexation-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action' => __DIR__ . '/../..' . '/src/actions/indexables/indexable-head-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Abstract_Indexing_Action' => __DIR__ . '/../..' . '/src/actions/indexing/abstract-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Abstract_Link_Indexing_Action' => __DIR__ . '/../..' . '/src/actions/indexing/abstract-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexable-general-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexable-indexing-complete-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexable-post-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexable-post-type-archive-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexable-term-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexation_Action_Interface' => __DIR__ . '/../..' . '/src/actions/indexing/indexation-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Complete_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexing-complete-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action' => __DIR__ . '/../..' . '/src/actions/indexing/indexing-prepare-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Limited_Indexing_Action_Interface' => __DIR__ . '/../..' . '/src/actions/indexing/limited-indexing-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action' => __DIR__ . '/../..' . '/src/actions/indexing/post-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action' => __DIR__ . '/../..' . '/src/actions/indexing/term-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Integrations_Action' => __DIR__ . '/../..' . '/src/actions/integrations-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Login_Action' => __DIR__ . '/../..' . '/src/actions/semrush/semrush-login-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Options_Action' => __DIR__ . '/../..' . '/src/actions/semrush/semrush-options-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Phrases_Action' => __DIR__ . '/../..' . '/src/actions/semrush/semrush-phrases-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action' => __DIR__ . '/../..' . '/src/actions/wincher/wincher-account-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action' => __DIR__ . '/../..' . '/src/actions/wincher/wincher-keyphrases-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Login_Action' => __DIR__ . '/../..' . '/src/actions/wincher/wincher-login-action.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Default_SEO_Data\\Default_SEO_Data_Alert' => __DIR__ . '/../..' . '/src/alerts/application/default-seo-data/default-seo-data-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Indexables_Disabled\\Indexables_Disabled_Alert' => __DIR__ . '/../..' . '/src/alerts/application/indexables-disabled/indexables-disabled-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Ping_Other_Admins\\Ping_Other_Admins_Alert' => __DIR__ . '/../..' . '/src/alerts/application/ping-other-admins/ping-other-admins-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector' => __DIR__ . '/../..' . '/src/alerts/infrastructure/default-seo-data/default-seo-data-collector.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Cron_Callback_Integration' => __DIR__ . '/../..' . '/src/alerts/user-interface/default-seo-data/default-seo-data-cron-callback-integration.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Watcher' => __DIR__ . '/../..' . '/src/alerts/user-interface/default-seo-data/default-seo-data-watcher.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler' => __DIR__ . '/../..' . '/src/alerts/user-interface/default-seo-data/default-seo-data-cron-scheduler.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Resolve_Alert_Route' => __DIR__ . '/../..' . '/src/alerts/user-interface/resolve-alert-route.php', 'Yoast\\WP\\SEO\\Analytics\\Application\\Missing_Indexables_Collector' => __DIR__ . '/../..' . '/src/analytics/application/missing-indexables-collector.php', 'Yoast\\WP\\SEO\\Analytics\\Application\\To_Be_Cleaned_Indexables_Collector' => __DIR__ . '/../..' . '/src/analytics/application/to-be-cleaned-indexables-collector.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Bucket' => __DIR__ . '/../..' . '/src/analytics/domain/missing-indexable-bucket.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Count' => __DIR__ . '/../..' . '/src/analytics/domain/missing-indexable-count.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Bucket' => __DIR__ . '/../..' . '/src/analytics/domain/to-be-cleaned-indexable-bucket.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Count' => __DIR__ . '/../..' . '/src/analytics/domain/to-be-cleaned-indexable-count.php', 'Yoast\\WP\\SEO\\Analytics\\User_Interface\\Last_Completed_Indexation_Integration' => __DIR__ . '/../..' . '/src/analytics/user-interface/last-completed-indexation-integration.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-author-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Date_Archive_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-date-archive-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-hierarchy-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-home-page-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-link-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-post-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-post-type-archive-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Social_Image_Trait' => __DIR__ . '/../..' . '/src/builders/indexable-social-image-trait.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_System_Page_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-system-page-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder' => __DIR__ . '/../..' . '/src/builders/indexable-term-builder.php', 'Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder' => __DIR__ . '/../..' . '/src/builders/primary-term-builder.php', 'Yoast\\WP\\SEO\\Commands\\Cleanup_Command' => __DIR__ . '/../..' . '/src/commands/cleanup-command.php', 'Yoast\\WP\\SEO\\Commands\\Command_Interface' => __DIR__ . '/../..' . '/src/commands/command-interface.php', 'Yoast\\WP\\SEO\\Commands\\Index_Command' => __DIR__ . '/../..' . '/src/commands/index-command.php', 'Yoast\\WP\\SEO\\Conditionals\\AI_Conditional' => __DIR__ . '/../..' . '/src/conditionals/ai-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\AI_Editor_Conditional' => __DIR__ . '/../..' . '/src/conditionals/ai-editor-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional' => __DIR__ . '/../..' . '/src/conditionals/addon-installation-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Doing_Post_Quick_Edit_Save_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/doing-post-quick-edit-save-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Estimated_Reading_Time_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/estimated-reading-time-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Licenses_Page_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/licenses-page-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Non_Network_Admin_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/non-network-admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/post-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Posts_Overview_Or_Ajax_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/posts-overview-or-ajax-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Yoast_Admin_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin/yoast-admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional' => __DIR__ . '/../..' . '/src/conditionals/admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Attachment_Redirections_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/attachment-redirections-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional' => __DIR__ . '/../..' . '/src/conditionals/check-required-version-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Conditional' => __DIR__ . '/../..' . '/src/conditionals/conditional-interface.php', 'Yoast\\WP\\SEO\\Conditionals\\Deactivating_Yoast_Seo_Conditional' => __DIR__ . '/../..' . '/src/conditionals/deactivating-yoast-seo-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Development_Conditional' => __DIR__ . '/../..' . '/src/conditionals/development-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional' => __DIR__ . '/../..' . '/src/conditionals/dynamic-product-permalinks-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Feature_Flag_Conditional' => __DIR__ . '/../..' . '/src/conditionals/feature-flag-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional' => __DIR__ . '/../..' . '/src/conditionals/front-end-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional' => __DIR__ . '/../..' . '/src/conditionals/get-request-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional' => __DIR__ . '/../..' . '/src/deprecated/src/conditionals/google-site-kit-feature-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Headless_Rest_Endpoints_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/headless-rest-endpoints-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Import_Tool_Selected_Conditional' => __DIR__ . '/../..' . '/src/conditionals/import-tool-selected-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional' => __DIR__ . '/../..' . '/src/conditionals/jetpack-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Migrations_Conditional' => __DIR__ . '/../..' . '/src/conditionals/migrations-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional' => __DIR__ . '/../..' . '/src/conditionals/new-settings-ui-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\News_Conditional' => __DIR__ . '/../..' . '/src/conditionals/news-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\No_Conditionals' => __DIR__ . '/../..' . '/src/conditionals/no-conditionals-trait.php', 'Yoast\\WP\\SEO\\Conditionals\\No_Tool_Selected_Conditional' => __DIR__ . '/../..' . '/src/conditionals/no-tool-selected-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Non_Multisite_Conditional' => __DIR__ . '/../..' . '/src/conditionals/non-multisite-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Not_Admin_Ajax_Conditional' => __DIR__ . '/../..' . '/src/conditionals/not-admin-ajax-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Open_Graph_Conditional' => __DIR__ . '/../..' . '/src/conditionals/open-graph-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Active_Conditional' => __DIR__ . '/../..' . '/src/conditionals/premium-active-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Inactive_Conditional' => __DIR__ . '/../..' . '/src/conditionals/premium-inactive-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Primary_Category_Conditional' => __DIR__ . '/../..' . '/src/conditionals/primary-category-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Robots_Txt_Conditional' => __DIR__ . '/../..' . '/src/conditionals/robots-txt-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\SEMrush_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/semrush-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Schema_Disabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/schema-disabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Settings_Conditional' => __DIR__ . '/../..' . '/src/conditionals/settings-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Should_Index_Links_Conditional' => __DIR__ . '/../..' . '/src/conditionals/should-index-links-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Task_List_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/task-list-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional' => __DIR__ . '/../..' . '/src/conditionals/text-formality-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/edd-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/elementor-activated-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Edit_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/elementor-edit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/polylang-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/site-kit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/translatepress-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\W3_Total_Cache_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/w3-total-cache-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/wpml-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional' => __DIR__ . '/../..' . '/src/conditionals/third-party/wpml-wpseo-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Traits\\Admin_Conditional_Trait' => __DIR__ . '/../..' . '/src/conditionals/traits/admin-conditional-trait.php', 'Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional' => __DIR__ . '/../..' . '/src/conditionals/updated-importer-framework-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Edit_Users_Conditional' => __DIR__ . '/../..' . '/src/conditionals/user-can-edit-users-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional' => __DIR__ . '/../..' . '/src/conditionals/user-can-manage-wpseo-options-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Publish_Posts_And_Pages_Conditional' => __DIR__ . '/../..' . '/src/conditionals/user-can-publish-posts-and-pages-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Edit_Conditional' => __DIR__ . '/../..' . '/src/conditionals/user-edit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Profile_Conditional' => __DIR__ . '/../..' . '/src/conditionals/user-profile-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wp-cron-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_Robots_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wp-robots-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_Tests_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wp-tests-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Web_Stories_Conditional' => __DIR__ . '/../..' . '/src/conditionals/web-stories-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Automatically_Track_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wincher-automatically-track-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wincher-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Enabled_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wincher-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Token_Conditional' => __DIR__ . '/../..' . '/src/conditionals/wincher-token-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional' => __DIR__ . '/../..' . '/src/conditionals/woocommerce-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Version_Conditional' => __DIR__ . '/../..' . '/src/conditionals/woocommerce-version-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Woo_SEO_Inactive_Conditional' => __DIR__ . '/../..' . '/src/conditionals/woo-seo-inactive-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\XMLRPC_Conditional' => __DIR__ . '/../..' . '/src/conditionals/xmlrpc-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional' => __DIR__ . '/../..' . '/src/conditionals/yoast-admin-and-dashboard-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Tools_Page_Conditional' => __DIR__ . '/../..' . '/src/conditionals/yoast-tools-page-conditional.php', 'Yoast\\WP\\SEO\\Config\\Badge_Group_Names' => __DIR__ . '/../..' . '/src/config/badge-group-names.php', 'Yoast\\WP\\SEO\\Config\\Conflicting_Plugins' => __DIR__ . '/../..' . '/src/config/conflicting-plugins.php', 'Yoast\\WP\\SEO\\Config\\Indexing_Reasons' => __DIR__ . '/../..' . '/src/config/indexing-reasons.php', 'Yoast\\WP\\SEO\\Config\\Migration_Status' => __DIR__ . '/../..' . '/src/config/migration-status.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddCollationToTables' => __DIR__ . '/../..' . '/src/config/migrations/20200408101900_AddCollationToTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddColumnsToIndexables' => __DIR__ . '/../..' . '/src/config/migrations/20200420073606_AddColumnsToIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddEstimatedReadingTime' => __DIR__ . '/../..' . '/src/config/migrations/20201202144329_AddEstimatedReadingTime.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddHasAncestorsColumn' => __DIR__ . '/../..' . '/src/config/migrations/20200609154515_AddHasAncestorsColumn.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddInclusiveLanguageScore' => __DIR__ . '/../..' . '/src/config/migrations/20230417083836_AddInclusiveLanguageScore.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexableObjectIdAndTypeIndex' => __DIR__ . '/../..' . '/src/config/migrations/20200430075614_AddIndexableObjectIdAndTypeIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexesForProminentWordsOnIndexables' => __DIR__ . '/../..' . '/src/config/migrations/20200728095334_AddIndexesForProminentWordsOnIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddObjectTimestamps' => __DIR__ . '/../..' . '/src/config/migrations/20211020091404_AddObjectTimestamps.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddSeoLinksIndex' => __DIR__ . '/../..' . '/src/config/migrations/20260105111111_AddSeoLinksIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddVersionColumnToIndexables' => __DIR__ . '/../..' . '/src/config/migrations/20210817092415_AddVersionColumnToIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\BreadcrumbTitleAndHierarchyReset' => __DIR__ . '/../..' . '/src/config/migrations/20200428123747_BreadcrumbTitleAndHierarchyReset.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ClearIndexableTables' => __DIR__ . '/../..' . '/src/config/migrations/20200430150130_ClearIndexableTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateIndexableSubpagesIndex' => __DIR__ . '/../..' . '/src/config/migrations/20200702141921_CreateIndexableSubpagesIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateSEOLinksTable' => __DIR__ . '/../..' . '/src/config/migrations/20200617122511_CreateSEOLinksTable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\DeleteDuplicateIndexables' => __DIR__ . '/../..' . '/src/config/migrations/20200507054848_DeleteDuplicateIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableColumnLengths' => __DIR__ . '/../..' . '/src/config/migrations/20200428194858_ExpandIndexableColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableIDColumnLengths' => __DIR__ . '/../..' . '/src/config/migrations/20201216124002_ExpandIndexableIDColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandPrimaryTermIDColumnLengths' => __DIR__ . '/../..' . '/src/config/migrations/20201216141134_ExpandPrimaryTermIDColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ReplacePermalinkHashIndex' => __DIR__ . '/../..' . '/src/config/migrations/20200616130143_ReplacePermalinkHashIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ResetIndexableHierarchyTable' => __DIR__ . '/../..' . '/src/config/migrations/20200513133401_ResetIndexableHierarchyTable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\TruncateIndexableTables' => __DIR__ . '/../..' . '/src/config/migrations/20200429105310_TruncateIndexableTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastDropIndexableMetaTableIfExists' => __DIR__ . '/../..' . '/src/config/migrations/20190529075038_WpYoastDropIndexableMetaTableIfExists.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexable' => __DIR__ . '/../..' . '/src/config/migrations/20171228151840_WpYoastIndexable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexableHierarchy' => __DIR__ . '/../..' . '/src/config/migrations/20191011111109_WpYoastIndexableHierarchy.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastPrimaryTerm' => __DIR__ . '/../..' . '/src/config/migrations/20171228151841_WpYoastPrimaryTerm.php', 'Yoast\\WP\\SEO\\Config\\OAuth_Client' => __DIR__ . '/../..' . '/src/config/oauth-client.php', 'Yoast\\WP\\SEO\\Config\\Researcher_Languages' => __DIR__ . '/../..' . '/src/config/researcher-languages.php', 'Yoast\\WP\\SEO\\Config\\SEMrush_Client' => __DIR__ . '/../..' . '/src/config/semrush-client.php', 'Yoast\\WP\\SEO\\Config\\Schema_IDs' => __DIR__ . '/../..' . '/src/config/schema-ids.php', 'Yoast\\WP\\SEO\\Config\\Schema_Types' => __DIR__ . '/../..' . '/src/config/schema-types.php', 'Yoast\\WP\\SEO\\Config\\Wincher_Client' => __DIR__ . '/../..' . '/src/config/wincher-client.php', 'Yoast\\WP\\SEO\\Config\\Wincher_PKCE_Provider' => __DIR__ . '/../..' . '/src/config/wincher-pkce-provider.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications' => __DIR__ . '/../..' . '/src/content-type-visibility/application/content-type-visibility-dismiss-notifications.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Watcher_Actions' => __DIR__ . '/../..' . '/src/content-type-visibility/application/content-type-visibility-watcher-actions.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\User_Interface\\Content_Type_Visibility_Dismiss_New_Route' => __DIR__ . '/../..' . '/src/content-type-visibility/user-interface/content-type-visibility-dismiss-new-route.php', 'Yoast\\WP\\SEO\\Context\\Meta_Tags_Context' => __DIR__ . '/../..' . '/src/context/meta-tags-context.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Configuration\\Dashboard_Configuration' => __DIR__ . '/../..' . '/src/dashboard/application/configuration/dashboard-configuration.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Content_Types\\Content_Types_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/content-types/content-types-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Endpoints\\Endpoints_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/endpoints/endpoints-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Filter_Pairs\\Filter_Pairs_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/filter-pairs/filter-pairs-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Groups\\SEO_Score_Groups\\SEO_Score_Groups_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/score-groups/seo-score-groups/seo-score-groups-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Abstract_Score_Results_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/score-results/abstract-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/score-results/current-scores-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Search_Ranking_Compare_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/search-rankings/search-ranking-compare-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Page_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/search-rankings/top-page-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Query_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/search-rankings/top-query-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/taxonomies/taxonomies-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Tracking\\Setup_Steps_Tracking' => __DIR__ . '/../..' . '/src/dashboard/application/tracking/setup-steps-tracking.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Compare_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/traffic/organic-sessions-compare-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Daily_Repository' => __DIR__ . '/../..' . '/src/dashboard/application/traffic/organic-sessions-daily-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Failed_Request_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/analytics-4/failed-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Invalid_Request_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/analytics-4/invalid-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Unexpected_Response_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/analytics-4/unexpected-response-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Type' => __DIR__ . '/../..' . '/src/dashboard/domain/content-types/content-type.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Types_List' => __DIR__ . '/../..' . '/src/dashboard/domain/content-types/content-types-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Dashboard_Repository_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/data-provider/dashboard-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Data_Container' => __DIR__ . '/../..' . '/src/dashboard/domain/data-provider/data-container.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Data_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/data-provider/data-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Parameters' => __DIR__ . '/../..' . '/src/dashboard/domain/data-provider/parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Endpoint\\Endpoint_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Endpoint\\Endpoint_List' => __DIR__ . '/../..' . '/src/dashboard/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Filter_Pairs_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/filter-pairs/filter-pairs-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Product_Category_Filter_Pair' => __DIR__ . '/../..' . '/src/dashboard/domain/filter-pairs/product-category-filter-pair.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Abstract_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/abstract-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Abstract_Readability_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Bad_Readability_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Good_Readability_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\No_Readability_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Ok_Readability_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Readability_Score_Groups_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Abstract_SEO_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\SEO_Score_Groups_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Score_Groups_Interface' => __DIR__ . '/../..' . '/src/dashboard/domain/score-groups/score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Score' => __DIR__ . '/../..' . '/src/dashboard/domain/score-results/current-score.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Scores_List' => __DIR__ . '/../..' . '/src/dashboard/domain/score-results/current-scores-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Result' => __DIR__ . '/../..' . '/src/dashboard/domain/score-results/score-result.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Results_Not_Found_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/score-results/score-results-not-found-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Failed_Request_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/search-console/failed-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Unexpected_Response_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/search-console/unexpected-response-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Comparison_Search_Ranking_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/search-rankings/comparison-search-ranking-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Search_Ranking_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/search-rankings/search-ranking-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Top_Page_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/search-rankings/top-page-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Taxonomies\\Taxonomy' => __DIR__ . '/../..' . '/src/dashboard/domain/taxonomies/taxonomy.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_SEO_Metrics\\Repository_Not_Found_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/time-based-seo-metrics/repository-not-found-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_Seo_Metrics\\Data_Source_Not_Available_Exception' => __DIR__ . '/../..' . '/src/dashboard/domain/time-based-seo-metrics/data-source-not-available-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Comparison_Traffic_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/traffic/comparison-traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Daily_Traffic_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/traffic/daily-traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Traffic_Data' => __DIR__ . '/../..' . '/src/dashboard/domain/traffic/traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Analytics_4_Parameters' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/analytics-4/analytics-4-parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Adapter' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/analytics-4/site-kit-analytics-4-adapter.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Api_Call' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/analytics-4/site-kit-analytics-4-api-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Browser_Cache\\Browser_Cache_Configuration' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/browser-cache/browser-cache-configuration.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/configuration/site-kit-consent-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository_Interface' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/configuration/site-kit-consent-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Connection\\Site_Kit_Is_Connected_Call' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/connection/site-kit-is-connected-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/content-types/content-types-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Readability_Scores_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\SEO_Scores_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Setup_Steps_Tracking_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/setup-steps-tracking-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Configuration_Dismissal_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/site-kit-configuration-dismissal-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/site-kit-consent-management-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Time_Based_SEO_Metrics_Endpoint' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/endpoints/time-based-seo-metrics-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Indexables\\Top_Page_Indexable_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/indexables/top-page-indexable-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/integrations/site-kit.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Nonces\\Nonce_Repository' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/nonces/nonce-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Groups\\Score_Group_Link_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-groups/score-group-link-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Cached_Readability_Score_Results_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-results/readability-score-results/cached-readability-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\Cached_SEO_Score_Results_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-results/seo-score-results/cached-seo-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Score_Results_Collector_Interface' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/score-results/score-results-collector-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Search_Console_Parameters' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/search-console/search-console-parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Adapter' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/search-console/site-kit-search-console-adapter.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Api_Call' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/search-console/site-kit-search-console-api-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomies_Collector' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomy_Validator' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/taxonomies/taxonomy-validator.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/tracking/setup-steps-tracking-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository_Interface' => __DIR__ . '/../..' . '/src/dashboard/infrastructure/tracking/setup-steps-tracking-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Capabilities_Integration' => __DIR__ . '/../..' . '/src/dashboard/user-interface/configuration/site-kit-capabilities-integration.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Configuration_Dismissal_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/configuration/site-kit-configuration-dismissal-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Consent_Management_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/configuration/site-kit-consent-management-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Abstract_Scores_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/scores/abstract-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Readability_Scores_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/scores/readability-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\SEO_Scores_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/scores/seo-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Flow_Interceptor' => __DIR__ . '/../..' . '/src/dashboard/user-interface/setup/setup-flow-interceptor.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Url_Interceptor' => __DIR__ . '/../..' . '/src/dashboard/user-interface/setup/setup-url-interceptor.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Time_Based_SEO_Metrics\\Time_Based_SEO_Metrics_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/time-based-seo-metrics/time-based-seo-metrics-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Tracking\\Setup_Steps_Tracking_Route' => __DIR__ . '/../..' . '/src/dashboard/user-interface/tracking/setup-steps-tracking-route.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Analysis_Features\\Enabled_Analysis_Features_Repository' => __DIR__ . '/../..' . '/src/editors/application/analysis-features/enabled-analysis-features-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Integrations\\Integration_Information_Repository' => __DIR__ . '/../..' . '/src/editors/application/integrations/integration-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Post_Seo_Information_Repository' => __DIR__ . '/../..' . '/src/editors/application/seo/post-seo-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Term_Seo_Information_Repository' => __DIR__ . '/../..' . '/src/editors/application/seo/term-seo-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Site\\Website_Information_Repository' => __DIR__ . '/../..' . '/src/editors/application/site/website-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Feature' => __DIR__ . '/../..' . '/src/editors/domain/analysis-features/analysis-feature.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Feature_Interface' => __DIR__ . '/../..' . '/src/editors/domain/analysis-features/analysis-feature-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Features_List' => __DIR__ . '/../..' . '/src/editors/domain/analysis-features/analysis-features-list.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Integrations\\Integration_Data_Provider_Interface' => __DIR__ . '/../..' . '/src/editors/domain/integrations/integration-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Description' => __DIR__ . '/../..' . '/src/editors/domain/seo/description.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Keyphrase' => __DIR__ . '/../..' . '/src/editors/domain/seo/keyphrase.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Seo_Plugin_Data_Interface' => __DIR__ . '/../..' . '/src/editors/domain/seo/seo-plugin-data-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Social' => __DIR__ . '/../..' . '/src/editors/domain/seo/social.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Title' => __DIR__ . '/../..' . '/src/editors/domain/seo/title.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Cornerstone_Content' => __DIR__ . '/../..' . '/src/editors/framework/cornerstone-content.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Inclusive_Language_Analysis' => __DIR__ . '/../..' . '/src/editors/framework/inclusive-language-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Jetpack_Markdown' => __DIR__ . '/../..' . '/src/editors/framework/integrations/jetpack-markdown.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Multilingual' => __DIR__ . '/../..' . '/src/editors/framework/integrations/multilingual.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\News_SEO' => __DIR__ . '/../..' . '/src/editors/framework/integrations/news-seo.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Semrush' => __DIR__ . '/../..' . '/src/editors/framework/integrations/semrush.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Wincher' => __DIR__ . '/../..' . '/src/editors/framework/integrations/wincher.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce' => __DIR__ . '/../..' . '/src/editors/framework/integrations/woocommerce.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce_SEO' => __DIR__ . '/../..' . '/src/editors/framework/integrations/woocommerce-seo.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Keyphrase_Analysis' => __DIR__ . '/../..' . '/src/editors/framework/keyphrase-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Previously_Used_Keyphrase' => __DIR__ . '/../..' . '/src/editors/framework/previously-used-keyphrase.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Readability_Analysis' => __DIR__ . '/../..' . '/src/editors/framework/readability-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Description_Data_Provider_Interface' => __DIR__ . '/../..' . '/src/editors/framework/seo/description-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Keyphrase_Interface' => __DIR__ . '/../..' . '/src/editors/framework/seo/keyphrase-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Abstract_Post_Seo_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/posts/abstract-post-seo-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Description_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/posts/description-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Keyphrase_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/posts/keyphrase-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Social_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/posts/social-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Title_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/posts/title-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Social_Data_Provider_Interface' => __DIR__ . '/../..' . '/src/editors/framework/seo/social-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Abstract_Term_Seo_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/terms/abstract-term-seo-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Description_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/terms/description-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Keyphrase_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/terms/keyphrase-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Social_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/terms/social-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Title_Data_Provider' => __DIR__ . '/../..' . '/src/editors/framework/seo/terms/title-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Title_Data_Provider_Interface' => __DIR__ . '/../..' . '/src/editors/framework/seo/title-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Base_Site_Information' => __DIR__ . '/../..' . '/src/editors/framework/site/base-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Post_Site_Information' => __DIR__ . '/../..' . '/src/editors/framework/site/post-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Term_Site_Information' => __DIR__ . '/../..' . '/src/editors/framework/site/term-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Word_Form_Recognition' => __DIR__ . '/../..' . '/src/editors/framework/word-form-recognition.php', 'Yoast\\WP\\SEO\\Elementor\\Infrastructure\\Request_Post' => __DIR__ . '/../..' . '/src/elementor/infrastructure/request-post.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Activation_Error_Exception' => __DIR__ . '/../..' . '/src/exceptions/addon-installation/addon-activation-error-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Already_Installed_Exception' => __DIR__ . '/../..' . '/src/exceptions/addon-installation/addon-already-installed-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Installation_Error_Exception' => __DIR__ . '/../..' . '/src/exceptions/addon-installation/addon-installation-error-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\User_Cannot_Activate_Plugins_Exception' => __DIR__ . '/../..' . '/src/exceptions/addon-installation/user-cannot-activate-plugins-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\User_Cannot_Install_Plugins_Exception' => __DIR__ . '/../..' . '/src/exceptions/addon-installation/user-cannot-install-plugins-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Forbidden_Property_Mutation_Exception' => __DIR__ . '/../..' . '/src/exceptions/forbidden-property-mutation-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Importing\\Aioseo_Validation_Exception' => __DIR__ . '/../..' . '/src/exceptions/importing/aioseo-validation-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Author_Not_Built_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/author-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Indexable_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/indexable-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Invalid_Term_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/invalid-term-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Not_Built_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Not_Built_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/post-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Not_Found_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/post-not-found-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Type_Not_Built_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/post-type-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Source_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/source-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Term_Not_Built_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/term-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Term_Not_Found_Exception' => __DIR__ . '/../..' . '/src/exceptions/indexable/term-not-found-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Missing_Method' => __DIR__ . '/../..' . '/src/exceptions/missing-method.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Authentication_Failed_Exception' => __DIR__ . '/../..' . '/src/exceptions/oauth/authentication-failed-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Empty_Property_Exception' => __DIR__ . '/../..' . '/src/exceptions/oauth/tokens/empty-property-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Empty_Token_Exception' => __DIR__ . '/../..' . '/src/exceptions/oauth/tokens/empty-token-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Failed_Storage_Exception' => __DIR__ . '/../..' . '/src/exceptions/oauth/tokens/failed-storage-exception.php', 'Yoast\\WP\\SEO\\General\\User_Interface\\General_Page_Integration' => __DIR__ . '/../..' . '/src/general/user-interface/general-page-integration.php', 'Yoast\\WP\\SEO\\General\\User_Interface\\Opt_In_Route' => __DIR__ . '/../..' . '/src/general/user-interface/opt-in-route.php', 'Yoast\\WP\\SEO\\Generated\\Cached_Container' => __DIR__ . '/../..' . '/src/generated/container.php', 'Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator' => __DIR__ . '/../..' . '/src/generators/breadcrumbs-generator.php', 'Yoast\\WP\\SEO\\Generators\\Generator_Interface' => __DIR__ . '/../..' . '/src/generators/generator-interface.php', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator' => __DIR__ . '/../..' . '/src/generators/open-graph-image-generator.php', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator' => __DIR__ . '/../..' . '/src/generators/open-graph-locale-generator.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Abstract_Schema_Piece' => __DIR__ . '/../..' . '/src/generators/schema/abstract-schema-piece.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Article' => __DIR__ . '/../..' . '/src/generators/schema/article.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Author' => __DIR__ . '/../..' . '/src/generators/schema/author.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Breadcrumb' => __DIR__ . '/../..' . '/src/generators/schema/breadcrumb.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\FAQ' => __DIR__ . '/../..' . '/src/generators/schema/faq.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\HowTo' => __DIR__ . '/../..' . '/src/generators/schema/howto.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Main_Image' => __DIR__ . '/../..' . '/src/generators/schema/main-image.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Organization' => __DIR__ . '/../..' . '/src/generators/schema/organization.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Person' => __DIR__ . '/../..' . '/src/generators/schema/person.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\WebPage' => __DIR__ . '/../..' . '/src/generators/schema/webpage.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Website' => __DIR__ . '/../..' . '/src/generators/schema/website.php', 'Yoast\\WP\\SEO\\Generators\\Schema_Generator' => __DIR__ . '/../..' . '/src/generators/schema-generator.php', 'Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator' => __DIR__ . '/../..' . '/src/generators/twitter-image-generator.php', 'Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper' => __DIR__ . '/../..' . '/src/helpers/aioseo-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Asset_Helper' => __DIR__ . '/../..' . '/src/helpers/asset-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper' => __DIR__ . '/../..' . '/src/helpers/attachment-cleanup-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper' => __DIR__ . '/../..' . '/src/helpers/author-archive-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Blocks_Helper' => __DIR__ . '/../..' . '/src/helpers/blocks-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Capability_Helper' => __DIR__ . '/../..' . '/src/helpers/capability-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Crawl_Cleanup_Helper' => __DIR__ . '/../..' . '/src/helpers/crawl-cleanup-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Curl_Helper' => __DIR__ . '/../..' . '/src/helpers/curl-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper' => __DIR__ . '/../..' . '/src/helpers/current-page-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Date_Helper' => __DIR__ . '/../..' . '/src/helpers/date-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Environment_Helper' => __DIR__ . '/../..' . '/src/helpers/environment-helper.php', 'Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper' => __DIR__ . '/../..' . '/src/helpers/first-time-configuration-notice-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Home_Url_Helper' => __DIR__ . '/../..' . '/src/helpers/home-url-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Image_Helper' => __DIR__ . '/../..' . '/src/helpers/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper' => __DIR__ . '/../..' . '/src/helpers/import-cursor-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Import_Helper' => __DIR__ . '/../..' . '/src/helpers/import-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexable_Helper' => __DIR__ . '/../..' . '/src/helpers/indexable-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexable_To_Postmeta_Helper' => __DIR__ . '/../..' . '/src/helpers/indexable-to-postmeta-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexing_Helper' => __DIR__ . '/../..' . '/src/helpers/indexing-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Language_Helper' => __DIR__ . '/../..' . '/src/helpers/language-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Meta_Helper' => __DIR__ . '/../..' . '/src/helpers/meta-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Notification_Helper' => __DIR__ . '/../..' . '/src/helpers/notification-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper' => __DIR__ . '/../..' . '/src/helpers/open-graph/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper' => __DIR__ . '/../..' . '/src/helpers/open-graph/values-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Options_Helper' => __DIR__ . '/../..' . '/src/helpers/options-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Pagination_Helper' => __DIR__ . '/../..' . '/src/helpers/pagination-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Permalink_Helper' => __DIR__ . '/../..' . '/src/helpers/permalink-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Post_Helper' => __DIR__ . '/../..' . '/src/helpers/post-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper' => __DIR__ . '/../..' . '/src/helpers/post-type-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper' => __DIR__ . '/../..' . '/src/helpers/primary-term-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Product_Helper' => __DIR__ . '/../..' . '/src/helpers/product-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Redirect_Helper' => __DIR__ . '/../..' . '/src/helpers/redirect-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Request_Helper' => __DIR__ . '/../..' . '/src/deprecated/src/helpers/request-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Require_File_Helper' => __DIR__ . '/../..' . '/src/helpers/require-file-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Robots_Helper' => __DIR__ . '/../..' . '/src/helpers/robots-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper' => __DIR__ . '/../..' . '/src/helpers/robots-txt-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Route_Helper' => __DIR__ . '/../..' . '/src/helpers/route-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper' => __DIR__ . '/../..' . '/src/helpers/sanitization-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/article-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/html-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/id-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Image_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/language-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Replace_Vars_Helper' => __DIR__ . '/../..' . '/src/helpers/schema/replace-vars-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Score_Icon_Helper' => __DIR__ . '/../..' . '/src/helpers/score-icon-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper' => __DIR__ . '/../..' . '/src/helpers/short-link-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Site_Helper' => __DIR__ . '/../..' . '/src/helpers/site-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper' => __DIR__ . '/../..' . '/src/helpers/social-profiles-helper.php', 'Yoast\\WP\\SEO\\Helpers\\String_Helper' => __DIR__ . '/../..' . '/src/helpers/string-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper' => __DIR__ . '/../..' . '/src/helpers/taxonomy-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper' => __DIR__ . '/../..' . '/src/helpers/twitter/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Url_Helper' => __DIR__ . '/../..' . '/src/helpers/url-helper.php', 'Yoast\\WP\\SEO\\Helpers\\User_Helper' => __DIR__ . '/../..' . '/src/helpers/user-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wincher_Helper' => __DIR__ . '/../..' . '/src/helpers/wincher-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper' => __DIR__ . '/../..' . '/src/helpers/woocommerce-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wordpress_Helper' => __DIR__ . '/../..' . '/src/helpers/wordpress-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wpdb_Helper' => __DIR__ . '/../..' . '/src/helpers/wpdb-helper.php', 'Yoast\\WP\\SEO\\Images\\Application\\Image_Content_Extractor' => __DIR__ . '/../..' . '/src/images/Application/image-content-extractor.php', 'Yoast\\WP\\SEO\\Initializers\\Crawl_Cleanup_Permalinks' => __DIR__ . '/../..' . '/src/initializers/crawl-cleanup-permalinks.php', 'Yoast\\WP\\SEO\\Initializers\\Disable_Core_Sitemaps' => __DIR__ . '/../..' . '/src/initializers/disable-core-sitemaps.php', 'Yoast\\WP\\SEO\\Initializers\\Initializer_Interface' => __DIR__ . '/../..' . '/src/initializers/initializer-interface.php', 'Yoast\\WP\\SEO\\Initializers\\Migration_Runner' => __DIR__ . '/../..' . '/src/initializers/migration-runner.php', 'Yoast\\WP\\SEO\\Initializers\\Plugin_Headers' => __DIR__ . '/../..' . '/src/initializers/plugin-headers.php', 'Yoast\\WP\\SEO\\Initializers\\Silence_Load_Textdomain_Just_In_Time_Notices' => __DIR__ . '/../..' . '/src/initializers/silence-load-textdomain-just-in-time-notices.php', 'Yoast\\WP\\SEO\\Initializers\\Woocommerce' => __DIR__ . '/../..' . '/src/initializers/woocommerce.php', 'Yoast\\WP\\SEO\\Integrations\\Abstract_Exclude_Post_Type' => __DIR__ . '/../..' . '/src/integrations/abstract-exclude-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Academy_Integration' => __DIR__ . '/../..' . '/src/integrations/academy-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Activation_Cleanup_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/activation-cleanup-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Dialog_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/addon-installation/dialog-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Installation_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/addon-installation/installation-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/admin-columns-cache-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Background_Indexing_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/background-indexing-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Brand_Insights_Page' => __DIR__ . '/../..' . '/src/integrations/admin/brand-insights-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Check_Required_Version' => __DIR__ . '/../..' . '/src/integrations/admin/check-required-version.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Crawl_Settings_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/crawl-settings-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Cron_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/cron-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Deactivated_Premium_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/deactivated-premium-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/first-time-configuration-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Notice_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/first-time-configuration-notice-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Fix_News_Dependencies_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/fix-news-dependencies-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Health_Check_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/health-check-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\HelpScout_Beacon' => __DIR__ . '/../..' . '/src/integrations/admin/helpscout-beacon.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Import_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/import-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexables_Exclude_Taxonomy_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/indexables-exclude-taxonomy-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/indexing-notification-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Tool_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/indexing-tool-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Installation_Success_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/installation-success-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Integrations_Page' => __DIR__ . '/../..' . '/src/integrations/admin/integrations-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Link_Count_Columns_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/link-count-columns-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Menu_Badge_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/menu-badge-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Migration_Error_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/migration-error-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Old_Configuration_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/old-configuration-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirect_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/redirect-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirections_Tools_Page' => __DIR__ . '/../..' . '/src/integrations/admin/redirections-tools-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirects_Page_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/redirects-page-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice' => __DIR__ . '/../..' . '/src/deprecated/src/integrations/admin/unsupported-php-version-notice.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Workouts_Integration' => __DIR__ . '/../..' . '/src/integrations/admin/workouts-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Abstract_Dismissable_Alert' => __DIR__ . '/../..' . '/src/integrations/alerts/abstract-dismissable-alert.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Ai_Generator_Tip_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/ai-generator-tip-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Product_Editor_Checklist_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/black-friday-product-editor-checklist-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Promotion_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/black-friday-promotion-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Sidebar_Checklist_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/black-friday-sidebar-checklist-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Trustpilot_Review_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/trustpilot-review-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Webinar_Promo_Notification' => __DIR__ . '/../..' . '/src/integrations/alerts/webinar-promo-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Block_Editor_Integration' => __DIR__ . '/../..' . '/src/integrations/blocks/block-editor-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Breadcrumbs_Block' => __DIR__ . '/../..' . '/src/integrations/blocks/breadcrumbs-block.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Dynamic_Block' => __DIR__ . '/../..' . '/src/integrations/blocks/abstract-dynamic-block.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Dynamic_Block_V3' => __DIR__ . '/../..' . '/src/integrations/blocks/abstract-dynamic-block-v3.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Internal_Linking_Category' => __DIR__ . '/../..' . '/src/integrations/blocks/block-categories.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Structured_Data_Blocks' => __DIR__ . '/../..' . '/src/integrations/blocks/structured-data-blocks.php', 'Yoast\\WP\\SEO\\Integrations\\Breadcrumbs_Integration' => __DIR__ . '/../..' . '/src/integrations/breadcrumbs-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration' => __DIR__ . '/../..' . '/src/integrations/cleanup-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Estimated_Reading_Time' => __DIR__ . '/../..' . '/src/integrations/estimated-reading-time.php', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Attachment_Post_Type' => __DIR__ . '/../..' . '/src/integrations/exclude-attachment-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Oembed_Cache_Post_Type' => __DIR__ . '/../..' . '/src/integrations/exclude-oembed-cache-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Feature_Flag_Integration' => __DIR__ . '/../..' . '/src/integrations/feature-flag-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Backwards_Compatibility' => __DIR__ . '/../..' . '/src/integrations/front-end/backwards-compatibility.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Category_Term_Description' => __DIR__ . '/../..' . '/src/integrations/front-end/category-term-description.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Comment_Link_Fixer' => __DIR__ . '/../..' . '/src/integrations/front-end/comment-link-fixer.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Basic' => __DIR__ . '/../..' . '/src/integrations/front-end/crawl-cleanup-basic.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Rss' => __DIR__ . '/../..' . '/src/integrations/front-end/crawl-cleanup-rss.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Searches' => __DIR__ . '/../..' . '/src/integrations/front-end/crawl-cleanup-searches.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Feed_Improvements' => __DIR__ . '/../..' . '/src/integrations/front-end/feed-improvements.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Force_Rewrite_Title' => __DIR__ . '/../..' . '/src/integrations/front-end/force-rewrite-title.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Handle_404' => __DIR__ . '/../..' . '/src/integrations/front-end/handle-404.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Indexing_Controls' => __DIR__ . '/../..' . '/src/integrations/front-end/indexing-controls.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Open_Graph_OEmbed' => __DIR__ . '/../..' . '/src/integrations/front-end/open-graph-oembed.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\RSS_Footer_Embed' => __DIR__ . '/../..' . '/src/integrations/front-end/rss-footer-embed.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Redirects' => __DIR__ . '/../..' . '/src/integrations/front-end/redirects.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Robots_Txt_Integration' => __DIR__ . '/../..' . '/src/integrations/front-end/robots-txt-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Schema_Accessibility_Feature' => __DIR__ . '/../..' . '/src/integrations/front-end/schema-accessibility-feature.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\WP_Robots_Integration' => __DIR__ . '/../..' . '/src/integrations/front-end/wp-robots-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End_Integration' => __DIR__ . '/../..' . '/src/integrations/front-end-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Integration_Interface' => __DIR__ . '/../..' . '/src/integrations/integration-interface.php', 'Yoast\\WP\\SEO\\Integrations\\Primary_Category' => __DIR__ . '/../..' . '/src/integrations/primary-category.php', 'Yoast\\WP\\SEO\\Integrations\\Settings_Integration' => __DIR__ . '/../..' . '/src/integrations/settings-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Support_Integration' => __DIR__ . '/../..' . '/src/integrations/support-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\AMP' => __DIR__ . '/../..' . '/src/integrations/third-party/amp.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\BbPress' => __DIR__ . '/../..' . '/src/integrations/third-party/bbpress.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Elementor' => __DIR__ . '/../..' . '/src/integrations/third-party/elementor.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_Elementor_Post_Types' => __DIR__ . '/../..' . '/src/integrations/third-party/exclude-elementor-post-types.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_WooCommerce_Post_Types' => __DIR__ . '/../..' . '/src/integrations/third-party/exclude-woocommerce-post-types.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Jetpack' => __DIR__ . '/../..' . '/src/integrations/third-party/jetpack.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\W3_Total_Cache' => __DIR__ . '/../..' . '/src/integrations/third-party/w3-total-cache.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML' => __DIR__ . '/../..' . '/src/integrations/third-party/wpml.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML_WPSEO_Notification' => __DIR__ . '/../..' . '/src/integrations/third-party/wpml-wpseo-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories' => __DIR__ . '/../..' . '/src/integrations/third-party/web-stories.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories_Post_Edit' => __DIR__ . '/../..' . '/src/integrations/third-party/web-stories-post-edit.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Wincher_Publish' => __DIR__ . '/../..' . '/src/integrations/third-party/wincher-publish.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce' => __DIR__ . '/../..' . '/src/integrations/third-party/woocommerce.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce_Post_Edit' => __DIR__ . '/../..' . '/src/integrations/third-party/woocommerce-post-edit.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Woocommerce_Permalinks' => __DIR__ . '/../..' . '/src/integrations/third-party/woocommerce-permalinks.php', 'Yoast\\WP\\SEO\\Integrations\\Uninstall_Integration' => __DIR__ . '/../..' . '/src/integrations/uninstall-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Addon_Update_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/addon-update-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Auto_Update_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/auto-update-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Ancestor_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-ancestor-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Attachment_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-attachment-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Archive_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-author-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-author-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Category_Permalink_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-category-permalink-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Date_Archive_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-date-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_HomeUrl_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-homeurl-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Home_Page_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-home-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Permalink_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-permalink-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Meta_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-post-meta-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Archive_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-post-type-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Change_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-post-type-change-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-post-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Static_Home_Page_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-static-home-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_System_Page_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-system-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Taxonomy_Change_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-taxonomy-change-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Term_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/indexable-term-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Titles_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/option-titles-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Wpseo_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/option-wpseo-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Category_Quick_Edit_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/primary-category-quick-edit-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Term_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/primary-term-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Search_Engines_Discouraged_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/search-engines-discouraged-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Woocommerce_Beta_Editor_Watcher' => __DIR__ . '/../..' . '/src/integrations/watchers/woocommerce-beta-editor-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Woocommerce_Product_Category_Permalink_Integration' => __DIR__ . '/../..' . '/src/integrations/woocommerce-product-category-permalink-integration.php', 'Yoast\\WP\\SEO\\Integrations\\XMLRPC' => __DIR__ . '/../..' . '/src/integrations/xmlrpc.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Post_Launch' => __DIR__ . '/../..' . '/src/introductions/application/ai-brand-insights-post-launch.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Pre_Launch' => __DIR__ . '/../..' . '/src/introductions/application/ai-brand-insights-pre-launch.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Ai_Fix_Assessments_Upsell' => __DIR__ . '/../..' . '/src/introductions/application/ai-fix-assessments-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Black_Friday_Announcement' => __DIR__ . '/../..' . '/src/introductions/application/black-friday-announcement.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Current_Page_Trait' => __DIR__ . '/../..' . '/src/introductions/application/current-page-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Delayed_Premium_Upsell' => __DIR__ . '/../..' . '/src/introductions/application/delayed-premium-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Google_Docs_Addon_Upsell' => __DIR__ . '/../..' . '/src/introductions/application/google-docs-addon-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector' => __DIR__ . '/../..' . '/src/introductions/application/introductions-collector.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\User_Allowed_Trait' => __DIR__ . '/../..' . '/src/introductions/application/user-allowed-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Version_Trait' => __DIR__ . '/../..' . '/src/introductions/application/version-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introduction_Interface' => __DIR__ . '/../..' . '/src/introductions/domain/introduction-interface.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introduction_Item' => __DIR__ . '/../..' . '/src/introductions/domain/introduction-item.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introductions_Bucket' => __DIR__ . '/../..' . '/src/introductions/domain/introductions-bucket.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Invalid_User_Id_Exception' => __DIR__ . '/../..' . '/src/introductions/domain/invalid-user-id-exception.php', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository' => __DIR__ . '/../..' . '/src/introductions/infrastructure/introductions-seen-repository.php', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository' => __DIR__ . '/../..' . '/src/introductions/infrastructure/wistia-embed-permission-repository.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Integration' => __DIR__ . '/../..' . '/src/introductions/user-interface/introductions-integration.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Seen_Route' => __DIR__ . '/../..' . '/src/introductions/user-interface/introductions-seen-route.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Wistia_Embed_Permission_Route' => __DIR__ . '/../..' . '/src/introductions/user-interface/wistia-embed-permission-route.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Available_Posts\\Available_Posts_Repository' => __DIR__ . '/../..' . '/src/llms-txt/application/available-posts/available-posts-repository.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Configuration\\Llms_Txt_Configuration' => __DIR__ . '/../..' . '/src/llms-txt/application/configuration/llms-txt-configuration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler' => __DIR__ . '/../..' . '/src/llms-txt/application/file/commands/populate-file-command-handler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler' => __DIR__ . '/../..' . '/src/llms-txt/application/file/commands/remove-file-command-handler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\File_Failure_Notification_Presenter' => __DIR__ . '/../..' . '/src/llms-txt/application/file/file-failure-notification-presenter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler' => __DIR__ . '/../..' . '/src/llms-txt/application/file/llms-txt-cron-scheduler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Check' => __DIR__ . '/../..' . '/src/llms-txt/application/health-check/file-check.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner' => __DIR__ . '/../..' . '/src/llms-txt/application/health-check/file-runner.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Description_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/description-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Intro_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/intro-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Link_Lists_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/link-lists-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Markdown_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/markdown-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Optional_Link_List_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/optional-link-list-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Title_Builder' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-builders/title-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Escaper' => __DIR__ . '/../..' . '/src/llms-txt/application/markdown-escaper.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Data' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/data-provider/available-posts-data.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Repository_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/data-provider/available-posts-repository-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Container' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/data-provider/data-container.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/data-provider/data-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Parameters' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/data-provider/parameters.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Invalid_Post_Type_Exception' => __DIR__ . '/../..' . '/src/llms-txt/domain/available-posts/invalid-post-type-exception.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Content\\Post_Collection_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/content/post-collection-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Content_Types\\Content_Type_Entry' => __DIR__ . '/../..' . '/src/llms-txt/domain/content-types/content-type-entry.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_File_System_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/file/llms-file-system-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_Txt_Permission_Gate_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/file/llms-txt-permission-gate-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Items\\Item_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/items/item-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Items\\Link' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/items/link.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Llms_Txt_Renderer' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/llms-txt-renderer.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Description' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/sections/description.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Intro' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/sections/intro.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Link_List' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/sections/link-list.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Section_Interface' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/sections/section-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Title' => __DIR__ . '/../..' . '/src/llms-txt/domain/markdown/sections/title.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/content/automatic-post-collection.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/content/manual-post-collection.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Post_Collection_Factory' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/content/post-collection-factory.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/file/wordpress-file-system-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/file/wordpress-llms-txt-permission-gate.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Content_Types_Collector' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/markdown-services/content-types-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Description_Adapter' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/markdown-services/description-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Sitemap_Link_Collector' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/markdown-services/sitemap-link-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Terms_Collector' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/markdown-services/terms-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Title_Adapter' => __DIR__ . '/../..' . '/src/llms-txt/infrastructure/markdown-services/title-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Available_Posts_Route' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/available-posts-route.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Cleanup_Llms_Txt_On_Deactivation' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/cleanup-llms-txt-on-deactivation.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Enable_Llms_Txt_Option_Watcher' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/enable-llms-txt-option-watcher.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\File_Failure_Llms_Txt_Notification_Integration' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/file-failure-llms-txt-notification-integration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Health_Check\\File_Reports' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/health-check/file-reports.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Llms_Txt_Cron_Callback_Integration' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/llms-txt-cron-callback-integration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Schedule_Population_On_Activation_Integration' => __DIR__ . '/../..' . '/src/llms-txt/user-interface/schedule-population-on-activation-integration.php', 'Yoast\\WP\\SEO\\Loadable_Interface' => __DIR__ . '/../..' . '/src/loadable-interface.php', 'Yoast\\WP\\SEO\\Loader' => __DIR__ . '/../..' . '/src/loader.php', 'Yoast\\WP\\SEO\\Loggers\\Logger' => __DIR__ . '/../..' . '/src/loggers/logger.php', 'Yoast\\WP\\SEO\\Main' => __DIR__ . '/../..' . '/src/main.php', 'Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer' => __DIR__ . '/../..' . '/src/memoizers/meta-tags-context-memoizer.php', 'Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer' => __DIR__ . '/../..' . '/src/memoizers/presentation-memoizer.php', 'Yoast\\WP\\SEO\\Models\\Indexable' => __DIR__ . '/../..' . '/src/models/indexable.php', 'Yoast\\WP\\SEO\\Models\\Indexable_Extension' => __DIR__ . '/../..' . '/src/models/indexable-extension.php', 'Yoast\\WP\\SEO\\Models\\Indexable_Hierarchy' => __DIR__ . '/../..' . '/src/models/indexable-hierarchy.php', 'Yoast\\WP\\SEO\\Models\\Primary_Term' => __DIR__ . '/../..' . '/src/models/primary-term.php', 'Yoast\\WP\\SEO\\Models\\SEO_Links' => __DIR__ . '/../..' . '/src/models/seo-links.php', 'Yoast\\WP\\SEO\\Models\\SEO_Meta' => __DIR__ . '/../..' . '/src/models/seo-meta.php', 'Yoast\\WP\\SEO\\Plans\\Application\\Add_Ons_Collector' => __DIR__ . '/../..' . '/src/plans/application/add-ons-collector.php', 'Yoast\\WP\\SEO\\Plans\\Application\\Duplicate_Post_Manager' => __DIR__ . '/../..' . '/src/plans/application/duplicate-post-manager.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Add_On_Interface' => __DIR__ . '/../..' . '/src/plans/domain/add-ons/add-on-interface.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Premium' => __DIR__ . '/../..' . '/src/plans/domain/add-ons/premium.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Woo' => __DIR__ . '/../..' . '/src/plans/domain/add-ons/woo.php', 'Yoast\\WP\\SEO\\Plans\\Infrastructure\\Add_Ons\\Managed_Add_On' => __DIR__ . '/../..' . '/src/plans/infrastructure/add-ons/managed-add-on.php', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Plans_Page_Integration' => __DIR__ . '/../..' . '/src/plans/user-interface/plans-page-integration.php', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Upgrade_Sidebar_Menu_Integration' => __DIR__ . '/../..' . '/src/plans/user-interface/upgrade-sidebar-menu-integration.php', 'Yoast\\WP\\SEO\\Presentations\\Abstract_Presentation' => __DIR__ . '/../..' . '/src/presentations/abstract-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Archive_Adjacent' => __DIR__ . '/../..' . '/src/presentations/archive-adjacent-trait.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Author_Archive_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-author-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Date_Archive_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-date-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Error_Page_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-error-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Home_Page_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-home-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Archive_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-post-type-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-post-type-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Search_Result_Page_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-search-result-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Home_Page_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-static-home-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Posts_Page_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-static-posts-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Term_Archive_Presentation' => __DIR__ . '/../..' . '/src/presentations/indexable-term-archive-presentation.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Indexable_Presenter' => __DIR__ . '/../..' . '/src/presenters/abstract-indexable-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Indexable_Tag_Presenter' => __DIR__ . '/../..' . '/src/presenters/abstract-indexable-tag-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Presenter' => __DIR__ . '/../..' . '/src/presenters/abstract-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Alert_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/alert-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Badge_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Beta_Badge_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/beta-badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Help_Link_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/help-link-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Error_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/indexing-error-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Failed_Notification_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/indexing-failed-notification-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_List_Item_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/indexing-list-item-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Notification_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/indexing-notification-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Light_Switch_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/light-switch-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Meta_Fields_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/meta-fields-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Migration_Error_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/migration-error-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Notice_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/notice-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Premium_Badge_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/premium-badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Search_Engines_Discouraged_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/search-engines-discouraged-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Sidebar_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/sidebar-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Woocommerce_Beta_Editor_Presenter' => __DIR__ . '/../..' . '/src/presenters/admin/woocommerce-beta-editor-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Breadcrumbs_Presenter' => __DIR__ . '/../..' . '/src/presenters/breadcrumbs-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Canonical_Presenter' => __DIR__ . '/../..' . '/src/presenters/canonical-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Debug\\Marker_Close_Presenter' => __DIR__ . '/../..' . '/src/presenters/debug/marker-close-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Debug\\Marker_Open_Presenter' => __DIR__ . '/../..' . '/src/presenters/debug/marker-open-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Meta_Author_Presenter' => __DIR__ . '/../..' . '/src/presenters/meta-author-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Meta_Description_Presenter' => __DIR__ . '/../..' . '/src/presenters/meta-description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Author_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/article-author-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Modified_Time_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/article-modified-time-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Published_Time_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/article-published-time-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Publisher_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/article-publisher-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Description_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Image_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/image-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Locale_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/locale-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Site_Name_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/site-name-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Title_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Type_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/type-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Url_Presenter' => __DIR__ . '/../..' . '/src/presenters/open-graph/url-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Rel_Next_Presenter' => __DIR__ . '/../..' . '/src/presenters/rel-next-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Rel_Prev_Presenter' => __DIR__ . '/../..' . '/src/presenters/rel-prev-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' => __DIR__ . '/../..' . '/src/presenters/robots-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Robots_Txt_Presenter' => __DIR__ . '/../..' . '/src/presenters/robots-txt-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Schema_Presenter' => __DIR__ . '/../..' . '/src/presenters/schema-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Score_Icon_Presenter' => __DIR__ . '/../..' . '/src/presenters/score-icon-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Slack\\Enhanced_Data_Presenter' => __DIR__ . '/../..' . '/src/presenters/slack/enhanced-data-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Title_Presenter' => __DIR__ . '/../..' . '/src/presenters/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Card_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/card-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Creator_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/creator-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Description_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Image_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/image-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Site_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/site-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Title_Presenter' => __DIR__ . '/../..' . '/src/presenters/twitter/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Url_List_Presenter' => __DIR__ . '/../..' . '/src/presenters/url-list-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Ahrefs_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/ahrefs-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Baidu_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/baidu-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Bing_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/bing-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Google_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/google-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Pinterest_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/pinterest-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Yandex_Presenter' => __DIR__ . '/../..' . '/src/presenters/webmaster/yandex-presenter.php', 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager' => __DIR__ . '/../..' . '/src/promotions/application/promotion-manager.php', 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager_Interface' => __DIR__ . '/../..' . '/src/promotions/application/promotion-manager-interface.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Abstract_Promotion' => __DIR__ . '/../..' . '/src/promotions/domain/abstract-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Black_Friday_Checklist_Promotion' => __DIR__ . '/../..' . '/src/deprecated/src/promotions/domain/black-friday-checklist-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Black_Friday_Promotion' => __DIR__ . '/../..' . '/src/promotions/domain/black-friday-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Promotion_Interface' => __DIR__ . '/../..' . '/src/promotions/domain/promotion-interface.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Time_Interval' => __DIR__ . '/../..' . '/src/promotions/domain/time-interval.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository' => __DIR__ . '/../..' . '/src/repositories/indexable-cleanup-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository' => __DIR__ . '/../..' . '/src/repositories/indexable-hierarchy-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' => __DIR__ . '/../..' . '/src/repositories/indexable-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository' => __DIR__ . '/../..' . '/src/repositories/primary-term-repository.php', 'Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository' => __DIR__ . '/../..' . '/src/repositories/seo-links-repository.php', 'Yoast\\WP\\SEO\\Routes\\Abstract_Action_Route' => __DIR__ . '/../..' . '/src/routes/abstract-action-route.php', 'Yoast\\WP\\SEO\\Routes\\Abstract_Indexation_Route' => __DIR__ . '/../..' . '/src/routes/abstract-indexation-route.php', 'Yoast\\WP\\SEO\\Routes\\Alert_Dismissal_Route' => __DIR__ . '/../..' . '/src/routes/alert-dismissal-route.php', 'Yoast\\WP\\SEO\\Routes\\Endpoint_Interface' => __DIR__ . '/../..' . '/src/routes/endpoint-interface.php', 'Yoast\\WP\\SEO\\Routes\\First_Time_Configuration_Route' => __DIR__ . '/../..' . '/src/routes/first-time-configuration-route.php', 'Yoast\\WP\\SEO\\Routes\\Importing_Route' => __DIR__ . '/../..' . '/src/routes/importing-route.php', 'Yoast\\WP\\SEO\\Routes\\Indexables_Head_Route' => __DIR__ . '/../..' . '/src/routes/indexables-head-route.php', 'Yoast\\WP\\SEO\\Routes\\Indexing_Route' => __DIR__ . '/../..' . '/src/routes/indexing-route.php', 'Yoast\\WP\\SEO\\Routes\\Integrations_Route' => __DIR__ . '/../..' . '/src/routes/integrations-route.php', 'Yoast\\WP\\SEO\\Routes\\Meta_Search_Route' => __DIR__ . '/../..' . '/src/routes/meta-search-route.php', 'Yoast\\WP\\SEO\\Routes\\Route_Interface' => __DIR__ . '/../..' . '/src/routes/route-interface.php', 'Yoast\\WP\\SEO\\Routes\\SEMrush_Route' => __DIR__ . '/../..' . '/src/routes/semrush-route.php', 'Yoast\\WP\\SEO\\Routes\\Supported_Features_Route' => __DIR__ . '/../..' . '/src/routes/supported-features-route.php', 'Yoast\\WP\\SEO\\Routes\\Wincher_Route' => __DIR__ . '/../..' . '/src/routes/wincher-route.php', 'Yoast\\WP\\SEO\\Routes\\Workouts_Route' => __DIR__ . '/../..' . '/src/routes/workouts-route.php', 'Yoast\\WP\\SEO\\Routes\\Yoast_Head_REST_Field' => __DIR__ . '/../..' . '/src/routes/yoast-head-rest-field.php', 'Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration' => __DIR__ . '/../..' . '/src/schema/application/configuration/schema-configuration.php', 'Yoast\\WP\\SEO\\Schema\\Infrastructure\\Disable_Schema_Integration' => __DIR__ . '/../..' . '/src/schema/infrastructure/disable-schema-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command' => __DIR__ . '/../..' . '/src/schema-aggregator/application/aggregate-site-schema-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler' => __DIR__ . '/../..' . '/src/schema-aggregator/application/aggregate-site-schema-command-handler.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command' => __DIR__ . '/../..' . '/src/schema-aggregator/application/aggregate-site-schema-map-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command_Handler' => __DIR__ . '/../..' . '/src/schema-aggregator/application/aggregate-site-schema-map-command-handler.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager' => __DIR__ . '/../..' . '/src/schema-aggregator/application/cache/manager.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager' => __DIR__ . '/../..' . '/src/schema-aggregator/application/cache/xml-manager.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Abstract_Schema_Enhancer' => __DIR__ . '/../..' . '/src/schema-aggregator/application/enhancement/abstract-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Article_Schema_Enhancer' => __DIR__ . '/../..' . '/src/schema-aggregator/application/enhancement/article-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Person_Schema_Enhancer' => __DIR__ . '/../..' . '/src/schema-aggregator/application/enhancement/person-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Schema_Enhancement_Factory' => __DIR__ . '/../..' . '/src/schema-aggregator/application/enhancement/schema-enhancement-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Default_Filter' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/default-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Filtering_Strategy_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/filtering-strategy-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\Schema_Node_Filter_Decider_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-filter/schema-node-filter-decider-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebPage_Schema_Node_Filter' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-filter/webpage-schema-node-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebSite_Schema_Node_Filter' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-filter/website-schema-node-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\Base_Schema_Node_Property_Filter' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-property-filter/base-schema-node-property-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\Schema_Node_Property_Filter_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-property-filter/schema-node-property-filter-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\WebPage_Schema_Node_Property_Filter' => __DIR__ . '/../..' . '/src/schema-aggregator/application/filtering/schema-node-property-filter/webpage-schema-node-property-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Meta\\Response_Meta_Provider' => __DIR__ . '/../..' . '/src/schema-aggregator/application/meta/response-meta-provider.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Properties_Merger' => __DIR__ . '/../..' . '/src/schema-aggregator/application/properties-merger.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Announcement' => __DIR__ . '/../..' . '/src/schema-aggregator/application/schema-aggregator-announcement.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Response_Composer' => __DIR__ . '/../..' . '/src/schema-aggregator/application/schema-aggregator-response-composer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Builder' => __DIR__ . '/../..' . '/src/schema-aggregator/application/schema_map/schema-map-builder.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Xml_Renderer' => __DIR__ . '/../..' . '/src/schema-aggregator/application/schema_map/schema-map-xml-renderer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Pieces_Aggregator' => __DIR__ . '/../..' . '/src/schema-aggregator/application/schema-pieces-aggregator.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Current_Site_URL_Provider_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/current-site-url-provider-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Enhancement\\Enhancement_Config_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/enhancement/enhancement-config-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Enhancement\\Schema_Enhancement_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/enhancement/schema-enhancement-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\External_Schema_Piece_Repository_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/external-schema-piece-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/indexable-count.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count_Collection' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/indexable-count-collection.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Page_Controls' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/page-controls.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/schema-piece.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Collection' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/schema-piece-collection.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Repository_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/domain/schema-piece-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/aggregator-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Base_Map_Loader' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/base-map-loader.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Default_Elements_Context_Map' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/default-elements-context-map.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Filtered_Map_Loader' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/filtered-map-loader.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Map_Loader_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/elements-context-map/map-loader-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Article_Config' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/enhancement/article-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Person_Config' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/enhancement/person-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Filtering_Strategy_Factory' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/filtering-strategy-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository_Factory' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\WordPress_Query_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/indexable-repository/wordpress-query-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Meta_Tags_Context_Memoizer_Adapter' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/meta-tags-context-memoizer-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Conditional' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-aggregator-conditional.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Watcher' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-aggregator-watcher.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Config' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Header_Adapter' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-header-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Indexable_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-indexable-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Repository_Factory' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-repository-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Repository_Interface' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_WordPress_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema_map/schema-map-wordpress-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Edd_Schema_Piece_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-pieces/edd-schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Schema_Piece_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-pieces/schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Woo_Schema_Piece_Repository' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-pieces/woo-schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\WordPress_Global_State_Adapter' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/schema-pieces/wordpress-global-state-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\WordPress_Current_Site_URL_Provider' => __DIR__ . '/../..' . '/src/schema-aggregator/infrastructure/wordpress-current-site-url-provider.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Abstract_Cache_Listener_Integration' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/cache/abstract-cache-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Indexables_Update_Listener_Integration' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/cache/indexables-update-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\WooCommerce_Product_Type_Change_Listener_Integration' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/cache/woocommerce-product-type-change-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cache_Cli_Command' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-aggregator-cache-cli-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cli_Command' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-aggregator-cli-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Route' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-aggregator-route.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Xml_Route' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-aggregator-xml-route.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Response_Header_Integration' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-response-header-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Robots_Txt_Integration' => __DIR__ . '/../..' . '/src/schema-aggregator/user-interface/site-schema-robots-txt-integration.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Check' => __DIR__ . '/../..' . '/src/services/health-check/default-tagline-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Reports' => __DIR__ . '/../..' . '/src/services/health-check/default-tagline-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner' => __DIR__ . '/../..' . '/src/services/health-check/default-tagline-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Health_Check' => __DIR__ . '/../..' . '/src/services/health-check/health-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Check' => __DIR__ . '/../..' . '/src/services/health-check/links-table-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Reports' => __DIR__ . '/../..' . '/src/services/health-check/links-table-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Runner' => __DIR__ . '/../..' . '/src/services/health-check/links-table-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\MyYoast_Api_Request_Factory' => __DIR__ . '/../..' . '/src/services/health-check/myyoast-api-request-factory.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Check' => __DIR__ . '/../..' . '/src/services/health-check/page-comments-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Reports' => __DIR__ . '/../..' . '/src/services/health-check/page-comments-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner' => __DIR__ . '/../..' . '/src/services/health-check/page-comments-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Check' => __DIR__ . '/../..' . '/src/services/health-check/postname-permalink-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Reports' => __DIR__ . '/../..' . '/src/services/health-check/postname-permalink-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner' => __DIR__ . '/../..' . '/src/services/health-check/postname-permalink-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder' => __DIR__ . '/../..' . '/src/services/health-check/report-builder.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory' => __DIR__ . '/../..' . '/src/services/health-check/report-builder-factory.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Reports_Trait' => __DIR__ . '/../..' . '/src/services/health-check/reports-trait.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Runner_Interface' => __DIR__ . '/../..' . '/src/services/health-check/runner-interface.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service' => __DIR__ . '/../..' . '/src/services/importing/aioseo/aioseo-replacevar-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service' => __DIR__ . '/../..' . '/src/services/importing/aioseo/aioseo-robots-provider-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service' => __DIR__ . '/../..' . '/src/services/importing/aioseo/aioseo-robots-transformer-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Social_Images_Provider_Service' => __DIR__ . '/../..' . '/src/services/importing/aioseo/aioseo-social-images-provider-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service' => __DIR__ . '/../..' . '/src/services/importing/conflicting-plugins-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service' => __DIR__ . '/../..' . '/src/services/importing/importable-detector-service.php', 'Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager' => __DIR__ . '/../..' . '/src/services/indexables/indexable-version-manager.php', 'Yoast\\WP\\SEO\\Surfaces\\Classes_Surface' => __DIR__ . '/../..' . '/src/surfaces/classes-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface' => __DIR__ . '/../..' . '/src/surfaces/helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Meta_Surface' => __DIR__ . '/../..' . '/src/surfaces/meta-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface' => __DIR__ . '/../..' . '/src/surfaces/open-graph-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface' => __DIR__ . '/../..' . '/src/surfaces/schema-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface' => __DIR__ . '/../..' . '/src/surfaces/twitter-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Values\\Meta' => __DIR__ . '/../..' . '/src/surfaces/values/meta.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Configuration\\Task_List_Configuration' => __DIR__ . '/../..' . '/src/task-list/application/configuration/task-list-configuration.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Endpoints\\Endpoints_Repository' => __DIR__ . '/../..' . '/src/task-list/application/endpoints/endpoints-repository.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Complete_FTC' => __DIR__ . '/../..' . '/src/task-list/application/tasks/complete-ftc.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Create_New_Content' => __DIR__ . '/../..' . '/src/task-list/application/tasks/create-new-content.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Delete_Hello_World' => __DIR__ . '/../..' . '/src/task-list/application/tasks/delete-hello-world.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Enable_Llms_Txt' => __DIR__ . '/../..' . '/src/task-list/application/tasks/enable-llms-txt.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates' => __DIR__ . '/../..' . '/src/task-list/application/tasks/set-search-appearance-templates.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks_Repository' => __DIR__ . '/../..' . '/src/task-list/application/tasks-repository.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Call_To_Action_Entry' => __DIR__ . '/../..' . '/src/task-list/domain/components/call-to-action-entry.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Copy_Set' => __DIR__ . '/../..' . '/src/task-list/domain/components/copy-set.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Endpoint\\Endpoint_Interface' => __DIR__ . '/../..' . '/src/task-list/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Endpoint\\Endpoint_List' => __DIR__ . '/../..' . '/src/task-list/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_Hello_World_Task_Exception' => __DIR__ . '/../..' . '/src/task-list/domain/exceptions/complete-hello-world-task-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_LLMS_Task_Exception' => __DIR__ . '/../..' . '/src/task-list/domain/exceptions/complete-llms-task-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Post_Type_Tasks_Exception' => __DIR__ . '/../..' . '/src/task-list/domain/exceptions/invalid-post-type-tasks-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Tasks_Exception' => __DIR__ . '/../..' . '/src/task-list/domain/exceptions/invalid-tasks-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Task_Not_Found_Exception' => __DIR__ . '/../..' . '/src/task-list/domain/exceptions/task-not-found-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Completeable_Task' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/abstract-completeable-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Post_Type_Task' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/abstract-post-type-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Task' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/abstract-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Completeable_Task_Interface' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/completeable-task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Post_Type_Task_Interface' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/post-type-task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Task_Interface' => __DIR__ . '/../..' . '/src/task-list/domain/tasks/task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Complete_Task_Endpoint' => __DIR__ . '/../..' . '/src/task-list/infrastructure/endpoints/complete-task-endpoint.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Get_Tasks_Endpoint' => __DIR__ . '/../..' . '/src/task-list/infrastructure/endpoints/get-tasks-endpoint.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Register_Post_Type_Tasks_Integration' => __DIR__ . '/../..' . '/src/task-list/infrastructure/register-post-type-tasks-integration.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Cached_Tasks_Collector' => __DIR__ . '/../..' . '/src/task-list/infrastructure/tasks-collectors/cached-tasks-collector.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector' => __DIR__ . '/../..' . '/src/task-list/infrastructure/tasks-collectors/tasks-collector.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector_Interface' => __DIR__ . '/../..' . '/src/task-list/infrastructure/tasks-collectors/tasks-collector-interface.php', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Complete_Task_Route' => __DIR__ . '/../..' . '/src/task-list/user-interface/tasks/complete-task-route.php', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Get_Tasks_Route' => __DIR__ . '/../..' . '/src/task-list/user-interface/tasks/get-tasks-route.php', 'Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker' => __DIR__ . '/../..' . '/src/tracking/application/action-tracker.php', 'Yoast\\WP\\SEO\\Tracking\\Domain\\Exceptions\\Invalid_Tracked_Action_Exception' => __DIR__ . '/../..' . '/src/tracking/domain/exceptions/invalid-tracked-action-exception.php', 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_Link_Adapter' => __DIR__ . '/../..' . '/src/tracking/infrastructure/tracking-link-adapter.php', 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_On_Page_Load_Integration' => __DIR__ . '/../..' . '/src/tracking/infrastructure/tracking-on-page-load-integration.php', 'Yoast\\WP\\SEO\\Tracking\\User_Interface\\Action_Tracking_Route' => __DIR__ . '/../..' . '/src/tracking/user-interface/action-tracking-route.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector' => __DIR__ . '/../..' . '/src/user-meta/application/additional-contactmethods-collector.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Cleanup_Service' => __DIR__ . '/../..' . '/src/user-meta/application/cleanup-service.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector' => __DIR__ . '/../..' . '/src/user-meta/application/custom-meta-collector.php', 'Yoast\\WP\\SEO\\User_Meta\\Domain\\Additional_Contactmethod_Interface' => __DIR__ . '/../..' . '/src/user-meta/domain/additional-contactmethod-interface.php', 'Yoast\\WP\\SEO\\User_Meta\\Domain\\Custom_Meta_Interface' => __DIR__ . '/../..' . '/src/user-meta/domain/custom-meta-interface.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Facebook' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/facebook.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Instagram' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/instagram.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Linkedin' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/linkedin.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Myspace' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/myspace.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Pinterest' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/pinterest.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Soundcloud' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/soundcloud.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Tumblr' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/tumblr.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Wikipedia' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/wikipedia.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\X' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/x.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Youtube' => __DIR__ . '/../..' . '/src/user-meta/framework/additional-contactmethods/youtube.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Metadesc' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/author-metadesc.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Pronouns' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/author-pronouns.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Title' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/author-title.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Content_Analysis_Disable' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/content-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Inclusive_Language_Analysis_Disable' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/inclusive-language-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Keyword_Analysis_Disable' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/keyword-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Noindex_Author' => __DIR__ . '/../..' . '/src/user-meta/framework/custom-meta/noindex-author.php', 'Yoast\\WP\\SEO\\User_Meta\\Infrastructure\\Cleanup_Repository' => __DIR__ . '/../..' . '/src/user-meta/infrastructure/cleanup-repository.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Additional_Contactmethods_Integration' => __DIR__ . '/../..' . '/src/user-meta/user-interface/additional-contactmethods-integration.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Cleanup_Integration' => __DIR__ . '/../..' . '/src/user-meta/user-interface/cleanup-integration.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Custom_Meta_Integration' => __DIR__ . '/../..' . '/src/user-meta/user-interface/custom-meta-integration.php', 'Yoast\\WP\\SEO\\User_Profiles_Additions\\User_Interface\\User_Profiles_Additions_Ui' => __DIR__ . '/../..' . '/src/user-profiles-additions/user-interface/user-profiles-additions-ui.php', 'Yoast\\WP\\SEO\\Values\\Images' => __DIR__ . '/../..' . '/src/values/images.php', 'Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions' => __DIR__ . '/../..' . '/src/values/indexables/indexable-builder-versions.php', 'Yoast\\WP\\SEO\\Values\\OAuth\\OAuth_Token' => __DIR__ . '/../..' . '/src/values/oauth/oauth-token.php', 'Yoast\\WP\\SEO\\Values\\Open_Graph\\Images' => __DIR__ . '/../..' . '/src/values/open-graph/images.php', 'Yoast\\WP\\SEO\\Values\\Robots\\Directive' => __DIR__ . '/../..' . '/src/values/robots/directive.php', 'Yoast\\WP\\SEO\\Values\\Robots\\User_Agent' => __DIR__ . '/../..' . '/src/values/robots/user-agent.php', 'Yoast\\WP\\SEO\\Values\\Robots\\User_Agent_List' => __DIR__ . '/../..' . '/src/values/robots/user-agent-list.php', 'Yoast\\WP\\SEO\\Values\\Twitter\\Images' => __DIR__ . '/../..' . '/src/values/twitter/images.php', 'Yoast\\WP\\SEO\\WordPress\\Wrapper' => __DIR__ . '/../..' . '/src/wordpress/wrapper.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper' => __DIR__ . '/../..' . '/src/wrappers/wp-query-wrapper.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler' => __DIR__ . '/../..' . '/src/wrappers/wp-remote-handler.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper' => __DIR__ . '/../..' . '/src/wrappers/wp-rewrite-wrapper.php', 'Yoast_Dashboard_Widget' => __DIR__ . '/../..' . '/admin/class-yoast-dashboard-widget.php', 'Yoast_Dismissable_Notice_Ajax' => __DIR__ . '/../..' . '/admin/ajax/class-yoast-dismissable-notice.php', 'Yoast_Dynamic_Rewrites' => __DIR__ . '/../..' . '/inc/class-yoast-dynamic-rewrites.php', 'Yoast_Feature_Toggle' => __DIR__ . '/../..' . '/admin/views/class-yoast-feature-toggle.php', 'Yoast_Feature_Toggles' => __DIR__ . '/../..' . '/admin/views/class-yoast-feature-toggles.php', 'Yoast_Form' => __DIR__ . '/../..' . '/admin/class-yoast-form.php', 'Yoast_Form_Element' => __DIR__ . '/../..' . '/admin/views/interface-yoast-form-element.php', 'Yoast_Input_Select' => __DIR__ . '/../..' . '/admin/views/class-yoast-input-select.php', 'Yoast_Input_Validation' => __DIR__ . '/../..' . '/admin/class-yoast-input-validation.php', 'Yoast_Integration_Toggles' => __DIR__ . '/../..' . '/admin/views/class-yoast-integration-toggles.php', 'Yoast_Network_Admin' => __DIR__ . '/../..' . '/admin/class-yoast-network-admin.php', 'Yoast_Network_Settings_API' => __DIR__ . '/../..' . '/admin/class-yoast-network-settings-api.php', 'Yoast_Notification' => __DIR__ . '/../..' . '/admin/class-yoast-notification.php', 'Yoast_Notification_Center' => __DIR__ . '/../..' . '/admin/class-yoast-notification-center.php', 'Yoast_Notifications' => __DIR__ . '/../..' . '/admin/class-yoast-notifications.php', 'Yoast_Plugin_Conflict' => __DIR__ . '/../..' . '/admin/class-yoast-plugin-conflict.php', 'Yoast_Plugin_Conflict_Ajax' => __DIR__ . '/../..' . '/admin/ajax/class-yoast-plugin-conflict-ajax.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit39a23b5c2ed5051c90d7939162f27c90::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit39a23b5c2ed5051c90d7939162f27c90::$prefixDirsPsr4; $loader->classMap = ComposerStaticInit39a23b5c2ed5051c90d7939162f27c90::$classMap; }, null, ClassLoader::class); } } vendor/composer/autoload_classmap.php 0000644 00000601562 15174712003 0014106 0 ustar 00 <?php // autoload_classmap.php @generated by Composer $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Composer\\Installers\\AglInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php', 'Composer\\Installers\\AkauntingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AkauntingInstaller.php', 'Composer\\Installers\\AnnotateCmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', 'Composer\\Installers\\AsgardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', 'Composer\\Installers\\AttogramInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', 'Composer\\Installers\\BaseInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BaseInstaller.php', 'Composer\\Installers\\BitrixInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', 'Composer\\Installers\\BonefishInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', 'Composer\\Installers\\BotbleInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BotbleInstaller.php', 'Composer\\Installers\\CakePHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', 'Composer\\Installers\\ChefInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ChefInstaller.php', 'Composer\\Installers\\CiviCrmInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', 'Composer\\Installers\\ClanCatsFrameworkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', 'Composer\\Installers\\CockpitInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', 'Composer\\Installers\\CodeIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', 'Composer\\Installers\\Concrete5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', 'Composer\\Installers\\ConcreteCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ConcreteCMSInstaller.php', 'Composer\\Installers\\CroogoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', 'Composer\\Installers\\DecibelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', 'Composer\\Installers\\DframeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DframeInstaller.php', 'Composer\\Installers\\DokuWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', 'Composer\\Installers\\DolibarrInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', 'Composer\\Installers\\DrupalInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', 'Composer\\Installers\\ElggInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ElggInstaller.php', 'Composer\\Installers\\EliasisInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', 'Composer\\Installers\\ExpressionEngineInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', 'Composer\\Installers\\EzPlatformInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', 'Composer\\Installers\\ForkCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ForkCMSInstaller.php', 'Composer\\Installers\\FuelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelInstaller.php', 'Composer\\Installers\\FuelphpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', 'Composer\\Installers\\GravInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/GravInstaller.php', 'Composer\\Installers\\HuradInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/HuradInstaller.php', 'Composer\\Installers\\ImageCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', 'Composer\\Installers\\Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Installer.php', 'Composer\\Installers\\ItopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ItopInstaller.php', 'Composer\\Installers\\KanboardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', 'Composer\\Installers\\KnownInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KnownInstaller.php', 'Composer\\Installers\\KodiCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', 'Composer\\Installers\\KohanaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', 'Composer\\Installers\\LanManagementSystemInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', 'Composer\\Installers\\LaravelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', 'Composer\\Installers\\LavaLiteInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', 'Composer\\Installers\\LithiumInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', 'Composer\\Installers\\MODULEWorkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', 'Composer\\Installers\\MODXEvoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', 'Composer\\Installers\\MagentoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', 'Composer\\Installers\\MajimaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', 'Composer\\Installers\\MakoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MakoInstaller.php', 'Composer\\Installers\\MantisBTInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', 'Composer\\Installers\\MatomoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MatomoInstaller.php', 'Composer\\Installers\\MauticInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MauticInstaller.php', 'Composer\\Installers\\MayaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MayaInstaller.php', 'Composer\\Installers\\MediaWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', 'Composer\\Installers\\MiaoxingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', 'Composer\\Installers\\MicroweberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', 'Composer\\Installers\\ModxInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ModxInstaller.php', 'Composer\\Installers\\MoodleInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', 'Composer\\Installers\\OctoberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', 'Composer\\Installers\\OntoWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', 'Composer\\Installers\\OsclassInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', 'Composer\\Installers\\OxidInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OxidInstaller.php', 'Composer\\Installers\\PPIInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PPIInstaller.php', 'Composer\\Installers\\PantheonInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', 'Composer\\Installers\\PhiftyInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', 'Composer\\Installers\\PhpBBInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', 'Composer\\Installers\\PiwikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', 'Composer\\Installers\\PlentymarketsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', 'Composer\\Installers\\Plugin' => $vendorDir . '/composer/installers/src/Composer/Installers/Plugin.php', 'Composer\\Installers\\PortoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PortoInstaller.php', 'Composer\\Installers\\PrestashopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', 'Composer\\Installers\\ProcessWireInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', 'Composer\\Installers\\PuppetInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', 'Composer\\Installers\\PxcmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', 'Composer\\Installers\\RadPHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', 'Composer\\Installers\\ReIndexInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', 'Composer\\Installers\\Redaxo5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', 'Composer\\Installers\\RedaxoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', 'Composer\\Installers\\RoundcubeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', 'Composer\\Installers\\SMFInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SMFInstaller.php', 'Composer\\Installers\\ShopwareInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', 'Composer\\Installers\\SilverStripeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', 'Composer\\Installers\\SiteDirectInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', 'Composer\\Installers\\StarbugInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', 'Composer\\Installers\\SyDESInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', 'Composer\\Installers\\SyliusInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', 'Composer\\Installers\\TaoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TaoInstaller.php', 'Composer\\Installers\\TastyIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', 'Composer\\Installers\\TheliaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', 'Composer\\Installers\\TuskInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TuskInstaller.php', 'Composer\\Installers\\UserFrostingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', 'Composer\\Installers\\VanillaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', 'Composer\\Installers\\VgmcpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', 'Composer\\Installers\\WHMCSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', 'Composer\\Installers\\WinterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WinterInstaller.php', 'Composer\\Installers\\WolfCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', 'Composer\\Installers\\WordPressInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', 'Composer\\Installers\\YawikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/YawikInstaller.php', 'Composer\\Installers\\ZendInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZendInstaller.php', 'Composer\\Installers\\ZikulaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', 'WPSEO_Abstract_Capability_Manager' => $baseDir . '/admin/capabilities/class-abstract-capability-manager.php', 'WPSEO_Abstract_Metabox_Tab_With_Sections' => $baseDir . '/admin/metabox/class-abstract-sectioned-metabox-tab.php', 'WPSEO_Abstract_Post_Filter' => $baseDir . '/admin/filters/class-abstract-post-filter.php', 'WPSEO_Abstract_Role_Manager' => $baseDir . '/admin/roles/class-abstract-role-manager.php', 'WPSEO_Addon_Manager' => $baseDir . '/inc/class-addon-manager.php', 'WPSEO_Admin' => $baseDir . '/admin/class-admin.php', 'WPSEO_Admin_Asset' => $baseDir . '/admin/class-asset.php', 'WPSEO_Admin_Asset_Analysis_Worker_Location' => $baseDir . '/admin/class-admin-asset-analysis-worker-location.php', 'WPSEO_Admin_Asset_Dev_Server_Location' => $baseDir . '/admin/class-admin-asset-dev-server-location.php', 'WPSEO_Admin_Asset_Location' => $baseDir . '/admin/class-admin-asset-location.php', 'WPSEO_Admin_Asset_Manager' => $baseDir . '/admin/class-admin-asset-manager.php', 'WPSEO_Admin_Asset_SEO_Location' => $baseDir . '/admin/class-admin-asset-seo-location.php', 'WPSEO_Admin_Bar_Menu' => $baseDir . '/inc/class-wpseo-admin-bar-menu.php', 'WPSEO_Admin_Editor_Specific_Replace_Vars' => $baseDir . '/admin/class-admin-editor-specific-replace-vars.php', 'WPSEO_Admin_Gutenberg_Compatibility_Notification' => $baseDir . '/admin/class-admin-gutenberg-compatibility-notification.php', 'WPSEO_Admin_Help_Panel' => $baseDir . '/admin/class-admin-help-panel.php', 'WPSEO_Admin_Init' => $baseDir . '/admin/class-admin-init.php', 'WPSEO_Admin_Menu' => $baseDir . '/admin/menu/class-admin-menu.php', 'WPSEO_Admin_Pages' => $baseDir . '/admin/class-config.php', 'WPSEO_Admin_Recommended_Replace_Vars' => $baseDir . '/admin/class-admin-recommended-replace-vars.php', 'WPSEO_Admin_Settings_Changed_Listener' => $baseDir . '/admin/admin-settings-changed-listener.php', 'WPSEO_Admin_User_Profile' => $baseDir . '/admin/class-admin-user-profile.php', 'WPSEO_Admin_Utils' => $baseDir . '/admin/class-admin-utils.php', 'WPSEO_Author_Sitemap_Provider' => $baseDir . '/inc/sitemaps/class-author-sitemap-provider.php', 'WPSEO_Base_Menu' => $baseDir . '/admin/menu/class-base-menu.php', 'WPSEO_Breadcrumbs' => $baseDir . '/src/deprecated/frontend/breadcrumbs.php', 'WPSEO_Bulk_Description_List_Table' => $baseDir . '/admin/class-bulk-description-editor-list-table.php', 'WPSEO_Bulk_List_Table' => $baseDir . '/admin/class-bulk-editor-list-table.php', 'WPSEO_Bulk_Title_Editor_List_Table' => $baseDir . '/admin/class-bulk-title-editor-list-table.php', 'WPSEO_Capability_Manager' => $baseDir . '/admin/capabilities/class-capability-manager.php', 'WPSEO_Capability_Manager_Factory' => $baseDir . '/admin/capabilities/class-capability-manager-factory.php', 'WPSEO_Capability_Manager_Integration' => $baseDir . '/admin/capabilities/class-capability-manager-integration.php', 'WPSEO_Capability_Manager_VIP' => $baseDir . '/admin/capabilities/class-capability-manager-vip.php', 'WPSEO_Capability_Manager_WP' => $baseDir . '/admin/capabilities/class-capability-manager-wp.php', 'WPSEO_Capability_Utils' => $baseDir . '/admin/capabilities/class-capability-utils.php', 'WPSEO_Collection' => $baseDir . '/admin/interface-collection.php', 'WPSEO_Collector' => $baseDir . '/admin/class-collector.php', 'WPSEO_Content_Images' => $baseDir . '/inc/class-wpseo-content-images.php', 'WPSEO_Cornerstone_Filter' => $baseDir . '/admin/filters/class-cornerstone-filter.php', 'WPSEO_Custom_Fields' => $baseDir . '/inc/class-wpseo-custom-fields.php', 'WPSEO_Custom_Taxonomies' => $baseDir . '/inc/class-wpseo-custom-taxonomies.php', 'WPSEO_Database_Proxy' => $baseDir . '/admin/class-database-proxy.php', 'WPSEO_Date_Helper' => $baseDir . '/inc/date-helper.php', 'WPSEO_Dismissible_Notification' => $baseDir . '/admin/notifiers/dismissible-notification.php', 'WPSEO_Endpoint' => $baseDir . '/admin/endpoints/class-endpoint.php', 'WPSEO_Endpoint_File_Size' => $baseDir . '/admin/endpoints/class-endpoint-file-size.php', 'WPSEO_Endpoint_Statistics' => $baseDir . '/admin/endpoints/class-endpoint-statistics.php', 'WPSEO_Export' => $baseDir . '/admin/class-export.php', 'WPSEO_Expose_Shortlinks' => $baseDir . '/admin/class-expose-shortlinks.php', 'WPSEO_File_Size_Exception' => $baseDir . '/admin/exceptions/class-file-size-exception.php', 'WPSEO_File_Size_Service' => $baseDir . '/admin/services/class-file-size.php', 'WPSEO_Frontend' => $baseDir . '/src/deprecated/frontend/frontend.php', 'WPSEO_GSC' => $baseDir . '/admin/google_search_console/class-gsc.php', 'WPSEO_Gutenberg_Compatibility' => $baseDir . '/admin/class-gutenberg-compatibility.php', 'WPSEO_Image_Utils' => $baseDir . '/inc/class-wpseo-image-utils.php', 'WPSEO_Import_AIOSEO' => $baseDir . '/admin/import/plugins/class-import-aioseo.php', 'WPSEO_Import_AIOSEO_V4' => $baseDir . '/admin/import/plugins/class-import-aioseo-v4.php', 'WPSEO_Import_Greg_SEO' => $baseDir . '/admin/import/plugins/class-import-greg-high-performance-seo.php', 'WPSEO_Import_HeadSpace' => $baseDir . '/admin/import/plugins/class-import-headspace.php', 'WPSEO_Import_Jetpack_SEO' => $baseDir . '/admin/import/plugins/class-import-jetpack.php', 'WPSEO_Import_Platinum_SEO' => $baseDir . '/admin/import/plugins/class-import-platinum-seo-pack.php', 'WPSEO_Import_Plugin' => $baseDir . '/admin/import/class-import-plugin.php', 'WPSEO_Import_Plugins_Detector' => $baseDir . '/admin/import/class-import-detector.php', 'WPSEO_Import_Premium_SEO_Pack' => $baseDir . '/admin/import/plugins/class-import-premium-seo-pack.php', 'WPSEO_Import_RankMath' => $baseDir . '/admin/import/plugins/class-import-rankmath.php', 'WPSEO_Import_SEOPressor' => $baseDir . '/admin/import/plugins/class-import-seopressor.php', 'WPSEO_Import_SEO_Framework' => $baseDir . '/admin/import/plugins/class-import-seo-framework.php', 'WPSEO_Import_Settings' => $baseDir . '/admin/import/class-import-settings.php', 'WPSEO_Import_Smartcrawl_SEO' => $baseDir . '/admin/import/plugins/class-import-smartcrawl.php', 'WPSEO_Import_Squirrly' => $baseDir . '/admin/import/plugins/class-import-squirrly.php', 'WPSEO_Import_Status' => $baseDir . '/admin/import/class-import-status.php', 'WPSEO_Import_Ultimate_SEO' => $baseDir . '/admin/import/plugins/class-import-ultimate-seo.php', 'WPSEO_Import_WPSEO' => $baseDir . '/admin/import/plugins/class-import-wpseo.php', 'WPSEO_Import_WP_Meta_SEO' => $baseDir . '/admin/import/plugins/class-import-wp-meta-seo.php', 'WPSEO_Import_WooThemes_SEO' => $baseDir . '/admin/import/plugins/class-import-woothemes-seo.php', 'WPSEO_Installable' => $baseDir . '/admin/interface-installable.php', 'WPSEO_Installation' => $baseDir . '/inc/class-wpseo-installation.php', 'WPSEO_Language_Utils' => $baseDir . '/inc/language-utils.php', 'WPSEO_Listener' => $baseDir . '/admin/listeners/class-listener.php', 'WPSEO_Menu' => $baseDir . '/admin/menu/class-menu.php', 'WPSEO_Meta' => $baseDir . '/inc/class-wpseo-meta.php', 'WPSEO_Meta_Columns' => $baseDir . '/admin/class-meta-columns.php', 'WPSEO_Metabox' => $baseDir . '/admin/metabox/class-metabox.php', 'WPSEO_Metabox_Analysis' => $baseDir . '/admin/metabox/interface-metabox-analysis.php', 'WPSEO_Metabox_Analysis_Inclusive_Language' => $baseDir . '/admin/metabox/class-metabox-analysis-inclusive-language.php', 'WPSEO_Metabox_Analysis_Readability' => $baseDir . '/admin/metabox/class-metabox-analysis-readability.php', 'WPSEO_Metabox_Analysis_SEO' => $baseDir . '/admin/metabox/class-metabox-analysis-seo.php', 'WPSEO_Metabox_Collapsible' => $baseDir . '/admin/metabox/class-metabox-collapsible.php', 'WPSEO_Metabox_Collapsibles_Sections' => $baseDir . '/admin/metabox/class-metabox-collapsibles-section.php', 'WPSEO_Metabox_Editor' => $baseDir . '/admin/metabox/class-metabox-editor.php', 'WPSEO_Metabox_Form_Tab' => $baseDir . '/admin/metabox/class-metabox-form-tab.php', 'WPSEO_Metabox_Formatter' => $baseDir . '/admin/formatter/class-metabox-formatter.php', 'WPSEO_Metabox_Formatter_Interface' => $baseDir . '/admin/formatter/interface-metabox-formatter.php', 'WPSEO_Metabox_Null_Tab' => $baseDir . '/admin/metabox/class-metabox-null-tab.php', 'WPSEO_Metabox_Section' => $baseDir . '/admin/metabox/interface-metabox-section.php', 'WPSEO_Metabox_Section_Additional' => $baseDir . '/admin/metabox/class-metabox-section-additional.php', 'WPSEO_Metabox_Section_Inclusive_Language' => $baseDir . '/admin/metabox/class-metabox-section-inclusive-language.php', 'WPSEO_Metabox_Section_React' => $baseDir . '/admin/metabox/class-metabox-section-react.php', 'WPSEO_Metabox_Section_Readability' => $baseDir . '/admin/metabox/class-metabox-section-readability.php', 'WPSEO_Metabox_Tab' => $baseDir . '/admin/metabox/interface-metabox-tab.php', 'WPSEO_MyYoast_Api_Request' => $baseDir . '/inc/class-my-yoast-api-request.php', 'WPSEO_MyYoast_Bad_Request_Exception' => $baseDir . '/inc/exceptions/class-myyoast-bad-request-exception.php', 'WPSEO_MyYoast_Invalid_JSON_Exception' => $baseDir . '/inc/exceptions/class-myyoast-invalid-json-exception.php', 'WPSEO_MyYoast_Proxy' => $baseDir . '/admin/class-my-yoast-proxy.php', 'WPSEO_Network_Admin_Menu' => $baseDir . '/admin/menu/class-network-admin-menu.php', 'WPSEO_Notification_Handler' => $baseDir . '/admin/notifiers/interface-notification-handler.php', 'WPSEO_Option' => $baseDir . '/inc/options/class-wpseo-option.php', 'WPSEO_Option_Llmstxt' => $baseDir . '/inc/options/class-wpseo-option-llmstxt.php', 'WPSEO_Option_MS' => $baseDir . '/inc/options/class-wpseo-option-ms.php', 'WPSEO_Option_Social' => $baseDir . '/inc/options/class-wpseo-option-social.php', 'WPSEO_Option_Tab' => $baseDir . '/admin/class-option-tab.php', 'WPSEO_Option_Tabs' => $baseDir . '/admin/class-option-tabs.php', 'WPSEO_Option_Tabs_Formatter' => $baseDir . '/admin/class-option-tabs-formatter.php', 'WPSEO_Option_Titles' => $baseDir . '/inc/options/class-wpseo-option-titles.php', 'WPSEO_Option_Tracking_Only' => $baseDir . '/inc/options/class-wpseo-option-tracking-only.php', 'WPSEO_Option_Wpseo' => $baseDir . '/inc/options/class-wpseo-option-wpseo.php', 'WPSEO_Options' => $baseDir . '/inc/options/class-wpseo-options.php', 'WPSEO_Paper_Presenter' => $baseDir . '/admin/class-paper-presenter.php', 'WPSEO_Plugin_Availability' => $baseDir . '/admin/class-plugin-availability.php', 'WPSEO_Plugin_Conflict' => $baseDir . '/admin/class-plugin-conflict.php', 'WPSEO_Plugin_Importer' => $baseDir . '/admin/import/plugins/class-abstract-plugin-importer.php', 'WPSEO_Plugin_Importers' => $baseDir . '/admin/import/plugins/class-importers.php', 'WPSEO_Post_Metabox_Formatter' => $baseDir . '/admin/formatter/class-post-metabox-formatter.php', 'WPSEO_Post_Type' => $baseDir . '/inc/class-post-type.php', 'WPSEO_Post_Type_Sitemap_Provider' => $baseDir . '/inc/sitemaps/class-post-type-sitemap-provider.php', 'WPSEO_Premium_Popup' => $baseDir . '/admin/class-premium-popup.php', 'WPSEO_Premium_Upsell_Admin_Block' => $baseDir . '/admin/class-premium-upsell-admin-block.php', 'WPSEO_Primary_Term' => $baseDir . '/inc/class-wpseo-primary-term.php', 'WPSEO_Primary_Term_Admin' => $baseDir . '/admin/class-primary-term-admin.php', 'WPSEO_Product_Upsell_Notice' => $baseDir . '/admin/class-product-upsell-notice.php', 'WPSEO_Rank' => $baseDir . '/inc/class-wpseo-rank.php', 'WPSEO_Register_Capabilities' => $baseDir . '/admin/capabilities/class-register-capabilities.php', 'WPSEO_Register_Roles' => $baseDir . '/admin/roles/class-register-roles.php', 'WPSEO_Remote_Request' => $baseDir . '/admin/class-remote-request.php', 'WPSEO_Replace_Vars' => $baseDir . '/inc/class-wpseo-replace-vars.php', 'WPSEO_Replacement_Variable' => $baseDir . '/inc/class-wpseo-replacement-variable.php', 'WPSEO_Replacevar_Editor' => $baseDir . '/admin/menu/class-replacevar-editor.php', 'WPSEO_Replacevar_Field' => $baseDir . '/admin/menu/class-replacevar-field.php', 'WPSEO_Rewrite' => $baseDir . '/inc/class-rewrite.php', 'WPSEO_Role_Manager' => $baseDir . '/admin/roles/class-role-manager.php', 'WPSEO_Role_Manager_Factory' => $baseDir . '/admin/roles/class-role-manager-factory.php', 'WPSEO_Role_Manager_WP' => $baseDir . '/admin/roles/class-role-manager-wp.php', 'WPSEO_Schema_Person_Upgrade_Notification' => $baseDir . '/admin/class-schema-person-upgrade-notification.php', 'WPSEO_Shortcode_Filter' => $baseDir . '/admin/ajax/class-shortcode-filter.php', 'WPSEO_Shortlinker' => $baseDir . '/inc/class-wpseo-shortlinker.php', 'WPSEO_Sitemap_Cache_Data' => $baseDir . '/inc/sitemaps/class-sitemap-cache-data.php', 'WPSEO_Sitemap_Cache_Data_Interface' => $baseDir . '/inc/sitemaps/interface-sitemap-cache-data.php', 'WPSEO_Sitemap_Image_Parser' => $baseDir . '/inc/sitemaps/class-sitemap-image-parser.php', 'WPSEO_Sitemap_Provider' => $baseDir . '/inc/sitemaps/interface-sitemap-provider.php', 'WPSEO_Sitemaps' => $baseDir . '/inc/sitemaps/class-sitemaps.php', 'WPSEO_Sitemaps_Admin' => $baseDir . '/inc/sitemaps/class-sitemaps-admin.php', 'WPSEO_Sitemaps_Cache' => $baseDir . '/inc/sitemaps/class-sitemaps-cache.php', 'WPSEO_Sitemaps_Cache_Validator' => $baseDir . '/inc/sitemaps/class-sitemaps-cache-validator.php', 'WPSEO_Sitemaps_Renderer' => $baseDir . '/inc/sitemaps/class-sitemaps-renderer.php', 'WPSEO_Sitemaps_Router' => $baseDir . '/inc/sitemaps/class-sitemaps-router.php', 'WPSEO_Slug_Change_Watcher' => $baseDir . '/admin/watchers/class-slug-change-watcher.php', 'WPSEO_Statistic_Integration' => $baseDir . '/admin/statistics/class-statistics-integration.php', 'WPSEO_Statistics' => $baseDir . '/inc/class-wpseo-statistics.php', 'WPSEO_Statistics_Service' => $baseDir . '/admin/statistics/class-statistics-service.php', 'WPSEO_Submenu_Capability_Normalize' => $baseDir . '/admin/menu/class-submenu-capability-normalize.php', 'WPSEO_Suggested_Plugins' => $baseDir . '/admin/class-suggested-plugins.php', 'WPSEO_Taxonomy' => $baseDir . '/admin/taxonomy/class-taxonomy.php', 'WPSEO_Taxonomy_Columns' => $baseDir . '/admin/taxonomy/class-taxonomy-columns.php', 'WPSEO_Taxonomy_Fields' => $baseDir . '/admin/taxonomy/class-taxonomy-fields.php', 'WPSEO_Taxonomy_Fields_Presenter' => $baseDir . '/admin/taxonomy/class-taxonomy-fields-presenter.php', 'WPSEO_Taxonomy_Meta' => $baseDir . '/inc/options/class-wpseo-taxonomy-meta.php', 'WPSEO_Taxonomy_Metabox' => $baseDir . '/admin/taxonomy/class-taxonomy-metabox.php', 'WPSEO_Taxonomy_Sitemap_Provider' => $baseDir . '/inc/sitemaps/class-taxonomy-sitemap-provider.php', 'WPSEO_Term_Metabox_Formatter' => $baseDir . '/admin/formatter/class-term-metabox-formatter.php', 'WPSEO_Tracking' => $baseDir . '/admin/tracking/class-tracking.php', 'WPSEO_Tracking_Addon_Data' => $baseDir . '/admin/tracking/class-tracking-addon-data.php', 'WPSEO_Tracking_Default_Data' => $baseDir . '/admin/tracking/class-tracking-default-data.php', 'WPSEO_Tracking_Plugin_Data' => $baseDir . '/admin/tracking/class-tracking-plugin-data.php', 'WPSEO_Tracking_Server_Data' => $baseDir . '/admin/tracking/class-tracking-server-data.php', 'WPSEO_Tracking_Settings_Data' => $baseDir . '/admin/tracking/class-tracking-settings-data.php', 'WPSEO_Tracking_Theme_Data' => $baseDir . '/admin/tracking/class-tracking-theme-data.php', 'WPSEO_Upgrade' => $baseDir . '/inc/class-upgrade.php', 'WPSEO_Upgrade_History' => $baseDir . '/inc/class-upgrade-history.php', 'WPSEO_Utils' => $baseDir . '/inc/class-wpseo-utils.php', 'WPSEO_WordPress_AJAX_Integration' => $baseDir . '/inc/interface-wpseo-wordpress-ajax-integration.php', 'WPSEO_WordPress_Integration' => $baseDir . '/inc/interface-wpseo-wordpress-integration.php', 'WPSEO_Yoast_Columns' => $baseDir . '/admin/class-yoast-columns.php', 'Wincher_Dashboard_Widget' => $baseDir . '/admin/class-wincher-dashboard-widget.php', 'YoastSEO_Vendor\\GuzzleHttp\\BodySummarizer' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizer.php', 'YoastSEO_Vendor\\GuzzleHttp\\BodySummarizerInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/BodySummarizerInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Client' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Client.php', 'YoastSEO_Vendor\\GuzzleHttp\\ClientInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/ClientInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\ClientTrait' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/ClientTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\CookieJar' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\CookieJarInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\FileCookieJar' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\SessionCookieJar' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php', 'YoastSEO_Vendor\\GuzzleHttp\\Cookie\\SetCookie' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Cookie/SetCookie.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\BadResponseException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/BadResponseException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ClientException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ClientException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ConnectException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ConnectException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\GuzzleException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/GuzzleException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\InvalidArgumentException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\RequestException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/RequestException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\ServerException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/ServerException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\TooManyRedirectsException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Exception\\TransferException' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Exception/TransferException.php', 'YoastSEO_Vendor\\GuzzleHttp\\HandlerStack' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/HandlerStack.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlFactory' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlFactoryInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlHandler' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\CurlMultiHandler' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\EasyHandle' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/EasyHandle.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\HeaderProcessor' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\MockHandler' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/MockHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\Proxy' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/Proxy.php', 'YoastSEO_Vendor\\GuzzleHttp\\Handler\\StreamHandler' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php', 'YoastSEO_Vendor\\GuzzleHttp\\MessageFormatter' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatter.php', 'YoastSEO_Vendor\\GuzzleHttp\\MessageFormatterInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/MessageFormatterInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Middleware' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Middleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\Pool' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Pool.php', 'YoastSEO_Vendor\\GuzzleHttp\\PrepareBodyMiddleware' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\AggregateException' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/AggregateException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\CancellationException' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/CancellationException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Coroutine' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Coroutine.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Create' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Create.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Each' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Each.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\EachPromise' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/EachPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\FulfilledPromise' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/FulfilledPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Is' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Is.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Promise' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Promise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\PromiseInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/PromiseInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\PromisorInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/PromisorInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\RejectedPromise' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/RejectedPromise.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\RejectionException' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/RejectionException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\TaskQueue' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/TaskQueue.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\TaskQueueInterface' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/TaskQueueInterface.php', 'YoastSEO_Vendor\\GuzzleHttp\\Promise\\Utils' => $baseDir . '/vendor_prefixed/guzzlehttp/promises/src/Utils.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\AppendStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/AppendStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\BufferStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/BufferStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\CachingStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/CachingStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\DroppingStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/DroppingStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Exception\\MalformedUriException' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Exception/MalformedUriException.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\FnStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/FnStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Header' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Header.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\HttpFactory' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/HttpFactory.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\InflateStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/InflateStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\LazyOpenStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/LazyOpenStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\LimitStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/LimitStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Message' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Message.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MessageTrait' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/MessageTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MimeType' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/MimeType.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\MultipartStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/MultipartStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\NoSeekStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/NoSeekStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\PumpStream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/PumpStream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Query' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Query.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Request' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Request.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Response' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Response.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Rfc7230' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Rfc7230.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\ServerRequest' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/ServerRequest.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Stream' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Stream.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\StreamDecoratorTrait' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/StreamDecoratorTrait.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\StreamWrapper' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/StreamWrapper.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UploadedFile' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/UploadedFile.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Uri' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Uri.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriComparator' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/UriComparator.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriNormalizer' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/UriNormalizer.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\UriResolver' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/UriResolver.php', 'YoastSEO_Vendor\\GuzzleHttp\\Psr7\\Utils' => $baseDir . '/vendor_prefixed/guzzlehttp/psr7/src/Utils.php', 'YoastSEO_Vendor\\GuzzleHttp\\RedirectMiddleware' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/RedirectMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\RequestOptions' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/RequestOptions.php', 'YoastSEO_Vendor\\GuzzleHttp\\RetryMiddleware' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/RetryMiddleware.php', 'YoastSEO_Vendor\\GuzzleHttp\\TransferStats' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/TransferStats.php', 'YoastSEO_Vendor\\GuzzleHttp\\Utils' => $baseDir . '/vendor_prefixed/guzzlehttp/guzzle/src/Utils.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\AbstractGrant' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/AbstractGrant.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\AuthorizationCode' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/AuthorizationCode.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\ClientCredentials' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/ClientCredentials.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\Exception\\InvalidGrantException' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/Exception/InvalidGrantException.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\GrantFactory' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/GrantFactory.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\Password' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/Password.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Grant\\RefreshToken' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Grant/RefreshToken.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\HttpBasicAuthOptionProvider' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/HttpBasicAuthOptionProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\OptionProviderInterface' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/OptionProviderInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\OptionProvider\\PostAuthOptionProvider' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/OptionProvider/PostAuthOptionProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\AbstractProvider' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Provider/AbstractProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\Exception\\IdentityProviderException' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Provider/Exception/IdentityProviderException.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\GenericProvider' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Provider/GenericProvider.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\GenericResourceOwner' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Provider/GenericResourceOwner.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Provider\\ResourceOwnerInterface' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Provider/ResourceOwnerInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\AccessToken' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Token/AccessToken.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\AccessTokenInterface' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Token/AccessTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\ResourceOwnerAccessTokenInterface' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Token/ResourceOwnerAccessTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Token\\SettableRefreshTokenInterface' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Token/SettableRefreshTokenInterface.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\ArrayAccessorTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/ArrayAccessorTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\BearerAuthorizationTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/BearerAuthorizationTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\GuardedPropertyTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/GuardedPropertyTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\MacAuthorizationTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/MacAuthorizationTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\ProviderRedirectTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/ProviderRedirectTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\QueryBuilderTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/QueryBuilderTrait.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\RequestFactory' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/RequestFactory.php', 'YoastSEO_Vendor\\League\\OAuth2\\Client\\Tool\\RequiredParameterTrait' => $baseDir . '/vendor_prefixed/league/oauth2-client/src/Tool/RequiredParameterTrait.php', 'YoastSEO_Vendor\\Psr\\Container\\ContainerExceptionInterface' => $baseDir . '/vendor_prefixed/psr/container/src/ContainerExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Container\\ContainerInterface' => $baseDir . '/vendor_prefixed/psr/container/src/ContainerInterface.php', 'YoastSEO_Vendor\\Psr\\Container\\NotFoundExceptionInterface' => $baseDir . '/vendor_prefixed/psr/container/src/NotFoundExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\ClientExceptionInterface' => $baseDir . '/vendor_prefixed/psr/http-client/src/ClientExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\ClientInterface' => $baseDir . '/vendor_prefixed/psr/http-client/src/ClientInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\NetworkExceptionInterface' => $baseDir . '/vendor_prefixed/psr/http-client/src/NetworkExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Client\\RequestExceptionInterface' => $baseDir . '/vendor_prefixed/psr/http-client/src/RequestExceptionInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\MessageInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/MessageInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\RequestFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/RequestFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\RequestInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/RequestInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ResponseFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/ResponseFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ResponseInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/ResponseInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ServerRequestFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/ServerRequestFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\ServerRequestInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/ServerRequestInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\StreamFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/StreamFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\StreamInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/StreamInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UploadedFileFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/UploadedFileFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UploadedFileInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/UploadedFileInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UriFactoryInterface' => $baseDir . '/vendor_prefixed/psr/http-factory/src/UriFactoryInterface.php', 'YoastSEO_Vendor\\Psr\\Http\\Message\\UriInterface' => $baseDir . '/vendor_prefixed/psr/http-message/src/UriInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\AbstractLogger' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/AbstractLogger.php', 'YoastSEO_Vendor\\Psr\\Log\\InvalidArgumentException' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/InvalidArgumentException.php', 'YoastSEO_Vendor\\Psr\\Log\\LogLevel' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/LogLevel.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerAwareInterface' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/LoggerAwareInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerAwareTrait' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/LoggerAwareTrait.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerInterface' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/LoggerInterface.php', 'YoastSEO_Vendor\\Psr\\Log\\LoggerTrait' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/LoggerTrait.php', 'YoastSEO_Vendor\\Psr\\Log\\NullLogger' => $baseDir . '/vendor_prefixed/psr/log/Psr/Log/NullLogger.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\RewindableGenerator' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Argument/RewindableGenerator.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\ServiceLocator' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocator.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Argument\\ServiceLocatorArgument' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Argument/ServiceLocatorArgument.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Container' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Container.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ContainerInterface' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ContainerInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\EnvNotFoundException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/EnvNotFoundException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ExceptionInterface' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/ExceptionInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/InvalidArgumentException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\LogicException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/LogicException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ParameterCircularReferenceException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/ParameterCircularReferenceException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\RuntimeException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/RuntimeException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ServiceCircularReferenceException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/ServiceCircularReferenceException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\Exception\\ServiceNotFoundException' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/Exception/ServiceNotFoundException.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\EnvPlaceholderParameterBag' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/EnvPlaceholderParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\FrozenParameterBag' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/FrozenParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBag' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBag.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ParameterBag\\ParameterBagInterface' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ParameterBag/ParameterBagInterface.php', 'YoastSEO_Vendor\\Symfony\\Component\\DependencyInjection\\ServiceLocator' => $baseDir . '/vendor_prefixed/symfony/dependency-injection/ServiceLocator.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ResetInterface' => $baseDir . '/vendor_prefixed/symfony/service-contracts/ResetInterface.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ServiceLocatorTrait' => $baseDir . '/vendor_prefixed/symfony/service-contracts/ServiceLocatorTrait.php', 'YoastSEO_Vendor\\Symfony\\Contracts\\Service\\ServiceProviderInterface' => $baseDir . '/vendor_prefixed/symfony/service-contracts/ServiceProviderInterface.php', 'Yoast\\WP\\Lib\\Abstract_Main' => $baseDir . '/lib/abstract-main.php', 'Yoast\\WP\\Lib\\Dependency_Injection\\Container_Registry' => $baseDir . '/lib/dependency-injection/container-registry.php', 'Yoast\\WP\\Lib\\Migrations\\Adapter' => $baseDir . '/lib/migrations/adapter.php', 'Yoast\\WP\\Lib\\Migrations\\Column' => $baseDir . '/lib/migrations/column.php', 'Yoast\\WP\\Lib\\Migrations\\Constants' => $baseDir . '/lib/migrations/constants.php', 'Yoast\\WP\\Lib\\Migrations\\Migration' => $baseDir . '/lib/migrations/migration.php', 'Yoast\\WP\\Lib\\Migrations\\Table' => $baseDir . '/lib/migrations/table.php', 'Yoast\\WP\\Lib\\Model' => $baseDir . '/lib/model.php', 'Yoast\\WP\\Lib\\ORM' => $baseDir . '/lib/orm.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Generator_Interface' => $baseDir . '/src/ai-authorization/application/code-generator-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler' => $baseDir . '/src/ai-authorization/application/code-verifier-handler.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Code_Verifier_Handler_Interface' => $baseDir . '/src/ai-authorization/application/code-verifier-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager' => $baseDir . '/src/ai-authorization/application/token-manager.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Application\\Token_Manager_Interface' => $baseDir . '/src/ai-authorization/application/token-manager-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Code_Verifier' => $baseDir . '/src/ai-authorization/domain/code-verifier.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Domain\\Token' => $baseDir . '/src/ai-authorization/domain/token.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository' => $baseDir . '/src/ai-authorization/infrastructure/access-token-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Access_Token_User_Meta_Repository_Interface' => $baseDir . '/src/ai-authorization/infrastructure/access-token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Generator' => $baseDir . '/src/ai-authorization/infrastructure/code-generator.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository' => $baseDir . '/src/ai-authorization/infrastructure/code-verifier-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Code_Verifier_User_Meta_Repository_Interface' => $baseDir . '/src/ai-authorization/infrastructure/code-verifier-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository' => $baseDir . '/src/ai-authorization/infrastructure/refresh-token-user-meta-repository.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Refresh_Token_User_Meta_Repository_Interface' => $baseDir . '/src/ai-authorization/infrastructure/refresh-token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\Infrastructure\\Token_User_Meta_Repository_Interface' => $baseDir . '/src/ai-authorization/infrastructure/token-user-meta-repository-interface.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Abstract_Callback_Route' => $baseDir . '/src/ai-authorization/user-interface/abstract-callback-route.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Callback_Route' => $baseDir . '/src/ai-authorization/user-interface/callback-route.php', 'Yoast\\WP\\SEO\\AI_Authorization\\User_Interface\\Refresh_Callback_Route' => $baseDir . '/src/ai-authorization/user-interface/refresh-callback-route.php', 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler' => $baseDir . '/src/ai-consent/application/consent-handler.php', 'Yoast\\WP\\SEO\\AI_Consent\\Application\\Consent_Handler_Interface' => $baseDir . '/src/ai-consent/application/consent-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Consent\\Domain\\Endpoint\\Endpoint_Interface' => $baseDir . '/src/ai-consent/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\AI_Consent\\Infrastructure\\Endpoints\\Consent_Endpoint' => $baseDir . '/src/ai-consent/infrastructure/endpoints/consent-endpoint.php', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Ai_Consent_Integration' => $baseDir . '/src/ai-consent/user-interface/ai-consent-integration.php', 'Yoast\\WP\\SEO\\AI_Consent\\User_Interface\\Consent_Route' => $baseDir . '/src/ai-consent/user-interface/consent-route.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler' => $baseDir . '/src/ai-free-sparks/application/free-sparks-handler.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Application\\Free_Sparks_Handler_Interface' => $baseDir . '/src/ai-free-sparks/application/free-sparks-handler-interface.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\Infrastructure\\Endpoints\\Free_Sparks_Endpoint' => $baseDir . '/src/ai-free-sparks/infrastructure/endpoints/free-sparks-endpoint.php', 'Yoast\\WP\\SEO\\AI_Free_Sparks\\User_Interface\\Free_Sparks_Route' => $baseDir . '/src/ai-free-sparks/user-interface/free-sparks-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\Application\\Suggestions_Provider' => $baseDir . '/src/ai-generator/application/suggestions-provider.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Endpoint\\Endpoint_Interface' => $baseDir . '/src/ai-generator/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Endpoint\\Endpoint_List' => $baseDir . '/src/ai-generator/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestion' => $baseDir . '/src/ai-generator/domain/suggestion.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\Suggestions_Bucket' => $baseDir . '/src/ai-generator/domain/suggestions-bucket.php', 'Yoast\\WP\\SEO\\AI_Generator\\Domain\\URLs_Interface' => $baseDir . '/src/ai-generator/domain/urls-interface.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Suggestions_Endpoint' => $baseDir . '/src/ai-generator/infrastructure/endpoints/get-suggestions-endpoint.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\Endpoints\\Get_Usage_Endpoint' => $baseDir . '/src/ai-generator/infrastructure/endpoints/get-usage-endpoint.php', 'Yoast\\WP\\SEO\\AI_Generator\\Infrastructure\\WordPress_URLs' => $baseDir . '/src/ai-generator/infrastructure/wordpress-urls.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Ai_Generator_Integration' => $baseDir . '/src/ai-generator/user-interface/ai-generator-integration.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Bust_Subscription_Cache_Route' => $baseDir . '/src/ai-generator/user-interface/bust-subscription-cache-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Suggestions_Route' => $baseDir . '/src/ai-generator/user-interface/get-suggestions-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Get_Usage_Route' => $baseDir . '/src/ai-generator/user-interface/get-usage-route.php', 'Yoast\\WP\\SEO\\AI_Generator\\User_Interface\\Route_Permission_Trait' => $baseDir . '/src/ai-generator/user-interface/route-permission-trait.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler' => $baseDir . '/src/ai-http-request/application/request-handler.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Request_Handler_Interface' => $baseDir . '/src/ai-http-request/application/request-handler-interface.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser' => $baseDir . '/src/ai-http-request/application/response-parser.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Application\\Response_Parser_Interface' => $baseDir . '/src/ai-http-request/application/response-parser-interface.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Bad_Request_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/bad-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Forbidden_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/forbidden-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Internal_Server_Error_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/internal-server-error-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Not_Found_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/not-found-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Payment_Required_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/payment-required-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Remote_Request_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/remote-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Request_Timeout_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/request-timeout-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Service_Unavailable_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/service-unavailable-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Too_Many_Requests_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/too-many-requests-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\Unauthorized_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/unauthorized-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Exceptions\\WP_Request_Exception' => $baseDir . '/src/ai-http-request/domain/exceptions/wp-request-exception.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Request' => $baseDir . '/src/ai-http-request/domain/request.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Domain\\Response' => $baseDir . '/src/ai-http-request/domain/response.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client' => $baseDir . '/src/ai-http-request/infrastructure/api-client.php', 'Yoast\\WP\\SEO\\AI_HTTP_Request\\Infrastructure\\API_Client_Interface' => $baseDir . '/src/ai-http-request/infrastructure/api-client-interface.php', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Activate_Action' => $baseDir . '/src/actions/addon-installation/addon-activate-action.php', 'Yoast\\WP\\SEO\\Actions\\Addon_Installation\\Addon_Install_Action' => $baseDir . '/src/actions/addon-installation/addon-install-action.php', 'Yoast\\WP\\SEO\\Actions\\Alert_Dismissal_Action' => $baseDir . '/src/actions/alert-dismissal-action.php', 'Yoast\\WP\\SEO\\Actions\\Configuration\\First_Time_Configuration_Action' => $baseDir . '/src/actions/configuration/first-time-configuration-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Abstract_Aioseo_Importing_Action' => $baseDir . '/src/actions/importing/abstract-aioseo-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Abstract_Aioseo_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/abstract-aioseo-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Cleanup_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-cleanup-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Custom_Archive_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-custom-archive-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Default_Archive_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-default-archive-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_General_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-general-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posts_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-posts-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Posttype_Defaults_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Taxonomy_Settings_Importing_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-taxonomy-settings-importing-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Aioseo\\Aioseo_Validate_Data_Action' => $baseDir . '/src/actions/importing/aioseo/aioseo-validate-data-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Deactivate_Conflicting_Plugins_Action' => $baseDir . '/src/actions/importing/deactivate-conflicting-plugins-action.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Importing_Action_Interface' => $baseDir . '/src/actions/importing/importing-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Importing\\Importing_Indexation_Action_Interface' => $baseDir . '/src/actions/importing/importing-indexation-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexables\\Indexable_Head_Action' => $baseDir . '/src/actions/indexables/indexable-head-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Abstract_Indexing_Action' => $baseDir . '/src/actions/indexing/abstract-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Abstract_Link_Indexing_Action' => $baseDir . '/src/actions/indexing/abstract-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_General_Indexation_Action' => $baseDir . '/src/actions/indexing/indexable-general-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Indexing_Complete_Action' => $baseDir . '/src/actions/indexing/indexable-indexing-complete-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Indexation_Action' => $baseDir . '/src/actions/indexing/indexable-post-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Post_Type_Archive_Indexation_Action' => $baseDir . '/src/actions/indexing/indexable-post-type-archive-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexable_Term_Indexation_Action' => $baseDir . '/src/actions/indexing/indexable-term-indexation-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexation_Action_Interface' => $baseDir . '/src/actions/indexing/indexation-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Complete_Action' => $baseDir . '/src/actions/indexing/indexing-complete-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Indexing_Prepare_Action' => $baseDir . '/src/actions/indexing/indexing-prepare-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Limited_Indexing_Action_Interface' => $baseDir . '/src/actions/indexing/limited-indexing-action-interface.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Post_Link_Indexing_Action' => $baseDir . '/src/actions/indexing/post-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Indexing\\Term_Link_Indexing_Action' => $baseDir . '/src/actions/indexing/term-link-indexing-action.php', 'Yoast\\WP\\SEO\\Actions\\Integrations_Action' => $baseDir . '/src/actions/integrations-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Login_Action' => $baseDir . '/src/actions/semrush/semrush-login-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Options_Action' => $baseDir . '/src/actions/semrush/semrush-options-action.php', 'Yoast\\WP\\SEO\\Actions\\SEMrush\\SEMrush_Phrases_Action' => $baseDir . '/src/actions/semrush/semrush-phrases-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Account_Action' => $baseDir . '/src/actions/wincher/wincher-account-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Keyphrases_Action' => $baseDir . '/src/actions/wincher/wincher-keyphrases-action.php', 'Yoast\\WP\\SEO\\Actions\\Wincher\\Wincher_Login_Action' => $baseDir . '/src/actions/wincher/wincher-login-action.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Default_SEO_Data\\Default_SEO_Data_Alert' => $baseDir . '/src/alerts/application/default-seo-data/default-seo-data-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Indexables_Disabled\\Indexables_Disabled_Alert' => $baseDir . '/src/alerts/application/indexables-disabled/indexables-disabled-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Application\\Ping_Other_Admins\\Ping_Other_Admins_Alert' => $baseDir . '/src/alerts/application/ping-other-admins/ping-other-admins-alert.php', 'Yoast\\WP\\SEO\\Alerts\\Infrastructure\\Default_SEO_Data\\Default_SEO_Data_Collector' => $baseDir . '/src/alerts/infrastructure/default-seo-data/default-seo-data-collector.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Cron_Callback_Integration' => $baseDir . '/src/alerts/user-interface/default-seo-data/default-seo-data-cron-callback-integration.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_SEO_Data\\Default_SEO_Data_Watcher' => $baseDir . '/src/alerts/user-interface/default-seo-data/default-seo-data-watcher.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Default_Seo_Data\\Default_SEO_Data_Cron_Scheduler' => $baseDir . '/src/alerts/user-interface/default-seo-data/default-seo-data-cron-scheduler.php', 'Yoast\\WP\\SEO\\Alerts\\User_Interface\\Resolve_Alert_Route' => $baseDir . '/src/alerts/user-interface/resolve-alert-route.php', 'Yoast\\WP\\SEO\\Analytics\\Application\\Missing_Indexables_Collector' => $baseDir . '/src/analytics/application/missing-indexables-collector.php', 'Yoast\\WP\\SEO\\Analytics\\Application\\To_Be_Cleaned_Indexables_Collector' => $baseDir . '/src/analytics/application/to-be-cleaned-indexables-collector.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Bucket' => $baseDir . '/src/analytics/domain/missing-indexable-bucket.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\Missing_Indexable_Count' => $baseDir . '/src/analytics/domain/missing-indexable-count.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Bucket' => $baseDir . '/src/analytics/domain/to-be-cleaned-indexable-bucket.php', 'Yoast\\WP\\SEO\\Analytics\\Domain\\To_Be_Cleaned_Indexable_Count' => $baseDir . '/src/analytics/domain/to-be-cleaned-indexable-count.php', 'Yoast\\WP\\SEO\\Analytics\\User_Interface\\Last_Completed_Indexation_Integration' => $baseDir . '/src/analytics/user-interface/last-completed-indexation-integration.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Author_Builder' => $baseDir . '/src/builders/indexable-author-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' => $baseDir . '/src/builders/indexable-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Date_Archive_Builder' => $baseDir . '/src/builders/indexable-date-archive-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Hierarchy_Builder' => $baseDir . '/src/builders/indexable-hierarchy-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Home_Page_Builder' => $baseDir . '/src/builders/indexable-home-page-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Link_Builder' => $baseDir . '/src/builders/indexable-link-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Builder' => $baseDir . '/src/builders/indexable-post-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Post_Type_Archive_Builder' => $baseDir . '/src/builders/indexable-post-type-archive-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Social_Image_Trait' => $baseDir . '/src/builders/indexable-social-image-trait.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_System_Page_Builder' => $baseDir . '/src/builders/indexable-system-page-builder.php', 'Yoast\\WP\\SEO\\Builders\\Indexable_Term_Builder' => $baseDir . '/src/builders/indexable-term-builder.php', 'Yoast\\WP\\SEO\\Builders\\Primary_Term_Builder' => $baseDir . '/src/builders/primary-term-builder.php', 'Yoast\\WP\\SEO\\Commands\\Cleanup_Command' => $baseDir . '/src/commands/cleanup-command.php', 'Yoast\\WP\\SEO\\Commands\\Command_Interface' => $baseDir . '/src/commands/command-interface.php', 'Yoast\\WP\\SEO\\Commands\\Index_Command' => $baseDir . '/src/commands/index-command.php', 'Yoast\\WP\\SEO\\Conditionals\\AI_Conditional' => $baseDir . '/src/conditionals/ai-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\AI_Editor_Conditional' => $baseDir . '/src/conditionals/ai-editor-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Addon_Installation_Conditional' => $baseDir . '/src/conditionals/addon-installation-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Doing_Post_Quick_Edit_Save_Conditional' => $baseDir . '/src/conditionals/admin/doing-post-quick-edit-save-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Estimated_Reading_Time_Conditional' => $baseDir . '/src/conditionals/admin/estimated-reading-time-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Licenses_Page_Conditional' => $baseDir . '/src/conditionals/admin/licenses-page-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Non_Network_Admin_Conditional' => $baseDir . '/src/conditionals/admin/non-network-admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Post_Conditional' => $baseDir . '/src/conditionals/admin/post-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Posts_Overview_Or_Ajax_Conditional' => $baseDir . '/src/conditionals/admin/posts-overview-or-ajax-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin\\Yoast_Admin_Conditional' => $baseDir . '/src/conditionals/admin/yoast-admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Admin_Conditional' => $baseDir . '/src/conditionals/admin-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Attachment_Redirections_Enabled_Conditional' => $baseDir . '/src/conditionals/attachment-redirections-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Check_Required_Version_Conditional' => $baseDir . '/src/conditionals/check-required-version-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Conditional' => $baseDir . '/src/conditionals/conditional-interface.php', 'Yoast\\WP\\SEO\\Conditionals\\Deactivating_Yoast_Seo_Conditional' => $baseDir . '/src/conditionals/deactivating-yoast-seo-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Development_Conditional' => $baseDir . '/src/conditionals/development-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Dynamic_Product_Permalinks_Conditional' => $baseDir . '/src/conditionals/dynamic-product-permalinks-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Feature_Flag_Conditional' => $baseDir . '/src/conditionals/feature-flag-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Front_End_Conditional' => $baseDir . '/src/conditionals/front-end-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Get_Request_Conditional' => $baseDir . '/src/conditionals/get-request-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Google_Site_Kit_Feature_Conditional' => $baseDir . '/src/deprecated/src/conditionals/google-site-kit-feature-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Headless_Rest_Endpoints_Enabled_Conditional' => $baseDir . '/src/conditionals/headless-rest-endpoints-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Import_Tool_Selected_Conditional' => $baseDir . '/src/conditionals/import-tool-selected-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Jetpack_Conditional' => $baseDir . '/src/conditionals/jetpack-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Migrations_Conditional' => $baseDir . '/src/conditionals/migrations-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\New_Settings_Ui_Conditional' => $baseDir . '/src/conditionals/new-settings-ui-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\News_Conditional' => $baseDir . '/src/conditionals/news-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\No_Conditionals' => $baseDir . '/src/conditionals/no-conditionals-trait.php', 'Yoast\\WP\\SEO\\Conditionals\\No_Tool_Selected_Conditional' => $baseDir . '/src/conditionals/no-tool-selected-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Non_Multisite_Conditional' => $baseDir . '/src/conditionals/non-multisite-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Not_Admin_Ajax_Conditional' => $baseDir . '/src/conditionals/not-admin-ajax-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Open_Graph_Conditional' => $baseDir . '/src/conditionals/open-graph-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Active_Conditional' => $baseDir . '/src/conditionals/premium-active-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Premium_Inactive_Conditional' => $baseDir . '/src/conditionals/premium-inactive-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Primary_Category_Conditional' => $baseDir . '/src/conditionals/primary-category-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Robots_Txt_Conditional' => $baseDir . '/src/conditionals/robots-txt-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\SEMrush_Enabled_Conditional' => $baseDir . '/src/conditionals/semrush-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Schema_Disabled_Conditional' => $baseDir . '/src/conditionals/schema-disabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Settings_Conditional' => $baseDir . '/src/conditionals/settings-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Should_Index_Links_Conditional' => $baseDir . '/src/conditionals/should-index-links-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Task_List_Enabled_Conditional' => $baseDir . '/src/conditionals/task-list-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Text_Formality_Conditional' => $baseDir . '/src/conditionals/text-formality-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\EDD_Conditional' => $baseDir . '/src/conditionals/third-party/edd-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Activated_Conditional' => $baseDir . '/src/conditionals/third-party/elementor-activated-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Elementor_Edit_Conditional' => $baseDir . '/src/conditionals/third-party/elementor-edit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Polylang_Conditional' => $baseDir . '/src/conditionals/third-party/polylang-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\Site_Kit_Conditional' => $baseDir . '/src/conditionals/third-party/site-kit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\TranslatePress_Conditional' => $baseDir . '/src/conditionals/third-party/translatepress-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\W3_Total_Cache_Conditional' => $baseDir . '/src/conditionals/third-party/w3-total-cache-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_Conditional' => $baseDir . '/src/conditionals/third-party/wpml-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Third_Party\\WPML_WPSEO_Conditional' => $baseDir . '/src/conditionals/third-party/wpml-wpseo-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Traits\\Admin_Conditional_Trait' => $baseDir . '/src/conditionals/traits/admin-conditional-trait.php', 'Yoast\\WP\\SEO\\Conditionals\\Updated_Importer_Framework_Conditional' => $baseDir . '/src/conditionals/updated-importer-framework-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Edit_Users_Conditional' => $baseDir . '/src/conditionals/user-can-edit-users-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Manage_Wpseo_Options_Conditional' => $baseDir . '/src/conditionals/user-can-manage-wpseo-options-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Can_Publish_Posts_And_Pages_Conditional' => $baseDir . '/src/conditionals/user-can-publish-posts-and-pages-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Edit_Conditional' => $baseDir . '/src/conditionals/user-edit-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\User_Profile_Conditional' => $baseDir . '/src/conditionals/user-profile-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_CRON_Enabled_Conditional' => $baseDir . '/src/conditionals/wp-cron-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_Robots_Conditional' => $baseDir . '/src/conditionals/wp-robots-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WP_Tests_Conditional' => $baseDir . '/src/conditionals/wp-tests-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Web_Stories_Conditional' => $baseDir . '/src/conditionals/web-stories-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Automatically_Track_Conditional' => $baseDir . '/src/conditionals/wincher-automatically-track-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Conditional' => $baseDir . '/src/conditionals/wincher-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Enabled_Conditional' => $baseDir . '/src/conditionals/wincher-enabled-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Wincher_Token_Conditional' => $baseDir . '/src/conditionals/wincher-token-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Conditional' => $baseDir . '/src/conditionals/woocommerce-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\WooCommerce_Version_Conditional' => $baseDir . '/src/conditionals/woocommerce-version-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Woo_SEO_Inactive_Conditional' => $baseDir . '/src/conditionals/woo-seo-inactive-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\XMLRPC_Conditional' => $baseDir . '/src/conditionals/xmlrpc-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Admin_And_Dashboard_Conditional' => $baseDir . '/src/conditionals/yoast-admin-and-dashboard-conditional.php', 'Yoast\\WP\\SEO\\Conditionals\\Yoast_Tools_Page_Conditional' => $baseDir . '/src/conditionals/yoast-tools-page-conditional.php', 'Yoast\\WP\\SEO\\Config\\Badge_Group_Names' => $baseDir . '/src/config/badge-group-names.php', 'Yoast\\WP\\SEO\\Config\\Conflicting_Plugins' => $baseDir . '/src/config/conflicting-plugins.php', 'Yoast\\WP\\SEO\\Config\\Indexing_Reasons' => $baseDir . '/src/config/indexing-reasons.php', 'Yoast\\WP\\SEO\\Config\\Migration_Status' => $baseDir . '/src/config/migration-status.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddCollationToTables' => $baseDir . '/src/config/migrations/20200408101900_AddCollationToTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddColumnsToIndexables' => $baseDir . '/src/config/migrations/20200420073606_AddColumnsToIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddEstimatedReadingTime' => $baseDir . '/src/config/migrations/20201202144329_AddEstimatedReadingTime.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddHasAncestorsColumn' => $baseDir . '/src/config/migrations/20200609154515_AddHasAncestorsColumn.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddInclusiveLanguageScore' => $baseDir . '/src/config/migrations/20230417083836_AddInclusiveLanguageScore.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexableObjectIdAndTypeIndex' => $baseDir . '/src/config/migrations/20200430075614_AddIndexableObjectIdAndTypeIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddIndexesForProminentWordsOnIndexables' => $baseDir . '/src/config/migrations/20200728095334_AddIndexesForProminentWordsOnIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddObjectTimestamps' => $baseDir . '/src/config/migrations/20211020091404_AddObjectTimestamps.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddSeoLinksIndex' => $baseDir . '/src/config/migrations/20260105111111_AddSeoLinksIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\AddVersionColumnToIndexables' => $baseDir . '/src/config/migrations/20210817092415_AddVersionColumnToIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\BreadcrumbTitleAndHierarchyReset' => $baseDir . '/src/config/migrations/20200428123747_BreadcrumbTitleAndHierarchyReset.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ClearIndexableTables' => $baseDir . '/src/config/migrations/20200430150130_ClearIndexableTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateIndexableSubpagesIndex' => $baseDir . '/src/config/migrations/20200702141921_CreateIndexableSubpagesIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\CreateSEOLinksTable' => $baseDir . '/src/config/migrations/20200617122511_CreateSEOLinksTable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\DeleteDuplicateIndexables' => $baseDir . '/src/config/migrations/20200507054848_DeleteDuplicateIndexables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableColumnLengths' => $baseDir . '/src/config/migrations/20200428194858_ExpandIndexableColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandIndexableIDColumnLengths' => $baseDir . '/src/config/migrations/20201216124002_ExpandIndexableIDColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ExpandPrimaryTermIDColumnLengths' => $baseDir . '/src/config/migrations/20201216141134_ExpandPrimaryTermIDColumnLengths.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ReplacePermalinkHashIndex' => $baseDir . '/src/config/migrations/20200616130143_ReplacePermalinkHashIndex.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\ResetIndexableHierarchyTable' => $baseDir . '/src/config/migrations/20200513133401_ResetIndexableHierarchyTable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\TruncateIndexableTables' => $baseDir . '/src/config/migrations/20200429105310_TruncateIndexableTables.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastDropIndexableMetaTableIfExists' => $baseDir . '/src/config/migrations/20190529075038_WpYoastDropIndexableMetaTableIfExists.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexable' => $baseDir . '/src/config/migrations/20171228151840_WpYoastIndexable.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastIndexableHierarchy' => $baseDir . '/src/config/migrations/20191011111109_WpYoastIndexableHierarchy.php', 'Yoast\\WP\\SEO\\Config\\Migrations\\WpYoastPrimaryTerm' => $baseDir . '/src/config/migrations/20171228151841_WpYoastPrimaryTerm.php', 'Yoast\\WP\\SEO\\Config\\OAuth_Client' => $baseDir . '/src/config/oauth-client.php', 'Yoast\\WP\\SEO\\Config\\Researcher_Languages' => $baseDir . '/src/config/researcher-languages.php', 'Yoast\\WP\\SEO\\Config\\SEMrush_Client' => $baseDir . '/src/config/semrush-client.php', 'Yoast\\WP\\SEO\\Config\\Schema_IDs' => $baseDir . '/src/config/schema-ids.php', 'Yoast\\WP\\SEO\\Config\\Schema_Types' => $baseDir . '/src/config/schema-types.php', 'Yoast\\WP\\SEO\\Config\\Wincher_Client' => $baseDir . '/src/config/wincher-client.php', 'Yoast\\WP\\SEO\\Config\\Wincher_PKCE_Provider' => $baseDir . '/src/config/wincher-pkce-provider.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Dismiss_Notifications' => $baseDir . '/src/content-type-visibility/application/content-type-visibility-dismiss-notifications.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\Application\\Content_Type_Visibility_Watcher_Actions' => $baseDir . '/src/content-type-visibility/application/content-type-visibility-watcher-actions.php', 'Yoast\\WP\\SEO\\Content_Type_Visibility\\User_Interface\\Content_Type_Visibility_Dismiss_New_Route' => $baseDir . '/src/content-type-visibility/user-interface/content-type-visibility-dismiss-new-route.php', 'Yoast\\WP\\SEO\\Context\\Meta_Tags_Context' => $baseDir . '/src/context/meta-tags-context.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Configuration\\Dashboard_Configuration' => $baseDir . '/src/dashboard/application/configuration/dashboard-configuration.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Content_Types\\Content_Types_Repository' => $baseDir . '/src/dashboard/application/content-types/content-types-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Endpoints\\Endpoints_Repository' => $baseDir . '/src/dashboard/application/endpoints/endpoints-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Filter_Pairs\\Filter_Pairs_Repository' => $baseDir . '/src/dashboard/application/filter-pairs/filter-pairs-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Groups\\SEO_Score_Groups\\SEO_Score_Groups_Repository' => $baseDir . '/src/dashboard/application/score-groups/seo-score-groups/seo-score-groups-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Abstract_Score_Results_Repository' => $baseDir . '/src/dashboard/application/score-results/abstract-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Current_Scores_Repository' => $baseDir . '/src/dashboard/application/score-results/current-scores-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Repository' => $baseDir . '/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Repository' => $baseDir . '/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Search_Ranking_Compare_Repository' => $baseDir . '/src/dashboard/application/search-rankings/search-ranking-compare-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Page_Repository' => $baseDir . '/src/dashboard/application/search-rankings/top-page-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Search_Rankings\\Top_Query_Repository' => $baseDir . '/src/dashboard/application/search-rankings/top-query-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Taxonomies\\Taxonomies_Repository' => $baseDir . '/src/dashboard/application/taxonomies/taxonomies-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Tracking\\Setup_Steps_Tracking' => $baseDir . '/src/dashboard/application/tracking/setup-steps-tracking.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Compare_Repository' => $baseDir . '/src/dashboard/application/traffic/organic-sessions-compare-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Application\\Traffic\\Organic_Sessions_Daily_Repository' => $baseDir . '/src/dashboard/application/traffic/organic-sessions-daily-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Failed_Request_Exception' => $baseDir . '/src/dashboard/domain/analytics-4/failed-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Invalid_Request_Exception' => $baseDir . '/src/dashboard/domain/analytics-4/invalid-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Analytics_4\\Unexpected_Response_Exception' => $baseDir . '/src/dashboard/domain/analytics-4/unexpected-response-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Type' => $baseDir . '/src/dashboard/domain/content-types/content-type.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Content_Types\\Content_Types_List' => $baseDir . '/src/dashboard/domain/content-types/content-types-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Dashboard_Repository_Interface' => $baseDir . '/src/dashboard/domain/data-provider/dashboard-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Data_Container' => $baseDir . '/src/dashboard/domain/data-provider/data-container.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Data_Interface' => $baseDir . '/src/dashboard/domain/data-provider/data-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Data_Provider\\Parameters' => $baseDir . '/src/dashboard/domain/data-provider/parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Endpoint\\Endpoint_Interface' => $baseDir . '/src/dashboard/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Endpoint\\Endpoint_List' => $baseDir . '/src/dashboard/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Filter_Pairs_Interface' => $baseDir . '/src/dashboard/domain/filter-pairs/filter-pairs-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Filter_Pairs\\Product_Category_Filter_Pair' => $baseDir . '/src/dashboard/domain/filter-pairs/product-category-filter-pair.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Abstract_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/abstract-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Abstract_Readability_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Bad_Readability_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Good_Readability_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\No_Readability_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Ok_Readability_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Readability_Score_Groups\\Readability_Score_Groups_Interface' => $baseDir . '/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Abstract_SEO_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Bad_SEO_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Good_SEO_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\No_SEO_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\Ok_SEO_Score_Group' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\SEO_Score_Groups\\SEO_Score_Groups_Interface' => $baseDir . '/src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Groups\\Score_Groups_Interface' => $baseDir . '/src/dashboard/domain/score-groups/score-groups-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Score' => $baseDir . '/src/dashboard/domain/score-results/current-score.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Current_Scores_List' => $baseDir . '/src/dashboard/domain/score-results/current-scores-list.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Result' => $baseDir . '/src/dashboard/domain/score-results/score-result.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Score_Results\\Score_Results_Not_Found_Exception' => $baseDir . '/src/dashboard/domain/score-results/score-results-not-found-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Failed_Request_Exception' => $baseDir . '/src/dashboard/domain/search-console/failed-request-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Console\\Unexpected_Response_Exception' => $baseDir . '/src/dashboard/domain/search-console/unexpected-response-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Comparison_Search_Ranking_Data' => $baseDir . '/src/dashboard/domain/search-rankings/comparison-search-ranking-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Search_Ranking_Data' => $baseDir . '/src/dashboard/domain/search-rankings/search-ranking-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Search_Rankings\\Top_Page_Data' => $baseDir . '/src/dashboard/domain/search-rankings/top-page-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Taxonomies\\Taxonomy' => $baseDir . '/src/dashboard/domain/taxonomies/taxonomy.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_SEO_Metrics\\Repository_Not_Found_Exception' => $baseDir . '/src/dashboard/domain/time-based-seo-metrics/repository-not-found-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Time_Based_Seo_Metrics\\Data_Source_Not_Available_Exception' => $baseDir . '/src/dashboard/domain/time-based-seo-metrics/data-source-not-available-exception.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Comparison_Traffic_Data' => $baseDir . '/src/dashboard/domain/traffic/comparison-traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Daily_Traffic_Data' => $baseDir . '/src/dashboard/domain/traffic/daily-traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Domain\\Traffic\\Traffic_Data' => $baseDir . '/src/dashboard/domain/traffic/traffic-data.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Analytics_4_Parameters' => $baseDir . '/src/dashboard/infrastructure/analytics-4/analytics-4-parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Adapter' => $baseDir . '/src/dashboard/infrastructure/analytics-4/site-kit-analytics-4-adapter.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Analytics_4\\Site_Kit_Analytics_4_Api_Call' => $baseDir . '/src/dashboard/infrastructure/analytics-4/site-kit-analytics-4-api-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Browser_Cache\\Browser_Cache_Configuration' => $baseDir . '/src/dashboard/infrastructure/browser-cache/browser-cache-configuration.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository' => $baseDir . '/src/dashboard/infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface' => $baseDir . '/src/dashboard/infrastructure/configuration/permanently-dismissed-site-kit-configuration-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository' => $baseDir . '/src/dashboard/infrastructure/configuration/site-kit-consent-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Configuration\\Site_Kit_Consent_Repository_Interface' => $baseDir . '/src/dashboard/infrastructure/configuration/site-kit-consent-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Connection\\Site_Kit_Is_Connected_Call' => $baseDir . '/src/dashboard/infrastructure/connection/site-kit-is-connected-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Content_Types\\Content_Types_Collector' => $baseDir . '/src/dashboard/infrastructure/content-types/content-types-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Readability_Scores_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\SEO_Scores_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Setup_Steps_Tracking_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/setup-steps-tracking-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Configuration_Dismissal_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/site-kit-configuration-dismissal-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Site_Kit_Consent_Management_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/site-kit-consent-management-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Endpoints\\Time_Based_SEO_Metrics_Endpoint' => $baseDir . '/src/dashboard/infrastructure/endpoints/time-based-seo-metrics-endpoint.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Indexables\\Top_Page_Indexable_Collector' => $baseDir . '/src/dashboard/infrastructure/indexables/top-page-indexable-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Integrations\\Site_Kit' => $baseDir . '/src/dashboard/infrastructure/integrations/site-kit.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Nonces\\Nonce_Repository' => $baseDir . '/src/dashboard/infrastructure/nonces/nonce-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Groups\\Score_Group_Link_Collector' => $baseDir . '/src/dashboard/infrastructure/score-groups/score-group-link-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Cached_Readability_Score_Results_Collector' => $baseDir . '/src/dashboard/infrastructure/score-results/readability-score-results/cached-readability-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Readability_Score_Results\\Readability_Score_Results_Collector' => $baseDir . '/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\Cached_SEO_Score_Results_Collector' => $baseDir . '/src/dashboard/infrastructure/score-results/seo-score-results/cached-seo-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\SEO_Score_Results\\SEO_Score_Results_Collector' => $baseDir . '/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Score_Results\\Score_Results_Collector_Interface' => $baseDir . '/src/dashboard/infrastructure/score-results/score-results-collector-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Search_Console_Parameters' => $baseDir . '/src/dashboard/infrastructure/search-console/search-console-parameters.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Adapter' => $baseDir . '/src/dashboard/infrastructure/search-console/site-kit-search-console-adapter.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Search_Console\\Site_Kit_Search_Console_Api_Call' => $baseDir . '/src/dashboard/infrastructure/search-console/site-kit-search-console-api-call.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomies_Collector' => $baseDir . '/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Taxonomies\\Taxonomy_Validator' => $baseDir . '/src/dashboard/infrastructure/taxonomies/taxonomy-validator.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository' => $baseDir . '/src/dashboard/infrastructure/tracking/setup-steps-tracking-repository.php', 'Yoast\\WP\\SEO\\Dashboard\\Infrastructure\\Tracking\\Setup_Steps_Tracking_Repository_Interface' => $baseDir . '/src/dashboard/infrastructure/tracking/setup-steps-tracking-repository-interface.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Capabilities_Integration' => $baseDir . '/src/dashboard/user-interface/configuration/site-kit-capabilities-integration.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Configuration_Dismissal_Route' => $baseDir . '/src/dashboard/user-interface/configuration/site-kit-configuration-dismissal-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Configuration\\Site_Kit_Consent_Management_Route' => $baseDir . '/src/dashboard/user-interface/configuration/site-kit-consent-management-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Abstract_Scores_Route' => $baseDir . '/src/dashboard/user-interface/scores/abstract-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\Readability_Scores_Route' => $baseDir . '/src/dashboard/user-interface/scores/readability-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Scores\\SEO_Scores_Route' => $baseDir . '/src/dashboard/user-interface/scores/seo-scores-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Flow_Interceptor' => $baseDir . '/src/dashboard/user-interface/setup/setup-flow-interceptor.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Setup\\Setup_Url_Interceptor' => $baseDir . '/src/dashboard/user-interface/setup/setup-url-interceptor.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Time_Based_SEO_Metrics\\Time_Based_SEO_Metrics_Route' => $baseDir . '/src/dashboard/user-interface/time-based-seo-metrics/time-based-seo-metrics-route.php', 'Yoast\\WP\\SEO\\Dashboard\\User_Interface\\Tracking\\Setup_Steps_Tracking_Route' => $baseDir . '/src/dashboard/user-interface/tracking/setup-steps-tracking-route.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Analysis_Features\\Enabled_Analysis_Features_Repository' => $baseDir . '/src/editors/application/analysis-features/enabled-analysis-features-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Integrations\\Integration_Information_Repository' => $baseDir . '/src/editors/application/integrations/integration-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Post_Seo_Information_Repository' => $baseDir . '/src/editors/application/seo/post-seo-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Seo\\Term_Seo_Information_Repository' => $baseDir . '/src/editors/application/seo/term-seo-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Application\\Site\\Website_Information_Repository' => $baseDir . '/src/editors/application/site/website-information-repository.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Feature' => $baseDir . '/src/editors/domain/analysis-features/analysis-feature.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Feature_Interface' => $baseDir . '/src/editors/domain/analysis-features/analysis-feature-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Analysis_Features\\Analysis_Features_List' => $baseDir . '/src/editors/domain/analysis-features/analysis-features-list.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Integrations\\Integration_Data_Provider_Interface' => $baseDir . '/src/editors/domain/integrations/integration-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Description' => $baseDir . '/src/editors/domain/seo/description.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Keyphrase' => $baseDir . '/src/editors/domain/seo/keyphrase.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Seo_Plugin_Data_Interface' => $baseDir . '/src/editors/domain/seo/seo-plugin-data-interface.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Social' => $baseDir . '/src/editors/domain/seo/social.php', 'Yoast\\WP\\SEO\\Editors\\Domain\\Seo\\Title' => $baseDir . '/src/editors/domain/seo/title.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Cornerstone_Content' => $baseDir . '/src/editors/framework/cornerstone-content.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Inclusive_Language_Analysis' => $baseDir . '/src/editors/framework/inclusive-language-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Jetpack_Markdown' => $baseDir . '/src/editors/framework/integrations/jetpack-markdown.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Multilingual' => $baseDir . '/src/editors/framework/integrations/multilingual.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\News_SEO' => $baseDir . '/src/editors/framework/integrations/news-seo.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Semrush' => $baseDir . '/src/editors/framework/integrations/semrush.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\Wincher' => $baseDir . '/src/editors/framework/integrations/wincher.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce' => $baseDir . '/src/editors/framework/integrations/woocommerce.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Integrations\\WooCommerce_SEO' => $baseDir . '/src/editors/framework/integrations/woocommerce-seo.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Keyphrase_Analysis' => $baseDir . '/src/editors/framework/keyphrase-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Previously_Used_Keyphrase' => $baseDir . '/src/editors/framework/previously-used-keyphrase.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Readability_Analysis' => $baseDir . '/src/editors/framework/readability-analysis.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Description_Data_Provider_Interface' => $baseDir . '/src/editors/framework/seo/description-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Keyphrase_Interface' => $baseDir . '/src/editors/framework/seo/keyphrase-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Abstract_Post_Seo_Data_Provider' => $baseDir . '/src/editors/framework/seo/posts/abstract-post-seo-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Description_Data_Provider' => $baseDir . '/src/editors/framework/seo/posts/description-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Keyphrase_Data_Provider' => $baseDir . '/src/editors/framework/seo/posts/keyphrase-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Social_Data_Provider' => $baseDir . '/src/editors/framework/seo/posts/social-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Posts\\Title_Data_Provider' => $baseDir . '/src/editors/framework/seo/posts/title-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Social_Data_Provider_Interface' => $baseDir . '/src/editors/framework/seo/social-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Abstract_Term_Seo_Data_Provider' => $baseDir . '/src/editors/framework/seo/terms/abstract-term-seo-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Description_Data_Provider' => $baseDir . '/src/editors/framework/seo/terms/description-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Keyphrase_Data_Provider' => $baseDir . '/src/editors/framework/seo/terms/keyphrase-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Social_Data_Provider' => $baseDir . '/src/editors/framework/seo/terms/social-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Terms\\Title_Data_Provider' => $baseDir . '/src/editors/framework/seo/terms/title-data-provider.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Seo\\Title_Data_Provider_Interface' => $baseDir . '/src/editors/framework/seo/title-data-provider-interface.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Base_Site_Information' => $baseDir . '/src/editors/framework/site/base-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Post_Site_Information' => $baseDir . '/src/editors/framework/site/post-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Site\\Term_Site_Information' => $baseDir . '/src/editors/framework/site/term-site-information.php', 'Yoast\\WP\\SEO\\Editors\\Framework\\Word_Form_Recognition' => $baseDir . '/src/editors/framework/word-form-recognition.php', 'Yoast\\WP\\SEO\\Elementor\\Infrastructure\\Request_Post' => $baseDir . '/src/elementor/infrastructure/request-post.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Activation_Error_Exception' => $baseDir . '/src/exceptions/addon-installation/addon-activation-error-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Already_Installed_Exception' => $baseDir . '/src/exceptions/addon-installation/addon-already-installed-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\Addon_Installation_Error_Exception' => $baseDir . '/src/exceptions/addon-installation/addon-installation-error-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\User_Cannot_Activate_Plugins_Exception' => $baseDir . '/src/exceptions/addon-installation/user-cannot-activate-plugins-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Addon_Installation\\User_Cannot_Install_Plugins_Exception' => $baseDir . '/src/exceptions/addon-installation/user-cannot-install-plugins-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Forbidden_Property_Mutation_Exception' => $baseDir . '/src/exceptions/forbidden-property-mutation-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Importing\\Aioseo_Validation_Exception' => $baseDir . '/src/exceptions/importing/aioseo-validation-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Author_Not_Built_Exception' => $baseDir . '/src/exceptions/indexable/author-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Indexable_Exception' => $baseDir . '/src/exceptions/indexable/indexable-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Invalid_Term_Exception' => $baseDir . '/src/exceptions/indexable/invalid-term-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Not_Built_Exception' => $baseDir . '/src/exceptions/indexable/not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Not_Built_Exception' => $baseDir . '/src/exceptions/indexable/post-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Not_Found_Exception' => $baseDir . '/src/exceptions/indexable/post-not-found-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Post_Type_Not_Built_Exception' => $baseDir . '/src/exceptions/indexable/post-type-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Source_Exception' => $baseDir . '/src/exceptions/indexable/source-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Term_Not_Built_Exception' => $baseDir . '/src/exceptions/indexable/term-not-built-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Indexable\\Term_Not_Found_Exception' => $baseDir . '/src/exceptions/indexable/term-not-found-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\Missing_Method' => $baseDir . '/src/exceptions/missing-method.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Authentication_Failed_Exception' => $baseDir . '/src/exceptions/oauth/authentication-failed-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Empty_Property_Exception' => $baseDir . '/src/exceptions/oauth/tokens/empty-property-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Empty_Token_Exception' => $baseDir . '/src/exceptions/oauth/tokens/empty-token-exception.php', 'Yoast\\WP\\SEO\\Exceptions\\OAuth\\Tokens\\Failed_Storage_Exception' => $baseDir . '/src/exceptions/oauth/tokens/failed-storage-exception.php', 'Yoast\\WP\\SEO\\General\\User_Interface\\General_Page_Integration' => $baseDir . '/src/general/user-interface/general-page-integration.php', 'Yoast\\WP\\SEO\\General\\User_Interface\\Opt_In_Route' => $baseDir . '/src/general/user-interface/opt-in-route.php', 'Yoast\\WP\\SEO\\Generated\\Cached_Container' => $baseDir . '/src/generated/container.php', 'Yoast\\WP\\SEO\\Generators\\Breadcrumbs_Generator' => $baseDir . '/src/generators/breadcrumbs-generator.php', 'Yoast\\WP\\SEO\\Generators\\Generator_Interface' => $baseDir . '/src/generators/generator-interface.php', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Image_Generator' => $baseDir . '/src/generators/open-graph-image-generator.php', 'Yoast\\WP\\SEO\\Generators\\Open_Graph_Locale_Generator' => $baseDir . '/src/generators/open-graph-locale-generator.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Abstract_Schema_Piece' => $baseDir . '/src/generators/schema/abstract-schema-piece.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Article' => $baseDir . '/src/generators/schema/article.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Author' => $baseDir . '/src/generators/schema/author.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Breadcrumb' => $baseDir . '/src/generators/schema/breadcrumb.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\FAQ' => $baseDir . '/src/generators/schema/faq.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\HowTo' => $baseDir . '/src/generators/schema/howto.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Main_Image' => $baseDir . '/src/generators/schema/main-image.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Organization' => $baseDir . '/src/generators/schema/organization.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Person' => $baseDir . '/src/generators/schema/person.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\WebPage' => $baseDir . '/src/generators/schema/webpage.php', 'Yoast\\WP\\SEO\\Generators\\Schema\\Website' => $baseDir . '/src/generators/schema/website.php', 'Yoast\\WP\\SEO\\Generators\\Schema_Generator' => $baseDir . '/src/generators/schema-generator.php', 'Yoast\\WP\\SEO\\Generators\\Twitter_Image_Generator' => $baseDir . '/src/generators/twitter-image-generator.php', 'Yoast\\WP\\SEO\\Helpers\\Aioseo_Helper' => $baseDir . '/src/helpers/aioseo-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Asset_Helper' => $baseDir . '/src/helpers/asset-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Attachment_Cleanup_Helper' => $baseDir . '/src/helpers/attachment-cleanup-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Author_Archive_Helper' => $baseDir . '/src/helpers/author-archive-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Blocks_Helper' => $baseDir . '/src/helpers/blocks-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Capability_Helper' => $baseDir . '/src/helpers/capability-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Crawl_Cleanup_Helper' => $baseDir . '/src/helpers/crawl-cleanup-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Curl_Helper' => $baseDir . '/src/helpers/curl-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Current_Page_Helper' => $baseDir . '/src/helpers/current-page-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Date_Helper' => $baseDir . '/src/helpers/date-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Environment_Helper' => $baseDir . '/src/helpers/environment-helper.php', 'Yoast\\WP\\SEO\\Helpers\\First_Time_Configuration_Notice_Helper' => $baseDir . '/src/helpers/first-time-configuration-notice-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Home_Url_Helper' => $baseDir . '/src/helpers/home-url-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Image_Helper' => $baseDir . '/src/helpers/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Import_Cursor_Helper' => $baseDir . '/src/helpers/import-cursor-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Import_Helper' => $baseDir . '/src/helpers/import-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexable_Helper' => $baseDir . '/src/helpers/indexable-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexable_To_Postmeta_Helper' => $baseDir . '/src/helpers/indexable-to-postmeta-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Indexing_Helper' => $baseDir . '/src/helpers/indexing-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Language_Helper' => $baseDir . '/src/helpers/language-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Meta_Helper' => $baseDir . '/src/helpers/meta-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Notification_Helper' => $baseDir . '/src/helpers/notification-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Image_Helper' => $baseDir . '/src/helpers/open-graph/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Open_Graph\\Values_Helper' => $baseDir . '/src/helpers/open-graph/values-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Options_Helper' => $baseDir . '/src/helpers/options-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Pagination_Helper' => $baseDir . '/src/helpers/pagination-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Permalink_Helper' => $baseDir . '/src/helpers/permalink-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Post_Helper' => $baseDir . '/src/helpers/post-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Post_Type_Helper' => $baseDir . '/src/helpers/post-type-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Primary_Term_Helper' => $baseDir . '/src/helpers/primary-term-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Product_Helper' => $baseDir . '/src/helpers/product-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Redirect_Helper' => $baseDir . '/src/helpers/redirect-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Request_Helper' => $baseDir . '/src/deprecated/src/helpers/request-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Require_File_Helper' => $baseDir . '/src/helpers/require-file-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Robots_Helper' => $baseDir . '/src/helpers/robots-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Robots_Txt_Helper' => $baseDir . '/src/helpers/robots-txt-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Route_Helper' => $baseDir . '/src/helpers/route-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Sanitization_Helper' => $baseDir . '/src/helpers/sanitization-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Article_Helper' => $baseDir . '/src/helpers/schema/article-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\HTML_Helper' => $baseDir . '/src/helpers/schema/html-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\ID_Helper' => $baseDir . '/src/helpers/schema/id-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Image_Helper' => $baseDir . '/src/helpers/schema/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Language_Helper' => $baseDir . '/src/helpers/schema/language-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Schema\\Replace_Vars_Helper' => $baseDir . '/src/helpers/schema/replace-vars-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Score_Icon_Helper' => $baseDir . '/src/helpers/score-icon-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Short_Link_Helper' => $baseDir . '/src/helpers/short-link-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Site_Helper' => $baseDir . '/src/helpers/site-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Social_Profiles_Helper' => $baseDir . '/src/helpers/social-profiles-helper.php', 'Yoast\\WP\\SEO\\Helpers\\String_Helper' => $baseDir . '/src/helpers/string-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Taxonomy_Helper' => $baseDir . '/src/helpers/taxonomy-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Twitter\\Image_Helper' => $baseDir . '/src/helpers/twitter/image-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Url_Helper' => $baseDir . '/src/helpers/url-helper.php', 'Yoast\\WP\\SEO\\Helpers\\User_Helper' => $baseDir . '/src/helpers/user-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wincher_Helper' => $baseDir . '/src/helpers/wincher-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Woocommerce_Helper' => $baseDir . '/src/helpers/woocommerce-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wordpress_Helper' => $baseDir . '/src/helpers/wordpress-helper.php', 'Yoast\\WP\\SEO\\Helpers\\Wpdb_Helper' => $baseDir . '/src/helpers/wpdb-helper.php', 'Yoast\\WP\\SEO\\Images\\Application\\Image_Content_Extractor' => $baseDir . '/src/images/Application/image-content-extractor.php', 'Yoast\\WP\\SEO\\Initializers\\Crawl_Cleanup_Permalinks' => $baseDir . '/src/initializers/crawl-cleanup-permalinks.php', 'Yoast\\WP\\SEO\\Initializers\\Disable_Core_Sitemaps' => $baseDir . '/src/initializers/disable-core-sitemaps.php', 'Yoast\\WP\\SEO\\Initializers\\Initializer_Interface' => $baseDir . '/src/initializers/initializer-interface.php', 'Yoast\\WP\\SEO\\Initializers\\Migration_Runner' => $baseDir . '/src/initializers/migration-runner.php', 'Yoast\\WP\\SEO\\Initializers\\Plugin_Headers' => $baseDir . '/src/initializers/plugin-headers.php', 'Yoast\\WP\\SEO\\Initializers\\Silence_Load_Textdomain_Just_In_Time_Notices' => $baseDir . '/src/initializers/silence-load-textdomain-just-in-time-notices.php', 'Yoast\\WP\\SEO\\Initializers\\Woocommerce' => $baseDir . '/src/initializers/woocommerce.php', 'Yoast\\WP\\SEO\\Integrations\\Abstract_Exclude_Post_Type' => $baseDir . '/src/integrations/abstract-exclude-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Academy_Integration' => $baseDir . '/src/integrations/academy-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Activation_Cleanup_Integration' => $baseDir . '/src/integrations/admin/activation-cleanup-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Dialog_Integration' => $baseDir . '/src/integrations/admin/addon-installation/dialog-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Addon_Installation\\Installation_Integration' => $baseDir . '/src/integrations/admin/addon-installation/installation-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Admin_Columns_Cache_Integration' => $baseDir . '/src/integrations/admin/admin-columns-cache-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Background_Indexing_Integration' => $baseDir . '/src/integrations/admin/background-indexing-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Brand_Insights_Page' => $baseDir . '/src/integrations/admin/brand-insights-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Check_Required_Version' => $baseDir . '/src/integrations/admin/check-required-version.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Crawl_Settings_Integration' => $baseDir . '/src/integrations/admin/crawl-settings-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Cron_Integration' => $baseDir . '/src/integrations/admin/cron-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Deactivated_Premium_Integration' => $baseDir . '/src/integrations/admin/deactivated-premium-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Integration' => $baseDir . '/src/integrations/admin/first-time-configuration-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\First_Time_Configuration_Notice_Integration' => $baseDir . '/src/integrations/admin/first-time-configuration-notice-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Fix_News_Dependencies_Integration' => $baseDir . '/src/integrations/admin/fix-news-dependencies-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Health_Check_Integration' => $baseDir . '/src/integrations/admin/health-check-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\HelpScout_Beacon' => $baseDir . '/src/integrations/admin/helpscout-beacon.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Import_Integration' => $baseDir . '/src/integrations/admin/import-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexables_Exclude_Taxonomy_Integration' => $baseDir . '/src/integrations/admin/indexables-exclude-taxonomy-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Notification_Integration' => $baseDir . '/src/integrations/admin/indexing-notification-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Indexing_Tool_Integration' => $baseDir . '/src/integrations/admin/indexing-tool-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Installation_Success_Integration' => $baseDir . '/src/integrations/admin/installation-success-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Integrations_Page' => $baseDir . '/src/integrations/admin/integrations-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Link_Count_Columns_Integration' => $baseDir . '/src/integrations/admin/link-count-columns-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Menu_Badge_Integration' => $baseDir . '/src/integrations/admin/menu-badge-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Migration_Error_Integration' => $baseDir . '/src/integrations/admin/migration-error-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Old_Configuration_Integration' => $baseDir . '/src/integrations/admin/old-configuration-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirect_Integration' => $baseDir . '/src/integrations/admin/redirect-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirections_Tools_Page' => $baseDir . '/src/integrations/admin/redirections-tools-page.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Redirects_Page_Integration' => $baseDir . '/src/integrations/admin/redirects-page-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Unsupported_PHP_Version_Notice' => $baseDir . '/src/deprecated/src/integrations/admin/unsupported-php-version-notice.php', 'Yoast\\WP\\SEO\\Integrations\\Admin\\Workouts_Integration' => $baseDir . '/src/integrations/admin/workouts-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Abstract_Dismissable_Alert' => $baseDir . '/src/integrations/alerts/abstract-dismissable-alert.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Ai_Generator_Tip_Notification' => $baseDir . '/src/integrations/alerts/ai-generator-tip-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Product_Editor_Checklist_Notification' => $baseDir . '/src/integrations/alerts/black-friday-product-editor-checklist-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Promotion_Notification' => $baseDir . '/src/integrations/alerts/black-friday-promotion-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Black_Friday_Sidebar_Checklist_Notification' => $baseDir . '/src/integrations/alerts/black-friday-sidebar-checklist-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Trustpilot_Review_Notification' => $baseDir . '/src/integrations/alerts/trustpilot-review-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Alerts\\Webinar_Promo_Notification' => $baseDir . '/src/integrations/alerts/webinar-promo-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Block_Editor_Integration' => $baseDir . '/src/integrations/blocks/block-editor-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Breadcrumbs_Block' => $baseDir . '/src/integrations/blocks/breadcrumbs-block.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Dynamic_Block' => $baseDir . '/src/integrations/blocks/abstract-dynamic-block.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Dynamic_Block_V3' => $baseDir . '/src/integrations/blocks/abstract-dynamic-block-v3.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Internal_Linking_Category' => $baseDir . '/src/integrations/blocks/block-categories.php', 'Yoast\\WP\\SEO\\Integrations\\Blocks\\Structured_Data_Blocks' => $baseDir . '/src/integrations/blocks/structured-data-blocks.php', 'Yoast\\WP\\SEO\\Integrations\\Breadcrumbs_Integration' => $baseDir . '/src/integrations/breadcrumbs-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Cleanup_Integration' => $baseDir . '/src/integrations/cleanup-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Estimated_Reading_Time' => $baseDir . '/src/integrations/estimated-reading-time.php', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Attachment_Post_Type' => $baseDir . '/src/integrations/exclude-attachment-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Exclude_Oembed_Cache_Post_Type' => $baseDir . '/src/integrations/exclude-oembed-cache-post-type.php', 'Yoast\\WP\\SEO\\Integrations\\Feature_Flag_Integration' => $baseDir . '/src/integrations/feature-flag-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Backwards_Compatibility' => $baseDir . '/src/integrations/front-end/backwards-compatibility.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Category_Term_Description' => $baseDir . '/src/integrations/front-end/category-term-description.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Comment_Link_Fixer' => $baseDir . '/src/integrations/front-end/comment-link-fixer.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Basic' => $baseDir . '/src/integrations/front-end/crawl-cleanup-basic.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Rss' => $baseDir . '/src/integrations/front-end/crawl-cleanup-rss.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Crawl_Cleanup_Searches' => $baseDir . '/src/integrations/front-end/crawl-cleanup-searches.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Feed_Improvements' => $baseDir . '/src/integrations/front-end/feed-improvements.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Force_Rewrite_Title' => $baseDir . '/src/integrations/front-end/force-rewrite-title.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Handle_404' => $baseDir . '/src/integrations/front-end/handle-404.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Indexing_Controls' => $baseDir . '/src/integrations/front-end/indexing-controls.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Open_Graph_OEmbed' => $baseDir . '/src/integrations/front-end/open-graph-oembed.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\RSS_Footer_Embed' => $baseDir . '/src/integrations/front-end/rss-footer-embed.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Redirects' => $baseDir . '/src/integrations/front-end/redirects.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Robots_Txt_Integration' => $baseDir . '/src/integrations/front-end/robots-txt-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\Schema_Accessibility_Feature' => $baseDir . '/src/integrations/front-end/schema-accessibility-feature.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End\\WP_Robots_Integration' => $baseDir . '/src/integrations/front-end/wp-robots-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Front_End_Integration' => $baseDir . '/src/integrations/front-end-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Integration_Interface' => $baseDir . '/src/integrations/integration-interface.php', 'Yoast\\WP\\SEO\\Integrations\\Primary_Category' => $baseDir . '/src/integrations/primary-category.php', 'Yoast\\WP\\SEO\\Integrations\\Settings_Integration' => $baseDir . '/src/integrations/settings-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Support_Integration' => $baseDir . '/src/integrations/support-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\AMP' => $baseDir . '/src/integrations/third-party/amp.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\BbPress' => $baseDir . '/src/integrations/third-party/bbpress.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Elementor' => $baseDir . '/src/integrations/third-party/elementor.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_Elementor_Post_Types' => $baseDir . '/src/integrations/third-party/exclude-elementor-post-types.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Exclude_WooCommerce_Post_Types' => $baseDir . '/src/integrations/third-party/exclude-woocommerce-post-types.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Jetpack' => $baseDir . '/src/integrations/third-party/jetpack.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\W3_Total_Cache' => $baseDir . '/src/integrations/third-party/w3-total-cache.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML' => $baseDir . '/src/integrations/third-party/wpml.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WPML_WPSEO_Notification' => $baseDir . '/src/integrations/third-party/wpml-wpseo-notification.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories' => $baseDir . '/src/integrations/third-party/web-stories.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Web_Stories_Post_Edit' => $baseDir . '/src/integrations/third-party/web-stories-post-edit.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Wincher_Publish' => $baseDir . '/src/integrations/third-party/wincher-publish.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce' => $baseDir . '/src/integrations/third-party/woocommerce.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\WooCommerce_Post_Edit' => $baseDir . '/src/integrations/third-party/woocommerce-post-edit.php', 'Yoast\\WP\\SEO\\Integrations\\Third_Party\\Woocommerce_Permalinks' => $baseDir . '/src/integrations/third-party/woocommerce-permalinks.php', 'Yoast\\WP\\SEO\\Integrations\\Uninstall_Integration' => $baseDir . '/src/integrations/uninstall-integration.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Addon_Update_Watcher' => $baseDir . '/src/integrations/watchers/addon-update-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Auto_Update_Watcher' => $baseDir . '/src/integrations/watchers/auto-update-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Ancestor_Watcher' => $baseDir . '/src/integrations/watchers/indexable-ancestor-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Attachment_Watcher' => $baseDir . '/src/integrations/watchers/indexable-attachment-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Archive_Watcher' => $baseDir . '/src/integrations/watchers/indexable-author-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Author_Watcher' => $baseDir . '/src/integrations/watchers/indexable-author-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Category_Permalink_Watcher' => $baseDir . '/src/integrations/watchers/indexable-category-permalink-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Date_Archive_Watcher' => $baseDir . '/src/integrations/watchers/indexable-date-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_HomeUrl_Watcher' => $baseDir . '/src/integrations/watchers/indexable-homeurl-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Home_Page_Watcher' => $baseDir . '/src/integrations/watchers/indexable-home-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Permalink_Watcher' => $baseDir . '/src/integrations/watchers/indexable-permalink-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Meta_Watcher' => $baseDir . '/src/integrations/watchers/indexable-post-meta-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Archive_Watcher' => $baseDir . '/src/integrations/watchers/indexable-post-type-archive-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Type_Change_Watcher' => $baseDir . '/src/integrations/watchers/indexable-post-type-change-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Post_Watcher' => $baseDir . '/src/integrations/watchers/indexable-post-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Static_Home_Page_Watcher' => $baseDir . '/src/integrations/watchers/indexable-static-home-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_System_Page_Watcher' => $baseDir . '/src/integrations/watchers/indexable-system-page-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Taxonomy_Change_Watcher' => $baseDir . '/src/integrations/watchers/indexable-taxonomy-change-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Indexable_Term_Watcher' => $baseDir . '/src/integrations/watchers/indexable-term-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Titles_Watcher' => $baseDir . '/src/integrations/watchers/option-titles-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Option_Wpseo_Watcher' => $baseDir . '/src/integrations/watchers/option-wpseo-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Category_Quick_Edit_Watcher' => $baseDir . '/src/integrations/watchers/primary-category-quick-edit-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Primary_Term_Watcher' => $baseDir . '/src/integrations/watchers/primary-term-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Search_Engines_Discouraged_Watcher' => $baseDir . '/src/integrations/watchers/search-engines-discouraged-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Watchers\\Woocommerce_Beta_Editor_Watcher' => $baseDir . '/src/integrations/watchers/woocommerce-beta-editor-watcher.php', 'Yoast\\WP\\SEO\\Integrations\\Woocommerce_Product_Category_Permalink_Integration' => $baseDir . '/src/integrations/woocommerce-product-category-permalink-integration.php', 'Yoast\\WP\\SEO\\Integrations\\XMLRPC' => $baseDir . '/src/integrations/xmlrpc.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Post_Launch' => $baseDir . '/src/introductions/application/ai-brand-insights-post-launch.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\AI_Brand_Insights_Pre_Launch' => $baseDir . '/src/introductions/application/ai-brand-insights-pre-launch.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Ai_Fix_Assessments_Upsell' => $baseDir . '/src/introductions/application/ai-fix-assessments-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Black_Friday_Announcement' => $baseDir . '/src/introductions/application/black-friday-announcement.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Current_Page_Trait' => $baseDir . '/src/introductions/application/current-page-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Delayed_Premium_Upsell' => $baseDir . '/src/introductions/application/delayed-premium-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Google_Docs_Addon_Upsell' => $baseDir . '/src/introductions/application/google-docs-addon-upsell.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Introductions_Collector' => $baseDir . '/src/introductions/application/introductions-collector.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\User_Allowed_Trait' => $baseDir . '/src/introductions/application/user-allowed-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Application\\Version_Trait' => $baseDir . '/src/introductions/application/version-trait.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introduction_Interface' => $baseDir . '/src/introductions/domain/introduction-interface.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introduction_Item' => $baseDir . '/src/introductions/domain/introduction-item.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Introductions_Bucket' => $baseDir . '/src/introductions/domain/introductions-bucket.php', 'Yoast\\WP\\SEO\\Introductions\\Domain\\Invalid_User_Id_Exception' => $baseDir . '/src/introductions/domain/invalid-user-id-exception.php', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Introductions_Seen_Repository' => $baseDir . '/src/introductions/infrastructure/introductions-seen-repository.php', 'Yoast\\WP\\SEO\\Introductions\\Infrastructure\\Wistia_Embed_Permission_Repository' => $baseDir . '/src/introductions/infrastructure/wistia-embed-permission-repository.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Integration' => $baseDir . '/src/introductions/user-interface/introductions-integration.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Introductions_Seen_Route' => $baseDir . '/src/introductions/user-interface/introductions-seen-route.php', 'Yoast\\WP\\SEO\\Introductions\\User_Interface\\Wistia_Embed_Permission_Route' => $baseDir . '/src/introductions/user-interface/wistia-embed-permission-route.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Available_Posts\\Available_Posts_Repository' => $baseDir . '/src/llms-txt/application/available-posts/available-posts-repository.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Configuration\\Llms_Txt_Configuration' => $baseDir . '/src/llms-txt/application/configuration/llms-txt-configuration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Populate_File_Command_Handler' => $baseDir . '/src/llms-txt/application/file/commands/populate-file-command-handler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Commands\\Remove_File_Command_Handler' => $baseDir . '/src/llms-txt/application/file/commands/remove-file-command-handler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\File_Failure_Notification_Presenter' => $baseDir . '/src/llms-txt/application/file/file-failure-notification-presenter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\File\\Llms_Txt_Cron_Scheduler' => $baseDir . '/src/llms-txt/application/file/llms-txt-cron-scheduler.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Check' => $baseDir . '/src/llms-txt/application/health-check/file-check.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Health_Check\\File_Runner' => $baseDir . '/src/llms-txt/application/health-check/file-runner.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Description_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/description-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Intro_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/intro-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Link_Lists_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/link-lists-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Markdown_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/markdown-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Optional_Link_List_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/optional-link-list-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Builders\\Title_Builder' => $baseDir . '/src/llms-txt/application/markdown-builders/title-builder.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Application\\Markdown_Escaper' => $baseDir . '/src/llms-txt/application/markdown-escaper.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Data' => $baseDir . '/src/llms-txt/domain/available-posts/data-provider/available-posts-data.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Available_Posts_Repository_Interface' => $baseDir . '/src/llms-txt/domain/available-posts/data-provider/available-posts-repository-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Container' => $baseDir . '/src/llms-txt/domain/available-posts/data-provider/data-container.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Data_Interface' => $baseDir . '/src/llms-txt/domain/available-posts/data-provider/data-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Data_Provider\\Parameters' => $baseDir . '/src/llms-txt/domain/available-posts/data-provider/parameters.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Available_Posts\\Invalid_Post_Type_Exception' => $baseDir . '/src/llms-txt/domain/available-posts/invalid-post-type-exception.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Content\\Post_Collection_Interface' => $baseDir . '/src/llms-txt/domain/content/post-collection-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Content_Types\\Content_Type_Entry' => $baseDir . '/src/llms-txt/domain/content-types/content-type-entry.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_File_System_Interface' => $baseDir . '/src/llms-txt/domain/file/llms-file-system-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\File\\Llms_Txt_Permission_Gate_Interface' => $baseDir . '/src/llms-txt/domain/file/llms-txt-permission-gate-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Items\\Item_Interface' => $baseDir . '/src/llms-txt/domain/markdown/items/item-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Items\\Link' => $baseDir . '/src/llms-txt/domain/markdown/items/link.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Llms_Txt_Renderer' => $baseDir . '/src/llms-txt/domain/markdown/llms-txt-renderer.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Description' => $baseDir . '/src/llms-txt/domain/markdown/sections/description.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Intro' => $baseDir . '/src/llms-txt/domain/markdown/sections/intro.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Link_List' => $baseDir . '/src/llms-txt/domain/markdown/sections/link-list.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Section_Interface' => $baseDir . '/src/llms-txt/domain/markdown/sections/section-interface.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Domain\\Markdown\\Sections\\Title' => $baseDir . '/src/llms-txt/domain/markdown/sections/title.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Automatic_Post_Collection' => $baseDir . '/src/llms-txt/infrastructure/content/automatic-post-collection.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Manual_Post_Collection' => $baseDir . '/src/llms-txt/infrastructure/content/manual-post-collection.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Content\\Post_Collection_Factory' => $baseDir . '/src/llms-txt/infrastructure/content/post-collection-factory.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_File_System_Adapter' => $baseDir . '/src/llms-txt/infrastructure/file/wordpress-file-system-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\File\\WordPress_Llms_Txt_Permission_Gate' => $baseDir . '/src/llms-txt/infrastructure/file/wordpress-llms-txt-permission-gate.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Content_Types_Collector' => $baseDir . '/src/llms-txt/infrastructure/markdown-services/content-types-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Description_Adapter' => $baseDir . '/src/llms-txt/infrastructure/markdown-services/description-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Sitemap_Link_Collector' => $baseDir . '/src/llms-txt/infrastructure/markdown-services/sitemap-link-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Terms_Collector' => $baseDir . '/src/llms-txt/infrastructure/markdown-services/terms-collector.php', 'Yoast\\WP\\SEO\\Llms_Txt\\Infrastructure\\Markdown_Services\\Title_Adapter' => $baseDir . '/src/llms-txt/infrastructure/markdown-services/title-adapter.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Available_Posts_Route' => $baseDir . '/src/llms-txt/user-interface/available-posts-route.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Cleanup_Llms_Txt_On_Deactivation' => $baseDir . '/src/llms-txt/user-interface/cleanup-llms-txt-on-deactivation.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Enable_Llms_Txt_Option_Watcher' => $baseDir . '/src/llms-txt/user-interface/enable-llms-txt-option-watcher.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\File_Failure_Llms_Txt_Notification_Integration' => $baseDir . '/src/llms-txt/user-interface/file-failure-llms-txt-notification-integration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Health_Check\\File_Reports' => $baseDir . '/src/llms-txt/user-interface/health-check/file-reports.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Llms_Txt_Cron_Callback_Integration' => $baseDir . '/src/llms-txt/user-interface/llms-txt-cron-callback-integration.php', 'Yoast\\WP\\SEO\\Llms_Txt\\User_Interface\\Schedule_Population_On_Activation_Integration' => $baseDir . '/src/llms-txt/user-interface/schedule-population-on-activation-integration.php', 'Yoast\\WP\\SEO\\Loadable_Interface' => $baseDir . '/src/loadable-interface.php', 'Yoast\\WP\\SEO\\Loader' => $baseDir . '/src/loader.php', 'Yoast\\WP\\SEO\\Loggers\\Logger' => $baseDir . '/src/loggers/logger.php', 'Yoast\\WP\\SEO\\Main' => $baseDir . '/src/main.php', 'Yoast\\WP\\SEO\\Memoizers\\Meta_Tags_Context_Memoizer' => $baseDir . '/src/memoizers/meta-tags-context-memoizer.php', 'Yoast\\WP\\SEO\\Memoizers\\Presentation_Memoizer' => $baseDir . '/src/memoizers/presentation-memoizer.php', 'Yoast\\WP\\SEO\\Models\\Indexable' => $baseDir . '/src/models/indexable.php', 'Yoast\\WP\\SEO\\Models\\Indexable_Extension' => $baseDir . '/src/models/indexable-extension.php', 'Yoast\\WP\\SEO\\Models\\Indexable_Hierarchy' => $baseDir . '/src/models/indexable-hierarchy.php', 'Yoast\\WP\\SEO\\Models\\Primary_Term' => $baseDir . '/src/models/primary-term.php', 'Yoast\\WP\\SEO\\Models\\SEO_Links' => $baseDir . '/src/models/seo-links.php', 'Yoast\\WP\\SEO\\Models\\SEO_Meta' => $baseDir . '/src/models/seo-meta.php', 'Yoast\\WP\\SEO\\Plans\\Application\\Add_Ons_Collector' => $baseDir . '/src/plans/application/add-ons-collector.php', 'Yoast\\WP\\SEO\\Plans\\Application\\Duplicate_Post_Manager' => $baseDir . '/src/plans/application/duplicate-post-manager.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Add_On_Interface' => $baseDir . '/src/plans/domain/add-ons/add-on-interface.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Premium' => $baseDir . '/src/plans/domain/add-ons/premium.php', 'Yoast\\WP\\SEO\\Plans\\Domain\\Add_Ons\\Woo' => $baseDir . '/src/plans/domain/add-ons/woo.php', 'Yoast\\WP\\SEO\\Plans\\Infrastructure\\Add_Ons\\Managed_Add_On' => $baseDir . '/src/plans/infrastructure/add-ons/managed-add-on.php', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Plans_Page_Integration' => $baseDir . '/src/plans/user-interface/plans-page-integration.php', 'Yoast\\WP\\SEO\\Plans\\User_Interface\\Upgrade_Sidebar_Menu_Integration' => $baseDir . '/src/plans/user-interface/upgrade-sidebar-menu-integration.php', 'Yoast\\WP\\SEO\\Presentations\\Abstract_Presentation' => $baseDir . '/src/presentations/abstract-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Archive_Adjacent' => $baseDir . '/src/presentations/archive-adjacent-trait.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Author_Archive_Presentation' => $baseDir . '/src/presentations/indexable-author-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Date_Archive_Presentation' => $baseDir . '/src/presentations/indexable-date-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Error_Page_Presentation' => $baseDir . '/src/presentations/indexable-error-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Home_Page_Presentation' => $baseDir . '/src/presentations/indexable-home-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Archive_Presentation' => $baseDir . '/src/presentations/indexable-post-type-archive-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Post_Type_Presentation' => $baseDir . '/src/presentations/indexable-post-type-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Presentation' => $baseDir . '/src/presentations/indexable-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Search_Result_Page_Presentation' => $baseDir . '/src/presentations/indexable-search-result-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Home_Page_Presentation' => $baseDir . '/src/presentations/indexable-static-home-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Static_Posts_Page_Presentation' => $baseDir . '/src/presentations/indexable-static-posts-page-presentation.php', 'Yoast\\WP\\SEO\\Presentations\\Indexable_Term_Archive_Presentation' => $baseDir . '/src/presentations/indexable-term-archive-presentation.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Indexable_Presenter' => $baseDir . '/src/presenters/abstract-indexable-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Indexable_Tag_Presenter' => $baseDir . '/src/presenters/abstract-indexable-tag-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Abstract_Presenter' => $baseDir . '/src/presenters/abstract-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Alert_Presenter' => $baseDir . '/src/presenters/admin/alert-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Badge_Presenter' => $baseDir . '/src/presenters/admin/badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Beta_Badge_Presenter' => $baseDir . '/src/presenters/admin/beta-badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Help_Link_Presenter' => $baseDir . '/src/presenters/admin/help-link-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Error_Presenter' => $baseDir . '/src/presenters/admin/indexing-error-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Failed_Notification_Presenter' => $baseDir . '/src/presenters/admin/indexing-failed-notification-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_List_Item_Presenter' => $baseDir . '/src/presenters/admin/indexing-list-item-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Indexing_Notification_Presenter' => $baseDir . '/src/presenters/admin/indexing-notification-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Light_Switch_Presenter' => $baseDir . '/src/presenters/admin/light-switch-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Meta_Fields_Presenter' => $baseDir . '/src/presenters/admin/meta-fields-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Migration_Error_Presenter' => $baseDir . '/src/presenters/admin/migration-error-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Notice_Presenter' => $baseDir . '/src/presenters/admin/notice-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Premium_Badge_Presenter' => $baseDir . '/src/presenters/admin/premium-badge-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Search_Engines_Discouraged_Presenter' => $baseDir . '/src/presenters/admin/search-engines-discouraged-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Sidebar_Presenter' => $baseDir . '/src/presenters/admin/sidebar-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Admin\\Woocommerce_Beta_Editor_Presenter' => $baseDir . '/src/presenters/admin/woocommerce-beta-editor-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Breadcrumbs_Presenter' => $baseDir . '/src/presenters/breadcrumbs-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Canonical_Presenter' => $baseDir . '/src/presenters/canonical-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Debug\\Marker_Close_Presenter' => $baseDir . '/src/presenters/debug/marker-close-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Debug\\Marker_Open_Presenter' => $baseDir . '/src/presenters/debug/marker-open-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Meta_Author_Presenter' => $baseDir . '/src/presenters/meta-author-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Meta_Description_Presenter' => $baseDir . '/src/presenters/meta-description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Author_Presenter' => $baseDir . '/src/presenters/open-graph/article-author-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Modified_Time_Presenter' => $baseDir . '/src/presenters/open-graph/article-modified-time-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Published_Time_Presenter' => $baseDir . '/src/presenters/open-graph/article-published-time-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Article_Publisher_Presenter' => $baseDir . '/src/presenters/open-graph/article-publisher-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Description_Presenter' => $baseDir . '/src/presenters/open-graph/description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Image_Presenter' => $baseDir . '/src/presenters/open-graph/image-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Locale_Presenter' => $baseDir . '/src/presenters/open-graph/locale-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Site_Name_Presenter' => $baseDir . '/src/presenters/open-graph/site-name-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Title_Presenter' => $baseDir . '/src/presenters/open-graph/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Type_Presenter' => $baseDir . '/src/presenters/open-graph/type-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Open_Graph\\Url_Presenter' => $baseDir . '/src/presenters/open-graph/url-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Rel_Next_Presenter' => $baseDir . '/src/presenters/rel-next-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Rel_Prev_Presenter' => $baseDir . '/src/presenters/rel-prev-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' => $baseDir . '/src/presenters/robots-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Robots_Txt_Presenter' => $baseDir . '/src/presenters/robots-txt-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Schema_Presenter' => $baseDir . '/src/presenters/schema-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Score_Icon_Presenter' => $baseDir . '/src/presenters/score-icon-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Slack\\Enhanced_Data_Presenter' => $baseDir . '/src/presenters/slack/enhanced-data-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Title_Presenter' => $baseDir . '/src/presenters/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Card_Presenter' => $baseDir . '/src/presenters/twitter/card-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Creator_Presenter' => $baseDir . '/src/presenters/twitter/creator-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Description_Presenter' => $baseDir . '/src/presenters/twitter/description-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Image_Presenter' => $baseDir . '/src/presenters/twitter/image-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Site_Presenter' => $baseDir . '/src/presenters/twitter/site-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Twitter\\Title_Presenter' => $baseDir . '/src/presenters/twitter/title-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Url_List_Presenter' => $baseDir . '/src/presenters/url-list-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Ahrefs_Presenter' => $baseDir . '/src/presenters/webmaster/ahrefs-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Baidu_Presenter' => $baseDir . '/src/presenters/webmaster/baidu-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Bing_Presenter' => $baseDir . '/src/presenters/webmaster/bing-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Google_Presenter' => $baseDir . '/src/presenters/webmaster/google-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Pinterest_Presenter' => $baseDir . '/src/presenters/webmaster/pinterest-presenter.php', 'Yoast\\WP\\SEO\\Presenters\\Webmaster\\Yandex_Presenter' => $baseDir . '/src/presenters/webmaster/yandex-presenter.php', 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager' => $baseDir . '/src/promotions/application/promotion-manager.php', 'Yoast\\WP\\SEO\\Promotions\\Application\\Promotion_Manager_Interface' => $baseDir . '/src/promotions/application/promotion-manager-interface.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Abstract_Promotion' => $baseDir . '/src/promotions/domain/abstract-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Black_Friday_Checklist_Promotion' => $baseDir . '/src/deprecated/src/promotions/domain/black-friday-checklist-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Black_Friday_Promotion' => $baseDir . '/src/promotions/domain/black-friday-promotion.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Promotion_Interface' => $baseDir . '/src/promotions/domain/promotion-interface.php', 'Yoast\\WP\\SEO\\Promotions\\Domain\\Time_Interval' => $baseDir . '/src/promotions/domain/time-interval.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Cleanup_Repository' => $baseDir . '/src/repositories/indexable-cleanup-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Hierarchy_Repository' => $baseDir . '/src/repositories/indexable-hierarchy-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' => $baseDir . '/src/repositories/indexable-repository.php', 'Yoast\\WP\\SEO\\Repositories\\Primary_Term_Repository' => $baseDir . '/src/repositories/primary-term-repository.php', 'Yoast\\WP\\SEO\\Repositories\\SEO_Links_Repository' => $baseDir . '/src/repositories/seo-links-repository.php', 'Yoast\\WP\\SEO\\Routes\\Abstract_Action_Route' => $baseDir . '/src/routes/abstract-action-route.php', 'Yoast\\WP\\SEO\\Routes\\Abstract_Indexation_Route' => $baseDir . '/src/routes/abstract-indexation-route.php', 'Yoast\\WP\\SEO\\Routes\\Alert_Dismissal_Route' => $baseDir . '/src/routes/alert-dismissal-route.php', 'Yoast\\WP\\SEO\\Routes\\Endpoint_Interface' => $baseDir . '/src/routes/endpoint-interface.php', 'Yoast\\WP\\SEO\\Routes\\First_Time_Configuration_Route' => $baseDir . '/src/routes/first-time-configuration-route.php', 'Yoast\\WP\\SEO\\Routes\\Importing_Route' => $baseDir . '/src/routes/importing-route.php', 'Yoast\\WP\\SEO\\Routes\\Indexables_Head_Route' => $baseDir . '/src/routes/indexables-head-route.php', 'Yoast\\WP\\SEO\\Routes\\Indexing_Route' => $baseDir . '/src/routes/indexing-route.php', 'Yoast\\WP\\SEO\\Routes\\Integrations_Route' => $baseDir . '/src/routes/integrations-route.php', 'Yoast\\WP\\SEO\\Routes\\Meta_Search_Route' => $baseDir . '/src/routes/meta-search-route.php', 'Yoast\\WP\\SEO\\Routes\\Route_Interface' => $baseDir . '/src/routes/route-interface.php', 'Yoast\\WP\\SEO\\Routes\\SEMrush_Route' => $baseDir . '/src/routes/semrush-route.php', 'Yoast\\WP\\SEO\\Routes\\Supported_Features_Route' => $baseDir . '/src/routes/supported-features-route.php', 'Yoast\\WP\\SEO\\Routes\\Wincher_Route' => $baseDir . '/src/routes/wincher-route.php', 'Yoast\\WP\\SEO\\Routes\\Workouts_Route' => $baseDir . '/src/routes/workouts-route.php', 'Yoast\\WP\\SEO\\Routes\\Yoast_Head_REST_Field' => $baseDir . '/src/routes/yoast-head-rest-field.php', 'Yoast\\WP\\SEO\\Schema\\Application\\Configuration\\Schema_Configuration' => $baseDir . '/src/schema/application/configuration/schema-configuration.php', 'Yoast\\WP\\SEO\\Schema\\Infrastructure\\Disable_Schema_Integration' => $baseDir . '/src/schema/infrastructure/disable-schema-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command' => $baseDir . '/src/schema-aggregator/application/aggregate-site-schema-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Command_Handler' => $baseDir . '/src/schema-aggregator/application/aggregate-site-schema-command-handler.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command' => $baseDir . '/src/schema-aggregator/application/aggregate-site-schema-map-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Aggregate_Site_Schema_Map_Command_Handler' => $baseDir . '/src/schema-aggregator/application/aggregate-site-schema-map-command-handler.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Manager' => $baseDir . '/src/schema-aggregator/application/cache/manager.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Cache\\Xml_Manager' => $baseDir . '/src/schema-aggregator/application/cache/xml-manager.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Abstract_Schema_Enhancer' => $baseDir . '/src/schema-aggregator/application/enhancement/abstract-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Article_Schema_Enhancer' => $baseDir . '/src/schema-aggregator/application/enhancement/article-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Person_Schema_Enhancer' => $baseDir . '/src/schema-aggregator/application/enhancement/person-schema-enhancer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Enhancement\\Schema_Enhancement_Factory' => $baseDir . '/src/schema-aggregator/application/enhancement/schema-enhancement-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Default_Filter' => $baseDir . '/src/schema-aggregator/application/filtering/default-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Filtering_Strategy_Interface' => $baseDir . '/src/schema-aggregator/application/filtering/filtering-strategy-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\Schema_Node_Filter_Decider_Interface' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-filter/schema-node-filter-decider-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebPage_Schema_Node_Filter' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-filter/webpage-schema-node-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Filter\\WebSite_Schema_Node_Filter' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-filter/website-schema-node-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\Base_Schema_Node_Property_Filter' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-property-filter/base-schema-node-property-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\Schema_Node_Property_Filter_Interface' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-property-filter/schema-node-property-filter-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Filtering\\Schema_Node_Property_Filter\\WebPage_Schema_Node_Property_Filter' => $baseDir . '/src/schema-aggregator/application/filtering/schema-node-property-filter/webpage-schema-node-property-filter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Meta\\Response_Meta_Provider' => $baseDir . '/src/schema-aggregator/application/meta/response-meta-provider.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Properties_Merger' => $baseDir . '/src/schema-aggregator/application/properties-merger.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Announcement' => $baseDir . '/src/schema-aggregator/application/schema-aggregator-announcement.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Aggregator_Response_Composer' => $baseDir . '/src/schema-aggregator/application/schema-aggregator-response-composer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Builder' => $baseDir . '/src/schema-aggregator/application/schema_map/schema-map-builder.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Map\\Schema_Map_Xml_Renderer' => $baseDir . '/src/schema-aggregator/application/schema_map/schema-map-xml-renderer.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Application\\Schema_Pieces_Aggregator' => $baseDir . '/src/schema-aggregator/application/schema-pieces-aggregator.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Current_Site_URL_Provider_Interface' => $baseDir . '/src/schema-aggregator/domain/current-site-url-provider-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Enhancement\\Enhancement_Config_Interface' => $baseDir . '/src/schema-aggregator/domain/enhancement/enhancement-config-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Enhancement\\Schema_Enhancement_Interface' => $baseDir . '/src/schema-aggregator/domain/enhancement/schema-enhancement-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\External_Schema_Piece_Repository_Interface' => $baseDir . '/src/schema-aggregator/domain/external-schema-piece-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count' => $baseDir . '/src/schema-aggregator/domain/indexable-count.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Indexable_Count_Collection' => $baseDir . '/src/schema-aggregator/domain/indexable-count-collection.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Page_Controls' => $baseDir . '/src/schema-aggregator/domain/page-controls.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece' => $baseDir . '/src/schema-aggregator/domain/schema-piece.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Collection' => $baseDir . '/src/schema-aggregator/domain/schema-piece-collection.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Domain\\Schema_Piece_Repository_Interface' => $baseDir . '/src/schema-aggregator/domain/schema-piece-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Aggregator_Config' => $baseDir . '/src/schema-aggregator/infrastructure/aggregator-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Config' => $baseDir . '/src/schema-aggregator/infrastructure/config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Base_Map_Loader' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/base-map-loader.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Default_Elements_Context_Map' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/default-elements-context-map.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Elements_Context_Map_Repository_Interface' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/elements-context-map-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Filtered_Map_Loader' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/filtered-map-loader.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Elements_Context_Map\\Map_Loader_Interface' => $baseDir . '/src/schema-aggregator/infrastructure/elements-context-map/map-loader-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Article_Config' => $baseDir . '/src/schema-aggregator/infrastructure/enhancement/article-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Enhancement\\Person_Config' => $baseDir . '/src/schema-aggregator/infrastructure/enhancement/person-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Filtering_Strategy_Factory' => $baseDir . '/src/schema-aggregator/infrastructure/filtering-strategy-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository_Factory' => $baseDir . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\Indexable_Repository_Interface' => $baseDir . '/src/schema-aggregator/infrastructure/indexable-repository/indexable-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Indexable_Repository\\WordPress_Query_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/indexable-repository/wordpress-query-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Meta_Tags_Context_Memoizer_Adapter' => $baseDir . '/src/schema-aggregator/infrastructure/meta-tags-context-memoizer-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Conditional' => $baseDir . '/src/schema-aggregator/infrastructure/schema-aggregator-conditional.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Aggregator_Watcher' => $baseDir . '/src/schema-aggregator/infrastructure/schema-aggregator-watcher.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Config' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-config.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Header_Adapter' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-header-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Indexable_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-indexable-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Repository_Factory' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-repository-factory.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_Repository_Interface' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-repository-interface.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Map\\Schema_Map_WordPress_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/schema_map/schema-map-wordpress-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Edd_Schema_Piece_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/schema-pieces/edd-schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Schema_Piece_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/schema-pieces/schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\Woo_Schema_Piece_Repository' => $baseDir . '/src/schema-aggregator/infrastructure/schema-pieces/woo-schema-piece-repository.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\Schema_Pieces\\WordPress_Global_State_Adapter' => $baseDir . '/src/schema-aggregator/infrastructure/schema-pieces/wordpress-global-state-adapter.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\Infrastructure\\WordPress_Current_Site_URL_Provider' => $baseDir . '/src/schema-aggregator/infrastructure/wordpress-current-site-url-provider.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Abstract_Cache_Listener_Integration' => $baseDir . '/src/schema-aggregator/user-interface/cache/abstract-cache-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\Indexables_Update_Listener_Integration' => $baseDir . '/src/schema-aggregator/user-interface/cache/indexables-update-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Cache\\WooCommerce_Product_Type_Change_Listener_Integration' => $baseDir . '/src/schema-aggregator/user-interface/cache/woocommerce-product-type-change-listener-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cache_Cli_Command' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-aggregator-cache-cli-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Cli_Command' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-aggregator-cli-command.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Route' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-aggregator-route.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Aggregator_Xml_Route' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-aggregator-xml-route.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Response_Header_Integration' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-response-header-integration.php', 'Yoast\\WP\\SEO\\Schema_Aggregator\\User_Interface\\Site_Schema_Robots_Txt_Integration' => $baseDir . '/src/schema-aggregator/user-interface/site-schema-robots-txt-integration.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Check' => $baseDir . '/src/services/health-check/default-tagline-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Reports' => $baseDir . '/src/services/health-check/default-tagline-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Default_Tagline_Runner' => $baseDir . '/src/services/health-check/default-tagline-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Health_Check' => $baseDir . '/src/services/health-check/health-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Check' => $baseDir . '/src/services/health-check/links-table-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Reports' => $baseDir . '/src/services/health-check/links-table-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Links_Table_Runner' => $baseDir . '/src/services/health-check/links-table-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\MyYoast_Api_Request_Factory' => $baseDir . '/src/services/health-check/myyoast-api-request-factory.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Check' => $baseDir . '/src/services/health-check/page-comments-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Reports' => $baseDir . '/src/services/health-check/page-comments-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Page_Comments_Runner' => $baseDir . '/src/services/health-check/page-comments-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Check' => $baseDir . '/src/services/health-check/postname-permalink-check.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Reports' => $baseDir . '/src/services/health-check/postname-permalink-reports.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Postname_Permalink_Runner' => $baseDir . '/src/services/health-check/postname-permalink-runner.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder' => $baseDir . '/src/services/health-check/report-builder.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Report_Builder_Factory' => $baseDir . '/src/services/health-check/report-builder-factory.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Reports_Trait' => $baseDir . '/src/services/health-check/reports-trait.php', 'Yoast\\WP\\SEO\\Services\\Health_Check\\Runner_Interface' => $baseDir . '/src/services/health-check/runner-interface.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Replacevar_Service' => $baseDir . '/src/services/importing/aioseo/aioseo-replacevar-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Provider_Service' => $baseDir . '/src/services/importing/aioseo/aioseo-robots-provider-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Robots_Transformer_Service' => $baseDir . '/src/services/importing/aioseo/aioseo-robots-transformer-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Aioseo\\Aioseo_Social_Images_Provider_Service' => $baseDir . '/src/services/importing/aioseo/aioseo-social-images-provider-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Conflicting_Plugins_Service' => $baseDir . '/src/services/importing/conflicting-plugins-service.php', 'Yoast\\WP\\SEO\\Services\\Importing\\Importable_Detector_Service' => $baseDir . '/src/services/importing/importable-detector-service.php', 'Yoast\\WP\\SEO\\Services\\Indexables\\Indexable_Version_Manager' => $baseDir . '/src/services/indexables/indexable-version-manager.php', 'Yoast\\WP\\SEO\\Surfaces\\Classes_Surface' => $baseDir . '/src/surfaces/classes-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Helpers_Surface' => $baseDir . '/src/surfaces/helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Meta_Surface' => $baseDir . '/src/surfaces/meta-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Open_Graph_Helpers_Surface' => $baseDir . '/src/surfaces/open-graph-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Schema_Helpers_Surface' => $baseDir . '/src/surfaces/schema-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Twitter_Helpers_Surface' => $baseDir . '/src/surfaces/twitter-helpers-surface.php', 'Yoast\\WP\\SEO\\Surfaces\\Values\\Meta' => $baseDir . '/src/surfaces/values/meta.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Configuration\\Task_List_Configuration' => $baseDir . '/src/task-list/application/configuration/task-list-configuration.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Endpoints\\Endpoints_Repository' => $baseDir . '/src/task-list/application/endpoints/endpoints-repository.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Complete_FTC' => $baseDir . '/src/task-list/application/tasks/complete-ftc.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Create_New_Content' => $baseDir . '/src/task-list/application/tasks/create-new-content.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Delete_Hello_World' => $baseDir . '/src/task-list/application/tasks/delete-hello-world.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Enable_Llms_Txt' => $baseDir . '/src/task-list/application/tasks/enable-llms-txt.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks\\Set_Search_Appearance_Templates' => $baseDir . '/src/task-list/application/tasks/set-search-appearance-templates.php', 'Yoast\\WP\\SEO\\Task_List\\Application\\Tasks_Repository' => $baseDir . '/src/task-list/application/tasks-repository.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Call_To_Action_Entry' => $baseDir . '/src/task-list/domain/components/call-to-action-entry.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Components\\Copy_Set' => $baseDir . '/src/task-list/domain/components/copy-set.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Endpoint\\Endpoint_Interface' => $baseDir . '/src/task-list/domain/endpoint/endpoint-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Endpoint\\Endpoint_List' => $baseDir . '/src/task-list/domain/endpoint/endpoint-list.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_Hello_World_Task_Exception' => $baseDir . '/src/task-list/domain/exceptions/complete-hello-world-task-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Complete_LLMS_Task_Exception' => $baseDir . '/src/task-list/domain/exceptions/complete-llms-task-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Post_Type_Tasks_Exception' => $baseDir . '/src/task-list/domain/exceptions/invalid-post-type-tasks-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Invalid_Tasks_Exception' => $baseDir . '/src/task-list/domain/exceptions/invalid-tasks-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Exceptions\\Task_Not_Found_Exception' => $baseDir . '/src/task-list/domain/exceptions/task-not-found-exception.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Completeable_Task' => $baseDir . '/src/task-list/domain/tasks/abstract-completeable-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Post_Type_Task' => $baseDir . '/src/task-list/domain/tasks/abstract-post-type-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Abstract_Task' => $baseDir . '/src/task-list/domain/tasks/abstract-task.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Completeable_Task_Interface' => $baseDir . '/src/task-list/domain/tasks/completeable-task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Post_Type_Task_Interface' => $baseDir . '/src/task-list/domain/tasks/post-type-task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Domain\\Tasks\\Task_Interface' => $baseDir . '/src/task-list/domain/tasks/task-interface.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Complete_Task_Endpoint' => $baseDir . '/src/task-list/infrastructure/endpoints/complete-task-endpoint.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Endpoints\\Get_Tasks_Endpoint' => $baseDir . '/src/task-list/infrastructure/endpoints/get-tasks-endpoint.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Register_Post_Type_Tasks_Integration' => $baseDir . '/src/task-list/infrastructure/register-post-type-tasks-integration.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Cached_Tasks_Collector' => $baseDir . '/src/task-list/infrastructure/tasks-collectors/cached-tasks-collector.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector' => $baseDir . '/src/task-list/infrastructure/tasks-collectors/tasks-collector.php', 'Yoast\\WP\\SEO\\Task_List\\Infrastructure\\Tasks_Collectors\\Tasks_Collector_Interface' => $baseDir . '/src/task-list/infrastructure/tasks-collectors/tasks-collector-interface.php', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Complete_Task_Route' => $baseDir . '/src/task-list/user-interface/tasks/complete-task-route.php', 'Yoast\\WP\\SEO\\Task_List\\User_Interface\\Tasks\\Get_Tasks_Route' => $baseDir . '/src/task-list/user-interface/tasks/get-tasks-route.php', 'Yoast\\WP\\SEO\\Tracking\\Application\\Action_Tracker' => $baseDir . '/src/tracking/application/action-tracker.php', 'Yoast\\WP\\SEO\\Tracking\\Domain\\Exceptions\\Invalid_Tracked_Action_Exception' => $baseDir . '/src/tracking/domain/exceptions/invalid-tracked-action-exception.php', 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_Link_Adapter' => $baseDir . '/src/tracking/infrastructure/tracking-link-adapter.php', 'Yoast\\WP\\SEO\\Tracking\\Infrastructure\\Tracking_On_Page_Load_Integration' => $baseDir . '/src/tracking/infrastructure/tracking-on-page-load-integration.php', 'Yoast\\WP\\SEO\\Tracking\\User_Interface\\Action_Tracking_Route' => $baseDir . '/src/tracking/user-interface/action-tracking-route.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Additional_Contactmethods_Collector' => $baseDir . '/src/user-meta/application/additional-contactmethods-collector.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Cleanup_Service' => $baseDir . '/src/user-meta/application/cleanup-service.php', 'Yoast\\WP\\SEO\\User_Meta\\Application\\Custom_Meta_Collector' => $baseDir . '/src/user-meta/application/custom-meta-collector.php', 'Yoast\\WP\\SEO\\User_Meta\\Domain\\Additional_Contactmethod_Interface' => $baseDir . '/src/user-meta/domain/additional-contactmethod-interface.php', 'Yoast\\WP\\SEO\\User_Meta\\Domain\\Custom_Meta_Interface' => $baseDir . '/src/user-meta/domain/custom-meta-interface.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Facebook' => $baseDir . '/src/user-meta/framework/additional-contactmethods/facebook.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Instagram' => $baseDir . '/src/user-meta/framework/additional-contactmethods/instagram.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Linkedin' => $baseDir . '/src/user-meta/framework/additional-contactmethods/linkedin.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Myspace' => $baseDir . '/src/user-meta/framework/additional-contactmethods/myspace.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Pinterest' => $baseDir . '/src/user-meta/framework/additional-contactmethods/pinterest.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Soundcloud' => $baseDir . '/src/user-meta/framework/additional-contactmethods/soundcloud.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Tumblr' => $baseDir . '/src/user-meta/framework/additional-contactmethods/tumblr.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Wikipedia' => $baseDir . '/src/user-meta/framework/additional-contactmethods/wikipedia.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\X' => $baseDir . '/src/user-meta/framework/additional-contactmethods/x.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Additional_Contactmethods\\Youtube' => $baseDir . '/src/user-meta/framework/additional-contactmethods/youtube.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Metadesc' => $baseDir . '/src/user-meta/framework/custom-meta/author-metadesc.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Pronouns' => $baseDir . '/src/user-meta/framework/custom-meta/author-pronouns.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Author_Title' => $baseDir . '/src/user-meta/framework/custom-meta/author-title.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Content_Analysis_Disable' => $baseDir . '/src/user-meta/framework/custom-meta/content-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Inclusive_Language_Analysis_Disable' => $baseDir . '/src/user-meta/framework/custom-meta/inclusive-language-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Keyword_Analysis_Disable' => $baseDir . '/src/user-meta/framework/custom-meta/keyword-analysis-disable.php', 'Yoast\\WP\\SEO\\User_Meta\\Framework\\Custom_Meta\\Noindex_Author' => $baseDir . '/src/user-meta/framework/custom-meta/noindex-author.php', 'Yoast\\WP\\SEO\\User_Meta\\Infrastructure\\Cleanup_Repository' => $baseDir . '/src/user-meta/infrastructure/cleanup-repository.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Additional_Contactmethods_Integration' => $baseDir . '/src/user-meta/user-interface/additional-contactmethods-integration.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Cleanup_Integration' => $baseDir . '/src/user-meta/user-interface/cleanup-integration.php', 'Yoast\\WP\\SEO\\User_Meta\\User_Interface\\Custom_Meta_Integration' => $baseDir . '/src/user-meta/user-interface/custom-meta-integration.php', 'Yoast\\WP\\SEO\\User_Profiles_Additions\\User_Interface\\User_Profiles_Additions_Ui' => $baseDir . '/src/user-profiles-additions/user-interface/user-profiles-additions-ui.php', 'Yoast\\WP\\SEO\\Values\\Images' => $baseDir . '/src/values/images.php', 'Yoast\\WP\\SEO\\Values\\Indexables\\Indexable_Builder_Versions' => $baseDir . '/src/values/indexables/indexable-builder-versions.php', 'Yoast\\WP\\SEO\\Values\\OAuth\\OAuth_Token' => $baseDir . '/src/values/oauth/oauth-token.php', 'Yoast\\WP\\SEO\\Values\\Open_Graph\\Images' => $baseDir . '/src/values/open-graph/images.php', 'Yoast\\WP\\SEO\\Values\\Robots\\Directive' => $baseDir . '/src/values/robots/directive.php', 'Yoast\\WP\\SEO\\Values\\Robots\\User_Agent' => $baseDir . '/src/values/robots/user-agent.php', 'Yoast\\WP\\SEO\\Values\\Robots\\User_Agent_List' => $baseDir . '/src/values/robots/user-agent-list.php', 'Yoast\\WP\\SEO\\Values\\Twitter\\Images' => $baseDir . '/src/values/twitter/images.php', 'Yoast\\WP\\SEO\\WordPress\\Wrapper' => $baseDir . '/src/wordpress/wrapper.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Query_Wrapper' => $baseDir . '/src/wrappers/wp-query-wrapper.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Remote_Handler' => $baseDir . '/src/wrappers/wp-remote-handler.php', 'Yoast\\WP\\SEO\\Wrappers\\WP_Rewrite_Wrapper' => $baseDir . '/src/wrappers/wp-rewrite-wrapper.php', 'Yoast_Dashboard_Widget' => $baseDir . '/admin/class-yoast-dashboard-widget.php', 'Yoast_Dismissable_Notice_Ajax' => $baseDir . '/admin/ajax/class-yoast-dismissable-notice.php', 'Yoast_Dynamic_Rewrites' => $baseDir . '/inc/class-yoast-dynamic-rewrites.php', 'Yoast_Feature_Toggle' => $baseDir . '/admin/views/class-yoast-feature-toggle.php', 'Yoast_Feature_Toggles' => $baseDir . '/admin/views/class-yoast-feature-toggles.php', 'Yoast_Form' => $baseDir . '/admin/class-yoast-form.php', 'Yoast_Form_Element' => $baseDir . '/admin/views/interface-yoast-form-element.php', 'Yoast_Input_Select' => $baseDir . '/admin/views/class-yoast-input-select.php', 'Yoast_Input_Validation' => $baseDir . '/admin/class-yoast-input-validation.php', 'Yoast_Integration_Toggles' => $baseDir . '/admin/views/class-yoast-integration-toggles.php', 'Yoast_Network_Admin' => $baseDir . '/admin/class-yoast-network-admin.php', 'Yoast_Network_Settings_API' => $baseDir . '/admin/class-yoast-network-settings-api.php', 'Yoast_Notification' => $baseDir . '/admin/class-yoast-notification.php', 'Yoast_Notification_Center' => $baseDir . '/admin/class-yoast-notification-center.php', 'Yoast_Notifications' => $baseDir . '/admin/class-yoast-notifications.php', 'Yoast_Plugin_Conflict' => $baseDir . '/admin/class-yoast-plugin-conflict.php', 'Yoast_Plugin_Conflict_Ajax' => $baseDir . '/admin/ajax/class-yoast-plugin-conflict-ajax.php', ); vendor/composer/InstalledVersions.php 0000644 00000041763 15174712003 0014064 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` * * @final */ class InstalledVersions { /** * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to * @internal */ private static $selfDir = null; /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null */ private static $installed; /** * @var bool */ private static $installedIsLocalDir; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list<string> */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list<string> */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); // when using reload, we disable the duplicate protection to ensure that self::$installed data is // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, // so we have to assume it does not, and that may result in duplicate data being returned when listing // all installed packages for example self::$installedIsLocalDir = false; } /** * @return string */ private static function getSelfDir() { if (self::$selfDir === null) { self::$selfDir = strtr(__DIR__, '\\', '/'); } return self::$selfDir; } /** * @return array[] * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); $copiedLocalDir = false; if (self::$canGetVendors) { $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require $vendorDir.'/composer/installed.php'; self::$installedByVendor[$vendorDir] = $required; $installed[] = $required; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { self::$installed = $required; self::$installedIsLocalDir = true; } } if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { $copiedLocalDir = true; } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ $required = require __DIR__ . '/installed.php'; self::$installed = $required; } else { self::$installed = array(); } } if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } return $installed; } } vendor/composer/autoload_real.php 0000644 00000002161 15174712003 0013214 0 ustar 00 <?php // autoload_real.php @generated by Composer class ComposerAutoloaderInit39a23b5c2ed5051c90d7939162f27c90 { private static $loader; public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } /** * @return \Composer\Autoload\ClassLoader */ public static function getLoader() { if (null !== self::$loader) { return self::$loader; } require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit39a23b5c2ed5051c90d7939162f27c90', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit39a23b5c2ed5051c90d7939162f27c90', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit39a23b5c2ed5051c90d7939162f27c90::getInitializer($loader)); $loader->register(true); return $loader; } } vendor/composer/ClassLoader.php 0000644 00000037772 15174712003 0012615 0 ustar 00 <?php /* * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier <fabien@symfony.com> * @author Jordi Boggiano <j.boggiano@seld.be> * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var \Closure(string):void */ private static $includeFile; /** @var string|null */ private $vendorDir; // PSR-4 /** * @var array<string, array<string, int>> */ private $prefixLengthsPsr4 = array(); /** * @var array<string, list<string>> */ private $prefixDirsPsr4 = array(); /** * @var list<string> */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * List of PSR-0 prefixes * * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) * * @var array<string, array<string, list<string>>> */ private $prefixesPsr0 = array(); /** * @var list<string> */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var array<string, string> */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var array<string, bool> */ private $missingClasses = array(); /** @var string|null */ private $apcuPrefix; /** * @var array<string, self> */ private static $registeredLoaders = array(); /** * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; self::initializeIncludeClosure(); } /** * @return array<string, list<string>> */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array<string, list<string>> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return list<string> */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return list<string> */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return array<string, string> Array of classname => path */ public function getClassMap() { return $this->classMap; } /** * @param array<string, string> $classMap Class to filename map * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param list<string>|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list<string>|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { $includeFile = self::$includeFile; $includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders keyed by their corresponding vendor directories. * * @return array<string, self> */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } /** * @return void */ private static function initializeIncludeClosure() { if (self::$includeFile !== null) { return; } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void */ self::$includeFile = \Closure::bind(static function($file) { include $file; }, null, null); } } vendor/autoload.php 0000644 00000001354 15174712003 0010365 0 ustar 00 <?php // autoload.php @generated by Composer if (PHP_VERSION_ID < 50600) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, $err); } elseif (!headers_sent()) { echo $err; } } throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit39a23b5c2ed5051c90d7939162f27c90::getLoader(); lib/orm.php 0000644 00000206400 15174712003 0006622 0 ustar 00 <?php namespace Yoast\WP\Lib; use ArrayAccess; use Exception; use InvalidArgumentException; use ReturnTypeWillChange; use wpdb; use Yoast\WP\SEO\Config\Migration_Status; /** * Yoast ORM class. * * Based on Idiorm * * URL: http://github.com/j4mie/idiorm/ * * A single-class super-simple database abstraction layer for PHP. * Provides (nearly) zero-configuration object-relational mapping * and a fluent interface for building basic, commonly-used queries. * * BSD Licensed. * * Copyright (c) 2010, Jamie Matthews * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The methods documented below are magic methods that conform to PSR-1. * This documentation exposes these methods to doc generators and IDEs. * * @see http://www.php-fig.org/psr/psr-1/ */ class ORM implements ArrayAccess { /* * --- CLASS CONSTANTS --- */ public const CONDITION_FRAGMENT = 0; public const CONDITION_VALUES = 1; /* * --- INSTANCE PROPERTIES --- */ /** * Holds the class name. Wrapped find_one and find_many classes will return an instance or instances of this class. * * @var string */ protected $class_name; /** * Holds the name of the table the current ORM instance is associated with. * * @var string */ protected $table_name; /** * Holds the alias for the table to be used in SELECT queries. * * @var string|null */ protected $table_alias = null; /** * Values to be bound to the query. * * @var array */ protected $values = []; /** * Columns to select in the result. * * @var array */ protected $result_columns = [ '*' ]; /** * Are we using the default result column or have these been manually changed? * * @var bool */ protected $using_default_result_columns = true; /** * Holds the join sources. * * @var array */ protected $join_sources = []; /** * Should the query include a DISTINCT keyword? * * @var bool */ protected $distinct = false; /** * Is this a raw query? * * @var bool */ protected $is_raw_query = false; /** * The raw query. * * @var string */ protected $raw_query = ''; /** * The raw query parameters. * * @var array */ protected $raw_parameters = []; /** * Array of WHERE clauses. * * @var array */ protected $where_conditions = []; /** * LIMIT. * * @var int|null */ protected $limit = null; /** * OFFSET. * * @var int|null */ protected $offset = null; /** * ORDER BY. * * @var array */ protected $order_by = []; /** * GROUP BY. * * @var array */ protected $group_by = []; /** * HAVING. * * @var array */ protected $having_conditions = []; /** * The data for a hydrated instance of the class. * * @var array */ protected $data = []; /** * Lifetime of the object. * * @var array */ protected $dirty_fields = []; /** * Fields that are to be inserted in the DB raw. * * @var array */ protected $expr_fields = []; /** * Is this a new object (has create() been called)? * * @var bool */ protected $is_new = false; /** * Name of the column to use as the primary key for * this instance only. Overrides the config settings. * * @var string|null */ protected $instance_id_column = null; /* * --- STATIC METHODS --- */ /** * Factory method, return an instance of this class bound to the supplied * table name. * * A repeat of content in parent::for_table, so that created class is ORM. * * @param string $table_name The table to create instance for. * * @return ORM Instance of the ORM. */ public static function for_table( $table_name ) { return new static( $table_name, [] ); } /** * Executes a raw query as a wrapper for wpdb::query. * Useful for queries that can't be accomplished through Idiorm, * particularly those using engine-specific features. * * @example raw_execute('INSERT OR REPLACE INTO `widget` (`id`, `name`) SELECT `id`, `name` FROM `other_table`') * @example raw_execute('SELECT `name`, AVG(`order`) FROM `customer` GROUP BY `name` HAVING AVG(`order`) > 10') * * @param string $query The raw SQL query. * @param array $parameters Optional bound parameters. * * @return bool Success. */ public static function raw_execute( $query, $parameters = [] ) { return self::execute( $query, $parameters ); } /** * Internal helper method for executing statements. * * @param string $query The query. * @param array $parameters An array of parameters to be bound in to the query. * * @return bool|int Response of wpdb::query */ protected static function execute( $query, $parameters = [] ) { /** * The global WordPress database variable. * * @var wpdb $wpdb */ global $wpdb; $show_errors = $wpdb->show_errors; if ( \YoastSEO()->classes->get( Migration_Status::class )->get_error( 'free' ) ) { $wpdb->show_errors = false; } $parameters = \array_filter( $parameters, static function ( $parameter ) { return $parameter !== null; }, ); if ( ! empty( $parameters ) ) { $query = $wpdb->prepare( $query, $parameters ); } $result = $wpdb->query( $query ); $wpdb->show_errors = $show_errors; return $result; } /* * --- INSTANCE METHODS --- */ /** * "Private" constructor; shouldn't be called directly. * Use the ORM::for_table factory method instead. * * @param string $table_name Table name. * @param array $data Data to populate table. */ protected function __construct( $table_name, $data = [] ) { $this->table_name = $table_name; $this->data = $data; } /** * Sets the name of the class which the wrapped methods should return instances of. * * @param string $class_name The classname to set. * * @return void */ public function set_class_name( $class_name ) { $this->class_name = $class_name; } /** * Creates a new, empty instance of the class. Used to add a new row to your database. May optionally be passed an * associative array of data to populate the instance. If so, all fields will be flagged as dirty so all will be * saved to the database when save() is called. * * @param array|null $data Data to populate table. * * @return bool|Model|ORM */ public function create( $data = null ) { $this->is_new = true; if ( $data !== null ) { $this->hydrate( $data )->force_all_dirty(); } return $this->create_model_instance( $this ); } /** * Specifies the ID column to use for this instance or array of instances only. * This overrides the id_column and id_column_overrides settings. * * This is mostly useful for libraries built on top of Idiorm, and will not normally be used in manually built * queries. If you don't know why you would want to use this, you should probably just ignore it. * * @param string $id_column The ID column. * * @return ORM */ public function use_id_column( $id_column ) { $this->instance_id_column = $id_column; return $this; } /** * Creates an ORM instance from the given row (an associative array of data fetched from the database). * * @param array $row A row from the database. * * @return bool|Model */ protected function create_instance_from_row( $row ) { $instance = self::for_table( $this->table_name ); $instance->use_id_column( $this->instance_id_column ); $instance->hydrate( $row ); return $this->create_model_instance( $instance ); } /** * Tells the ORM that you are expecting a single result back from your query, and execute it. Will return a single * instance of the ORM class, or false if no rows were returned. As a shortcut, you may supply an ID as a parameter * to this method. This will perform a primary key lookup on the table. * * @param int|null $id An (optional) ID. * * @return bool|Model */ public function find_one( $id = null ) { if ( $id !== null ) { $this->where_id_is( $id ); } $this->limit( 1 ); $rows = $this->run(); if ( empty( $rows ) ) { return false; } return $this->create_instance_from_row( $rows[0] ); } /** * Tells the ORM that you are expecting multiple results from your query, and execute it. Will return an array of * instances of the ORM class, or an empty array if no rows were returned. * * @return array */ public function find_many() { $rows = $this->run(); if ( $rows === false ) { return []; } return \array_map( [ $this, 'create_instance_from_row' ], $rows ); } /** * Creates an instance of the model class associated with this wrapper and populate it with the supplied Idiorm * instance. * * @param ORM $orm The ORM used by model. * * @return bool|Model Instance of the model class. */ protected function create_model_instance( $orm ) { if ( $orm === false ) { return false; } /** * An instance of Model is being made. * * @var Model $model */ $model = new $this->class_name(); $model->set_orm( $orm ); return $model; } /** * Tells the ORM that you are expecting multiple results from your query, and execute it. Will return an array, or * an empty array if no rows were returned. * * @return array The query results. */ public function find_array() { return $this->run(); } /** * Tells the ORM that you wish to execute a COUNT query. * * @param string $column The table column. * * @return float|int An integer representing the number of rows returned. */ public function count( $column = '*' ) { return $this->call_aggregate_db_function( __FUNCTION__, $column ); } /** * Tells the ORM that you wish to execute a MAX query. * * @param string $column The table column. * * @return float|int The max value of the chosen column. */ public function max( $column ) { return $this->call_aggregate_db_function( __FUNCTION__, $column ); } /** * Tells the ORM that you wish to execute a MIN query. * * @param string $column The table column. * * @return float|int The min value of the chosen column. */ public function min( $column ) { return $this->call_aggregate_db_function( __FUNCTION__, $column ); } /** * Tells the ORM that you wish to execute a AVG query. * * @param string $column The table column. * * @return float|int The average value of the chosen column. */ public function avg( $column ) { return $this->call_aggregate_db_function( __FUNCTION__, $column ); } /** * Tells the ORM that you wish to execute a SUM query. * * @param string $column The table column. * * @return float|int The sum of the chosen column. */ public function sum( $column ) { return $this->call_aggregate_db_function( __FUNCTION__, $column ); } /** * Returns the select query as SQL. * * @return string The select query in SQL. */ public function get_sql() { return $this->build_select(); } /** * Returns the update query as SQL. * * @return string The update query in SQL. */ public function get_update_sql() { return $this->build_update(); } /** * Executes an aggregate query on the current connection. * * @param string $sql_function The aggregate function to call eg. MIN, COUNT, etc. * @param string $column The column to execute the aggregate query against. * * @return int */ protected function call_aggregate_db_function( $sql_function, $column ) { $alias = \strtolower( $sql_function ); $sql_function = \strtoupper( $sql_function ); if ( $column !== '*' ) { $column = $this->quote_identifier( $column ); } $result_columns = $this->result_columns; $this->result_columns = []; $this->select_expr( "{$sql_function}({$column})", $alias ); $result = $this->find_one(); $this->result_columns = $result_columns; $return_value = 0; if ( $result !== false && isset( $result->{$alias} ) ) { if ( ! \is_numeric( $result->{$alias} ) ) { $return_value = $result->{$alias}; } // phpcs:ignore Universal.Operators.StrictComparisons -- Reason: This loose comparison seems intentional. elseif ( (int) $result->{$alias} == (float) $result->{$alias} ) { $return_value = (int) $result->{$alias}; } else { $return_value = (float) $result->{$alias}; } } return $return_value; } /** * Hydrates (populate) this instance of the class from an associative array of data. This will usually be called * only from inside the class, but it's public in case you need to call it directly. * * @param array $data Data to populate table. * * @return ORM */ public function hydrate( $data = [] ) { $this->data = $data; return $this; } /** * Forces the ORM to flag all the fields in the $data array as "dirty" and therefore update them when save() is * called. * * @return ORM */ public function force_all_dirty() { $this->dirty_fields = $this->data; return $this; } /** * Performs a raw query. The query can contain placeholders in either named or question mark style. If placeholders * are used, the parameters should be an array of values which will be bound to the placeholders in the query. * If this method is called, all other query building methods will be ignored. * * @param array $query The query. * @param array $parameters The parameters. Defaults to an empty array. * * @return ORM */ public function raw_query( $query, $parameters = [] ) { $this->is_raw_query = true; $this->raw_query = $query; $this->raw_parameters = $parameters; return $this; } /** * Adds an alias for the main table to be used in SELECT queries. * * @param string $alias The alias. * * @return ORM */ public function table_alias( $alias ) { $this->table_alias = $alias; return $this; } /** * Adds an unquoted expression to the set of columns returned by the SELECT query. Internal method. * * @param string $expr The expression. * @param string|null $alias The alias to return the expression as. Defaults to null. * * @return ORM */ protected function add_result_column( $expr, $alias = null ) { if ( $alias !== null ) { $expr .= ' AS ' . $this->quote_identifier( $alias ); } if ( $this->using_default_result_columns ) { $this->result_columns = [ $expr ]; $this->using_default_result_columns = false; } else { $this->result_columns[] = $expr; } return $this; } /** * Counts the number of columns that belong to the primary key and their value is null. * * @return int The amount of null columns. * * @throws Exception Primary key ID contains null value(s). * @throws Exception Primary key ID missing from row or is null. */ public function count_null_id_columns() { if ( \is_array( $this->get_id_column_name() ) ) { return \count( \array_filter( $this->id(), 'is_null' ) ); } else { return ( $this->id() === null ) ? 1 : 0; } } /** * Adds a column to the list of columns returned by the SELECT query. * * @param string $column The column. Defaults to '*'. * @param string|null $alias The alias to return the column as. Defaults to null. * * @return ORM */ public function select( $column, $alias = null ) { $column = $this->quote_identifier( $column ); return $this->add_result_column( $column, $alias ); } /** * Adds an unquoted expression to the list of columns returned by the SELECT query. * * @param string $expr The expression. * @param string|null $alias The alias to return the column as. Defaults to null. * * @return ORM */ public function select_expr( $expr, $alias = null ) { return $this->add_result_column( $expr, $alias ); } /** * Adds columns to the list of columns returned by the SELECT query. * * This defaults to '*'. * Many columns can be supplied as either an array or as a list of parameters to the method. * Note that the alias must not be numeric - if you want a numeric alias then prepend it with some alpha chars. eg. * a1. * * @example select_many(array('column', 'column2', 'column3'), 'column4', 'column5'); * @example select_many(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5'); * @example select_many('column', 'column2', 'column3'); * * @return ORM */ public function select_many() { $columns = \func_get_args(); if ( ! empty( $columns ) ) { $columns = $this->normalise_select_many_columns( $columns ); foreach ( $columns as $alias => $column ) { if ( \is_numeric( $alias ) ) { $alias = null; } $this->select( $column, $alias ); } } return $this; } /** * Adds an unquoted expression to the list of columns returned by the SELECT query. * * Many columns can be supplied as either an array or as a list of parameters to the method. * Note that the alias must not be numeric - if you want a numeric alias then prepend it with some alpha chars. eg. * a1 * * @example select_many_expr(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5') * @example select_many_expr('column', 'column2', 'column3') * @example select_many_expr(array('column', 'column2', 'column3'), 'column4', 'column5') * * @return ORM */ public function select_many_expr() { $columns = \func_get_args(); if ( ! empty( $columns ) ) { $columns = $this->normalise_select_many_columns( $columns ); foreach ( $columns as $alias => $column ) { if ( \is_numeric( $alias ) ) { $alias = null; } $this->select_expr( $column, $alias ); } } return $this; } /** * Takes a column specification for the select many methods and convert it into a normalised array of columns and * aliases. * * It is designed to turn the following styles into a normalised array: * array(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5')) * * @param array $columns The columns. * * @return array */ protected function normalise_select_many_columns( $columns ) { $return = []; foreach ( $columns as $column ) { if ( \is_array( $column ) ) { foreach ( $column as $key => $value ) { if ( ! \is_numeric( $key ) ) { $return[ $key ] = $value; } else { $return[] = $value; } } } else { $return[] = $column; } } return $return; } /** * Adds a DISTINCT keyword before the list of columns in the SELECT query. * * @return ORM */ public function distinct() { $this->distinct = true; return $this; } /** * Add a JOIN source to the query. Internal method. * * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this * will be prepended to JOIN. * * The table should be the name of the table to join to. * * The constraint may be either a string or an array with three elements. If it * is a string, it will be compiled into the query as-is, with no escaping. The * recommended way to supply the constraint is as an array with three elements: * * first_column, operator, second_column * * Example: array('user.id', '=', 'profile.user_id') * * will compile to * * ON `user`.`id` = `profile`.`user_id` * * The final (optional) argument specifies an alias for the joined table. * * @param string $join_operator The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be * prepended to JOIN. * @param string $table The table should be the name of the table to join to. * @param string $constraint The constraint. * @param string|null $table_alias The alias for the joined table. Defaults to null. * * @return ORM */ protected function add_join_source( $join_operator, $table, $constraint, $table_alias = null ) { $join_operator = \trim( "{$join_operator} JOIN" ); $table = $this->quote_identifier( $table ); // Add table alias if present. if ( $table_alias !== null ) { $table_alias = $this->quote_identifier( $table_alias ); $table .= " {$table_alias}"; } // Build the constraint. if ( \is_array( $constraint ) ) { list( $first_column, $operator, $second_column ) = $constraint; $first_column = $this->quote_identifier( $first_column ); $second_column = $this->quote_identifier( $second_column ); $constraint = "{$first_column} {$operator} {$second_column}"; } $this->join_sources[] = "{$join_operator} {$table} ON {$constraint}"; return $this; } /** * Adds a RAW JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string $table_alias The table alias. * @param array $parameters The parameters. Defaults to an empty array. * * @return ORM */ public function raw_join( $table, $constraint, $table_alias, $parameters = [] ) { // Add table alias if present. if ( $table_alias !== null ) { $table_alias = $this->quote_identifier( $table_alias ); $table .= " {$table_alias}"; } $this->values = \array_merge( $this->values, $parameters ); // Build the constraint. if ( \is_array( $constraint ) ) { list( $first_column, $operator, $second_column ) = $constraint; $first_column = $this->quote_identifier( $first_column ); $second_column = $this->quote_identifier( $second_column ); $constraint = "{$first_column} {$operator} {$second_column}"; } $this->join_sources[] = "{$table} ON {$constraint}"; return $this; } /** * Adds a simple JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string|null $table_alias The table alias. Defaults to null. * * @return ORM */ public function join( $table, $constraint, $table_alias = null ) { return $this->add_join_source( '', $table, $constraint, $table_alias ); } /** * Adds an INNER JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string|null $table_alias The table alias. Defaults to null. * * @return ORM */ public function inner_join( $table, $constraint, $table_alias = null ) { return $this->add_join_source( 'INNER', $table, $constraint, $table_alias ); } /** * Adds a LEFT OUTER JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string|null $table_alias The table alias. Defaults to null. * * @return ORM */ public function left_outer_join( $table, $constraint, $table_alias = null ) { return $this->add_join_source( 'LEFT OUTER', $table, $constraint, $table_alias ); } /** * Adds a RIGHT OUTER JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string|null $table_alias The table alias. Defaults to null. * * @return ORM */ public function right_outer_join( $table, $constraint, $table_alias = null ) { return $this->add_join_source( 'RIGHT OUTER', $table, $constraint, $table_alias ); } /** * Adds a FULL OUTER JOIN source to the query. * * @param string $table The table name. * @param string $constraint The constraint. * @param string|null $table_alias The table alias. Defaults to null. * * @return ORM */ public function full_outer_join( $table, $constraint, $table_alias = null ) { return $this->add_join_source( 'FULL OUTER', $table, $constraint, $table_alias ); } /** * Adds a HAVING condition to the query. Internal method. * * @param string $fragment The fragment. * @param array $values The values. Defaults to an empty array. * * @return ORM */ protected function add_having( $fragment, $values = [] ) { return $this->add_condition( 'having', $fragment, $values ); } /** * Adds a HAVING condition to the query. Internal method. * * @param string $column_name The table column. * @param string $separator The separator. * @param mixed $value The value. * * @return ORM */ protected function add_simple_having( $column_name, $separator, $value ) { return $this->add_simple_condition( 'having', $column_name, $separator, $value ); } /** * Adds a HAVING clause with multiple values (like IN and NOT IN). Internal method. * * @param string|array $column_name The table column. * @param string $separator The separator. * @param array $values The values. * * @return ORM */ public function add_having_placeholder( $column_name, $separator, $values ) { if ( ! \is_array( $column_name ) ) { $data = [ $column_name => $values ]; } else { $data = $column_name; } $result = $this; foreach ( $data as $key => $val ) { $column = $result->quote_identifier( $key ); $placeholders = $result->create_placeholders( $val ); $result = $result->add_having( "{$column} {$separator} ({$placeholders})", $val ); } return $result; } /** * Adds a HAVING clause with no parameters(like IS NULL and IS NOT NULL). Internal method. * * @param string $column_name The column name. * @param string $operator The operator. * * @return ORM */ public function add_having_no_value( $column_name, $operator ) { $conditions = \is_array( $column_name ) ? $column_name : [ $column_name ]; $result = $this; foreach ( $conditions as $column ) { $column = $this->quote_identifier( $column ); $result = $result->add_having( "{$column} {$operator}" ); } return $result; } /** * Adds a WHERE condition to the query. Internal method. * * @param string $fragment The fragment. * @param array $values The values. Defaults to an empty array. * * @return ORM */ protected function add_where( $fragment, $values = [] ) { return $this->add_condition( 'where', $fragment, $values ); } /** * Adds a WHERE condition to the query. Internal method. * * @param string|array $column_name The table column. * @param string $separator The separator. * @param mixed $value The value. * * @return ORM */ protected function add_simple_where( $column_name, $separator, $value ) { return $this->add_simple_condition( 'where', $column_name, $separator, $value ); } /** * Adds a WHERE clause with multiple values (like IN and NOT IN). * * @param string|array $column_name The table column. * @param string $separator The separator. * @param array $values The values. * * @return ORM */ public function add_where_placeholder( $column_name, $separator, $values ) { if ( ! \is_array( $column_name ) ) { $data = [ $column_name => $values ]; } else { $data = $column_name; } $result = $this; foreach ( $data as $key => $val ) { $column = $result->quote_identifier( $key ); $placeholders = $result->create_placeholders( $val ); $result = $result->add_where( "{$column} {$separator} ({$placeholders})", $val ); } return $result; } /** * Adds a WHERE clause with no parameters(like IS NULL and IS NOT NULL). * * @param string $column_name The column name. * @param string $operator The operator. * * @return ORM */ public function add_where_no_value( $column_name, $operator ) { $conditions = \is_array( $column_name ) ? $column_name : [ $column_name ]; $result = $this; foreach ( $conditions as $column ) { $column = $this->quote_identifier( $column ); $result = $result->add_where( "{$column} {$operator}" ); } return $result; } /** * Adds a HAVING or WHERE condition to the query. Internal method. * * @param string $type The type. * @param string $fragment The fragment. * @param array $values The values. Defaults to empty array. * * @return ORM */ protected function add_condition( $type, $fragment, $values = [] ) { $conditions_class_property_name = "{$type}_conditions"; if ( ! \is_array( $values ) ) { $values = [ $values ]; } \array_push( $this->{$conditions_class_property_name}, [ self::CONDITION_FRAGMENT => $fragment, self::CONDITION_VALUES => $values, ], ); return $this; } /** * Compiles a simple COLUMN SEPARATOR VALUE style HAVING or WHERE condition into a string and value ready to be * passed to the add_condition method. * * Avoids duplication of the call to quote_identifier. * If column_name is an associative array, it will add a condition for each column. * * @param string $type The type. * @param string|array $column_name The table column. * @param string $separator The separator. * @param mixed $value The value. * * @return ORM */ protected function add_simple_condition( $type, $column_name, $separator, $value ) { $multiple = \is_array( $column_name ) ? $column_name : [ $column_name => $value ]; $result = $this; foreach ( $multiple as $key => $val ) { // Add the table name in case of ambiguous columns. if ( \count( $result->join_sources ) > 0 && \strpos( $key, '.' ) === false ) { $table = $result->table_name; if ( $result->table_alias !== null ) { $table = $result->table_alias; } $key = "{$table}.{$key}"; } $key = $result->quote_identifier( $key ); $placeholder = ( $val === null ) ? 'NULL' : '%s'; $result = $result->add_condition( $type, "{$key} {$separator} {$placeholder}", $val ); } return $result; } /** * Returns a string containing the given number of question marks, separated by commas. Eg "?, ?, ?". * * @param array $fields Fields to create placeholder for. * * @return string */ protected function create_placeholders( $fields ) { if ( ! empty( $fields ) ) { $db_fields = []; foreach ( $fields as $key => $value ) { // Process expression fields directly into the query. if ( \array_key_exists( $key, $this->expr_fields ) ) { $db_fields[] = $value; } else { $db_fields[] = ( $value === null ) ? 'NULL' : '%s'; } } return \implode( ', ', $db_fields ); } return ''; } /** * Filters a column/value array returning only those columns that belong to a compound primary key. * * If the key contains a column that does not exist in the given array, a null value will be returned for it. * * @param mixed $value The value. * * @return array */ protected function get_compound_id_column_values( $value ) { $filtered = []; foreach ( $this->get_id_column_name() as $key ) { $filtered[ $key ] = ( $value[ $key ] ?? null ); } return $filtered; } /** * Filters an array containing compound column/value arrays. * * @param array $values The values. * * @return array */ protected function get_compound_id_column_values_array( $values ) { $filtered = []; foreach ( $values as $value ) { $filtered[] = $this->get_compound_id_column_values( $value ); } return $filtered; } /** * Add a WHERE column = value clause to your query. Each time this is called in the chain, an additional WHERE will * be added, and these will be ANDed together when the final query is built. * * If you use an array in $column_name, a new clause will be added for each element. In this case, $value is * ignored. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where( $column_name, $value = null ) { return $this->where_equal( $column_name, $value ); } /** * More explicitly named version of for the where() method. Can be used if preferred. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_equal( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '=', $value ); } /** * Add a WHERE column != value clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_not_equal( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '!=', $value ); } /** * Queries the table by its primary key. Special method. * * If primary key is compound, only the columns that belong to they key will be used for the query. * * @param string $id The ID. * * @return ORM */ public function where_id_is( $id ) { return \is_array( $this->get_id_column_name() ) ? $this->where( $this->get_compound_id_column_values( $id ), null ) : $this->where( $this->get_id_column_name(), $id ); } /** * Allows adding a WHERE clause that matches any of the conditions specified in the array. Each element in the * associative array will be a different condition, where the key will be the column name. * * By default, an equal operator will be used against all columns, but it can be overriden for any or every column * using the second parameter. * * Each condition will be ORed together when added to the final query. * * @param array $values The values. * @param string $operator The operator. * * @return ORM */ public function where_any_is( $values, $operator = '=' ) { $data = []; $query = [ '((' ]; $first = true; foreach ( $values as $value ) { if ( $first ) { $first = false; } else { $query[] = ') OR ('; } $firstsub = true; foreach ( $value as $key => $item ) { $op = \is_string( $operator ) ? $operator : ( $operator[ $key ] ?? '=' ); if ( $op === '=' && $item === null ) { $op = 'IS'; } if ( $firstsub ) { $firstsub = false; } else { $query[] = 'AND'; } $query[] = $this->quote_identifier( $key ); $data[] = $item; $query[] = $op; $query[] = ( ( $item === null ) ? 'NULL' : '%s' ); } } $query[] = '))'; return $this->where_raw( \implode( ' ', $query ), $data ); } /** * Queries the table by its primary key. * * Similar to where_id_is() but allowing multiple primary keys. * If primary key is compound, only the columns that belong to they key will be used for the query. * * @param string[] $ids The IDs. * * @return ORM */ public function where_id_in( $ids ) { return \is_array( $this->get_id_column_name() ) ? $this->where_any_is( $this->get_compound_id_column_values_array( $ids ) ) : $this->where_in( $this->get_id_column_name(), $ids ); } /** * Adds a WHERE ... LIKE clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_like( $column_name, $value = null ) { return $this->add_simple_where( $column_name, 'LIKE', $value ); } /** * Adds where WHERE ... NOT LIKE clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_not_like( $column_name, $value = null ) { return $this->add_simple_where( $column_name, 'NOT LIKE', $value ); } /** * Adds a WHERE ... > clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_gt( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '>', $value ); } /** * Adds a WHERE ... < clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_lt( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '<', $value ); } /** * Adds a WHERE ... >= clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_gte( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '>=', $value ); } /** * Adds a WHERE ... <= clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. Defaults to null. * * @return ORM */ public function where_lte( $column_name, $value = null ) { return $this->add_simple_where( $column_name, '<=', $value ); } /** * Adds a WHERE ... IN clause to your query. * * @param string|array $column_name The table column. * @param array $values The values. * * @return ORM */ public function where_in( $column_name, $values ) { return $this->add_where_placeholder( $column_name, 'IN', $values ); } /** * Adds a WHERE ... NOT IN clause to your query. * * @param string|array $column_name The table column. * @param array $values The values. * * @return ORM */ public function where_not_in( $column_name, $values ) { return $this->add_where_placeholder( $column_name, 'NOT IN', $values ); } /** * Adds a WHERE column IS NULL clause to your query. * * @param string|array $column_name The table column. * * @return ORM */ public function where_null( $column_name ) { return $this->add_where_no_value( $column_name, 'IS NULL' ); } /** * Adds a WHERE column IS NOT NULL clause to your query. * * @param string|array $column_name The table column. * * @return ORM */ public function where_not_null( $column_name ) { return $this->add_where_no_value( $column_name, 'IS NOT NULL' ); } /** * Adds a raw WHERE clause to the query. The clause should contain question mark placeholders, which will be bound * to the parameters supplied in the second argument. * * @param string $clause The clause that should contain question mark placeholders. * @param array $parameters The parameters to include in the query. * * @return ORM */ public function where_raw( $clause, $parameters = [] ) { return $this->add_where( $clause, $parameters ); } /** * Adds a LIMIT to the query. * * @param int $limit The limit. * * @return ORM */ public function limit( $limit ) { $this->limit = $limit; return $this; } /** * Adds an OFFSET to the query. * * @param int $offset The offset. * * @return ORM */ public function offset( $offset ) { $this->offset = $offset; return $this; } /** * Adds an ORDER BY clause to the query. * * @param string $column_name The column name. * @param string $ordering The ordering. DESC or ASC. * * @return ORM */ protected function add_order_by( $column_name, $ordering ) { $column_name = $this->quote_identifier( $column_name ); $this->order_by[] = "{$column_name} {$ordering}"; return $this; } /** * Adds an ORDER BY column DESC clause. * * @param string|array $column_name The table column. * * @return ORM */ public function order_by_desc( $column_name ) { return $this->add_order_by( $column_name, 'DESC' ); } /** * Adds an ORDER BY column ASC clause. * * @param string|array $column_name The table column. * * @return ORM */ public function order_by_asc( $column_name ) { return $this->add_order_by( $column_name, 'ASC' ); } /** * Adds an unquoted expression as an ORDER BY clause. * * @param string $clause The clause. * * @return ORM */ public function order_by_expr( $clause ) { $this->order_by[] = $clause; return $this; } /** * Adds a column to the list of columns to GROUP BY. * * @param string|array $column_name The table column. * * @return ORM */ public function group_by( $column_name ) { $column_name = $this->quote_identifier( $column_name ); $this->group_by[] = $column_name; return $this; } /** * Adds an unquoted expression to the list of columns to GROUP BY. * * @param string $expr The expression. * * @return ORM */ public function group_by_expr( $expr ) { $this->group_by[] = $expr; return $this; } /** * Adds a HAVING column = value clause to your query. * * Each time this is called in the chain, an additional HAVING will be added, and these will be ANDed together when * the final query is built. * * If you use an array in $column_name, a new clause will be added for each element. In this case, $value is * ignored. * * @param string|array $column_name The table column. * @param mixed|null $value The value. * * @return ORM */ public function having( $column_name, $value = null ) { return $this->having_equal( $column_name, $value ); } /** * Adds a having equal to your query. * * More explicitly named version of for the having() method. Can be used if preferred. * * @param string|array $column_name The table column. * @param mixed|null $value The value. * * @return ORM */ public function having_equal( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '=', $value ); } /** * Adds a HAVING column != value clause to your query. * * @param string|array $column_name The table column. * @param mixed|null $value The value. * * @return ORM */ public function having_not_equal( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '!=', $value ); } /** * Queries the table by its primary key. Special method. * * If primary key is compound, only the columns that belong to they key will be used for the query. * * @param string $id The ID. * * @return ORM */ public function having_id_is( $id ) { return \is_array( $this->get_id_column_name() ) ? $this->having( $this->get_compound_id_column_values( $id ), null ) : $this->having( $this->get_id_column_name(), $id ); } /** * Adds a HAVING ... LIKE clause to your query. * * @param string|array $column_name The table column. * @param string|null $value The value. * * @return ORM */ public function having_like( $column_name, $value = null ) { return $this->add_simple_having( $column_name, 'LIKE', $value ); } /** * Adds where HAVING ... NOT LIKE clause to your query. * * @param string|array $column_name The table column. * @param string|null $value The value. * * @return ORM */ public function having_not_like( $column_name, $value = null ) { return $this->add_simple_having( $column_name, 'NOT LIKE', $value ); } /** * Adds a HAVING ... > clause to your query. * * @param string|array $column_name The table column. * @param mixed $value The value. * * @return ORM */ public function having_gt( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '>', $value ); } /** * Adds a HAVING ... < clause to your query. * * @param string|array $column_name The table column. * @param mixed $value The value. * * @return ORM */ public function having_lt( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '<', $value ); } /** * Adds a HAVING ... >= clause to your query. * * @param string|array $column_name The table column. * @param mixed $value The value. Defaults to null. * * @return ORM */ public function having_gte( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '>=', $value ); } /** * Adds a HAVING ... <= clause to your query. * * @param string|array $column_name The table column. * @param mixed $value The value. * * @return ORM */ public function having_lte( $column_name, $value = null ) { return $this->add_simple_having( $column_name, '<=', $value ); } /** * Adds a HAVING ... IN clause to your query. * * @param string|array $column_name The table column. * @param array|null $values The values. Defaults to null. * * @return ORM */ public function having_in( $column_name, $values = null ) { return $this->add_having_placeholder( $column_name, 'IN', $values ); } /** * Adds a HAVING ... NOT IN clause to your query. * * @param string|array $column_name The table column. * @param array|null $values The values. Defaults to null. * * @return ORM */ public function having_not_in( $column_name, $values = null ) { return $this->add_having_placeholder( $column_name, 'NOT IN', $values ); } /** * Adds a HAVING column IS NULL clause to your query. * * @param string|array $column_name The table column. * * @return ORM */ public function having_null( $column_name ) { return $this->add_having_no_value( $column_name, 'IS NULL' ); } /** * Adds a HAVING column IS NOT NULL clause to your query. * * @param string|array $column_name The table column. * * @return ORM */ public function having_not_null( $column_name ) { return $this->add_having_no_value( $column_name, 'IS NOT NULL' ); } /** * Adds a raw HAVING clause to the query. The clause should contain question mark placeholders, which will be bound * to the parameters supplied in the second argument. * * @param string $clause The clause that should contain question mark placeholders. * @param array $parameters The parameters to include in the query. * * @return ORM */ public function having_raw( $clause, $parameters = [] ) { return $this->add_having( $clause, $parameters ); } /** * Builds a SELECT statement based on the clauses that have been passed to this instance by chaining method calls. * * @return string */ protected function build_select() { // If the query is raw, just set the $this->values to be the raw query parameters and return the raw query. if ( $this->is_raw_query ) { $this->values = $this->raw_parameters; return $this->raw_query; } // Build and return the full SELECT statement by concatenating the results of calling each separate builder method. return $this->join_if_not_empty( ' ', [ $this->build_select_start(), $this->build_join(), $this->build_where(), $this->build_group_by(), $this->build_having(), $this->build_order_by(), $this->build_limit(), $this->build_offset(), ], ); } /** * Builds the start of the SELECT statement. * * @return string */ protected function build_select_start() { $fragment = 'SELECT '; $result_columns = \implode( ', ', $this->result_columns ); if ( $this->distinct ) { $result_columns = 'DISTINCT ' . $result_columns; } $fragment .= "{$result_columns} FROM " . $this->quote_identifier( $this->table_name ); if ( $this->table_alias !== null ) { $fragment .= ' ' . $this->quote_identifier( $this->table_alias ); } return $fragment; } /** * Builds the JOIN sources. * * @return string */ protected function build_join() { if ( \count( $this->join_sources ) === 0 ) { return ''; } return \implode( ' ', $this->join_sources ); } /** * Builds the WHERE clause(s). * * @return string */ protected function build_where() { return $this->build_conditions( 'where' ); } /** * Build the HAVING clause(s) * * @return string */ protected function build_having() { return $this->build_conditions( 'having' ); } /** * Builds GROUP BY. * * @return string */ protected function build_group_by() { if ( \count( $this->group_by ) === 0 ) { return ''; } return 'GROUP BY ' . \implode( ', ', $this->group_by ); } /** * Builds a WHERE or HAVING clause. * * @param string $type Where or having. * * @return string */ protected function build_conditions( $type ) { $conditions_class_property_name = "{$type}_conditions"; // If there are no clauses, return empty string. if ( \count( $this->{$conditions_class_property_name} ) === 0 ) { return ''; } $conditions = []; foreach ( $this->{$conditions_class_property_name} as $condition ) { $conditions[] = $condition[ self::CONDITION_FRAGMENT ]; $this->values = \array_merge( $this->values, $condition[ self::CONDITION_VALUES ] ); } return \strtoupper( $type ) . ' ' . \implode( ' AND ', $conditions ); } /** * Builds ORDER BY. * * @return string */ protected function build_order_by() { if ( \count( $this->order_by ) === 0 ) { return ''; } return 'ORDER BY ' . \implode( ', ', $this->order_by ); } /** * Builds LIMIT. * * @return string */ protected function build_limit() { if ( $this->limit !== null ) { return "LIMIT {$this->limit}"; } return ''; } /** * Builds OFFSET. * * @return string */ protected function build_offset() { if ( $this->offset !== null ) { return 'OFFSET ' . $this->offset; } return ''; } /** * Joins strings if they are not empty. * * @param string $glue Glue. * @param string[] $pieces Pieces to join. * * @return string */ protected function join_if_not_empty( $glue, $pieces ) { $filtered_pieces = []; foreach ( $pieces as $piece ) { if ( \is_string( $piece ) ) { $piece = \trim( $piece ); } if ( ! empty( $piece ) ) { $filtered_pieces[] = $piece; } } return \implode( $glue, $filtered_pieces ); } /** * Quotes a string that is used as an identifier (table names, column names etc). * This method can also deal with dot-separated identifiers eg table.column. * * @param string|string[] $identifier One or more identifiers. * * @return string */ protected function quote_one_identifier( $identifier ) { $parts = \explode( '.', $identifier ); $parts = \array_map( [ $this, 'quote_identifier_part' ], $parts ); return \implode( '.', $parts ); } /** * Quotes a string that is used as an identifier (table names, column names etc) or an array containing multiple * identifiers. This method can also deal with dot-separated identifiers eg table.column. * * @param string|string[] $identifier One or more identifiers. * * @return string */ protected function quote_identifier( $identifier ) { if ( \is_array( $identifier ) ) { $result = \array_map( [ $this, 'quote_one_identifier' ], $identifier ); return \implode( ', ', $result ); } else { return $this->quote_one_identifier( $identifier ); } } /** * Quotes a single part of an identifier, using the identifier quote character specified in the config * (or autodetected). * * @param string $part The part to quote. * * @return string */ protected function quote_identifier_part( $part ) { if ( $part === '*' ) { return $part; } $quote_character = '`'; // Double up any identifier quotes to escape them. return $quote_character . \str_replace( $quote_character, $quote_character . $quote_character, $part ) . $quote_character; } /** * Executes the SELECT query that has been built up by chaining methods on this class. * Return an array of rows as associative arrays. * * @return array|false The result rows. False if the query failed. */ protected function run() { global $wpdb; $query = $this->build_select(); $success = self::execute( $query, $this->values ); if ( $success === false ) { // If the query fails run the migrations and try again. // Action is intentionally undocumented and should not be used by third-parties. \do_action( '_yoast_run_migrations' ); $success = self::execute( $query, $this->values ); } $this->reset_idiorm_state(); if ( $success === false ) { return false; } $rows = []; foreach ( $wpdb->last_result as $row ) { $rows[] = \get_object_vars( $row ); } return $rows; } /** * Resets the Idiorm instance state. * * @return void */ private function reset_idiorm_state() { $this->values = []; $this->result_columns = [ '*' ]; $this->using_default_result_columns = true; } /** * Returns the raw data wrapped by this ORM instance as an associative array. Column names may optionally be * supplied as arguments, if so, only those keys will be returned. * * @return array Associative array of the raw data. */ public function as_array() { if ( \func_num_args() === 0 ) { return $this->data; } $args = \func_get_args(); return \array_intersect_key( $this->data, \array_flip( $args ) ); } /** * Returns the value of a property of this object (database row) or null if not present. * * If a column-names array is passed, it will return a associative array with the value of each column or null if * it is not present. * * @param string|array $key Key. * * @return array|mixed|null */ public function get( $key ) { if ( \is_array( $key ) ) { $result = []; foreach ( $key as $column ) { $result[ $column ] = ( $this->data[ $column ] ?? null ); } return $result; } else { return ( $this->data[ $key ] ?? null ); } } /** * Returns the name of the column in the database table which contains the primary key ID of the row. * * @return string The primary key ID of the row. */ protected function get_id_column_name() { if ( $this->instance_id_column !== null ) { return $this->instance_id_column; } return 'id'; } /** * Gets the primary key ID of this object. * * @param bool $disallow_null Whether to allow null IDs. * * @return array|mixed|null * * @throws Exception Primary key ID contains null value(s). * @throws Exception Primary key ID missing from row or is null. */ public function id( $disallow_null = false ) { $id = $this->get( $this->get_id_column_name() ); if ( $disallow_null ) { if ( \is_array( $id ) ) { foreach ( $id as $id_part ) { if ( $id_part === null ) { throw new Exception( 'Primary key ID contains null value(s)' ); } } } elseif ( $id === null ) { throw new Exception( 'Primary key ID missing from row or is null' ); } } return $id; } /** * Sets a property to a particular value on this object. * * To set multiple properties at once, pass an associative array as the first parameter and leave out the second * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called. * * @param string|array $key Key. * @param string|null $value Value. * * @return ORM */ public function set( $key, $value = null ) { return $this->set_orm_property( $key, $value ); } /** * Set a property to a particular value on this object as expression. * * To set multiple properties at once, pass an associative array as the first parameter and leave out the second * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called. * * @param string|array $key Key. * @param string|null $value Value. * * @return ORM */ public function set_expr( $key, $value = null ) { return $this->set_orm_property( $key, $value, true ); } /** * Sets a property on the ORM object. * * @param string|array $key Key. * @param string|null $value Value. * @param bool $expr Expression. * * @return ORM */ protected function set_orm_property( $key, $value = null, $expr = false ) { if ( ! \is_array( $key ) ) { $key = [ $key => $value ]; } foreach ( $key as $field => $value ) { $this->data[ $field ] = $value; $this->dirty_fields[ $field ] = $value; if ( $expr === false && isset( $this->expr_fields[ $field ] ) ) { unset( $this->expr_fields[ $field ] ); } elseif ( $expr === true ) { $this->expr_fields[ $field ] = true; } } return $this; } /** * Checks whether the given field has been changed since this object was saved. * * @param mixed $key Key. * * @return bool */ public function is_dirty( $key ) { return \array_key_exists( $key, $this->dirty_fields ); } /** * Checks whether the model was the result of a call to create() or not. * * @return bool */ public function is_new() { return $this->is_new; } /** * Saves any fields which have been modified on this object to the database. * * @return bool True on success. * * @throws Exception Primary key ID contains null value(s). * @throws Exception Primary key ID missing from row or is null. */ public function save() { global $wpdb; // Remove any expression fields as they are already baked into the query. $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) ); if ( ! $this->is_new ) { // UPDATE. // If there are no dirty values, do nothing. if ( empty( $values ) && empty( $this->expr_fields ) ) { return true; } $query = \implode( ' ', [ $this->build_update(), $this->add_id_column_conditions() ] ); $id = $this->id( true ); if ( \is_array( $id ) ) { $values = \array_merge( $values, \array_values( $id ) ); } else { $values[] = $id; } } else { // INSERT. $query = $this->build_insert(); } $success = self::execute( $query, $values ); // If we've just inserted a new record, set the ID of this object. if ( $this->is_new ) { $this->is_new = false; if ( $this->count_null_id_columns() !== 0 ) { $column = $this->get_id_column_name(); // If the primary key is compound, assign the last inserted id to the first column. if ( \is_array( $column ) ) { $column = \reset( $column ); } // Explicitly cast to int to make dealing with Id's simpler. $this->data[ $column ] = (int) $wpdb->insert_id; } } $this->dirty_fields = []; $this->expr_fields = []; return $success; } /** * Extracts and gathers all dirty column names from the given model instances. * * @param array $models Array of model instances to be inserted. * * @return array The distinct set of columns that are dirty in at least one of the models. * * @throws InvalidArgumentException Instance to be inserted is not a new one. */ public function get_dirty_column_names( $models ) { $dirty_column_names = []; foreach ( $models as $model ) { if ( ! $model->orm->is_new() ) { throw new InvalidArgumentException( 'Instance to be inserted is not a new one' ); } // Remove any expression fields as they are already baked into the query. $dirty_fields = \array_diff_key( $model->orm->dirty_fields, $model->orm->expr_fields ); $dirty_column_names = \array_merge( $dirty_column_names, $dirty_fields ); } $dirty_column_names = \array_keys( $dirty_column_names ); return $dirty_column_names; } /** * Inserts multiple rows in a single query. Expects new rows as it's a strictly insert function, not an update one. * * @example From the Indexable_Link_Builder class: $this->seo_links_repository->query()->insert_many( $links ); * * @param array $models Array of model instances to be inserted. * * @return bool True for successful insert, false for failed. * * @throws InvalidArgumentException Invalid instances to be inserted. * @throws InvalidArgumentException Instance to be inserted is not a new one. */ public function insert_many( $models ) { // Validate the input first. if ( ! \is_array( $models ) ) { throw new InvalidArgumentException( 'Invalid instances to be inserted' ); } if ( empty( $models ) ) { return true; } $success = true; /** * Filter: 'wpseo_chunk_bulked_insert_queries' - Allow filtering the chunk size of each bulked INSERT query. * * @param int $chunk_size The chunk size of the bulked INSERT queries. */ $chunk = \apply_filters( 'wpseo_chunk_bulk_insert_queries', 100 ); $chunk = ! \is_int( $chunk ) ? 100 : $chunk; $chunk = ( $chunk <= 0 ) ? 100 : $chunk; $chunked_models = \array_chunk( $models, $chunk ); foreach ( $chunked_models as $models_chunk ) { $values = []; // First, we'll gather all the dirty fields throughout the models to be inserted. $dirty_column_names = $this->get_dirty_column_names( $models_chunk ); // Now, we're creating all dirty fields throughout the models and // setting them to null if they don't exist in each model. foreach ( $models_chunk as $model ) { $model_values = []; foreach ( $dirty_column_names as $dirty_column ) { // Set the value to null if it hasn't been set already. if ( ! isset( $model->orm->dirty_fields[ $dirty_column ] ) ) { $model->orm->dirty_fields[ $dirty_column ] = null; } // Only register the value if it is not null. if ( $model->orm->dirty_fields[ $dirty_column ] !== null ) { $model_values[] = $model->orm->dirty_fields[ $dirty_column ]; } } $values = \array_merge( $values, $model_values ); } // We now have the same set of dirty columns in all our models and also gathered all values. $query = $this->build_insert_many( $models_chunk, $dirty_column_names ); $success = $success && (bool) self::execute( $query, $values ); } return $success; } /** * Updates many records in the database. * * @return int|bool The number of rows changed if the query was succesful. False otherwise. */ public function update_many() { // Remove any expression fields as they are already baked into the query. $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) ); // UPDATE. // If there are no dirty values, do nothing. if ( empty( $values ) && empty( $this->expr_fields ) ) { return true; } $query = $this->join_if_not_empty( ' ', [ $this->build_update(), $this->build_where() ] ); $success = self::execute( $query, \array_merge( $values, $this->values ) ); $this->dirty_fields = []; $this->expr_fields = []; return $success; } /** * Adds a WHERE clause for every column that belongs to the primary key. * * @return string The where part of the query. */ public function add_id_column_conditions() { $query = []; $query[] = 'WHERE'; $keys = \is_array( $this->get_id_column_name() ) ? $this->get_id_column_name() : [ $this->get_id_column_name() ]; $first = true; foreach ( $keys as $key ) { if ( $first ) { $first = false; } else { $query[] = 'AND'; } $query[] = $this->quote_identifier( $key ); $query[] = '= %s'; } return \implode( ' ', $query ); } /** * Builds an UPDATE query. * * @return string The update query. */ protected function build_update() { $query = []; $query[] = "UPDATE {$this->quote_identifier($this->table_name)} SET"; $field_list = []; foreach ( $this->dirty_fields as $key => $value ) { if ( ! \array_key_exists( $key, $this->expr_fields ) ) { $value = ( $value === null ) ? 'NULL' : '%s'; } $field_list[] = "{$this->quote_identifier($key)} = {$value}"; } $query[] = \implode( ', ', $field_list ); return \implode( ' ', $query ); } /** * Builds an INSERT query. * * @return string The insert query. */ protected function build_insert() { $query = []; $query[] = 'INSERT INTO'; $query[] = $this->quote_identifier( $this->table_name ); $field_list = \array_map( [ $this, 'quote_identifier' ], \array_keys( $this->dirty_fields ) ); $query[] = '(' . \implode( ', ', $field_list ) . ')'; $query[] = 'VALUES'; $placeholders = $this->create_placeholders( $this->dirty_fields ); $query[] = "({$placeholders})"; return \implode( ' ', $query ); } /** * Builds a bulk INSERT query. * * @param array $models Array of model instances to be inserted. * @param array $dirty_column_names Array of dirty fields to be used in INSERT. * * @return string The insert query. */ protected function build_insert_many( $models, $dirty_column_names ) { $example_model = $models[0]; $total_placeholders = ''; $query = []; $query[] = 'INSERT INTO'; $query[] = $this->quote_identifier( $example_model->orm->table_name ); $field_list = \array_map( [ $this, 'quote_identifier' ], $dirty_column_names ); $query[] = '(' . \implode( ', ', $field_list ) . ')'; $query[] = 'VALUES'; // We assign placeholders per model for dirty fields that have values and NULL for dirty fields that don't. foreach ( $models as $model ) { $placeholder = []; foreach ( $dirty_column_names as $dirty_field ) { $placeholder[] = ( $model->orm->dirty_fields[ $dirty_field ] === null ) ? 'NULL' : '%s'; } $placeholders = \implode( ', ', $placeholder ); $total_placeholders .= "({$placeholders}),"; } $query[] = \rtrim( $total_placeholders, ',' ); return \implode( ' ', $query ); } /** * Deletes this record from the database. * * @return string The delete query. * * @throws Exception Primary key ID contains null value(s). * @throws Exception Primary key ID missing from row or is null. */ public function delete() { $query = [ 'DELETE FROM', $this->quote_identifier( $this->table_name ), $this->add_id_column_conditions() ]; return self::execute( \implode( ' ', $query ), \is_array( $this->id( true ) ) ? \array_values( $this->id( true ) ) : [ $this->id( true ) ] ); } /** * Deletes many records from the database. * * @return bool|int Response of wpdb::query. */ public function delete_many() { // Build and return the full DELETE statement by concatenating // the results of calling each separate builder method. $query = $this->join_if_not_empty( ' ', [ 'DELETE FROM', $this->quote_identifier( $this->table_name ), $this->build_where(), ], ); return self::execute( $query, $this->values ); } /* * --- ArrayAccess --- */ /** * Checks whether the data has the key. * * @param mixed $offset Key. * * @return bool Whether the data has the key. */ #[ReturnTypeWillChange] public function offsetExists( $offset ) { return \array_key_exists( $offset, $this->data ); } /** * Retrieves the value of the key. * * @param mixed $offset Key. * * @return array|mixed|null The value. */ #[ReturnTypeWillChange] public function offsetGet( $offset ) { return $this->get( $offset ); } /** * Sets the value of the key. * * @param string|int $offset Key. * @param mixed $value Value. * * @return void */ #[ReturnTypeWillChange] public function offsetSet( $offset, $value ) { if ( $offset === null ) { return; } $this->set( $offset, $value ); } /** * Removes the given key from the data. * * @param mixed $offset Key. * * @return void */ #[ReturnTypeWillChange] public function offsetUnset( $offset ) { unset( $this->data[ $offset ] ); unset( $this->dirty_fields[ $offset ] ); } /* * --- MAGIC METHODS --- */ /** * Handles magic get via offset. * * @param mixed $key Key. * * @return array|mixed|null The value in the offset. */ public function __get( $key ) { return $this->offsetGet( $key ); } /** * Handles magic set via offset. * * @param string|int $key Key. * @param mixed $value Value. * * @return void */ public function __set( $key, $value ) { $this->offsetSet( $key, $value ); } /** * Handles magic unset via offset. * * @param mixed $key Key. * * @return void */ public function __unset( $key ) { $this->offsetUnset( $key ); } /** * Handles magic isset via offset. * * @param mixed $key Key. * * @return bool Whether the offset has the key. */ public function __isset( $key ) { return $this->offsetExists( $key ); } } lib/model.php 0000644 00000055501 15174712003 0007131 0 ustar 00 <?php namespace Yoast\WP\Lib; use Exception; use JsonSerializable; use ReturnTypeWillChange; /** * Make Model compatible with WordPress. * * Model base class. Your model objects should extend * this class. A minimal subclass would look like: * * class Widget extends Model { * } */ class Model implements JsonSerializable { /** * Default ID column for all models. Can be overridden by adding * a public static $id_column property to your model classes. * * @var string */ public const DEFAULT_ID_COLUMN = 'id'; /** * Default foreign key suffix used by relationship methods. * * @var string */ public const DEFAULT_FOREIGN_KEY_SUFFIX = '_id'; /** * Set a prefix for model names. This can be a namespace or any other * abitrary prefix such as the PEAR naming convention. * * @example Model::$auto_prefix_models = 'MyProject_MyModels_'; //PEAR * @example Model::$auto_prefix_models = '\MyProject\MyModels\'; //Namespaces * * @var string */ public static $auto_prefix_models = '\Yoast\WP\SEO\Models\\'; /** * Set true to to ignore namespace information when computing table names * from class names. * * @example Model::$short_table_names = true; * @example Model::$short_table_names = false; // default * * @var bool */ public static $short_table_names = false; /** * The ORM instance used by this model instance to communicate with the database. * * @var ORM */ public $orm; /** * The table name for the implemented Model. * * @var string */ public static $table; /** * Whether or not this model uses timestamps. * * @var bool */ protected $uses_timestamps = false; /** * Which columns contain boolean values. * * @var array */ protected $boolean_columns = []; /** * Which columns contain int values. * * @var array */ protected $int_columns = []; /** * Which columns contain float values. * * @var array */ protected $float_columns = []; /** * Hacks around the Model to provide WordPress prefix to tables. * * @param string $class_name Type of Model to load. * @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix. * * @return ORM Wrapper to use. */ public static function of_type( $class_name, $yoast_prefix = true ) { // Prepend namespace to the class name. $class = static::$auto_prefix_models . $class_name; // Set the class variable to the custom value based on the WPDB prefix. $class::$table = static::get_table_name( $class_name, $yoast_prefix ); return static::factory( $class_name, null ); } /** * Creates a model without the Yoast prefix. * * @param string $class_name Type of Model to load. * * @return ORM */ public static function of_wp_type( $class_name ) { return static::of_type( $class_name, false ); } /** * Exposes method to get the table name to use. * * @param string $table_name Simple table name. * @param bool $yoast_prefix Optional. True to prefix the table name with the Yoast prefix. * * @return string Prepared full table name. */ public static function get_table_name( $table_name, $yoast_prefix = true ) { global $wpdb; // Allow the use of WordPress internal tables. if ( $yoast_prefix ) { $table_name = 'yoast_' . $table_name; } return $wpdb->prefix . \strtolower( $table_name ); } /** * Sets the table name for the given class name. * * @param string $class_name The class to set the table name for. * * @return void */ protected function set_table_name( $class_name ) { // Prepend namespace to the class name. $class = static::$auto_prefix_models . $class_name; $class::$table = static::get_table_name( $class_name ); } /** * Retrieve the value of a static property on a class. If the * class or the property does not exist, returns the default * value supplied as the third argument (which defaults to null). * * @param string $class_name The target class name. * @param string $property The property to get the value for. * @param mixed|null $default_value Default value when property does not exist. * * @return mixed|null The value of the property. */ protected static function get_static_property( $class_name, $property, $default_value = null ) { if ( ! \class_exists( $class_name ) || ! \property_exists( $class_name, $property ) ) { return $default_value; } if ( ! isset( $class_name::${$property} ) ) { return $default_value; } return $class_name::${$property}; } /** * Static method to get a table name given a class name. * If the supplied class has a public static property * named $table, the value of this property will be * returned. * * If not, the class name will be converted using * the class_name_to_table_name() method. * * If Model::$short_table_names == true or public static * property $table_use_short_name == true then $class_name passed * to class_name_to_table_name() is stripped of namespace information. * * @param string $class_name The class name to get the table name for. * * @return string The table name. */ protected static function get_table_name_for_class( $class_name ) { $specified_table_name = static::get_static_property( $class_name, 'table' ); $use_short_class_name = static::use_short_table_name( $class_name ); if ( $use_short_class_name ) { $exploded_class_name = \explode( '\\', $class_name ); $class_name = \end( $exploded_class_name ); } if ( $specified_table_name === null ) { return static::class_name_to_table_name( $class_name ); } return $specified_table_name; } /** * Should short table names, disregarding class namespaces, be computed? * * $class_property overrides $global_option, unless $class_property is null. * * @param string $class_name The class name to get short name for. * * @return bool True when short table name should be used. */ protected static function use_short_table_name( $class_name ) { $class_property = static::get_static_property( $class_name, 'table_use_short_name' ); if ( $class_property === null ) { return static::$short_table_names; } return $class_property; } /** * Convert a namespace to the standard PEAR underscore format. * * Then convert a class name in CapWords to a table name in * lowercase_with_underscores. * * Finally strip doubled up underscores. * * For example, CarTyre would be converted to car_tyre. And * Project\Models\CarTyre would be project_models_car_tyre. * * @param string $class_name The class name to get the table name for. * * @return string The table name. */ protected static function class_name_to_table_name( $class_name ) { $find = [ '/\\\\/', '/(?<=[a-z])([A-Z])/', '/__/', ]; $replacements = [ '_', '_$1', '_', ]; $class_name = \ltrim( $class_name, '\\' ); $class_name = \preg_replace( $find, $replacements, $class_name ); return \strtolower( $class_name ); } /** * Return the ID column name to use for this class. If it is * not set on the class, returns null. * * @param string $class_name The class name to get the ID column for. * * @return string|null The ID column name. */ protected static function get_id_column_name( $class_name ) { return static::get_static_property( $class_name, 'id_column', static::DEFAULT_ID_COLUMN ); } /** * Build a foreign key based on a table name. If the first argument * (the specified foreign key column name) is null, returns the second * argument (the name of the table) with the default foreign key column * suffix appended. * * @param string $specified_foreign_key_name The keyname to build. * @param string $table_name The table name to build the key name for. * * @return string The built foreign key name. */ protected static function build_foreign_key_name( $specified_foreign_key_name, $table_name ) { if ( $specified_foreign_key_name !== null ) { return $specified_foreign_key_name; } return $table_name . static::DEFAULT_FOREIGN_KEY_SUFFIX; } /** * Factory method used to acquire instances of the given class. * The class name should be supplied as a string, and the class * should already have been loaded by PHP (or a suitable autoloader * should exist). This method actually returns a wrapped ORM object * which allows a database query to be built. The wrapped ORM object is * responsible for returning instances of the correct class when * its find_one or find_many methods are called. * * @param string $class_name The target class name. * * @return ORM Instance of the ORM wrapper. */ public static function factory( $class_name ) { $class_name = static::$auto_prefix_models . $class_name; $table_name = static::get_table_name_for_class( $class_name ); $wrapper = ORM::for_table( $table_name ); $wrapper->set_class_name( $class_name ); $wrapper->use_id_column( static::get_id_column_name( $class_name ) ); return $wrapper; } /** * Internal method to construct the queries for both the has_one and * has_many methods. These two types of association are identical; the * only difference is whether find_one or find_many is used to complete * the method chain. * * @param string $associated_class_name The associated class name. * @param string|null $foreign_key_name The foreign key name in the associated table. * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table. * * @return ORM Instance of the ORM. * * @throws Exception When ID of current model has a null value. */ protected function has_one_or_many( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) { $base_table_name = static::get_table_name_for_class( static::class ); $foreign_key_name = static::build_foreign_key_name( $foreign_key_name, $base_table_name ); /* * Value of foreign_table.{$foreign_key_name} we're looking for. Where foreign_table is the actual * database table in the associated model. */ if ( $foreign_key_name_in_current_models_table === null ) { // Matches foreign_table.{$foreign_key_name} with the value of "{$this->table}.{$this->id()}". $where_value = $this->id(); } else { // Matches foreign_table.{$foreign_key_name} with "{$this->table}.{$foreign_key_name_in_current_models_table}". $where_value = $this->{$foreign_key_name_in_current_models_table}; } return static::factory( $associated_class_name )->where( $foreign_key_name, $where_value ); } /** * Helper method to manage one-to-one relations where the foreign * key is on the associated table. * * @param string $associated_class_name The associated class name. * @param string|null $foreign_key_name The foreign key name in the associated table. * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table. * * @return ORM Instance of the ORM. * * @throws Exception When ID of current model has a null value. */ protected function has_one( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) { return $this->has_one_or_many( $associated_class_name, $foreign_key_name, $foreign_key_name_in_current_models_table ); } /** * Helper method to manage one-to-many relations where the foreign * key is on the associated table. * * @param string $associated_class_name The associated class name. * @param string|null $foreign_key_name The foreign key name in the associated table. * @param string|null $foreign_key_name_in_current_models_table The foreign key in the current models table. * * @return ORM Instance of the ORM. * * @throws Exception When ID has a null value. */ protected function has_many( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_current_models_table = null ) { $this->set_table_name( $associated_class_name ); return $this->has_one_or_many( $associated_class_name, $foreign_key_name, $foreign_key_name_in_current_models_table ); } /** * Helper method to manage one-to-one and one-to-many relations where * the foreign key is on the base table. * * @param string $associated_class_name The associated class name. * @param string|null $foreign_key_name The foreign key in the current models table. * @param string|null $foreign_key_name_in_associated_models_table The foreign key in the associated table. * * @return $this|null Instance of the foreign model. */ protected function belongs_to( $associated_class_name, $foreign_key_name = null, $foreign_key_name_in_associated_models_table = null ) { $this->set_table_name( $associated_class_name ); $associated_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $associated_class_name ); $foreign_key_name = static::build_foreign_key_name( $foreign_key_name, $associated_table_name ); $associated_object_id = $this->{$foreign_key_name}; if ( $foreign_key_name_in_associated_models_table === null ) { /* * Comparison: "{$associated_table_name}.primary_key = {$associated_object_id}". * * NOTE: primary_key is a placeholder for the actual primary key column's name in $associated_table_name. */ return static::factory( $associated_class_name )->where_id_is( $associated_object_id ); } // Comparison: "{$associated_table_name}.{$foreign_key_name_in_associated_models_table} = {$associated_object_id}". return static::factory( $associated_class_name ) ->where( $foreign_key_name_in_associated_models_table, $associated_object_id ); } /** * Helper method to manage many-to-many relationships via an intermediate model. See * README for a full explanation of the parameters. * * @param string $associated_class_name The associated class name. * @param string|null $join_class_name The class name to join. * @param string|null $key_to_base_table The key to the the current models table. * @param string|null $key_to_associated_table The key to the associated table. * @param string|null $key_in_base_table The key in the current models table. * @param string|null $key_in_associated_table The key in the associated table. * * @return ORM Instance of the ORM. */ protected function has_many_through( $associated_class_name, $join_class_name = null, $key_to_base_table = null, $key_to_associated_table = null, $key_in_base_table = null, $key_in_associated_table = null ) { $base_class_name = static::class; /* * The class name of the join model, if not supplied, is formed by * concatenating the names of the base class and the associated class, * in alphabetical order. */ if ( $join_class_name === null ) { $base_model = \explode( '\\', $base_class_name ); $base_model_name = \end( $base_model ); if ( \strpos( $base_model_name, static::$auto_prefix_models ) === 0 ) { $base_model_name = \substr( $base_model_name, \strlen( static::$auto_prefix_models ), \strlen( $base_model_name ) ); } // Paris wasn't checking the name settings for the associated class. $associated_model = \explode( '\\', $associated_class_name ); $associated_model_name = \end( $associated_model ); if ( \strpos( $associated_model_name, static::$auto_prefix_models ) === 0 ) { $associated_model_name = \substr( $associated_model_name, \strlen( static::$auto_prefix_models ), \strlen( $associated_model_name ) ); } $class_names = [ $base_model_name, $associated_model_name ]; \sort( $class_names, \SORT_STRING ); $join_class_name = \implode( '', $class_names ); } // Get table names for each class. $base_table_name = static::get_table_name_for_class( $base_class_name ); $associated_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $associated_class_name ); $join_table_name = static::get_table_name_for_class( static::$auto_prefix_models . $join_class_name ); // Get ID column names. $base_table_id_column = ( $key_in_base_table === null ) ? static::get_id_column_name( $base_class_name ) : $key_in_base_table; $associated_table_id_column = ( $key_in_associated_table === null ) ? static::get_id_column_name( static::$auto_prefix_models . $associated_class_name ) : $key_in_associated_table; // Get the column names for each side of the join table. $key_to_base_table = static::build_foreign_key_name( $key_to_base_table, $base_table_name ); $key_to_associated_table = static::build_foreign_key_name( $key_to_associated_table, $associated_table_name ); /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Reason: This is commented out code. " SELECT {$associated_table_name}.* FROM {$associated_table_name} JOIN {$join_table_name} ON {$associated_table_name}.{$associated_table_id_column} = {$join_table_name}.{$key_to_associated_table} WHERE {$join_table_name}.{$key_to_base_table} = {$this->$base_table_id_column} ;" */ return static::factory( $associated_class_name ) ->select( "{$associated_table_name}.*" ) ->join( $join_table_name, [ "{$associated_table_name}.{$associated_table_id_column}", '=', "{$join_table_name}.{$key_to_associated_table}", ], ) ->where( "{$join_table_name}.{$key_to_base_table}", $this->{$base_table_id_column} ); } /** * Set the wrapped ORM instance associated with this Model instance. * * @param ORM $orm The ORM instance to set. * * @return void */ public function set_orm( $orm ) { $this->orm = $orm; } /** * Magic getter method, allows $model->property access to data. * * @param string $property The property to get. * * @return mixed The value of the property */ public function __get( $property ) { $value = $this->orm->get( $property ); if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) { return (bool) $value; } if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) { return (int) $value; } if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) { return (float) $value; } return $value; } /** * Magic setter method, allows $model->property = 'value' access to data. * * @param string $property The property to set. * @param string $value The value to set. * * @return void */ public function __set( $property, $value ) { if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) { $value = ( $value ) ? '1' : '0'; } if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) { $value = (string) $value; } if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) { $value = (string) $value; } $this->orm->set( $property, $value ); } /** * Magic unset method, allows unset($model->property) * * @param string $property The property to unset. * * @return void */ public function __unset( $property ) { $this->orm->__unset( $property ); } /** * JSON serializer. * * @return array The data of this object. */ #[ReturnTypeWillChange] public function jsonSerialize() { return $this->orm->as_array(); } /** * Strips all nested dependencies from the debug info. * * @return array */ public function __debugInfo() { if ( $this->orm ) { return $this->orm->as_array(); } return []; } /** * Magic isset method, allows isset($model->property) to work correctly. * * @param string $property The property to check. * * @return bool True when value is set. */ public function __isset( $property ) { return $this->orm->__isset( $property ); } /** * Getter method, allows $model->get('property') access to data * * @param string $property The property to get. * * @return string The value of a property. */ public function get( $property ) { return $this->orm->get( $property ); } /** * Setter method, allows $model->set('property', 'value') access to data. * * @param string|array $property The property to set. * @param string|null $value The value to give. * * @return static Current object. */ public function set( $property, $value = null ) { $this->orm->set( $property, $value ); return $this; } /** * Setter method, allows $model->set_expr('property', 'value') access to data. * * @param string|array $property The property to set. * @param string|null $value The value to give. * * @return static Current object. */ public function set_expr( $property, $value = null ) { $this->orm->set_expr( $property, $value ); return $this; } /** * Check whether the given property has changed since the object was created or saved. * * @param string $property The property to check. * * @return bool True when field is changed. */ public function is_dirty( $property ) { return $this->orm->is_dirty( $property ); } /** * Check whether the model was the result of a call to create() or not. * * @return bool True when is new. */ public function is_new() { return $this->orm->is_new(); } /** * Wrapper for Idiorm's as_array method. * * @return array The models data as array. */ public function as_array() { $args = \func_get_args(); return \call_user_func_array( [ $this->orm, 'as_array' ], $args ); } /** * Save the data associated with this model instance to the database. * * @return bool True on success. */ public function save() { if ( $this->uses_timestamps ) { if ( ! $this->created_at ) { $this->created_at = \gmdate( 'Y-m-d H:i:s' ); } $this->updated_at = \gmdate( 'Y-m-d H:i:s' ); } return $this->orm->save(); } /** * Delete the database row associated with this model instance. * * @return bool|int Response of wpdb::query. */ public function delete() { return $this->orm->delete(); } /** * Get the database ID of this model instance. * * @return int The database ID of the models instance. * * @throws Exception When the ID is a null value. */ public function id() { return $this->orm->id(); } /** * Hydrate this model instance with an associative array of data. * WARNING: The keys in the array MUST match with columns in the * corresponding database table. If any keys are supplied which * do not match up with columns, the database will throw an error. * * @param array $data The data to pass to the ORM. * * @return void */ public function hydrate( $data ) { $this->orm->hydrate( $data )->force_all_dirty(); } /** * Calls static methods directly on the ORM * * @param string $method The method to call. * @param array $arguments The arguments to use. * * @return array Result of the static call. */ public static function __callStatic( $method, $arguments ) { if ( ! \function_exists( 'get_called_class' ) ) { return []; } $model = static::factory( static::class ); return \call_user_func_array( [ $model, $method ], $arguments ); } } lib/abstract-main.php 0000644 00000010430 15174712003 0010546 0 ustar 00 <?php namespace Yoast\WP\Lib; use Exception; use WPSEO_Utils; use Yoast\WP\Lib\Dependency_Injection\Container_Registry; use Yoast\WP\SEO\Exceptions\Forbidden_Property_Mutation_Exception; use Yoast\WP\SEO\Loader; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface; /** * Abstract class to extend for the main class in a plugin. */ abstract class Abstract_Main { /** * The DI container. * * @var ContainerInterface|null */ protected $container; /** * A cache for previously requested and constructed surfaces. * * @var mixed[] */ private $cached_surfaces = []; /** * Loads the plugin. * * @return void * * @throws Exception If loading fails and YOAST_ENVIRONMENT is development. */ public function load() { if ( $this->container ) { return; } try { $this->container = $this->get_container(); Container_Registry::register( $this->get_name(), $this->container ); if ( ! $this->container ) { return; } if ( ! $this->container->has( Loader::class ) ) { return; } $this->container->get( Loader::class )->load(); } catch ( Exception $e ) { if ( $this->is_development() ) { throw $e; } // Don't crash the entire site, simply don't load. } } /** * Magic getter for retrieving a property from a surface. * * @param string $property The property to retrieve. * * @return mixed The value of the property. * * @throws Exception When the property doesn't exist. */ public function __get( $property ) { if ( \array_key_exists( $property, $this->cached_surfaces ) ) { return $this->cached_surfaces[ $property ]; } $surfaces = $this->get_surfaces(); if ( isset( $surfaces[ $property ] ) ) { $this->cached_surfaces[ $property ] = $this->container->get( $surfaces[ $property ] ); return $this->cached_surfaces[ $property ]; } throw new Exception( \sprintf( 'Property $%s does not exist.', $property ) ); } /** * Checks if the given property exists as a surface. * * @param string $property The property to retrieve. * * @return bool True when property is set. */ public function __isset( $property ) { if ( \array_key_exists( $property, $this->cached_surfaces ) ) { return true; } $surfaces = $this->get_surfaces(); if ( ! isset( $surfaces[ $property ] ) ) { return false; } return $this->container->has( $surfaces[ $property ] ); } /** * Prevents setting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * @param mixed $value The property value. * * @return void * * @throws Forbidden_Property_Mutation_Exception Set is never meant to be called. */ public function __set( $name, $value ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715. throw Forbidden_Property_Mutation_Exception::cannot_set_because_property_is_immutable( $name ); } /** * Prevents unsetting dynamic properties and unsetting declared properties * from an inaccessible context. * * @param string $name The property name. * * @return void * * @throws Forbidden_Property_Mutation_Exception Unset is never meant to be called. */ public function __unset( $name ) { throw Forbidden_Property_Mutation_Exception::cannot_unset_because_property_is_immutable( $name ); } /** * Loads the DI container. * * @return ContainerInterface|null The DI container. * * @throws Exception If something goes wrong generating the DI container. */ abstract protected function get_container(); /** * Gets the name of the plugin. * * @return string The name. */ abstract protected function get_name(); /** * Gets the surfaces of this plugin. * * @return array A mapping of surface name to the responsible class. */ abstract protected function get_surfaces(); /** * Returns whether or not we're in an environment for Yoast development. * * @return bool Whether or not to load in development mode. */ protected function is_development() { try { return WPSEO_Utils::is_development_mode(); } catch ( Exception $exception ) { // E.g. when WordPress and/or WordPress SEO are not loaded. return \defined( 'YOAST_ENVIRONMENT' ) && \YOAST_ENVIRONMENT === 'development'; } } } lib/dependency-injection/container-registry.php 0000644 00000004117 15174712003 0015754 0 ustar 00 <?php namespace Yoast\WP\Lib\Dependency_Injection; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; /** * Container_Registry class. */ class Container_Registry { /** * The registered containers. * * @var ContainerInterface[] */ private static $containers = []; /** * Register a container. * * @param string $name The name of the container. * @param ContainerInterface $container The container. * * @return void */ public static function register( $name, ContainerInterface $container ) { self::$containers[ $name ] = $container; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Get an instance from a specific container. * * @param string $name The name of the container. * @param string $id The ID of the service. * @param int $invalid_behaviour The behaviour when the service could not be found. * * @return object|null The service. * * @throws ServiceCircularReferenceException When a circular reference is detected. * @throws ServiceNotFoundException When the service is not defined. */ public static function get( $name, $id, $invalid_behaviour = 1 ) { if ( ! \array_key_exists( $name, self::$containers ) ) { if ( $invalid_behaviour === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE ) { throw new ServiceNotFoundException( $id ); } return null; } return self::$containers[ $name ]->get( $id, $invalid_behaviour ); } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Attempts to find a given service ID in all registered containers. * * @param string $id The service ID. * * @return string|null The name of the container if the service was found. */ public static function find( $id ) { foreach ( self::$containers as $name => $container ) { if ( $container->has( $id ) ) { return $name; } } } } lib/migrations/constants.php 0000644 00000001256 15174712003 0012217 0 ustar 00 <?php namespace Yoast\WP\Lib\Migrations; /** * Yoast migrations constants class. */ class Constants { public const MYSQL_MAX_IDENTIFIER_LENGTH = 64; public const SQL_UNKNOWN_QUERY_TYPE = 1; public const SQL_SELECT = 2; public const SQL_INSERT = 4; public const SQL_UPDATE = 8; public const SQL_DELETE = 16; public const SQL_ALTER = 32; public const SQL_DROP = 64; public const SQL_CREATE = 128; public const SQL_SHOW = 256; public const SQL_RENAME = 512; public const SQL_SET = 1024; } lib/migrations/table.php 0000644 00000014023 15174712003 0011266 0 ustar 00 <?php namespace Yoast\WP\Lib\Migrations; use Exception; /** * Yoast migrations table class. */ class Table { /** * The adapter. * * @var Adapter */ private $adapter; /** * The name * * @var string */ private $name; /** * The options * * @var array */ private $options; /** * The SQL representation of this table. * * @var string */ private $sql = ''; /** * Whether or not the table has been initialized. * * @var bool */ private $initialized = false; /** * The columns * * @var Column[] */ private $columns = []; /** * The primary keys. * * @var string[] */ private $primary_keys = []; /** * Whether or not to auto generate the id. * * @var bool */ private $auto_generate_id = true; /** * Creates an instance of the Adapter. * * @param Adapter $adapter The current adapter. * @param string $name The table name. * @param array $options The options. * * @throws Exception If invalid arguments are passed. */ public function __construct( $adapter, $name, $options = [] ) { // Sanity checks. if ( ! $adapter instanceof Adapter ) { throw new Exception( 'Invalid MySQL Adapter instance.' ); } if ( ! $name ) { throw new Exception( "Invalid 'name' parameter" ); } $this->adapter = $adapter; $this->name = $name; $this->options = $options; $this->init_sql( $name, $options ); if ( \array_key_exists( 'id', $options ) ) { if ( \is_bool( $options['id'] ) && $options['id'] === false ) { $this->auto_generate_id = false; } // If its a string then we want to auto-generate an integer-based // primary key with this name. if ( \is_string( $options['id'] ) ) { $this->auto_generate_id = true; $this->primary_keys[] = $options['id']; } } } /** * Create a column * * @param string $column_name The column name. * @param string $type The column type. * @param array $options The options. * * @return void */ public function column( $column_name, $type, $options = [] ) { // If there is already a column by the same name then silently fail and continue. foreach ( $this->columns as $column ) { if ( $column->name === $column_name ) { return; } } $column_options = []; if ( \array_key_exists( 'primary_key', $options ) ) { if ( $options['primary_key'] ) { $this->primary_keys[] = $column_name; } } if ( \array_key_exists( 'auto_increment', $options ) ) { if ( $options['auto_increment'] ) { $column_options['auto_increment'] = true; } } $column_options = \array_merge( $column_options, $options ); $column = new Column( $this->adapter, $column_name, $type, $column_options ); $this->columns[] = $column; } /** * Shortcut to create timestamps columns (default created_at, updated_at) * * @param string $created_column_name Created at column name. * @param string $updated_column_name Updated at column name. * * @return void */ public function timestamps( $created_column_name = 'created_at', $updated_column_name = 'updated_at' ) { $this->column( $created_column_name, 'datetime' ); $this->column( $updated_column_name, 'timestamp', [ 'null' => false, 'default' => 'CURRENT_TIMESTAMP', 'extra' => 'ON UPDATE CURRENT_TIMESTAMP', ], ); } /** * Get all primary keys * * @return string */ private function keys() { if ( \count( $this->primary_keys ) > 0 ) { $lead = ' PRIMARY KEY ('; $quoted = []; foreach ( $this->primary_keys as $key ) { $quoted[] = \sprintf( '%s', $this->adapter->identifier( $key ) ); } $primary_key_sql = ",\n" . $lead . \implode( ',', $quoted ) . ')'; return $primary_key_sql; } return ''; } /** * Table definition * * @param bool $wants_sql Whether or not to return SQL or execute the query. Defaults to false. * * @return bool|string * * @throws Exception If the table definition has not been intialized. */ public function finish( $wants_sql = false ) { if ( ! $this->initialized ) { throw new Exception( \sprintf( "Table Definition: '%s' has not been initialized", $this->name ) ); } $opt_str = ''; if ( \is_array( $this->options ) && \array_key_exists( 'options', $this->options ) ) { $opt_str = $this->options['options']; } elseif ( isset( $this->adapter->db_info['charset'] ) ) { $opt_str = ' DEFAULT CHARSET=' . $this->adapter->db_info['charset']; } else { $opt_str = ' DEFAULT CHARSET=utf8'; } $close_sql = \sprintf( ') %s;', $opt_str ); $create_table_sql = $this->sql; if ( $this->auto_generate_id === true ) { $this->primary_keys[] = 'id'; $primary_id = new Column( $this->adapter, 'id', 'integer', [ 'unsigned' => true, 'null' => false, 'auto_increment' => true, ], ); $create_table_sql .= $primary_id->to_sql() . ",\n"; } $create_table_sql .= $this->columns_to_str(); $create_table_sql .= $this->keys() . $close_sql; if ( $wants_sql ) { return $create_table_sql; } return $this->adapter->execute_ddl( $create_table_sql ); } /** * Get SQL for all columns. * * @return string The SQL. */ private function columns_to_str() { $fields = []; $len = \count( $this->columns ); for ( $i = 0; $i < $len; $i++ ) { $c = $this->columns[ $i ]; $fields[] = $c->__toString(); } return \implode( ",\n", $fields ); } /** * Init create sql statement. * * @param string $name The name. * @param array $options The options. * * @return void */ private function init_sql( $name, $options ) { // Are we forcing table creation? If so, drop it first. if ( \array_key_exists( 'force', $options ) && $options['force'] === true ) { $this->adapter->drop_table( $name ); } $temp = ''; if ( \array_key_exists( 'temporary', $options ) ) { $temp = ' TEMPORARY'; } $create_sql = \sprintf( 'CREATE%s TABLE ', $temp ); $create_sql .= \sprintf( "%s (\n", $this->adapter->identifier( $name ) ); $this->sql .= $create_sql; $this->initialized = true; } } lib/migrations/adapter.php 0000644 00000064764 15174712003 0011640 0 ustar 00 <?php namespace Yoast\WP\Lib\Migrations; use Exception; use Yoast\WP\Lib\Model; /** * Yoast migrations adapter class. */ class Adapter { /** * The version of this adapter. * * @var string */ private $version = '1.0'; /** * Whether or not a transaction has been started. * * @var bool */ private $in_transaction = false; /** * Returns the current database name. * * @return string */ public function get_database_name() { global $wpdb; return $wpdb->dbname; } /** * Checks support for migrations. * * @return bool */ public function supports_migrations() { return true; } /** * Returns all column native types. * * @return array */ public function native_database_types() { $types = [ 'primary_key' => [ 'name' => 'integer', 'limit' => 11, 'null' => false, ], 'string' => [ 'name' => 'varchar', 'limit' => 255, ], 'text' => [ 'name' => 'text' ], 'tinytext' => [ 'name' => 'tinytext' ], 'mediumtext' => [ 'name' => 'mediumtext' ], 'integer' => [ 'name' => 'int', 'limit' => 11, ], 'tinyinteger' => [ 'name' => 'tinyint' ], 'smallinteger' => [ 'name' => 'smallint' ], 'mediuminteger' => [ 'name' => 'mediumint' ], 'biginteger' => [ 'name' => 'bigint' ], 'float' => [ 'name' => 'float' ], 'decimal' => [ 'name' => 'decimal', 'scale' => 0, 'precision' => 10, ], 'datetime' => [ 'name' => 'datetime' ], 'timestamp' => [ 'name' => 'timestamp' ], 'time' => [ 'name' => 'time' ], 'date' => [ 'name' => 'date' ], 'binary' => [ 'name' => 'blob' ], 'tinybinary' => [ 'name' => 'tinyblob' ], 'mediumbinary' => [ 'name' => 'mediumblob' ], 'longbinary' => [ 'name' => 'longblob' ], 'boolean' => [ 'name' => 'tinyint', 'limit' => 1, ], 'enum' => [ 'name' => 'enum', 'values' => [], ], 'uuid' => [ 'name' => 'char', 'limit' => 36, ], 'char' => [ 'name' => 'char' ], ]; return $types; } /** * Checks if a table exists. * * @param string $table The table name. * * @return bool */ public function has_table( $table ) { return $this->table_exists( $table ); } /** * Allows overriding the hardcoded schema table name constant in case of parallel migrations. * * @return string */ public function get_schema_version_table_name() { return Model::get_table_name( 'migrations' ); } /** * Create the schema table, if necessary. * * @return void */ public function create_schema_version_table() { if ( ! $this->has_table( $this->get_schema_version_table_name() ) ) { $t = $this->create_table( $this->get_schema_version_table_name() ); $t->column( 'version', 'string', [ 'limit' => 191 ] ); $t->finish(); $this->add_index( $this->get_schema_version_table_name(), 'version', [ 'unique' => true ] ); } } /** * Starts a transaction. * * @return void */ public function start_transaction() { if ( $this->in_transaction() === false ) { $this->begin_transaction(); } } /** * Commits a transaction. * * @return void */ public function commit_transaction() { if ( $this->in_transaction() ) { $this->commit(); } } /** * Rollbacks a transaction. * * @return void */ public function rollback_transaction() { if ( $this->in_transaction() ) { $this->rollback(); } } /** * Quotes a table name string. * * @param string $text Table name. * * @return string */ public function quote_table( $text ) { return '`' . $text . '`'; } /** * Return the SQL definition of a column. * * @param string $column_name The column name. * @param string $type The type of the column. * @param array|null $options Column options. * * @return string */ public function column_definition( $column_name, $type, $options = null ) { $col = new Column( $this, $column_name, $type, $options ); return $col->__toString(); } /** * Checks if a database exists. * * @param string $database The database name. * * @return bool */ public function database_exists( $database ) { $ddl = 'SHOW DATABASES'; $result = $this->select_all( $ddl ); if ( \count( $result ) === 0 ) { return false; } foreach ( $result as $dbrow ) { if ( $dbrow['Database'] === $database ) { return true; } } return false; } /** * Creates a database. * * @param string $db The database name. * * @return bool */ public function create_database( $db ) { if ( $this->database_exists( $db ) ) { return false; } $ddl = \sprintf( 'CREATE DATABASE %s', $this->identifier( $db ) ); $result = $this->query( $ddl ); return $result === true; } /** * Drops a database. * * @param string $db The database name. * * @return bool */ public function drop_database( $db ) { if ( ! $this->database_exists( $db ) ) { return false; } $ddl = \sprintf( 'DROP DATABASE IF EXISTS %s', $this->identifier( $db ) ); $result = $this->query( $ddl ); return $result === true; } /** * Checks if a table exists. * * @param string $table The table name. * * @return bool */ public function table_exists( $table ) { global $wpdb; // We need last error to be clear so we can check against it easily. $previous_last_error = $wpdb->last_error; $previous_suppress_errors = $wpdb->suppress_errors; $wpdb->last_error = ''; $wpdb->suppress_errors = true; $result = $wpdb->query( "SELECT * FROM $table LIMIT 1" ); // Restore the last error, as this is not truly an error and we don't want to alarm people. $wpdb->last_error = $previous_last_error; $wpdb->suppress_errors = $previous_suppress_errors; return $result !== false; } /** * Wrapper to execute a query. * * @param string $query The query to run. * * @return bool */ public function execute( $query ) { return $this->query( $query ); } /** * Executes a query. * * @param string $query The query to run. * * @return bool Whether or not the query was performed succesfully. */ public function query( $query ) { global $wpdb; $query_type = $this->determine_query_type( $query ); $data = []; if ( $query_type === Constants::SQL_SELECT || $query_type === Constants::SQL_SHOW ) { $data = $wpdb->get_results( $query, \ARRAY_A ); if ( $data === false ) { return false; } return $data; } else { // INSERT, DELETE, etc... $result = $wpdb->query( $query ); if ( $result === false ) { return false; } if ( $query_type === Constants::SQL_INSERT ) { return $wpdb->insert_id; } return true; } } /** * Returns a single result for a query. * * @param string $query The query to run. * * @return array|false An associative array of the result. */ public function select_one( $query ) { global $wpdb; $query_type = $this->determine_query_type( $query ); if ( $query_type === Constants::SQL_SELECT || $query_type === Constants::SQL_SHOW ) { $result = $wpdb->query( $query ); if ( $result === false ) { return false; } return $wpdb->last_result[0]; } return false; } /** * Returns all results for a query. * * @param string $query The query to run. * * @return array An array of associative arrays. */ public function select_all( $query ) { return $this->query( $query ); } /** * Use this method for non-SELECT queries. * Or anything where you dont necessarily expect a result string, e.g. DROPs, CREATEs, etc. * * @param string $ddl The query to run. * * @return bool */ public function execute_ddl( $ddl ) { return $this->query( $ddl ); } /** * Drops a table * * @param string $table The table name. * * @return bool Whether or not the table was succesfully dropped. */ public function drop_table( $table ) { $ddl = \sprintf( 'DROP TABLE IF EXISTS %s', $this->identifier( $table ) ); return $this->query( $ddl ); } /** * Creates a table. * * @param string $table_name The table name. * @param array $options The options. * * @return Table */ public function create_table( $table_name, $options = [] ) { return new Table( $this, $table_name, $options ); } /** * Escapes a string for usage in queries. * * @param string $text The string. * * @return string */ public function quote_string( $text ) { global $wpdb; return $wpdb->_escape( $text ); } /** * Returns a quoted string. * * @param string $text The string. * * @return string */ public function identifier( $text ) { return '`' . $text . '`'; } /** * Renames a table. * * @param string $name The current table name. * @param string $new_name The new table name. * * @return bool */ public function rename_table( $name, $new_name ) { if ( empty( $name ) || empty( $new_name ) ) { return false; } $sql = \sprintf( 'RENAME TABLE %s TO %s', $this->identifier( $name ), $this->identifier( $new_name ) ); return $this->execute_ddl( $sql ); } /** * Adds a column. * * @param string $table_name The table name. * @param string $column_name The column name. * @param string $type The column type. * @param array $options Column options. * * @return bool */ public function add_column( $table_name, $column_name, $type, $options = [] ) { if ( empty( $table_name ) || empty( $column_name ) || empty( $type ) ) { return false; } // Default types. if ( ! \array_key_exists( 'limit', $options ) ) { $options['limit'] = null; } if ( ! \array_key_exists( 'precision', $options ) ) { $options['precision'] = null; } if ( ! \array_key_exists( 'scale', $options ) ) { $options['scale'] = null; } $sql = \sprintf( 'ALTER TABLE %s ADD `%s` %s', $this->identifier( $table_name ), $column_name, $this->type_to_sql( $type, $options ) ); $sql .= $this->add_column_options( $type, $options ); return $this->execute_ddl( $sql ); } /** * Drops a column. * * @param string $table_name The table name. * @param string $column_name The column name. * * @return bool */ public function remove_column( $table_name, $column_name ) { $sql = \sprintf( 'ALTER TABLE %s DROP COLUMN %s', $this->identifier( $table_name ), $this->identifier( $column_name ) ); return $this->execute_ddl( $sql ); } /** * Renames a column. * * @param string $table_name The table name. * @param string $column_name The column name. * @param string $new_column_name The new column name. * * @return bool */ public function rename_column( $table_name, $column_name, $new_column_name ) { if ( empty( $table_name ) || empty( $column_name ) || empty( $new_column_name ) ) { return false; } $column_info = $this->column_info( $table_name, $column_name ); $current_type = $column_info['type']; $sql = \sprintf( 'ALTER TABLE %s CHANGE %s %s %s', $this->identifier( $table_name ), $this->identifier( $column_name ), $this->identifier( $new_column_name ), $current_type ); $sql .= $this->add_column_options( $current_type, $column_info ); return $this->execute_ddl( $sql ); } /** * Changes a column. * * @param string $table_name The table name. * @param string $column_name The column name. * @param string $type The column type. * @param array $options Column options. * * @return bool */ public function change_column( $table_name, $column_name, $type, $options = [] ) { if ( empty( $table_name ) || empty( $column_name ) || empty( $type ) ) { return false; } $column_info = $this->column_info( $table_name, $column_name ); // Default types. if ( ! \array_key_exists( 'limit', $options ) ) { $options['limit'] = null; } if ( ! \array_key_exists( 'precision', $options ) ) { $options['precision'] = null; } if ( ! \array_key_exists( 'scale', $options ) ) { $options['scale'] = null; } $sql = \sprintf( 'ALTER TABLE `%s` CHANGE `%s` `%s` %s', $table_name, $column_name, $column_name, $this->type_to_sql( $type, $options ) ); $sql .= $this->add_column_options( $type, $options ); return $this->execute_ddl( $sql ); } /** * Returns the database information for a column. * * @param string $table The table name. * @param string $column The column name. * * @return array|null */ public function column_info( $table, $column ) { if ( empty( $table ) || empty( $column ) ) { return null; } try { $sql = \sprintf( "SHOW FULL COLUMNS FROM %s LIKE '%s'", $this->identifier( $table ), $column ); $result = $this->select_one( $sql ); if ( \is_array( $result ) ) { $result = \array_change_key_case( $result, \CASE_LOWER ); } return $result; } catch ( Exception $e ) { return null; } } /** * Adds an index. * * @param string $table_name The table name. * @param array|string $column_name The column name(s). * @param array $options Index options. * * @return bool */ public function add_index( $table_name, $column_name, $options = [] ) { if ( empty( $table_name ) || empty( $column_name ) ) { return false; } // Unique index? if ( \is_array( $options ) && \array_key_exists( 'unique', $options ) && $options['unique'] === true ) { $unique = true; } else { $unique = false; } // Did the user specify an index name? if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) { $index_name = $options['name']; } else { $index_name = $this->get_index_name( $table_name, $column_name ); } if ( \strlen( $index_name ) > Constants::MYSQL_MAX_IDENTIFIER_LENGTH ) { return false; } if ( ! \is_array( $column_name ) ) { $column_names = [ $column_name ]; } else { $column_names = $column_name; } $cols = []; foreach ( $column_names as $name ) { $cols[] = $this->identifier( $name ); } $sql = \sprintf( 'CREATE %sINDEX %s ON %s(%s)', ( $unique === true ) ? 'UNIQUE ' : '', $this->identifier( $index_name ), $this->identifier( $table_name ), \implode( ', ', $cols ), ); return $this->execute_ddl( $sql ); } /** * Drops an index. * * @param string $table_name The table name. * @param array|string $column_name The column name(s). * @param array $options Index options. * * @return bool */ public function remove_index( $table_name, $column_name, $options = [] ) { if ( empty( $table_name ) || empty( $column_name ) ) { return false; } // Did the user specify an index name? if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) { $index_name = $options['name']; } else { $index_name = $this->get_index_name( $table_name, $column_name ); } $sql = \sprintf( 'DROP INDEX %s ON %s', $this->identifier( $index_name ), $this->identifier( $table_name ) ); return $this->execute_ddl( $sql ); } /** * Adds timestamps. * * @param string $table_name The table name. * @param string $created_column_name Created at column name. * @param string $updated_column_name Updated at column name. * * @return bool */ public function add_timestamps( $table_name, $created_column_name, $updated_column_name ) { if ( empty( $table_name ) || empty( $created_column_name ) || empty( $updated_column_name ) ) { return false; } $created_at = $this->add_column( $table_name, $created_column_name, 'datetime' ); $updated_at = $this->add_column( $table_name, $updated_column_name, 'timestamp', [ 'null' => false, 'default' => 'CURRENT_TIMESTAMP', 'extra' => 'ON UPDATE CURRENT_TIMESTAMP', ], ); return $created_at && $updated_at; } /** * Removes timestamps. * * @param string $table_name The table name. * @param string $created_column_name Created at column name. * @param string $updated_column_name Updated at column name. * * @return bool Whether or not the timestamps were removed. */ public function remove_timestamps( $table_name, $created_column_name, $updated_column_name ) { if ( empty( $table_name ) || empty( $created_column_name ) || empty( $updated_column_name ) ) { return false; } $updated_at = $this->remove_column( $table_name, $created_column_name ); $created_at = $this->remove_column( $table_name, $updated_column_name ); return $created_at && $updated_at; } /** * Checks an index. * * @param string $table_name The table name. * @param array|string $column_name The column name(s). * @param array $options Index options. * * @return bool Whether or not the index exists. */ public function has_index( $table_name, $column_name, $options = [] ) { if ( empty( $table_name ) || empty( $column_name ) ) { return false; } // Did the user specify an index name? if ( \is_array( $options ) && \array_key_exists( 'name', $options ) ) { $index_name = $options['name']; } else { $index_name = $this->get_index_name( $table_name, $column_name ); } $indexes = $this->indexes( $table_name ); foreach ( $indexes as $idx ) { if ( $idx['name'] === $index_name ) { return true; } } return false; } /** * Returns all indexes of a table. * * @param string $table_name The table name. * * @return array */ public function indexes( $table_name ) { $sql = \sprintf( 'SHOW KEYS FROM %s', $this->identifier( $table_name ) ); $result = $this->select_all( $sql ); $indexes = []; foreach ( $result as $row ) { // Skip primary. if ( $row['Key_name'] === 'PRIMARY' ) { continue; } $indexes[] = [ 'name' => $row['Key_name'], 'unique' => (int) $row['Non_unique'] === 0, ]; } return $indexes; } /** * Converts a type to sql. Default options: * $limit = null, $precision = null, $scale = null * * @param string $type The native type. * @param array $options The options. * * @return string The SQL type. * * @throws Exception If invalid arguments are supplied. */ public function type_to_sql( $type, $options = [] ) { $natives = $this->native_database_types(); if ( ! \array_key_exists( $type, $natives ) ) { $error = \sprintf( "Error:I dont know what column type of '%s' maps to for MySQL.", $type ); $error .= "\nYou provided: {$type}\n"; $error .= "Valid types are: \n"; $types = \array_keys( $natives ); foreach ( $types as $t ) { if ( $t === 'primary_key' ) { continue; } $error .= "\t{$t}\n"; } throw new Exception( $error ); } $scale = null; $precision = null; $limit = null; if ( isset( $options['precision'] ) ) { $precision = $options['precision']; } if ( isset( $options['scale'] ) ) { $scale = $options['scale']; } if ( isset( $options['limit'] ) ) { $limit = $options['limit']; } if ( isset( $options['values'] ) ) { $values = $options['values']; } $native_type = $natives[ $type ]; if ( \is_array( $native_type ) && \array_key_exists( 'name', $native_type ) ) { $column_type_sql = $native_type['name']; } else { return $native_type; } if ( $type === 'decimal' || $type === 'float' ) { // Ignore limit, use precison and scale. if ( $precision === null && \array_key_exists( 'precision', $native_type ) ) { $precision = $native_type['precision']; } if ( $scale === null && \array_key_exists( 'scale', $native_type ) ) { $scale = $native_type['scale']; } if ( $precision !== null ) { if ( \is_int( $scale ) ) { $column_type_sql .= \sprintf( '(%d, %d)', $precision, $scale ); } else { $column_type_sql .= \sprintf( '(%d)', $precision ); } } elseif ( $scale ) { throw new Exception( "Error adding $type column: precision cannot be empty if scale is specified" ); } } elseif ( $type === 'enum' ) { if ( empty( $values ) ) { throw new Exception( 'Error adding enum column: there must be at least one value defined' ); } else { $column_type_sql .= \sprintf( "('%s')", \implode( "','", \array_map( [ $this, 'quote_string' ], $values ) ), ); } } // Not a decimal column. if ( $limit === null && \array_key_exists( 'limit', $native_type ) ) { $limit = $native_type['limit']; } if ( $limit ) { $column_type_sql .= \sprintf( '(%d)', $limit ); } return $column_type_sql; } /** * Adds column options. * * @param string $type The native type. * @param array $options The options. * * @return string The SQL statement for the column options. * * @throws Exception If invalid arguments are supplied. */ public function add_column_options( $type, $options ) { $sql = ''; if ( ! \is_array( $options ) ) { return $sql; } if ( \array_key_exists( 'unsigned', $options ) && $options['unsigned'] === true ) { $sql .= ' UNSIGNED'; } if ( \array_key_exists( 'character', $options ) ) { $sql .= \sprintf( ' CHARACTER SET %s', $this->identifier( $options['character'] ) ); } if ( \array_key_exists( 'collate', $options ) ) { $sql .= \sprintf( ' COLLATE %s', $this->identifier( $options['collate'] ) ); } if ( \array_key_exists( 'auto_increment', $options ) && $options['auto_increment'] === true ) { $sql .= ' auto_increment'; } if ( \array_key_exists( 'default', $options ) && $options['default'] !== null ) { if ( $this->is_sql_method_call( $options['default'] ) ) { throw new Exception( 'MySQL does not support function calls as default values, constants only.' ); } if ( \is_int( $options['default'] ) ) { $default_format = '%d'; } elseif ( \is_bool( $options['default'] ) ) { $default_format = "'%d'"; } elseif ( $options['default'] === 'CURRENT_TIMESTAMP' ) { $default_format = '%s'; } else { $default_format = "'%s'"; } $default_value = \sprintf( $default_format, $options['default'] ); $sql .= \sprintf( ' DEFAULT %s', $default_value ); } if ( \array_key_exists( 'null', $options ) ) { if ( $options['null'] === false || $options['null'] === 'NO' ) { $sql .= ' NOT NULL'; } elseif ( $type === 'timestamp' ) { $sql .= ' NULL'; } } if ( \array_key_exists( 'comment', $options ) ) { $sql .= \sprintf( " COMMENT '%s'", $this->quote_string( $options['comment'] ) ); } if ( \array_key_exists( 'extra', $options ) ) { $sql .= \sprintf( ' %s', $this->quote_string( $options['extra'] ) ); } if ( \array_key_exists( 'after', $options ) ) { $sql .= \sprintf( ' AFTER %s', $this->identifier( $options['after'] ) ); } return $sql; } /** * Returns a list of all versions that have been migrated. * * @return string[] The version numbers that have been migrated. */ public function get_migrated_versions() { $result = $this->select_all( \sprintf( 'SELECT version FROM %s', $this->get_schema_version_table_name() ) ); return \array_column( $result, 'version' ); } /** * Adds a migrated version. * * @param string $version The version. * * @return bool Whether or not the version was succesfully set. */ public function add_version( $version ) { $sql = \sprintf( "INSERT INTO %s (version) VALUES ('%s')", $this->get_schema_version_table_name(), $version ); return $this->execute_ddl( $sql ); } /** * Removes a migrated version. * * @param string $version The version. * * @return bool Whether or not the version was succesfully removed. */ public function remove_version( $version ) { $sql = \sprintf( "DELETE FROM %s WHERE version = '%s'", $this->get_schema_version_table_name(), $version ); return $this->execute_ddl( $sql ); } /** * Returns a message displaying the current version * * @return string */ public function __toString() { return self::class . ', version ' . $this->version; } /** * Returns an index name. * * @param string $table_name The table name. * @param string $column_name The column name. * * @return string The index name. */ private function get_index_name( $table_name, $column_name ) { $name = \preg_replace( '/\\W/', '_', $table_name ); $name = \preg_replace( '/\\_{2,}/', '_', $name ); // If the column parameter is an array then the user wants to create a multi-column index. if ( \is_array( $column_name ) ) { $column_str = \implode( '_and_', $column_name ); } else { $column_str = $column_name; } $name .= \sprintf( '_%s', $column_str ); return $name; } /** * Returns the type of a query. * * @param string $query The query to run. * * @return int The query type. */ private function determine_query_type( $query ) { $query = \strtolower( \trim( $query ) ); $match = []; \preg_match( '/^(\\w)*/i', $query, $match ); $type = $match[0]; switch ( $type ) { case 'select': return Constants::SQL_SELECT; case 'update': return Constants::SQL_UPDATE; case 'delete': return Constants::SQL_DELETE; case 'insert': return Constants::SQL_INSERT; case 'alter': return Constants::SQL_ALTER; case 'drop': return Constants::SQL_DROP; case 'create': return Constants::SQL_CREATE; case 'show': return Constants::SQL_SHOW; case 'rename': return Constants::SQL_RENAME; case 'set': return Constants::SQL_SET; default: return Constants::SQL_UNKNOWN_QUERY_TYPE; } } /** * Detect whether or not the string represents a function call and if so * do not wrap it in single-quotes, otherwise do wrap in single quotes. * * @param string $text The string. * * @return bool Whether or not it's a SQL function call. */ private function is_sql_method_call( $text ) { $text = \trim( $text ); if ( \substr( $text, -2, 2 ) === '()' ) { return true; } return false; } /** * Checks if a transaction is active. * * @return bool */ private function in_transaction() { return $this->in_transaction; } /** * Starts a transaction. * * @return void * * @throws Exception If a transaction was already started. */ private function begin_transaction() { global $wpdb; if ( $this->in_transaction === true ) { throw new Exception( 'Transaction already started' ); } $wpdb->query( 'START TRANSACTION' ); $this->in_transaction = true; } /** * Commits a transaction. * * @return void * * @throws Exception If no transaction was strated. */ private function commit() { global $wpdb; if ( $this->in_transaction === false ) { throw new Exception( 'Transaction not started' ); } $wpdb->query( 'COMMIT' ); $this->in_transaction = false; } /** * Rollbacks a transaction. * * @return void * * @throws Exception If no transaction was started. */ private function rollback() { global $wpdb; if ( $this->in_transaction === false ) { throw new Exception( 'Transaction not started' ); } $wpdb->query( 'ROLLBACK' ); $this->in_transaction = false; } } lib/migrations/column.php 0000644 00000003465 15174712003 0011504 0 ustar 00 <?php namespace Yoast\WP\Lib\Migrations; use Exception; /** * Yoast migrations column class. */ class Column { /** * The adapter. * * @var Adapter */ private $adapter; /** * The name. * * @var string */ public $name; /** * The type. * * @var mixed */ public $type; /** * The properties. * * @var mixed */ public $properties; /** * The options. * * @var array */ private $options = []; /** * Creates an instance of a column. * * @param Adapter $adapter The current adapter. * @param string $name The name of the column. * @param string $type The type of the column. * @param array $options The column options. * * @throws Exception If invalid arguments provided. */ public function __construct( $adapter, $name, $type, $options = [] ) { if ( ! $adapter instanceof Adapter ) { throw new Exception( 'Invalid Adapter instance.' ); } if ( empty( $name ) || ! \is_string( $name ) ) { throw new Exception( "Invalid 'name' parameter" ); } if ( empty( $type ) || ! \is_string( $type ) ) { throw new Exception( "Invalid 'type' parameter" ); } $this->adapter = $adapter; $this->name = $name; $this->type = $type; $this->options = $options; } /** * Returns the SQL of this column. * * @return string */ public function to_sql() { $column_sql = \sprintf( '%s %s', $this->adapter->identifier( $this->name ), $this->sql_type() ); $column_sql .= $this->adapter->add_column_options( $this->type, $this->options ); return $column_sql; } /** * The SQL string version. * * @return string */ public function __toString() { return $this->to_sql(); } /** * The SQL type. * * @return string */ private function sql_type() { return $this->adapter->type_to_sql( $this->type, $this->options ); } } lib/migrations/migration.php 0000644 00000014354 15174712003 0012177 0 ustar 00 <?php namespace Yoast\WP\Lib\Migrations; /** * Base migration class. */ abstract class Migration { /** * The plugin this migration belongs to. * * @var string */ public static $plugin = 'unknown'; /** * The adapter. * * @var Adapter */ private $adapter; /** * Performs the migration. * * @return void */ abstract public function up(); /** * Reverts the migration. * * @return void */ abstract public function down(); /** * Creates a new migration. * * @param Adapter $adapter The current adapter. */ public function __construct( Adapter $adapter ) { $this->set_adapter( $adapter ); } /** * Sets an adapter. * * @param Adapter $adapter The adapter to set. * * @return $this|null */ public function set_adapter( $adapter ) { if ( ! $adapter instanceof Adapter ) { return; } $this->adapter = $adapter; return $this; } /** * Returns the current adapter. * * @return object */ public function get_adapter() { return $this->adapter; } /** * Creates a database. * * @param string $name The name of the database. * @param array|null $options The options. * * @return bool */ public function create_database( $name, $options = null ) { return $this->adapter->create_database( $name, $options ); } /** * Drops a database. * * @param string $name The name of the database. * * @return bool */ public function drop_database( $name ) { return $this->adapter->drop_database( $name ); } /** * Drops a table. * * @param string $table_name The name of the table. * * @return bool */ public function drop_table( $table_name ) { return $this->adapter->drop_table( $table_name ); } /** * Renames a table. * * @param string $name The name of the table. * @param string $new_name The new name of the table. * * @return bool */ public function rename_table( $name, $new_name ) { return $this->adapter->rename_table( $name, $new_name ); } /** * Renames a column. * * @param string $table_name The name of the table. * @param string $column_name The column name. * @param string $new_column_name The new column name. * * @return bool */ public function rename_column( $table_name, $column_name, $new_column_name ) { return $this->adapter->rename_column( $table_name, $column_name, $new_column_name ); } /** * Adds a column. * * @param string $table_name The name of the table. * @param string $column_name The column name. * @param string $type The column type. * @param array|string $options The options. * * @return bool */ public function add_column( $table_name, $column_name, $type, $options = [] ) { return $this->adapter->add_column( $table_name, $column_name, $type, $options ); } /** * Removes a column. * * @param string $table_name The name of the table. * @param string $column_name The column name. * * @return bool */ public function remove_column( $table_name, $column_name ) { return $this->adapter->remove_column( $table_name, $column_name ); } /** * Changes a column. * * @param string $table_name The name of the table. * @param string $column_name The column name. * @param string $type The column type. * @param array|string $options The options. * * @return bool */ public function change_column( $table_name, $column_name, $type, $options = [] ) { return $this->adapter->change_column( $table_name, $column_name, $type, $options ); } /** * Adds an index. * * @param string $table_name The name of the table. * @param array|string $column_name The column name. * @param array|string $options The options. * * @return bool */ public function add_index( $table_name, $column_name, $options = [] ) { return $this->adapter->add_index( $table_name, $column_name, $options ); } /** * Removes an index. * * @param string $table_name The name of the table. * @param array|string $column_name The column name. * @param array|string $options The options. * * @return bool */ public function remove_index( $table_name, $column_name, $options = [] ) { return $this->adapter->remove_index( $table_name, $column_name, $options ); } /** * Adds timestamps. * * @param string $table_name The name of the table. * @param string $created_column_name Created at column name. * @param string $updated_column_name Updated at column name. * * @return bool */ public function add_timestamps( $table_name, $created_column_name = 'created_at', $updated_column_name = 'updated_at' ) { return $this->adapter->add_timestamps( $table_name, $created_column_name, $updated_column_name ); } /** * Removes timestamps. * * @param string $table_name The name of the table. * @param string $created_column_name Created at column name. * @param string $updated_column_name Updated at column name. * * @return bool */ public function remove_timestamps( $table_name, $created_column_name = 'created_at', $updated_column_name = 'updated_at' ) { return $this->adapter->remove_timestamps( $table_name, $created_column_name, $updated_column_name ); } /** * Creates a table. * * @param string $table_name The name of the table. * @param array|string $options The options. * * @return bool|Table */ public function create_table( $table_name, $options = [] ) { return $this->adapter->create_table( $table_name, $options ); } /** * Execute a query and return the first result. * * @param string $sql The query to run. * * @return array */ public function select_one( $sql ) { return $this->adapter->select_one( $sql ); } /** * Execute a query and return all results. * * @param string $sql The query to run. * * @return array */ public function select_all( $sql ) { return $this->adapter->select_all( $sql ); } /** * Execute a query. * * @param string $sql The query to run. * * @return bool */ public function query( $sql ) { return $this->adapter->query( $sql ); } /** * Returns a quoted string. * * @param string $str The string to quote. * * @return string */ public function quote_string( $str ) { return $this->adapter->quote_string( $str ); } } inc/class-wpseo-rank.php 0000644 00000016610 15174712003 0011223 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Holder for SEO Rank information. */ class WPSEO_Rank { /** * Constant used for determining a bad SEO rating. * * @var string */ public const BAD = 'bad'; /** * Constant used for determining an OK SEO rating. * * @var string */ public const OK = 'ok'; /** * Constant used for determining a good SEO rating. * * @var string */ public const GOOD = 'good'; /** * Constant used for determining that no focus keyphrase is set. * * @var string */ public const NO_FOCUS = 'na'; /** * Constant used for determining that this content is not indexed. * * @var string */ public const NO_INDEX = 'noindex'; /** * All possible ranks. * * @var array */ protected static $ranks = [ self::BAD, self::OK, self::GOOD, self::NO_FOCUS, self::NO_INDEX, ]; /** * Holds the translation from seo score slug to actual score range. * * @var array */ protected static $ranges = [ self::NO_FOCUS => [ 'start' => 0, 'end' => 0, ], self::BAD => [ 'start' => 1, 'end' => 40, ], self::OK => [ 'start' => 41, 'end' => 70, ], self::GOOD => [ 'start' => 71, 'end' => 100, ], ]; /** * The current rank. * * @var int */ protected $rank; /** * WPSEO_Rank constructor. * * @param int $rank The actual rank. */ public function __construct( $rank ) { if ( ! in_array( $rank, self::$ranks, true ) ) { $rank = self::BAD; } $this->rank = $rank; } /** * Returns the saved rank for this rank. * * @return string */ public function get_rank() { return $this->rank; } /** * Returns a CSS class for this rank. * * @return string */ public function get_css_class() { $labels = [ self::NO_FOCUS => 'na', self::NO_INDEX => 'noindex', self::BAD => 'bad', self::OK => 'ok', self::GOOD => 'good', ]; return $labels[ $this->rank ]; } /** * Returns a label for this rank. * * @return string */ public function get_label() { $labels = [ self::NO_FOCUS => __( 'Not available', 'wordpress-seo' ), self::NO_INDEX => __( 'No index', 'wordpress-seo' ), self::BAD => __( 'Needs improvement', 'wordpress-seo' ), self::OK => __( 'OK', 'wordpress-seo' ), self::GOOD => __( 'Good', 'wordpress-seo' ), ]; return $labels[ $this->rank ]; } /** * Returns an inclusive language label for this rank. * The only difference with get_label above is that we return "Potentially non-inclusive" for an OK rank. * * @return string */ public function get_inclusive_language_label() { if ( $this->rank === self::OK ) { return __( 'Potentially non-inclusive', 'wordpress-seo' ); } return $this->get_label(); } /** * Returns a label for use in a drop down. * * @return mixed */ public function get_drop_down_label() { $labels = [ self::NO_FOCUS => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'No Focus Keyphrase', 'wordpress-seo' ), ), self::BAD => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ), ), self::OK => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'OK', 'wordpress-seo' ), ), self::GOOD => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ), ), self::NO_INDEX => sprintf( /* translators: %s expands to the SEO score */ __( 'SEO: %s', 'wordpress-seo' ), __( 'Post Noindexed', 'wordpress-seo' ), ), ]; return $labels[ $this->rank ]; } /** * Gets the drop down labels for the readability score. * * @return string The readability rank label. */ public function get_drop_down_readability_labels() { $labels = [ self::BAD => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ), ), self::OK => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'OK', 'wordpress-seo' ), ), self::GOOD => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ), ), self::NO_FOCUS => sprintf( /* translators: %s expands to the readability score */ __( 'Readability: %s', 'wordpress-seo' ), __( 'Not analyzed', 'wordpress-seo' ), ), ]; return $labels[ $this->rank ]; } /** * Gets the drop down labels for the inclusive language score. * * @return string The inclusive language rank label. */ public function get_drop_down_inclusive_language_labels() { $labels = [ self::BAD => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Needs improvement', 'wordpress-seo' ), ), self::OK => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Potentially non-inclusive', 'wordpress-seo' ), ), self::GOOD => sprintf( /* translators: %s expands to the inclusive language score */ __( 'Inclusive language: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ), ), ]; return $labels[ $this->rank ]; } /** * Get the starting score for this rank. * * @return int The start score. */ public function get_starting_score() { // No index does not have a starting score. if ( $this->rank === self::NO_INDEX ) { return -1; } return self::$ranges[ $this->rank ]['start']; } /** * Get the ending score for this rank. * * @return int The end score. */ public function get_end_score() { // No index does not have an end score. if ( $this->rank === self::NO_INDEX ) { return -1; } return self::$ranges[ $this->rank ]['end']; } /** * Returns a rank for a specific numeric score. * * @param int $score The score to determine a rank for. * * @return self */ public static function from_numeric_score( $score ) { // Set up the default value. $rank = new self( self::BAD ); foreach ( self::$ranges as $rank_index => $range ) { if ( $range['start'] <= $score && $score <= $range['end'] ) { $rank = new self( $rank_index ); break; } } return $rank; } /** * Returns a list of all possible SEO Ranks. * * @return WPSEO_Rank[] */ public static function get_all_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], self::$ranks ); } /** * Returns a list of all possible Readability Ranks. * * @return WPSEO_Rank[] */ public static function get_all_readability_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD, self::NO_FOCUS ] ); } /** * Returns a list of all possible Inclusive Language Ranks. * * @return WPSEO_Rank[] */ public static function get_all_inclusive_language_ranks() { return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD ] ); } /** * Converts a numeric rank into a WPSEO_Rank object, for use in functional array_* functions. * * @param string $rank SEO Rank. * * @return WPSEO_Rank */ private static function create_rank( $rank ) { return new self( $rank ); } } inc/class-wpseo-custom-fields.php 0000644 00000003347 15174712003 0013051 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Custom_Fields. */ class WPSEO_Custom_Fields { /** * Custom fields cache. * * @var array|null */ protected static $custom_fields = null; /** * Retrieves the custom field names as an array. * * @link WordPress core: wp-admin/includes/template.php. Reused query from it. * * @return array The custom fields. */ public static function get_custom_fields() { global $wpdb; // Use cached value if available. if ( self::$custom_fields !== null ) { return self::$custom_fields; } self::$custom_fields = []; /** * Filters the number of custom fields to retrieve for the drop-down * in the Custom Fields meta box. * * @param int $limit Number of custom fields to retrieve. Default 30. */ $limit = apply_filters( 'postmeta_form_limit', 30 ); $sql = "SELECT DISTINCT meta_key FROM $wpdb->postmeta WHERE meta_key NOT BETWEEN '_' AND '_z' AND SUBSTRING(meta_key, 1, 1) != '_' LIMIT %d"; $fields = $wpdb->get_col( $wpdb->prepare( $sql, $limit ) ); /** * Filters the custom fields that are auto-completed and replaced as replacement variables * in the meta box and sidebar. * * @param string[] $fields The custom field names. */ $fields = apply_filters( 'wpseo_replacement_variables_custom_fields', $fields ); if ( is_array( $fields ) ) { self::$custom_fields = array_map( [ 'WPSEO_Custom_Fields', 'add_custom_field_prefix' ], $fields ); } return self::$custom_fields; } /** * Adds the cf_ prefix to a field. * * @param string $field The field to prefix. * * @return string The prefixed field. */ private static function add_custom_field_prefix( $field ) { return 'cf_' . $field; } } inc/class-wpseo-admin-bar-menu.php 0000644 00000070245 15174712003 0013070 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Score_Icon_Helper; use Yoast\WP\SEO\Integrations\Admin\Brand_Insights_Page; use Yoast\WP\SEO\Integrations\Support_Integration; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Class for the Yoast SEO admin bar menu. */ class WPSEO_Admin_Bar_Menu implements WPSEO_WordPress_Integration { /** * The identifier used for the menu. * * @var string */ public const MENU_IDENTIFIER = 'wpseo-menu'; /** * The identifier used for the Keyword Research submenu. * * @var string */ public const KEYWORD_RESEARCH_SUBMENU_IDENTIFIER = 'wpseo-kwresearch'; /** * The identifier used for the Analysis submenu. * * @var string */ public const ANALYSIS_SUBMENU_IDENTIFIER = 'wpseo-analysis'; /** * The identifier used for the Settings submenu. * * @var string */ public const SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-settings'; /** * The identifier used for the Network Settings submenu. * * @var string */ public const NETWORK_SETTINGS_SUBMENU_IDENTIFIER = 'wpseo-network-settings'; /** * Asset manager instance. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Holds the Score_Icon_Helper instance. * * @var Score_Icon_Helper */ protected $indexable_repository; /** * Holds the Score_Icon_Helper instance. * * @var Score_Icon_Helper */ protected $score_icon_helper; /** * Holds the Product_Helper instance. * * @var Product_Helper */ protected $product_helper; /** * Holds the shortlinker instance. * * @var WPSEO_Shortlinker */ protected $shortlinker; /** * Whether SEO Score is enabled. * * @var bool|null */ protected $is_seo_enabled = null; /** * Whether readability is enabled. * * @var bool|null */ protected $is_readability_enabled = null; /** * The indexable for the current WordPress page, if found. * * @var Indexable|bool|null */ protected $current_indexable = null; /** * Constructs the WPSEO_Admin_Bar_Menu. * * @param WPSEO_Admin_Asset_Manager|null $asset_manager Optional. Asset manager to use. * @param Indexable_Repository|null $indexable_repository Optional. The Indexable_Repository. * @param Score_Icon_Helper|null $score_icon_helper Optional. The Score_Icon_Helper. * @param Product_Helper|null $product_helper Optional. The product helper. * @param WPSEO_Shortlinker|null $shortlinker The shortlinker. */ public function __construct( ?WPSEO_Admin_Asset_Manager $asset_manager = null, ?Indexable_Repository $indexable_repository = null, ?Score_Icon_Helper $score_icon_helper = null, ?Product_Helper $product_helper = null, ?WPSEO_Shortlinker $shortlinker = null ) { if ( ! $asset_manager ) { $asset_manager = new WPSEO_Admin_Asset_Manager(); } if ( ! $indexable_repository ) { $indexable_repository = YoastSEO()->classes->get( Indexable_Repository::class ); } if ( ! $score_icon_helper ) { $score_icon_helper = YoastSEO()->helpers->score_icon; } if ( ! $product_helper ) { $product_helper = YoastSEO()->helpers->product; } if ( ! $shortlinker ) { $shortlinker = new WPSEO_Shortlinker(); } $this->product_helper = $product_helper; $this->asset_manager = $asset_manager; $this->indexable_repository = $indexable_repository; $this->score_icon_helper = $score_icon_helper; $this->shortlinker = $shortlinker; } /** * Gets whether SEO score is enabled, with cache applied. * * @return bool True if SEO score is enabled, false otherwise. */ protected function get_is_seo_enabled() { $this->is_seo_enabled ??= ( new WPSEO_Metabox_Analysis_SEO() )->is_enabled(); return $this->is_seo_enabled; } /** * Gets whether readability is enabled, with cache applied. * * @return bool True if readability is enabled, false otherwise. */ protected function get_is_readability_enabled() { $this->is_readability_enabled ??= ( new WPSEO_Metabox_Analysis_Readability() )->is_enabled(); return $this->is_readability_enabled; } /** * Returns the indexable for the current WordPress page, with cache applied. * * @return bool|Indexable The indexable, false if none could be found. */ protected function get_current_indexable() { $this->current_indexable ??= $this->indexable_repository->for_current_page(); return $this->current_indexable; } /** * Adds the admin bar menu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ public function add_menu( WP_Admin_Bar $wp_admin_bar ) { // On block editor pages, the admin bar only shows on mobile, where having this menu icon is not very helpful. if ( is_admin() ) { $screen = get_current_screen(); if ( isset( $screen ) && $screen->is_block_editor() ) { return; } } // If the current user can't write posts, this is all of no use, so let's not output an admin menu. if ( ! current_user_can( 'edit_posts' ) ) { return; } $this->add_root_menu( $wp_admin_bar ); /** * Adds a submenu item in the top of the adminbar. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * @param string $menu_identifier The menu identifier. */ do_action( 'wpseo_add_adminbar_submenu', $wp_admin_bar, self::MENU_IDENTIFIER ); if ( ! is_admin() ) { if ( is_singular() || is_tag() || is_tax() || is_category() ) { $is_seo_enabled = $this->get_is_seo_enabled(); $is_readability_enabled = $this->get_is_readability_enabled(); $indexable = $this->get_current_indexable(); if ( $is_seo_enabled ) { $focus_keyword = ( ! is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) || $indexable->primary_focus_keyword === null ) ? __( 'not set', 'wordpress-seo' ) : $indexable->primary_focus_keyword; $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-seo-focus-keyword', 'title' => __( 'Focus keyphrase: ', 'wordpress-seo' ) . '<span class="wpseo-focus-keyword">' . $focus_keyword . '</span>', 'meta' => [ 'tabindex' => '0' ], ], ); $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-seo-score', 'title' => __( 'SEO score', 'wordpress-seo' ) . ': ' . $this->score_icon_helper->for_seo( $indexable, 'adminbar-sub-menu-score' ) ->present(), 'meta' => [ 'tabindex' => '0' ], ], ); } if ( $is_readability_enabled ) { $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-readability-score', 'title' => __( 'Readability', 'wordpress-seo' ) . ': ' . $this->score_icon_helper->for_readability( $indexable->readability_score, 'adminbar-sub-menu-score' ) ->present(), 'meta' => [ 'tabindex' => '0' ], ], ); } if ( ! $this->product_helper->is_premium() ) { $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-frontend-inspector', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-frontend-inspector' ), 'title' => __( 'Front-end SEO inspector', 'wordpress-seo' ) . new Premium_Badge_Presenter( 'wpseo-frontend-inspector-badge' ), 'meta' => [ 'tabindex' => '0', 'target' => '_blank', ], ], ); } } $this->add_analysis_submenu( $wp_admin_bar ); $this->add_seo_tools_submenu( $wp_admin_bar ); $this->add_how_to_submenu( $wp_admin_bar ); $this->add_get_help_submenu( $wp_admin_bar ); } if ( ! is_admin() || is_blog_admin() ) { $this->add_settings_submenu( $wp_admin_bar ); } elseif ( is_network_admin() ) { $this->add_network_settings_submenu( $wp_admin_bar ); } $this->add_premium_link( $wp_admin_bar ); $this->add_brand_insights_link( $wp_admin_bar ); } /** * Enqueues admin bar assets. * * @return void */ public function enqueue_assets() { if ( ! is_admin_bar_showing() ) { return; } // If the current user can't write posts, this is all of no use, so let's not output an admin menu. if ( ! current_user_can( 'edit_posts' ) ) { return; } $this->asset_manager->register_assets(); $this->asset_manager->enqueue_style( 'adminbar' ); } /** * Registers the hooks. * * @return void */ public function register_hooks() { if ( ! $this->meets_requirements() ) { return; } add_action( 'admin_bar_menu', [ $this, 'add_menu' ], 95 ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Checks whether the requirements to use this class are met. * * @return bool True if requirements are met, false otherwise. */ public function meets_requirements() { if ( is_network_admin() ) { return WPSEO_Utils::is_plugin_network_active(); } if ( WPSEO_Options::get( 'enable_admin_bar_menu' ) !== true ) { return false; } return ! is_admin() || is_blog_admin(); } /** * Adds the admin bar root menu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_root_menu( WP_Admin_Bar $wp_admin_bar ) { $title = $this->get_title(); $score = ''; $settings_url = ''; $counter = ''; $notification_popup = ''; $notification_count = 0; $post = $this->get_singular_post(); if ( $post ) { $score = $this->get_post_score( $post ); } $term = $this->get_singular_term(); if ( $term ) { $score = $this->get_term_score( $term ); } $can_manage_options = $this->can_manage_options(); if ( $can_manage_options ) { $settings_url = $this->get_settings_page_url(); } if ( empty( $score ) && ! is_network_admin() && $can_manage_options ) { $notification_center = Yoast_Notification_Center::get(); $notification_count = $notification_center->get_notification_count(); $counter = $this->get_notification_counter( $notification_count ); $notification_popup = $this->get_notification_popup(); } $admin_bar_menu_args = [ 'id' => self::MENU_IDENTIFIER, 'title' => $title . $score . $counter . $notification_popup, 'href' => $settings_url, 'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ], ]; $wp_admin_bar->add_menu( $admin_bar_menu_args ); if ( $notification_count > 0 ) { $admin_bar_menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-notifications', 'title' => __( 'Notifications', 'wordpress-seo' ) . $counter, 'href' => empty( $settings_url ) ? '' : $settings_url . '#/alert-center', 'meta' => [ 'tabindex' => ! empty( $settings_url ) ? false : '0' ], ]; $wp_admin_bar->add_menu( $admin_bar_menu_args ); } } /** * Adds the admin bar analysis submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_analysis_submenu( WP_Admin_Bar $wp_admin_bar ) { try { $url = YoastSEO()->meta->for_current_page()->canonical; } catch ( Exception $e ) { // This is not the type of error we can handle here. return; } if ( ! $url ) { return; } $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::ANALYSIS_SUBMENU_IDENTIFIER, 'title' => __( 'Analyze this page', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $encoded_url = rawurlencode( $url ); $submenu_items = [ [ 'id' => 'wpseo-inlinks', 'title' => __( 'Check links to this URL', 'wordpress-seo' ), 'href' => 'https://search.google.com/search-console/links/drilldown?resource_id=' . rawurlencode( get_option( 'siteurl' ) ) . '&type=EXTERNAL&target=' . $encoded_url . '&domain=', ], [ 'id' => 'wpseo-structureddata', 'title' => __( 'Google Rich Results Test', 'wordpress-seo' ), 'href' => 'https://search.google.com/test/rich-results?url=' . $encoded_url, ], [ 'id' => 'wpseo-facebookdebug', 'title' => __( 'Facebook Debugger', 'wordpress-seo' ), 'href' => '//developers.facebook.com/tools/debug/?q=' . $encoded_url, ], [ 'id' => 'wpseo-pagespeed', 'title' => __( 'Google Page Speed Test', 'wordpress-seo' ), 'href' => '//developers.google.com/speed/pagespeed/insights/?url=' . $encoded_url, ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, self::ANALYSIS_SUBMENU_IDENTIFIER ); } /** * Adds the admin bar tools submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_seo_tools_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-tools', 'title' => __( 'SEO Tools', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-semrush', 'title' => 'Semrush', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-semrush' ), ], [ 'id' => 'wpseo-wincher', 'title' => 'Wincher', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-wincher' ), ], [ 'id' => 'wpseo-google-trends', 'title' => 'Google trends', 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-gtrends' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-tools' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_how_to_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-howto', 'title' => __( 'How to', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-learn-seo', 'title' => __( 'Learn more SEO', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-learn-more-seo' ), ], [ 'id' => 'wpseo-improve-blogpost', 'title' => __( 'Improve your blog post', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-improve-blog-post' ), ], [ 'id' => 'wpseo-write-better-content', 'title' => __( 'Write better content', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-write-better' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-howto' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_get_help_submenu( WP_Admin_Bar $wp_admin_bar ) { $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-sub-get-help', 'title' => __( 'Help', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; if ( current_user_can( Support_Integration::CAPABILITY ) ) { $menu_args['href'] = admin_url( 'admin.php?page=' . Support_Integration::PAGE ); $wp_admin_bar->add_menu( $menu_args ); return; } $wp_admin_bar->add_menu( $menu_args ); $submenu_items = [ [ 'id' => 'wpseo-yoast-help', 'title' => __( 'Yoast.com help section', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-yoast-help' ), ], [ 'id' => 'wpseo-premium-support', 'title' => __( 'Yoast Premium support', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-premium-support' ), ], [ 'id' => 'wpseo-wp-support-forums', 'title' => __( 'WordPress.org support forums', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-wp-support-forums' ), ], [ 'id' => 'wpseo-learn-seo-2', 'title' => __( 'Learn more SEO', 'wordpress-seo' ), 'href' => $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-learn-more-seo-help' ), ], ]; $this->add_submenu_items( $submenu_items, $wp_admin_bar, 'wpseo-sub-get-help' ); } /** * Adds the admin bar How To submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_premium_link( WP_Admin_Bar $wp_admin_bar ) { // Don't show the Upgrade button if Yoast SEO WooCommerce addon is active. $addon_manager = new WPSEO_Addon_Manager(); if ( $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { return; } $has_woocommerce = ( new Woocommerce_Conditional() )->is_met(); // Don't show the Upgrade button if Premium is active without the WooCommerce plugin. if ( $this->product_helper->is_premium() && ! $has_woocommerce ) { return; } $link = $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-get-premium' ); if ( $has_woocommerce ) { $link = $this->shortlinker->build_shortlink( 'https://yoa.st/admin-bar-get-premium-woocommerce' ); } $button_label = esc_html__( 'Upgrade', 'wordpress-seo' ); if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { $button_label = esc_html__( '30% off - BF Sale', 'wordpress-seo' ); } $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => 'wpseo-get-premium', // Circumvent an issue in the WP admin bar API in order to pass `data` attributes. See https://core.trac.wordpress.org/ticket/38636. 'title' => sprintf( '<a href="%1$s" target="_blank" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2">%2$s</a>', esc_url( $link ), $button_label, ), 'meta' => [ 'tabindex' => '0', ], ], ); } /** * Adds the Brand Insights link to the admin bar. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_brand_insights_link( WP_Admin_Bar $wp_admin_bar ) { $page = $this->product_helper->is_premium() ? 'wpseo_brand_insights_premium' : 'wpseo_brand_insights'; $button_content = 'AI Brand Insights'; $menu_title = '<span class="yoast-brand-insights-gradient-border">' . '<span class="yoast-brand-insights-content">' . $button_content . Brand_Insights_Page::EXTERNAL_LINK_ICON . '</span></span>'; $wp_admin_bar->add_menu( [ 'parent' => self::MENU_IDENTIFIER, 'id' => $page, 'title' => $menu_title, 'href' => admin_url( 'admin.php?page=' . $page ), 'meta' => [ 'tabindex' => '0', 'target' => '_blank', ], ], ); } /** * Adds the admin bar settings submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_settings_submenu( WP_Admin_Bar $wp_admin_bar ) { if ( ! $this->can_manage_options() ) { return; } $admin_menu = new WPSEO_Admin_Menu( new WPSEO_Menu() ); $submenu_pages = $admin_menu->get_submenu_pages(); $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::SETTINGS_SUBMENU_IDENTIFIER, 'title' => __( 'SEO Settings', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); foreach ( $submenu_pages as $submenu_page ) { if ( ! current_user_can( $submenu_page[3] ) ) { continue; } // Don't add the Google Search Console menu item. if ( $submenu_page[4] === 'wpseo_search_console' ) { continue; } // Don't add the Brand Insights menu items (they're now in the main menu). if ( $submenu_page[4] === 'wpseo_brand_insights' || $submenu_page[4] === 'wpseo_brand_insights_premium' ) { continue; } $id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) ); if ( $id === 'wpseo-dashboard' ) { $id = 'wpseo-general'; } $menu_args = [ 'parent' => self::SETTINGS_SUBMENU_IDENTIFIER, 'id' => $id, 'title' => $submenu_page[2], 'href' => admin_url( 'admin.php?page=' . rawurlencode( $submenu_page[4] ) ), ]; $wp_admin_bar->add_menu( $menu_args ); } } /** * Adds the admin bar network settings submenu. * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance to add the menu to. * * @return void */ protected function add_network_settings_submenu( WP_Admin_Bar $wp_admin_bar ) { if ( ! $this->can_manage_options() ) { return; } $network_admin_menu = new WPSEO_Network_Admin_Menu( new WPSEO_Menu() ); $submenu_pages = $network_admin_menu->get_submenu_pages(); $menu_args = [ 'parent' => self::MENU_IDENTIFIER, 'id' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER, 'title' => __( 'SEO Settings', 'wordpress-seo' ), 'meta' => [ 'tabindex' => '0' ], ]; $wp_admin_bar->add_menu( $menu_args ); foreach ( $submenu_pages as $submenu_page ) { if ( ! current_user_can( $submenu_page[3] ) ) { continue; } $id = 'wpseo-' . str_replace( '_', '-', str_replace( 'wpseo_', '', $submenu_page[4] ) ); if ( $id === 'wpseo-dashboard' ) { $id = 'wpseo-general'; } $menu_args = [ 'parent' => self::NETWORK_SETTINGS_SUBMENU_IDENTIFIER, 'id' => $id, 'title' => $submenu_page[2], 'href' => network_admin_url( 'admin.php?page=' . rawurlencode( $submenu_page[4] ) ), ]; $wp_admin_bar->add_menu( $menu_args ); } } /** * Gets the menu title markup. * * @return string Admin bar title markup. */ protected function get_title() { return '<div id="yoast-ab-icon" class="ab-item yoast-logo svg"><span class="screen-reader-text">' . __( 'SEO', 'wordpress-seo' ) . '</span></div>'; } /** * Gets the current post if in a singular post context. * * @global string $pagenow Current page identifier. * @global WP_Post|null $post Current post object, or null if none available. * * @return WP_Post|null Post object, or null if not in singular context. */ protected function get_singular_post() { global $pagenow, $post; if ( ! is_singular() && ( ! is_blog_admin() || ! WPSEO_Metabox::is_post_edit( $pagenow ) ) ) { return null; } if ( ! isset( $post ) || ! is_object( $post ) || ! $post instanceof WP_Post ) { return null; } return $post; } /** * Gets the focus keyword for a given post. * * @param WP_Post $post Post object to get its focus keyword. * * @return string Focus keyword, or empty string if none available. */ protected function get_post_focus_keyword( $post ) { if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) { return ''; } /** * Filter: 'wpseo_use_page_analysis' Determines if the analysis should be enabled. * * @param bool $enabled Determines if the analysis should be enabled. */ if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) { return ''; } return WPSEO_Meta::get_value( 'focuskw', $post->ID ); } /** * Gets the score for a given post. * * @param WP_Post $post Post object to get its score. * * @return string Score markup, or empty string if none available. */ protected function get_post_score( $post ) { if ( ! is_object( $post ) || ! property_exists( $post, 'ID' ) ) { return ''; } if ( apply_filters( 'wpseo_use_page_analysis', true ) !== true ) { return ''; } return $this->get_score_icon(); } /** * Gets the current term if in a singular term context. * * @global string $pagenow Current page identifier. * @global WP_Query $wp_query Current query object. * @global WP_Term|null $tag Current term object, or null if none available. * * @return WP_Term|null Term object, or null if not in singular context. */ protected function get_singular_term() { global $pagenow, $wp_query, $tag; if ( is_category() || is_tag() || is_tax() ) { return $wp_query->get_queried_object(); } if ( WPSEO_Taxonomy::is_term_edit( $pagenow ) && ! WPSEO_Taxonomy::is_term_overview( $pagenow ) && isset( $tag ) && is_object( $tag ) && ! is_wp_error( $tag ) ) { return get_term( $tag->term_id ); } return null; } /** * Gets the score for a given term. * * @param WP_Term $term Term object to get its score. * * @return string Score markup, or empty string if none available. */ protected function get_term_score( $term ) { if ( ! is_object( $term ) || ! property_exists( $term, 'term_id' ) || ! property_exists( $term, 'taxonomy' ) ) { return ''; } return $this->get_score_icon(); } /** * Create the score icon. * * @return string The score icon, or empty string. */ protected function get_score_icon() { $is_seo_enabled = $this->get_is_seo_enabled(); $is_readability_enabled = $this->get_is_readability_enabled(); $indexable = $this->get_current_indexable(); if ( $is_seo_enabled ) { return $this->score_icon_helper->for_seo( $indexable, 'adminbar-seo-score' )->present(); } if ( $is_readability_enabled ) { return $this->score_icon_helper->for_readability( $indexable->readability_score, 'adminbar-seo-score' ) ->present(); } return ''; } /** * Gets the URL to the main admin settings page. * * @return string Admin settings page URL. */ protected function get_settings_page_url() { return self_admin_url( 'admin.php?page=' . WPSEO_Admin::PAGE_IDENTIFIER ); } /** * Gets the notification counter if in a valid context. * * @param int $notification_count Number of notifications. * * @return string Notification counter markup, or empty string if not available. */ protected function get_notification_counter( $notification_count ) { /* translators: Hidden accessibility text; %s: number of notifications. */ $counter_screen_reader_text = sprintf( _n( '%s notification', '%s notifications', $notification_count, 'wordpress-seo' ), number_format_i18n( $notification_count ) ); return sprintf( ' <div class="wp-core-ui wp-ui-notification yoast-issue-counter%s"><span class="yoast-issues-count" aria-hidden="true">%d</span><span class="screen-reader-text">%s</span></div>', ( $notification_count ) ? '' : ' wpseo-no-adminbar-notifications', $notification_count, $counter_screen_reader_text, ); } /** * Gets the notification popup if in a valid context. * * @return string Notification popup markup, or empty string if not available. */ protected function get_notification_popup() { $notification_center = Yoast_Notification_Center::get(); $new_notifications = $notification_center->get_new_notifications(); $new_notifications_count = count( $new_notifications ); if ( ! $new_notifications_count ) { return ''; } $notification = sprintf( _n( 'There is a new notification.', 'There are new notifications.', $new_notifications_count, 'wordpress-seo', ), $new_notifications_count, ); return '<div class="yoast-issue-added">' . $notification . '</div>'; } /** * Checks whether the current user can manage options in the current context. * * @return bool True if capabilities are sufficient, false otherwise. */ protected function can_manage_options() { return ( is_network_admin() && current_user_can( 'wpseo_manage_network_options' ) ) || ( ! is_network_admin() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ); } /** * Add submenu items to a menu item. * * @param array<array{id: string, title: string, href: string}> $submenu_items Submenu items array. * @param WP_Admin_Bar $wp_admin_bar Admin bar object. * @param string $parent_id Parent menu item ID. * * @return void */ protected function add_submenu_items( array $submenu_items, WP_Admin_Bar $wp_admin_bar, $parent_id ) { foreach ( $submenu_items as $menu_item ) { $menu_args = [ 'parent' => $parent_id, 'id' => $menu_item['id'], 'title' => $menu_item['title'], 'href' => $menu_item['href'], 'meta' => [ 'target' => '_blank' ], ]; $wp_admin_bar->add_menu( $menu_args ); } } } inc/exceptions/class-myyoast-invalid-json-exception.php 0000644 00000000327 15174712003 0017410 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class WPSEO_MyYoast_Invalid_JSON_Exception. */ class WPSEO_MyYoast_Invalid_JSON_Exception extends WPSEO_MyYoast_Bad_Request_Exception { } inc/exceptions/class-myyoast-bad-request-exception.php 0000644 00000000273 15174712003 0017227 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class WPSEO_MyYoast_Bad_Request_Exception. */ class WPSEO_MyYoast_Bad_Request_Exception extends Exception { } inc/class-wpseo-statistics.php 0000644 00000002643 15174712003 0012463 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class that generates interesting statistics about things. */ class WPSEO_Statistics { /** * Returns the post count for a certain SEO rank. * * @todo Merge/DRY this with the logic virtually the same in WPSEO_Metabox::column_sort_orderby(). * * @param WPSEO_Rank $rank The SEO rank to get the post count for. * * @return int */ public function get_post_count( $rank ) { if ( $rank->get_rank() === WPSEO_Rank::NO_FOCUS ) { $posts = [ 'meta_query' => [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], ], ]; } elseif ( $rank->get_rank() === WPSEO_Rank::NO_INDEX ) { $posts = [ 'meta_key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'meta_value' => '1', 'compare' => '=', ]; } else { $posts = [ 'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'meta_value' => [ $rank->get_starting_score(), $rank->get_end_score() ], 'meta_compare' => 'BETWEEN', 'meta_type' => 'NUMERIC', ]; } $posts['fields'] = 'ids'; $posts['post_status'] = 'publish'; if ( current_user_can( 'edit_others_posts' ) === false ) { $posts['author'] = get_current_user_id(); } $posts = new WP_Query( $posts ); return (int) $posts->found_posts; } } inc/class-upgrade.php 0000644 00000156464 15174712003 0010600 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internal */ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Helpers\Taxonomy_Helper; use Yoast\WP\SEO\Integrations\Cleanup_Integration; use Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher; /** * This code handles the option upgrades. */ class WPSEO_Upgrade { /** * The taxonomy helper. * * @var Taxonomy_Helper */ private $taxonomy_helper; /** * Class constructor. */ public function __construct() { $this->taxonomy_helper = YoastSEO()->helpers->taxonomy; $version = WPSEO_Options::get( 'version' ); WPSEO_Options::maybe_set_multisite_defaults( false ); $routines = [ '1.5.0' => 'upgrade_15', '2.0' => 'upgrade_20', '2.1' => 'upgrade_21', '2.2' => 'upgrade_22', '2.3' => 'upgrade_23', '3.0' => 'upgrade_30', '3.3' => 'upgrade_33', '3.6' => 'upgrade_36', '4.0' => 'upgrade_40', '4.4' => 'upgrade_44', '4.7' => 'upgrade_47', '4.9' => 'upgrade_49', '5.0' => 'upgrade_50', '5.5' => 'upgrade_55', '6.3' => 'upgrade_63', '7.0-RC0' => 'upgrade_70', '7.1-RC0' => 'upgrade_71', '7.3-RC0' => 'upgrade_73', '7.4-RC0' => 'upgrade_74', '7.5.3' => 'upgrade_753', '7.7-RC0' => 'upgrade_77', '7.7.2-RC0' => 'upgrade_772', '9.0-RC0' => 'upgrade_90', '10.0-RC0' => 'upgrade_100', '11.1-RC0' => 'upgrade_111', // Reset notifications because we removed the AMP Glue plugin notification. '12.1-RC0' => 'clean_all_notifications', '12.3-RC0' => 'upgrade_123', '12.4-RC0' => 'upgrade_124', '12.8-RC0' => 'upgrade_128', '13.2-RC0' => 'upgrade_132', '14.0.3-RC0' => 'upgrade_1403', '14.1-RC0' => 'upgrade_141', '14.2-RC0' => 'upgrade_142', '14.5-RC0' => 'upgrade_145', '14.9-RC0' => 'upgrade_149', '15.1-RC0' => 'upgrade_151', '15.3-RC0' => 'upgrade_153', '15.5-RC0' => 'upgrade_155', '15.7-RC0' => 'upgrade_157', '15.9.1-RC0' => 'upgrade_1591', '16.2-RC0' => 'upgrade_162', '16.5-RC0' => 'upgrade_165', '17.2-RC0' => 'upgrade_172', '17.7.1-RC0' => 'upgrade_1771', '17.9-RC0' => 'upgrade_179', '18.3-RC3' => 'upgrade_183', '18.6-RC0' => 'upgrade_186', '18.9-RC0' => 'upgrade_189', '19.1-RC0' => 'upgrade_191', '19.3-RC0' => 'upgrade_193', '19.6-RC0' => 'upgrade_196', '19.11-RC0' => 'upgrade_1911', '20.2-RC0' => 'upgrade_202', '20.5-RC0' => 'upgrade_205', '20.7-RC0' => 'upgrade_207', '20.8-RC0' => 'upgrade_208', '22.6-RC0' => 'upgrade_226', ]; array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version ); if ( version_compare( $version, '12.5-RC0', '<' ) ) { /* * We have to run this by hook, because otherwise: * - the theme support check isn't available. * - the notification center notifications are not filled yet. */ add_action( 'init', [ $this, 'upgrade_125' ] ); } /** * Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO. * * @param string $version The current version of Yoast SEO */ do_action( 'wpseo_run_upgrade', $version ); $this->finish_up( $version ); } /** * Runs the upgrade routine. * * @param string $routine The method to call. * @param string $version The new version. * @param string $current_version The current set version. * * @return void */ protected function run_upgrade_routine( $routine, $version, $current_version ) { if ( version_compare( $current_version, $version, '<' ) ) { $this->$routine( $current_version ); } } /** * Adds a new upgrade history entry. * * @param string $current_version The old version from which we are upgrading. * @param string $new_version The version we are upgrading to. * * @return void */ protected function add_upgrade_history( $current_version, $new_version ) { $upgrade_history = new WPSEO_Upgrade_History(); $upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) ); } /** * Runs the needed cleanup after an update, setting the DB version to latest version, flushing caches etc. * * @param string|null $previous_version The previous version. * * @return void */ protected function finish_up( $previous_version = null ) { if ( $previous_version ) { WPSEO_Options::set( 'previous_version', $previous_version, 'wpseo' ); // Store timestamp when plugin is updated from a previous version. WPSEO_Options::set( 'last_updated_on', time(), 'wpseo' ); } WPSEO_Options::set( 'version', WPSEO_VERSION, 'wpseo' ); // Just flush rewrites, always, to at least make them work after an upgrade. add_action( 'shutdown', 'flush_rewrite_rules' ); // Flush the sitemap cache. WPSEO_Sitemaps_Cache::clear(); // Make sure all our options always exist - issue #1245. WPSEO_Options::ensure_options_exist(); } /** * Run the Yoast SEO 1.5 upgrade routine. * * @param string $version Current plugin version. * * @return void */ private function upgrade_15( $version ) { // Clean up options and meta. WPSEO_Options::clean_up( null, $version ); WPSEO_Meta::clean_up(); } /** * Moves options that moved position in WPSEO 2.0. * * @return void */ private function upgrade_20() { /** * Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table. * This could have been caused in many version of Yoast SEO, so deleting it for everything below 2.0. */ delete_option( 'wpseo_ms' ); $wpseo = $this->get_option_from_database( 'wpseo' ); $this->save_option_setting( $wpseo, 'pinterestverify' ); // Re-save option to trigger sanitization. $this->cleanup_option_data( 'wpseo' ); } /** * Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly. * * @return void */ private function upgrade_21() { $taxonomies = get_option( 'wpseo_taxonomy_meta', [] ); if ( ! empty( $taxonomies ) ) { foreach ( $taxonomies as $taxonomy => $tax_metas ) { foreach ( $tax_metas as $term_id => $tax_meta ) { if ( function_exists( 'wp_get_split_term' ) ) { $new_term_id = wp_get_split_term( $term_id, $taxonomy ); if ( $new_term_id !== false ) { $taxonomies[ $taxonomy ][ $new_term_id ] = $taxonomies[ $taxonomy ][ $term_id ]; unset( $taxonomies[ $taxonomy ][ $term_id ] ); } } } } update_option( 'wpseo_taxonomy_meta', $taxonomies ); } } /** * Performs upgrade functions to Yoast SEO 2.2. * * @return void */ private function upgrade_22() { // Unschedule our tracking. wp_clear_scheduled_hook( 'yoast_tracking' ); $this->cleanup_option_data( 'wpseo' ); } /** * Schedules upgrade function to Yoast SEO 2.3. * * @return void */ private function upgrade_23() { add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 ); add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 ); } /** * Performs upgrade query to Yoast SEO 2.3. * * @return void */ public function upgrade_23_query() { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: executed only during the upgrade routine. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Reason: executed only during the upgrade routine. $wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' ); if ( ! empty( $wp_query->posts ) ) { $options = get_option( 'wpseo_xml' ); $excluded_posts = []; if ( $options['excluded-posts'] !== '' ) { $excluded_posts = explode( ',', $options['excluded-posts'] ); } foreach ( $wp_query->posts as $post ) { if ( ! in_array( (string) $post->ID, $excluded_posts, true ) ) { $excluded_posts[] = $post->ID; } } // Updates the meta value. $options['excluded-posts'] = implode( ',', $excluded_posts ); // Update the option. update_option( 'wpseo_xml', $options ); } // Remove the meta fields. delete_post_meta_by_key( '_yoast_wpseo_sitemap-include' ); } /** * Performs upgrade functions to Yoast SEO 3.0. * * @return void */ private function upgrade_30() { // Remove the meta fields for sitemap prio. delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' ); } /** * Performs upgrade functions to Yoast SEO 3.3. * * @return void */ private function upgrade_33() { // Notification dismissals have been moved to User Meta instead of global option. delete_option( Yoast_Notification_Center::STORAGE_KEY ); } /** * Performs upgrade functions to Yoast SEO 3.6. * * @return void */ protected function upgrade_36() { global $wpdb; // Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s AND autoload IN ("on", "yes")', [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ], ), ); } /** * Removes the about notice when its still in the database. * * @return void */ private function upgrade_40() { $center = Yoast_Notification_Center::get(); $center->remove_notification_by_id( 'wpseo-dismiss-about' ); } /** * Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo. * * @return void */ private function upgrade_44() { $wpseo_titles = $this->get_option_from_database( 'wpseo_titles' ); $this->save_option_setting( $wpseo_titles, 'content-analysis-active', 'content_analysis_active' ); $this->save_option_setting( $wpseo_titles, 'keyword-analysis-active', 'keyword_analysis_active' ); // Remove irrelevant content from the option. $this->cleanup_option_data( 'wpseo_titles' ); } /** * Renames the meta name for the cornerstone content. It was a public meta field and it has to be private. * * @return void */ private function upgrade_47() { global $wpdb; // The meta key has to be private, so prefix it. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"', WPSEO_Cornerstone_Filter::META_NAME, ), ); } /** * Removes the 'wpseo-dismiss-about' notice for every user that still has it. * * @return void */ protected function upgrade_49() { global $wpdb; /* * Using a filter to remove the notification for the current logged in user. The notification center is * initializing the notifications before the upgrade routine has been executedd and is saving the stored * notifications on shutdown. This causes the returning notification. By adding this filter the shutdown * routine on the notification center will remove the notification. */ add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] ); $meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $usermetas = $wpdb->get_results( $wpdb->prepare( ' SELECT %i, %i FROM %i WHERE %i = %s AND %i LIKE %s ', [ 'user_id', 'meta_value', $wpdb->usermeta, 'meta_key', $meta_key, 'meta_value', '%wpseo-dismiss-about%', ], ), ARRAY_A, ); if ( empty( $usermetas ) ) { return; } foreach ( $usermetas as $usermeta ) { $notifications = maybe_unserialize( $usermeta['meta_value'] ); foreach ( $notifications as $notification_key => $notification ) { if ( ! empty( $notification['options']['id'] ) && $notification['options']['id'] === 'wpseo-dismiss-about' ) { unset( $notifications[ $notification_key ] ); } } update_user_option( $usermeta['user_id'], Yoast_Notification_Center::STORAGE_KEY, array_values( $notifications ) ); } } /** * Removes the wpseo-dismiss-about notice from a list of notifications. * * @param Yoast_Notification[] $notifications The notifications to filter. * * @return Yoast_Notification[] The filtered list of notifications. Excluding the wpseo-dismiss-about notification. */ public function remove_about_notice( $notifications ) { foreach ( $notifications as $notification_key => $notification ) { if ( $notification->get_id() === 'wpseo-dismiss-about' ) { unset( $notifications[ $notification_key ] ); } } return $notifications; } /** * Adds the yoast_seo_links table to the database. * * @return void */ protected function upgrade_50() { global $wpdb; // Deletes the post meta value, which might created in the RC. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = '_yst_content_links_processed'", [ $wpdb->postmeta, 'meta_key' ], ), ); } /** * Register new capabilities and roles. * * @return void */ private function upgrade_55() { // Register roles. do_action( 'wpseo_register_roles' ); WPSEO_Role_Manager_Factory::get()->add(); // Register capabilities. do_action( 'wpseo_register_capabilities' ); WPSEO_Capability_Manager_Factory::get()->add(); } /** * Removes some no longer used options for noindexing subpages and for meta keywords and its associated templates. * * @return void */ private function upgrade_63() { $this->cleanup_option_data( 'wpseo_titles' ); } /** * Perform the 7.0 upgrade, moves settings around, deletes several options. * * @return void */ private function upgrade_70() { $wpseo_permalinks = $this->get_option_from_database( 'wpseo_permalinks' ); $wpseo_xml = $this->get_option_from_database( 'wpseo_xml' ); $wpseo_rss = $this->get_option_from_database( 'wpseo_rss' ); $wpseo = $this->get_option_from_database( 'wpseo' ); $wpseo_internallinks = $this->get_option_from_database( 'wpseo_internallinks' ); // Move some permalink settings, then delete the option. $this->save_option_setting( $wpseo_permalinks, 'redirectattachment', 'disable-attachment' ); $this->save_option_setting( $wpseo_permalinks, 'stripcategorybase' ); // Move one XML sitemap setting, then delete the option. $this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' ); // Move the RSS settings to the search appearance settings, then delete the RSS option. $this->save_option_setting( $wpseo_rss, 'rssbefore' ); $this->save_option_setting( $wpseo_rss, 'rssafter' ); $this->save_option_setting( $wpseo, 'company_logo' ); $this->save_option_setting( $wpseo, 'company_name' ); $this->save_option_setting( $wpseo, 'company_or_person' ); $this->save_option_setting( $wpseo, 'person_name' ); // Remove the website name and altername name as we no longer need them. $this->cleanup_option_data( 'wpseo' ); // All the breadcrumbs settings have moved to the search appearance settings. foreach ( array_keys( $wpseo_internallinks ) as $key ) { $this->save_option_setting( $wpseo_internallinks, $key ); } // Convert hidden metabox options to display metabox options. $title_options = get_option( 'wpseo_titles' ); foreach ( $title_options as $key => $value ) { if ( strpos( $key, 'hideeditbox-tax-' ) === 0 ) { $taxonomy = substr( $key, strlen( 'hideeditbox-tax-' ) ); WPSEO_Options::set( 'display-metabox-tax-' . $taxonomy, ! $value ); continue; } if ( strpos( $key, 'hideeditbox-' ) === 0 ) { $post_type = substr( $key, strlen( 'hideeditbox-' ) ); WPSEO_Options::set( 'display-metabox-pt-' . $post_type, ! $value ); continue; } } // Cleanup removed options. delete_option( 'wpseo_xml' ); delete_option( 'wpseo_permalinks' ); delete_option( 'wpseo_rss' ); delete_option( 'wpseo_internallinks' ); // Remove possibly present plugin conflict notice for plugin that was removed from the list of conflicting plugins. $yoast_plugin_conflict = WPSEO_Plugin_Conflict::get_instance(); $yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' ); // Moves the user meta for excluding from the XML sitemap to a noindex. global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" ); } /** * Perform the 7.1 upgrade. * * @return void */ private function upgrade_71() { $this->cleanup_option_data( 'wpseo_social' ); // Move the breadcrumbs setting and invert it. $title_options = $this->get_option_from_database( 'wpseo_titles' ); if ( array_key_exists( 'breadcrumbs-blog-remove', $title_options ) ) { WPSEO_Options::set( 'breadcrumbs-display-blog-page', ! $title_options['breadcrumbs-blog-remove'] ); $this->cleanup_option_data( 'wpseo_titles' ); } } /** * Perform the 7.3 upgrade. * * @return void */ private function upgrade_73() { global $wpdb; // We've moved the cornerstone checkbox to our proper namespace. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" ); // Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2. delete_option( 'whip_dismiss_timestamp' ); } /** * Performs the 7.4 upgrade. * * @return void */ protected function upgrade_74() { $this->remove_sitemap_validators(); } /** * Performs the 7.5.3 upgrade. * * When upgrading purging media is potentially relevant. * * @return void */ private function upgrade_753() { // Only when attachments are not disabled. if ( WPSEO_Options::get( 'disable-attachment' ) === true ) { return; } // Only when attachments are not no-indexed. if ( WPSEO_Options::get( 'noindex-attachment' ) === true ) { return; } // Set purging relevancy. WPSEO_Options::set( 'is-media-purge-relevant', true ); } /** * Performs the 7.7 upgrade. * * @return void */ private function upgrade_77() { // Remove all OpenGraph content image cache. $this->delete_post_meta( '_yoast_wpseo_post_image_cache' ); } /** * Performs the 7.7.2 upgrade. * * @return void */ private function upgrade_772() { if ( YoastSEO()->helpers->woocommerce->is_active() ) { $this->migrate_woocommerce_archive_setting_to_shop_page(); } } /** * Performs the 9.0 upgrade. * * @return void */ protected function upgrade_90() { global $wpdb; // Invalidate all sitemap cache transients. WPSEO_Sitemaps_Cache_Validator::cleanup_database(); // Removes all scheduled tasks for hitting the sitemap index. wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s', [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ], ), ); } /** * Performs the 10.0 upgrade. * * @return void */ private function upgrade_100() { // Removes recalibration notifications. $this->clean_all_notifications(); // Removes recalibration options. WPSEO_Options::clean_up( 'wpseo' ); delete_option( 'wpseo_recalibration_beta_mailinglist_subscription' ); } /** * Performs the 11.1 upgrade. * * @return void */ private function upgrade_111() { // Set company_or_person to company when it's an invalid value. $company_or_person = WPSEO_Options::get( 'company_or_person', '' ); if ( ! in_array( $company_or_person, [ 'company', 'person' ], true ) ) { WPSEO_Options::set( 'company_or_person', 'company' ); } } /** * Performs the 12.3 upgrade. * * Removes the about notice when its still in the database. * * @return void */ private function upgrade_123() { $plugins = [ 'yoast-seo-premium', 'video-seo-for-wordpress-seo-by-yoast', 'yoast-news-seo', 'local-seo-for-yoast-seo', 'yoast-woocommerce-seo', 'yoast-acf-analysis', ]; $center = Yoast_Notification_Center::get(); foreach ( $plugins as $plugin ) { $center->remove_notification_by_id( 'wpseo-outdated-yoast-seo-plugin-' . $plugin ); } } /** * Performs the 12.4 upgrade. * * Removes the Google plus defaults from the database. * * @return void */ private function upgrade_124() { $this->cleanup_option_data( 'wpseo_social' ); } /** * Performs the 12.5 upgrade. * * @return void */ public function upgrade_125() { // Disables the force rewrite title when the theme supports it through WordPress. if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) { WPSEO_Options::set( 'forcerewritetitle', false ); } global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i = %s', [ $wpdb->usermeta, 'meta_key', 'wp_yoast_promo_hide_premium_upsell_admin_block' ], ), ); // Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released. $center = Yoast_Notification_Center::get(); $center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' ); } /** * Performs the 12.8 upgrade. * * @return void */ private function upgrade_128() { // Re-save wpseo to make sure bf_banner_2019_dismissed key is gone. $this->cleanup_option_data( 'wpseo' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-page_comments-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' ); } /** * Performs the 13.2 upgrade. * * @return void */ private function upgrade_132() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-tagline-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-permalink-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-onpageorg' ); // Transfers the onpage option value to the ryte option. $ryte_option = get_option( 'wpseo_ryte' ); $onpage_option = get_option( 'wpseo_onpage' ); if ( ! $ryte_option && $onpage_option ) { update_option( 'wpseo_ryte', $onpage_option ); delete_option( 'wpseo_onpage' ); } // Changes onpage_indexability to ryte_indexability. $wpseo_option = get_option( 'wpseo' ); if ( isset( $wpseo_option['onpage_indexability'] ) && ! isset( $wpseo_option['ryte_indexability'] ) ) { $wpseo_option['ryte_indexability'] = $wpseo_option['onpage_indexability']; unset( $wpseo_option['onpage_indexability'] ); update_option( 'wpseo', $wpseo_option ); } if ( wp_next_scheduled( 'wpseo_ryte_fetch' ) ) { wp_clear_scheduled_hook( 'wpseo_ryte_fetch' ); } /* * Re-register capabilities to add the new `view_site_health_checks` * capability to the SEO Manager role. */ do_action( 'wpseo_register_capabilities' ); WPSEO_Capability_Manager_Factory::get()->add(); } /** * Perform the 14.0.3 upgrade. * * @return void */ private function upgrade_1403() { WPSEO_Options::set( 'ignore_indexation_warning', false ); } /** * Performs the 14.1 upgrade. * * @return void */ private function upgrade_141() { /* * These notifications are retrieved from storage on the `init` hook with * priority 1. We need to remove them after they're retrieved. */ add_action( 'init', [ $this, 'remove_notifications_for_141' ] ); add_action( 'init', [ $this, 'clean_up_private_taxonomies_for_141' ] ); $this->reset_permalinks_of_attachments_for_141(); } /** * Performs the 14.2 upgrade. * * Removes the yoast-acf-analysis notice when it's still in the database. * * @return void */ private function upgrade_142() { add_action( 'init', [ $this, 'remove_acf_notification_for_142' ] ); } /** * Performs the 14.5 upgrade. * * @return void */ private function upgrade_145() { add_action( 'init', [ $this, 'set_indexation_completed_option_for_145' ] ); } /** * Performs the 14.9 upgrade. * * @return void */ private function upgrade_149() { $version = get_option( 'wpseo_license_server_version', 2 ); WPSEO_Options::set( 'license_server_version', $version ); delete_option( 'wpseo_license_server_version' ); } /** * Performs the 15.1 upgrade. * * @return void */ private function upgrade_151() { $this->set_home_url_for_151(); $this->move_indexables_indexation_reason_for_151(); add_action( 'init', [ $this, 'set_permalink_structure_option_for_151' ] ); add_action( 'init', [ $this, 'store_custom_taxonomy_slugs_for_151' ] ); } /** * Performs the 15.3 upgrade. * * @return void */ private function upgrade_153() { WPSEO_Options::set( 'category_base_url', get_option( 'category_base' ) ); WPSEO_Options::set( 'tag_base_url', get_option( 'tag_base' ) ); // Rename a couple of options. $indexation_started_value = WPSEO_Options::get( 'indexation_started' ); WPSEO_Options::set( 'indexing_started', $indexation_started_value ); $indexables_indexing_completed_value = WPSEO_Options::get( 'indexables_indexation_completed' ); WPSEO_Options::set( 'indexables_indexing_completed', $indexables_indexing_completed_value ); } /** * Performs the 15.5 upgrade. * * @return void */ private function upgrade_155() { // Unset the fbadminapp value in the wpseo_social option. $wpseo_social_option = get_option( 'wpseo_social' ); if ( isset( $wpseo_social_option['fbadminapp'] ) ) { unset( $wpseo_social_option['fbadminapp'] ); update_option( 'wpseo_social', $wpseo_social_option ); } } /** * Performs the 15.7 upgrade. * * @return void */ private function upgrade_157() { add_action( 'init', [ $this, 'remove_plugin_updated_notification_for_157' ] ); } /** * Performs the 15.9.1 upgrade routine. * * @return void */ private function upgrade_1591() { $enabled_auto_updates = get_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', [], $enabled_auto_updates ); } /** * Performs the 16.2 upgrade routine. * * @return void */ private function upgrade_162() { $enabled_auto_updates = get_site_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] ); } /** * Performs the 16.5 upgrade. * * @return void */ private function upgrade_165() { add_action( 'init', [ $this, 'copy_og_settings_from_social_to_titles' ], 99 ); // Run after the WPSEO_Options::enrich_defaults method which has priority 99. add_action( 'init', [ $this, 'reset_og_settings_to_default_values' ], 100 ); } /** * Performs the 17.2 upgrade. Cleans out any unnecessary indexables. See $cleanup_integration->get_cleanup_tasks() * to see what will be cleaned out. * * @return void */ private function upgrade_172() { wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' ); wp_unschedule_hook( 'wpseo_cleanup_indexables' ); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 17.7.1 upgrade routine. * * @return void */ private function upgrade_1771() { $enabled_auto_updates = get_site_option( 'auto_update_plugins' ); $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class ); $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] ); } /** * Performs the 17.9 upgrade routine. * * @return void */ private function upgrade_179() { WPSEO_Options::set( 'wincher_integration_active', true ); } /** * Performs the 18.3 upgrade routine. * * @return void */ private function upgrade_183() { $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' ); } /** * Performs the 18.6 upgrade routine. * * @return void */ private function upgrade_186() { if ( is_multisite() ) { WPSEO_Options::set( 'allow_wincher_integration_active', false ); } } /** * Performs the 18.9 upgrade routine. * * @return void */ private function upgrade_189() { // Make old users not get the Installation Success page after upgrading. WPSEO_Options::set( 'should_redirect_after_install_free', false ); // We're adding a hardcoded time here, so that in the future we can be able to identify whether the user did see the Installation Success page or not. // If they did, they wouldn't have this hardcoded value in that option, but rather (roughly) the timestamp of the moment they saw it. WPSEO_Options::set( 'activation_redirect_timestamp_free', 1_652_258_756 ); // Transfer the Social URLs. $other = []; $other[] = WPSEO_Options::get( 'instagram_url' ); $other[] = WPSEO_Options::get( 'linkedin_url' ); $other[] = WPSEO_Options::get( 'myspace_url' ); $other[] = WPSEO_Options::get( 'pinterest_url' ); $other[] = WPSEO_Options::get( 'youtube_url' ); $other[] = WPSEO_Options::get( 'wikipedia_url' ); WPSEO_Options::set( 'other_social_urls', array_values( array_unique( array_filter( $other ) ) ) ); // Transfer the progress of the old Configuration Workout. $workout_data = WPSEO_Options::get( 'workouts_data' ); $old_conf_progress = ( $workout_data['configuration']['finishedSteps'] ?? [] ); if ( in_array( 'optimizeSeoData', $old_conf_progress, true ) && in_array( 'siteRepresentation', $old_conf_progress, true ) ) { // If completed ‘SEO optimization’ and ‘Site representation’ step, we assume the workout was completed. $configuration_finished_steps = [ 'siteRepresentation', 'socialProfiles', 'personalPreferences', ]; WPSEO_Options::set( 'configuration_finished_steps', $configuration_finished_steps ); } } /** * Performs the 19.1 upgrade routine. * * @return void */ private function upgrade_191() { if ( is_multisite() ) { WPSEO_Options::set( 'allow_remove_feed_post_comments', true ); } } /** * Performs the 19.3 upgrade routine. * * @return void */ private function upgrade_193() { if ( empty( get_option( 'wpseo_premium', [] ) ) ) { WPSEO_Options::set( 'enable_index_now', true ); WPSEO_Options::set( 'enable_link_suggestions', true ); } } /** * Performs the 19.6 upgrade routine. * * @return void */ private function upgrade_196() { WPSEO_Options::set( 'ryte_indexability', false ); WPSEO_Options::set( 'allow_ryte_indexability', false ); wp_clear_scheduled_hook( 'wpseo_ryte_fetch' ); } /** * Performs the 19.11 upgrade routine. * * @return void */ private function upgrade_1911() { add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_post_types' ] ); add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_taxonomies' ] ); $this->deduplicate_unindexed_indexable_rows(); $this->remove_indexable_rows_for_disabled_authors_archive(); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.2 upgrade routine. * * @return void */ private function upgrade_202() { if ( WPSEO_Options::get( 'disable-attachment', true ) ) { $attachment_cleanup_helper = YoastSEO()->helpers->attachment_cleanup; $attachment_cleanup_helper->remove_attachment_indexables( true ); $attachment_cleanup_helper->clean_attachment_links_from_target_indexable_ids( true ); } $this->clean_unindexed_indexable_rows_with_no_object_id(); if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { // This schedules the cleanup routine cron again, since in combination of premium cleans up the prominent words table. We also want to cleanup possible orphaned hierarchies from the above cleanups. wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.5 upgrade routine. * * @return void */ private function upgrade_205() { if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 20.7 upgrade routine. * Removes the metadata related to the settings page introduction modal for all the users. * Also, schedules another cleanup scheduled action. * * @return void */ private function upgrade_207() { add_action( 'shutdown', [ $this, 'delete_user_introduction_meta' ] ); } /** * Performs the 20.8 upgrade routine. * Schedules another cleanup scheduled action. * * @return void */ private function upgrade_208() { if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) { wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK ); } } /** * Performs the 22.6 upgrade routine. * Schedules another cleanup scheduled action, but starting from the last cleanup action we just added (if there * aren't any running cleanups already). * * @return void */ private function upgrade_226() { if ( get_option( Cleanup_Integration::CURRENT_TASK_OPTION ) === false ) { $cleanup_integration = YoastSEO()->classes->get( Cleanup_Integration::class ); $cleanup_integration->start_cron_job( 'clean_selected_empty_usermeta', DAY_IN_SECONDS ); } } /** * Sets the home_url option for the 15.1 upgrade routine. * * @return void */ protected function set_home_url_for_151() { $home_url = WPSEO_Options::get( 'home_url' ); if ( empty( $home_url ) ) { WPSEO_Options::set( 'home_url', get_home_url() ); } } /** * Moves the `indexables_indexation_reason` option to the * renamed `indexing_reason` option. * * @return void */ protected function move_indexables_indexation_reason_for_151() { $reason = WPSEO_Options::get( 'indexables_indexation_reason', '' ); WPSEO_Options::set( 'indexing_reason', $reason ); } /** * Checks if the indexable indexation is completed. * If so, sets the `indexables_indexation_completed` option to `true`, * else to `false`. * * @return void */ public function set_indexation_completed_option_for_145() { WPSEO_Options::set( 'indexables_indexation_completed', YoastSEO()->helpers->indexing->get_limited_filtered_unindexed_count( 1 ) === 0 ); } /** * Cleans up the private taxonomies from the indexables table for the upgrade routine to 14.1. * * @return void */ public function clean_up_private_taxonomies_for_141() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // Clean up indexables of private taxonomies. $private_taxonomies = get_taxonomies( [ 'public' => false ], 'names' ); if ( empty( $private_taxonomies ) ) { return; } $replacements = array_merge( [ Model::get_table_name( 'Indexable' ), 'object_type', 'object_sub_type', ], $private_taxonomies, ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IN (" . implode( ', ', array_fill( 0, count( $private_taxonomies ), '%s' ) ) . ')', $replacements, ), ); $wpdb->show_errors = $show_errors; } /** * Resets the permalinks of attachments to `null` in the indexable table for the upgrade routine to 14.1. * * @return void */ private function reset_permalinks_of_attachments_for_141() { global $wpdb; // If migrations haven't been completed succesfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // Reset the permalinks of the attachments in the indexable table. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "UPDATE %i SET %i = NULL WHERE %i = 'post' AND %i = 'attachment'", [ Model::get_table_name( 'Indexable' ), 'permalink', 'object_type', 'object_sub_type' ], ), ); $wpdb->show_errors = $show_errors; } /** * Removes notifications from the Notification center for the 14.1 upgrade. * * @return void */ public function remove_notifications_for_141() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-recalculate' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-blog-public-notice' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-links-table-not-accessible' ); Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-post-type-archive-notification' ); } /** * Removes the wpseo-suggested-plugin-yoast-acf-analysis notification from the Notification center for the 14.2 * upgrade. * * @return void */ public function remove_acf_notification_for_142() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-suggested-plugin-yoast-acf-analysis' ); } /** * Removes the wpseo-plugin-updated notification from the Notification center for the 15.7 upgrade. * * @return void */ public function remove_plugin_updated_notification_for_157() { Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-plugin-updated' ); } /** * Removes all notifications saved in the database under 'wp_yoast_notifications'. * * @return void */ private function clean_all_notifications() { global $wpdb; delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY, '', true ); } /** * Removes the post meta fields for a given meta key. * * @param string $meta_key The meta key. * * @return void */ private function delete_post_meta( $meta_key ) { global $wpdb; $deleted = $wpdb->delete( $wpdb->postmeta, [ 'meta_key' => $meta_key ], [ '%s' ] ); if ( $deleted ) { wp_cache_set( 'last_changed', microtime(), 'posts' ); } } /** * Removes all sitemap validators. * * This should be executed on every upgrade routine until we have removed the sitemap caching in the database. * * @return void */ private function remove_sitemap_validators() { global $wpdb; // Remove all sitemap validators. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE %i LIKE %s', [ $wpdb->options, 'option_name', 'wpseo_sitemap%validator%' ], ), ); } /** * Retrieves the option value directly from the database. * * @param string $option_name Option to retrieve. * * @return int|string|bool|float|array<string|int|bool|float> The content of the option if exists, otherwise an * empty array. */ protected function get_option_from_database( $option_name ) { global $wpdb; // Load option directly from the database, to avoid filtering and sanitization. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $results = $wpdb->get_results( $wpdb->prepare( 'SELECT %i FROM %i WHERE %i = %s', [ 'option_value', $wpdb->options, 'option_name', $option_name ], ), ARRAY_A, ); if ( ! empty( $results ) ) { return maybe_unserialize( $results[0]['option_value'] ); } return []; } /** * Cleans the option to make sure only relevant settings are there. * * @param string $option_name Option name save. * * @return void */ protected function cleanup_option_data( $option_name ) { $data = get_option( $option_name, [] ); if ( ! is_array( $data ) || $data === [] ) { return; } /* * Clean up the option by re-saving it. * * The option framework will remove any settings that are not configured * for this option, removing any migrated settings. */ update_option( $option_name, $data ); } /** * Saves an option setting to where it should be stored. * * @param int|string|bool|float|array<string|int|bool|float> $source_data The option containing the value to be * migrated. * @param string $source_setting Name of the key in the "from" option. * @param string|null $target_setting Name of the key in the "to" option. * * @return void */ protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) { $target_setting ??= $source_setting; if ( isset( $source_data[ $source_setting ] ) ) { WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] ); } } /** * Migrates WooCommerce archive settings to the WooCommerce Shop page meta-data settings. * * If no Shop page is defined, nothing will be migrated. * * @return void */ private function migrate_woocommerce_archive_setting_to_shop_page() { $shop_page_id = wc_get_page_id( 'shop' ); if ( $shop_page_id === -1 ) { return; } $title = WPSEO_Meta::get_value( 'title', $shop_page_id ); if ( empty( $title ) ) { $option_title = WPSEO_Options::get( 'title-ptarchive-product' ); WPSEO_Meta::set_value( 'title', $option_title, $shop_page_id, ); WPSEO_Options::set( 'title-ptarchive-product', '' ); } $meta_description = WPSEO_Meta::get_value( 'metadesc', $shop_page_id ); if ( empty( $meta_description ) ) { $option_metadesc = WPSEO_Options::get( 'metadesc-ptarchive-product' ); WPSEO_Meta::set_value( 'metadesc', $option_metadesc, $shop_page_id, ); WPSEO_Options::set( 'metadesc-ptarchive-product', '' ); } $bc_title = WPSEO_Meta::get_value( 'bctitle', $shop_page_id ); if ( empty( $bc_title ) ) { $option_bctitle = WPSEO_Options::get( 'bctitle-ptarchive-product' ); WPSEO_Meta::set_value( 'bctitle', $option_bctitle, $shop_page_id, ); WPSEO_Options::set( 'bctitle-ptarchive-product', '' ); } $noindex = WPSEO_Meta::get_value( 'meta-robots-noindex', $shop_page_id ); if ( $noindex === '0' ) { $option_noindex = WPSEO_Options::get( 'noindex-ptarchive-product' ); WPSEO_Meta::set_value( 'meta-robots-noindex', $option_noindex, $shop_page_id, ); WPSEO_Options::set( 'noindex-ptarchive-product', false ); } } /** * Stores the initial `permalink_structure` option. * * @return void */ public function set_permalink_structure_option_for_151() { WPSEO_Options::set( 'permalink_structure', get_option( 'permalink_structure' ) ); } /** * Stores the initial slugs of custom taxonomies. * * @return void */ public function store_custom_taxonomy_slugs_for_151() { $taxonomies = $this->taxonomy_helper->get_custom_taxonomies(); $custom_taxonomies = []; foreach ( $taxonomies as $taxonomy ) { $slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy ); $custom_taxonomies[ $taxonomy ] = $slug; } WPSEO_Options::set( 'custom_taxonomy_slugs', $custom_taxonomies ); } /** * Copies the frontpage social settings to the titles options. * * @return void */ public function copy_og_settings_from_social_to_titles() { $wpseo_social = get_option( 'wpseo_social' ); $wpseo_titles = get_option( 'wpseo_titles' ); $copied_options = []; // Reset to the correct default value. $copied_options['open_graph_frontpage_title'] = '%%sitename%%'; $options = [ 'og_frontpage_title' => 'open_graph_frontpage_title', 'og_frontpage_desc' => 'open_graph_frontpage_desc', 'og_frontpage_image' => 'open_graph_frontpage_image', 'og_frontpage_image_id' => 'open_graph_frontpage_image_id', ]; foreach ( $options as $social_option => $titles_option ) { if ( ! empty( $wpseo_social[ $social_option ] ) ) { $copied_options[ $titles_option ] = $wpseo_social[ $social_option ]; } } $wpseo_titles = array_merge( $wpseo_titles, $copied_options ); update_option( 'wpseo_titles', $wpseo_titles ); } /** * Reset the social options with the correct default values. * * @return void */ public function reset_og_settings_to_default_values() { $wpseo_titles = get_option( 'wpseo_titles' ); $updated_options = []; $updated_options['social-title-author-wpseo'] = '%%name%%'; $updated_options['social-title-archive-wpseo'] = '%%date%%'; /* translators: %s expands to the name of a post type (plural). */ $post_type_archive_default = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' ); /* translators: %s expands to the variable used for term title. */ $term_archive_default = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' ); $post_type_objects = get_post_types( [ 'public' => true ], 'objects' ); if ( $post_type_objects ) { foreach ( $post_type_objects as $pt ) { // Post types. if ( isset( $wpseo_titles[ 'social-title-' . $pt->name ] ) ) { $updated_options[ 'social-title-' . $pt->name ] = '%%title%%'; } // Post type archives. if ( isset( $wpseo_titles[ 'social-title-ptarchive-' . $pt->name ] ) ) { $updated_options[ 'social-title-ptarchive-' . $pt->name ] = $post_type_archive_default; } } } $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' ); if ( $taxonomy_objects ) { foreach ( $taxonomy_objects as $tax ) { if ( isset( $wpseo_titles[ 'social-title-tax-' . $tax->name ] ) ) { $updated_options[ 'social-title-tax-' . $tax->name ] = $term_archive_default; } } } $wpseo_titles = array_merge( $wpseo_titles, $updated_options ); update_option( 'wpseo_titles', $wpseo_titles ); } /** * Removes all indexables for posts that are not publicly viewable. * This method should be called after init, because post_types can still be registered. * * @return void */ public function remove_indexable_rows_for_non_public_post_types() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = YoastSEO()->helpers->post_type->get_indexable_post_types(); if ( empty( $included_post_types ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'post' AND %i IS NOT NULL", [ $indexable_table, 'object_type', 'object_sub_type' ], ), ); } else { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'post' AND %i IS NOT NULL AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_post_types ), '%s' ) ) . ' )', array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type', ], $included_post_types, ), ), ); } $wpdb->show_errors = $show_errors; } /** * Removes all indexables for terms that are not publicly viewable. * This method should be called after init, because taxonomies can still be registered. * * @return void */ public function remove_indexable_rows_for_non_public_taxonomies() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; $indexable_table = Model::get_table_name( 'Indexable' ); $included_taxonomies = YoastSEO()->helpers->taxonomy->get_indexable_taxonomies(); if ( empty( $included_taxonomies ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IS NOT NULL", [ $indexable_table, 'object_type', 'object_sub_type' ], ), ); } else { // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'term' AND %i IS NOT NULL AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_taxonomies ), '%s' ) ) . ' )', array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type', ], $included_taxonomies, ), ), ); } $wpdb->show_errors = $show_errors; } /** * De-duplicates indexables that have more than one "unindexed" rows for the same object. Keeps the newest * indexable. * * @return void */ protected function deduplicate_unindexed_indexable_rows() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $duplicates = $wpdb->get_results( $wpdb->prepare( " SELECT MAX(id) as newest_id, object_id, object_type FROM %i WHERE post_status = 'unindexed' AND object_type IN ( 'term', 'post', 'user' ) GROUP BY object_id, object_type HAVING count(*) > 1", [ Model::get_table_name( 'Indexable' ) ], ), ARRAY_A, ); if ( empty( $duplicates ) ) { $wpdb->show_errors = $show_errors; return; } // Users, terms and posts may share the same object_id. So delete them in separate, more performant, queries. $delete_queries = [ $this->get_indexable_deduplication_query_for_type( 'post', $duplicates, $wpdb ), $this->get_indexable_deduplication_query_for_type( 'term', $duplicates, $wpdb ), $this->get_indexable_deduplication_query_for_type( 'user', $duplicates, $wpdb ), ]; foreach ( $delete_queries as $delete_query ) { if ( ! empty( $delete_query ) ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. $wpdb->query( $delete_query ); // phpcs:enable } } $wpdb->show_errors = $show_errors; } /** * Cleans up "unindexed" indexable rows when appropriate, aka when there's no object ID even though it should. * * @return void */ protected function clean_unindexed_indexable_rows_with_no_object_id() { global $wpdb; // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'unindexed' AND %i NOT IN ( 'home-page', 'date-archive', 'post-type-archive', 'system-page' ) AND %i IS NULL", [ Model::get_table_name( 'Indexable' ), 'post_status', 'object_type', 'object_id' ], ), ); $wpdb->show_errors = $show_errors; } /** * Removes all user indexable rows when the author archive is disabled. * * @return void */ protected function remove_indexable_rows_for_disabled_authors_archive() { global $wpdb; if ( ! YoastSEO()->helpers->author_archive->are_disabled() ) { return; } // If migrations haven't been completed successfully the following may give false errors. So suppress them. $show_errors = $wpdb->show_errors; $wpdb->show_errors = false; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE %i = 'user'", [ Model::get_table_name( 'Indexable' ), 'object_type' ], ), ); $wpdb->show_errors = $show_errors; } /** * Creates a query for de-duplicating indexables for a particular type. * * @param string $object_type The object type to deduplicate. * @param string|array<array<int,int,string>> $duplicates The result of the duplicate query. * @param wpdb $wpdb The wpdb object. * * @return string The query that removes all but one duplicate for each object of the object type. */ protected function get_indexable_deduplication_query_for_type( $object_type, $duplicates, $wpdb ) { $filtered_duplicates = array_filter( $duplicates, static function ( $duplicate ) use ( $object_type ) { return $duplicate['object_type'] === $object_type; }, ); if ( empty( $filtered_duplicates ) ) { return ''; } $object_ids = wp_list_pluck( $filtered_duplicates, 'object_id' ); $newest_indexable_ids = wp_list_pluck( $filtered_duplicates, 'newest_id' ); $replacements = array_merge( [ Model::get_table_name( 'Indexable' ), 'object_id', ], array_values( $object_ids ), array_values( $newest_indexable_ids ), ); $replacements[] = $object_type; // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. return $wpdb->prepare( 'DELETE FROM %i WHERE %i IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' ) AND id NOT IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' ) AND object_type = %s', $replacements, ); } /** * Removes the settings' introduction modal data for users. * * @return void */ public function delete_user_introduction_meta() { delete_metadata( 'user', 0, '_yoast_settings_introduction', '', true ); } } inc/class-wpseo-custom-taxonomies.php 0000644 00000003136 15174712003 0013765 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Custom_Taxonomies. */ class WPSEO_Custom_Taxonomies { /** * Custom taxonomies cache. * * @var array|null */ protected static $custom_taxonomies = null; /** * Gets the names of the custom taxonomies, prepends 'ct_' and 'ct_desc', and returns them in an array. * * @return array The custom taxonomy prefixed names. */ public static function get_custom_taxonomies() { // Use cached value if available. if ( self::$custom_taxonomies !== null ) { return self::$custom_taxonomies; } self::$custom_taxonomies = []; $args = [ 'public' => true, '_builtin' => false, ]; $custom_taxonomies = get_taxonomies( $args, 'names', 'and' ); if ( is_array( $custom_taxonomies ) ) { foreach ( $custom_taxonomies as $custom_taxonomy ) { array_push( self::$custom_taxonomies, self::add_custom_taxonomies_prefix( $custom_taxonomy ), self::add_custom_taxonomies_description_prefix( $custom_taxonomy ), ); } } return self::$custom_taxonomies; } /** * Adds the ct_ prefix to a taxonomy. * * @param string $taxonomy The taxonomy to prefix. * * @return string The prefixed taxonomy. */ private static function add_custom_taxonomies_prefix( $taxonomy ) { return 'ct_' . $taxonomy; } /** * Adds the ct_desc_ prefix to a taxonomy. * * @param string $taxonomy The taxonomy to prefix. * * @return string The prefixed taxonomy. */ private static function add_custom_taxonomies_description_prefix( $taxonomy ) { return 'ct_desc_' . $taxonomy; } } inc/sitemaps/interface-sitemap-cache-data.php 0000644 00000002315 15174712003 0015224 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Cache Data interface. */ interface WPSEO_Sitemap_Cache_Data_Interface { /** * Status for normal, usable sitemap. * * @var string */ public const OK = 'ok'; /** * Status for unusable sitemap. * * @var string */ public const ERROR = 'error'; /** * Status for unusable sitemap because it cannot be identified. * * @var string */ public const UNKNOWN = 'unknown'; /** * Set the content of the sitemap. * * @param string $sitemap The XML content of the sitemap. * * @return void */ public function set_sitemap( $sitemap ); /** * Set the status of the sitemap. * * @param bool|string $usable True/False or 'ok'/'error' for status. * * @return void */ public function set_status( $usable ); /** * Builds the sitemap. * * @return string The XML content of the sitemap. */ public function get_sitemap(); /** * Get the status of this sitemap. * * @return string Status 'ok', 'error' or 'unknown'. */ public function get_status(); /** * Is the sitemap content usable ? * * @return bool True if the sitemap is usable, False if not. */ public function is_usable(); } inc/sitemaps/class-post-type-sitemap-provider.php 0000644 00000046620 15174712003 0016222 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ use Yoast\WP\SEO\Models\SEO_Links; /** * Sitemap provider for author archives. */ class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Holds image parser instance. * * @var WPSEO_Sitemap_Image_Parser */ protected static $image_parser; /** * Holds the parsed home url. * * @var array */ protected static $parsed_home_url; /** * Determines whether images should be included in the XML sitemap. * * @var bool */ private $include_images; /** * Set up object properties for data reuse. */ public function __construct() { add_action( 'save_post', [ $this, 'save_post' ] ); /** * Filter - Allows excluding images from the XML sitemap. * * @param bool $include True to include, false to exclude. */ $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } /** * Gets the parsed home url. * * @return array The home url, as parsed by wp_parse_url. */ protected function get_parsed_home_url() { if ( ! isset( self::$parsed_home_url ) ) { self::$parsed_home_url = wp_parse_url( home_url() ); } return self::$parsed_home_url; } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { return post_type_exists( $type ); } /** * Retrieves the sitemap links. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { global $wpdb; $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] ); $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true ); $index = []; foreach ( $post_types as $post_type ) { $total_count = $this->get_post_type_count( $post_type ); if ( $total_count === 0 ) { continue; } $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $all_dates = []; if ( $max_pages > 1 ) { $all_dates = version_compare( $wpdb->db_version(), '8.0', '>=' ) ? $this->get_all_dates_using_with_clause( $post_type, $max_entries ) : $this->get_all_dates( $post_type, $max_entries ); } for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $date = false; if ( empty( $current_page ) || $current_page === $max_pages ) { if ( ! empty( $last_modified_times[ $post_type ] ) ) { $date = $last_modified_times[ $post_type ]; } } else { $date = $all_dates[ $page_counter ]; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; $post_type = $type; if ( ! $this->is_valid_post_type( $post_type ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $steps = min( 100, $max_entries ); $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; $total = ( $offset + $max_entries ); $post_type_entries = $this->get_post_type_count( $post_type ); if ( $total > $post_type_entries ) { $total = $post_type_entries; } if ( $current_page === 1 ) { $links = array_merge( $links, $this->get_first_links( $post_type ) ); } // If total post type count is lower than the offset, an invalid page is requested. if ( $post_type_entries < $offset ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } if ( $post_type_entries === 0 ) { return $links; } $posts_to_exclude = $this->get_excluded_posts( $type ); while ( $total > $offset ) { $posts = $this->get_posts( $post_type, $steps, $offset ); $offset += $steps; if ( empty( $posts ) ) { continue; } foreach ( $posts as $post ) { if ( in_array( $post->ID, $posts_to_exclude, true ) ) { continue; } if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) { continue; } $url = $this->get_url( $post ); if ( ! isset( $url['loc'] ) ) { continue; } /** * Filter URL entry before it gets added to the sitemap. * * @param array $url Array of URL parts. * @param string $type URL type. * @param object $post Data object for the URL. */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post ); if ( ! empty( $url ) ) { $links[] = $url; } } unset( $post, $url ); } return $links; } /** * Check for relevant post type before invalidation. * * @param int $post_id Post ID to possibly invalidate for. * * @return void */ public function save_post( $post_id ) { if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) { WPSEO_Sitemaps_Cache::invalidate_post( $post_id ); } } /** * Check if post type should be present in sitemaps. * * @param string $post_type Post type string to check for. * * @return bool */ public function is_valid_post_type( $post_type ) { if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) { return false; } /** * Filter decision if post type is excluded from the XML sitemap. * * @param bool $exclude Default false. * @param string $post_type Post type name. */ if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $post_type ) ) { return false; } return true; } /** * Retrieves a list with the excluded post ids. * * @param string $post_type Post type. * * @return array Array with post ids to exclude. */ protected function get_excluded_posts( $post_type ) { $excluded_posts_ids = []; $page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0; if ( $page_on_front_id > 0 ) { $excluded_posts_ids[] = $page_on_front_id; } /** * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude. * * @param array $posts_to_exclude The posts to exclude. */ $excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids ); if ( ! is_array( $excluded_posts_ids ) ) { $excluded_posts_ids = []; } $excluded_posts_ids = array_map( 'intval', $excluded_posts_ids ); $page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0; if ( $page_for_posts_id > 0 ) { $excluded_posts_ids[] = $page_for_posts_id; } return array_unique( $excluded_posts_ids ); } /** * Get count of posts for post type. * * @param string $post_type Post type to retrieve count for. * * @return int */ protected function get_post_type_count( $post_type ) { global $wpdb; /** * Filter JOIN query part for type count of post type. * * @param string $join SQL part, defaults to empty string. * @param string $post_type Post type name. */ $join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type ); /** * Filter WHERE query part for type count of post type. * * @param string $where SQL part, defaults to empty string. * @param string $post_type Post type name. */ $where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type ); $where = $this->get_sql_where_clause( $post_type ); $sql = " SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} "; return (int) $wpdb->get_var( $sql ); } /** * Produces set of links to prepend at start of first sitemap page. * * @param string $post_type Post type to produce links for. * * @return array */ protected function get_first_links( $post_type ) { $links = []; $archive_url = false; if ( $post_type === 'page' ) { $page_on_front_id = (int) get_option( 'page_on_front' ); if ( $page_on_front_id > 0 ) { $front_page = $this->get_url( get_post( $page_on_front_id ), ); } if ( empty( $front_page ) ) { $front_page = [ 'loc' => YoastSEO()->helpers->url->home(), ]; } // Deprecated, kept for backwards data compat. R. $front_page['chf'] = 'daily'; $front_page['pri'] = 1; $images = ( $front_page['images'] ?? [] ); /** * Filter images to be included for the term in XML sitemap. * * @param array $images Array of image items. * @return array $image_list Array of image items. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_front_page', $images ); if ( is_array( $image_list ) ) { $front_page['images'] = $image_list; } $links[] = $front_page; } elseif ( $post_type !== 'page' ) { /** * Filter the URL Yoast SEO uses in the XML sitemap for this post type archive. * * @param string $archive_url The URL of this archive * @param string $post_type The post type this archive is for. */ $archive_url = apply_filters( 'wpseo_sitemap_post_type_archive_link', $this->get_post_type_archive_link( $post_type ), $post_type, ); } if ( $archive_url ) { $links[] = [ 'loc' => $archive_url, 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; } /** * Filters the first post type links. * * @param array $links The first post type links. * @param string $post_type The post type this archive is for. */ return apply_filters( 'wpseo_sitemap_post_type_first_links', $links, $post_type ); } /** * Get URL for a post type archive. * * @since 5.3 * * @param string $post_type Post type. * * @return string|bool URL or false if it should be excluded. */ protected function get_post_type_archive_link( $post_type ) { $pt_archive_page_id = -1; if ( $post_type === 'post' ) { if ( get_option( 'show_on_front' ) === 'posts' ) { return YoastSEO()->helpers->url->home(); } $pt_archive_page_id = (int) get_option( 'page_for_posts' ); // Post archive should be excluded if posts page isn't set. if ( $pt_archive_page_id <= 0 ) { return false; } } if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) { return false; } return get_post_type_archive_link( $post_type ); } /** * Determines whether a post type archive is indexable. * * @since 11.5 * * @param string $post_type Post type. * @param int $archive_page_id The page id. * * @return bool True when post type archive is indexable. */ protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) { if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) { return false; } /** * Filter the page which is dedicated to this post type archive. * * @since 9.3 * * @param string $archive_page_id The post_id of the page. * @param string $post_type The post type this archive is for. */ $archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type ); if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) { return false; } return true; } /** * Retrieve set of posts with optimized query routine. * * @param string $post_type Post type to retrieve. * @param int $count Count of posts to retrieve. * @param int $offset Starting offset. * * @return object[] */ protected function get_posts( $post_type, $count, $offset ) { global $wpdb; static $filters = []; if ( ! isset( $filters[ $post_type ] ) ) { // Make sure you're wpdb->preparing everything you throw into this!! $filters[ $post_type ] = [ /** * Filter JOIN query part for the post type. * * @param string $join SQL part, defaults to false. * @param string $post_type Post type name. */ 'join' => apply_filters( 'wpseo_posts_join', false, $post_type ), /** * Filter WHERE query part for the post type. * * @param string $where SQL part, defaults to false. * @param string $post_type Post type name. */ 'where' => apply_filters( 'wpseo_posts_where', false, $post_type ), ]; } $join_filter = $filters[ $post_type ]['join']; $where_filter = $filters[ $post_type ]['where']; $where = $this->get_sql_where_clause( $post_type ); /* * Optimized query per this thread: * {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}. * Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}. */ $sql = " SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt FROM ( SELECT {$wpdb->posts}.ID FROM {$wpdb->posts} {$join_filter} {$where} {$where_filter} ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d ) o JOIN {$wpdb->posts} l ON l.ID = o.ID "; $posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); $post_ids = []; foreach ( $posts as $post_index => $post ) { $post->post_type = $post_type; $sanitized_post = sanitize_post( $post, 'raw' ); $posts[ $post_index ] = new WP_Post( $sanitized_post ); $post_ids[] = $sanitized_post->ID; } update_meta_cache( 'post', $post_ids ); return $posts; } /** * Constructs an SQL where clause for a given post type. * * @param string $post_type Post type slug. * * @return string */ protected function get_sql_where_clause( $post_type ) { global $wpdb; $join = ''; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')"; // Based on WP_Query->get_posts(). R. if ( $post_type === 'attachment' ) { $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) "; $parent_statuses = array_diff( $post_statuses, [ 'inherit' ] ); $status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''"; } $where_clause = " {$join} WHERE {$status_where} AND {$wpdb->posts}.post_type = %s AND {$wpdb->posts}.post_password = '' AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00' "; return $wpdb->prepare( $where_clause, $post_type ); } /** * Produce array of URL parts for given post object. * * @param object $post Post object to get URL parts for. * * @return array|bool */ protected function get_url( $post ) { $url = []; /** * Filter the URL Yoast SEO uses in the XML sitemap. * * Note that only absolute local URLs are allowed as the check after this removes external URLs. * * @param string $url URL to use in the XML sitemap * @param object $post Post object for the URL. */ $url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post ); $link_type = YoastSEO()->helpers->url->get_link_type( wp_parse_url( $url['loc'] ), $this->get_parsed_home_url(), ); /* * Do not include external URLs. * * {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs. */ if ( $link_type === SEO_Links::TYPE_EXTERNAL ) { return false; } $modified = max( $post->post_modified_gmt, $post->post_date_gmt ); if ( $modified !== '0000-00-00 00:00:00' ) { $url['mod'] = $modified; } $url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R. $canonical = WPSEO_Meta::get_value( 'canonical', $post->ID ); if ( $canonical !== '' && $canonical !== $url['loc'] ) { /* * Let's assume that if a canonical is set for this page and it's different from * the URL of this post, that page is either already in the XML sitemap OR is on * an external site, either way, we shouldn't include it here. */ return false; } unset( $canonical ); $url['pri'] = 1; // Deprecated, kept for backwards data compat. R. if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_images( $post ); } return $url; } /** * Get all dates for a post type by using the WITH clause for performance. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates_using_with_clause( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'ordering', 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, 'post_modified_gmt', 'post_modified_gmt', 'ordering', $max_entries, ], ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' WITH %i AS (SELECT ROW_NUMBER() OVER (ORDER BY %i) AS n, post_modified_gmt FROM %i USE INDEX ( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s ORDER BY %i) SELECT %i FROM %i WHERE MOD(n, %d) = 0; ', $replacements, ), ); } /** * Get all dates for a post type. * * @param string $post_type Post type to retrieve dates for. * @param int $max_entries Maximum number of entries to retrieve. * * @return array Array of dates. */ private function get_all_dates( $post_type, $max_entries ) { global $wpdb; $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, 'type_status_date', 'post_status', ], $post_statuses, [ 'post_type', $post_type, $max_entries, 'post_modified_gmt', ], ); return $wpdb->get_col( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i FROM ( SELECT @rownum:=0 ) init JOIN %i USE INDEX( %i ) WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i = %s AND ( @rownum:=@rownum+1 ) %% %d = 0 ORDER BY %i ASC ', $replacements, ), ); } } inc/sitemaps/class-sitemap-cache-data.php 0000644 00000011376 15174712003 0014400 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap Cache Data object, manages sitemap data stored in cache. */ class WPSEO_Sitemap_Cache_Data implements Serializable, WPSEO_Sitemap_Cache_Data_Interface { /** * Sitemap XML data. * * @var string */ private $sitemap = ''; /** * Status of the sitemap, usable or not. * * @var string */ private $status = self::UNKNOWN; /** * Set the sitemap XML data * * @param string $sitemap XML Content of the sitemap. * * @return void */ public function set_sitemap( $sitemap ) { if ( ! is_string( $sitemap ) ) { $sitemap = ''; } $this->sitemap = $sitemap; /* * Empty sitemap is not usable. */ if ( ! empty( $sitemap ) ) { $this->set_status( self::OK ); } else { $this->set_status( self::ERROR ); } } /** * Set the status of the sitemap, is it usable. * * @param bool|string $usable Is the sitemap usable or not. * * @return void */ public function set_status( $usable ) { if ( $usable === self::OK ) { $this->status = self::OK; return; } if ( $usable === self::ERROR ) { $this->status = self::ERROR; $this->sitemap = ''; return; } $this->status = self::UNKNOWN; } /** * Is the sitemap usable. * * @return bool True if usable, False if bad or unknown. */ public function is_usable() { return $this->status === self::OK; } /** * Get the XML content of the sitemap. * * @return string The content of the sitemap. */ public function get_sitemap() { return $this->sitemap; } /** * Get the status of the sitemap. * * @return string Status of the sitemap, 'ok'/'error'/'unknown'. */ public function get_status() { return $this->status; } /** * String representation of object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @return array The data to be serialized. */ public function __serialize() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound $data = [ 'status' => $this->status, 'xml' => $this->sitemap, ]; return $data; } /** * Constructs the object. * * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} * * @link https://www.php.net/language.oop5.magic#object.serialize * @link https://wiki.php.net/rfc/custom_object_serialization * * @since 17.8.0 * * @param array $data The unserialized data to use to (re)construct the object. * * @return void */ public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound $this->set_sitemap( $data['xml'] ); $this->set_status( $data['status'] ); } /** * String representation of object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.serialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @return string The string representation of the object or null in C-format. */ public function serialize() { return serialize( $this->__serialize() ); } /** * Constructs the object. * * {@internal The magic methods take precedence over the Serializable interface. * This means that in practice, this method will now only be called on PHP < 7.4. * For PHP 7.4 and higher, the magic methods will be used instead.} * * {@internal The Serializable interface is being phased out, in favour of the magic methods. * This method should be deprecated and removed and the class should no longer * implement the `Serializable` interface. * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} * * @link http://php.net/manual/en/serializable.unserialize.php * @link https://wiki.php.net/rfc/phase_out_serializable * * @since 5.1.0 * * @param string $data The string representation of the object in C or O-format. * * @return void */ public function unserialize( $data ) { $data = unserialize( $data ); $this->__unserialize( $data ); } } inc/sitemaps/interface-sitemap-provider.php 0000644 00000001443 15174712003 0015105 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap Provider interface. */ interface WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ); /** * Get set of sitemaps index link data. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ); /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array */ public function get_sitemap_links( $type, $max_entries, $current_page ); } inc/sitemaps/class-sitemaps-cache-validator.php 0000644 00000022655 15174712003 0015641 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Handles storage keys for sitemaps caching and invalidation. * * @since 3.2 */ class WPSEO_Sitemaps_Cache_Validator { /** * Prefix of the transient key for sitemap caches. * * @var string */ public const STORAGE_KEY_PREFIX = 'yst_sm_'; /** * Name of the option that holds the global validation value. * * @var string */ public const VALIDATION_GLOBAL_KEY = 'wpseo_sitemap_cache_validator_global'; /** * The format which creates the key of the option that holds the type validation value. * * @var string */ public const VALIDATION_TYPE_KEY_FORMAT = 'wpseo_sitemap_%s_cache_validator'; /** * Get the cache key for a certain type and page. * * A type of cache would be something like 'page', 'post' or 'video'. * * Example key format for sitemap type "post", page 1: wpseo_sitemap_post_1:akfw3e_23azBa . * * @since 3.2 * * @param string|null $type The type to get the key for. Null or self::SITEMAP_INDEX_TYPE for index cache. * @param int $page The page of cache to get the key for. * * @return bool|string The key where the cache is stored on. False if the key could not be generated. */ public static function get_storage_key( $type = null, $page = 1 ) { // Using SITEMAP_INDEX_TYPE for sitemap index cache. $type = ( $type === null ) ? WPSEO_Sitemaps::SITEMAP_INDEX_TYPE : $type; $global_cache_validator = self::get_validator(); $type_cache_validator = self::get_validator( $type ); $prefix = self::STORAGE_KEY_PREFIX; $postfix = sprintf( '_%d:%s_%s', $page, $global_cache_validator, $type_cache_validator ); try { $type = self::truncate_type( $type, $prefix, $postfix ); } catch ( OutOfBoundsException $exception ) { // Maybe do something with the exception, for now just mark as invalid. return false; } // Build key. $full_key = $prefix . $type . $postfix; return $full_key; } /** * If the type is over length make sure we compact it so we don't have any database problems. * * When there are more 'extremely long' post types, changes are they have variations in either the start or ending. * Because of this, we cut out the excess in the middle which should result in less chance of collision. * * @since 3.2 * * @param string $type The type of sitemap to be used. * @param string $prefix The part before the type in the cache key. Only the length is used. * @param string $postfix The part after the type in the cache key. Only the length is used. * * @return string The type with a safe length to use * * @throws OutOfRangeException When there is less than 15 characters of space for a key that is originally longer. */ public static function truncate_type( $type, $prefix = '', $postfix = '' ) { /* * This length has been restricted by the database column length of 64 in the past. * The prefix added by WordPress is '_transient_' because we are saving to a transient. * We need to use a timeout on the transient, otherwise the values get autoloaded, this adds * another restriction to the length. */ $max_length = 45; // 64 - 19 ('_transient_timeout_') $max_length -= strlen( $prefix ); $max_length -= strlen( $postfix ); if ( strlen( $type ) > $max_length ) { if ( $max_length < 15 ) { /* * If this happens the most likely cause is a page number that is too high. * * So this would not happen unintentionally. * Either by trying to cause a high server load, finding backdoors or misconfiguration. */ throw new OutOfRangeException( __( 'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.', 'wordpress-seo', ), ); } $half = ( $max_length / 2 ); $first_part = substr( $type, 0, ( ceil( $half ) - 1 ) ); $last_part = substr( $type, ( 1 - floor( $half ) ) ); $type = $first_part . '..' . $last_part; } return $type; } /** * Invalidate sitemap cache. * * @since 3.2 * * @param string|null $type The type to get the key for. Null for all caches. * * @return void */ public static function invalidate_storage( $type = null ) { // Global validator gets cleared when no type is provided. $old_validator = null; // Get the current type validator. if ( $type !== null ) { $old_validator = self::get_validator( $type ); } // Refresh validator. self::create_validator( $type ); if ( ! wp_using_ext_object_cache() ) { // Clean up current cache from the database. self::cleanup_database( $type, $old_validator ); } // External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that. } /** * Cleanup invalidated database cache. * * @since 3.2 * * @param string|null $type The type of sitemap to clear cache for. * @param string|null $validator The validator to clear cache of. * * @return void */ public static function cleanup_database( $type = null, $validator = null ) { global $wpdb; if ( $type === null ) { // Clear all cache if no type is provided. $like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX ); } else { // Clear type cache for all type keys. $like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type ); } /* * Add slashes to the LIKE "_" single character wildcard. * * We can't use `esc_like` here because we need the % in the query. */ $where = []; $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) ); $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) ); // Delete transients. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $wpdb->query( $wpdb->prepare( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. 'DELETE FROM %i WHERE ' . implode( ' OR ', array_fill( 0, count( $where ), '%s' ) ), array_merge( [ $wpdb->options ], $where ), ), ); wp_cache_delete( 'alloptions', 'options' ); } /** * Get the current cache validator. * * Without the type the global validator is returned. * This can invalidate -all- keys in cache at once. * * With the type parameter the validator for that specific type can be invalidated. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string|null The validator for the supplied type. */ public static function get_validator( $type = '' ) { $key = self::get_validator_key( $type ); $current = get_option( $key, null ); if ( $current !== null ) { return $current; } if ( self::create_validator( $type ) ) { return self::get_validator( $type ); } return null; } /** * Get the cache validator option key for the specified type. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return string Validator to be used to generate the cache key. */ public static function get_validator_key( $type = '' ) { if ( empty( $type ) ) { return self::VALIDATION_GLOBAL_KEY; } return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type ); } /** * Refresh the cache validator value. * * @since 3.2 * * @param string $type Provide a type for a specific type validator, empty for global validator. * * @return bool True if validator key has been saved as option. */ public static function create_validator( $type = '' ) { $key = self::get_validator_key( $type ); // Generate new validator. $microtime = microtime(); // Remove space. list( $milliseconds, $seconds ) = explode( ' ', $microtime ); // Transients are purged every 24h. $seconds = ( $seconds % DAY_IN_SECONDS ); $milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 ); // Combine seconds and milliseconds and convert to integer. $validator = intval( $seconds . '' . $milliseconds, 10 ); // Apply base 61 encoding. $compressed = self::convert_base10_to_base61( $validator ); return update_option( $key, $compressed, false ); } /** * Encode to base61 format. * * This is base64 (numeric + alpha + alpha upper case) without the 0. * * @since 3.2 * * @param int $base10 The number that has to be converted to base 61. * * @return string Base 61 converted string. * * @throws InvalidArgumentException When the input is not an integer. */ public static function convert_base10_to_base61( $base10 ) { if ( ! is_int( $base10 ) ) { throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) ); } // Characters that will be used in the conversion. $characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $length = strlen( $characters ); $remainder = $base10; $output = ''; do { // Building from right to left in the result. $index = ( $remainder % $length ); // Prepend the character to the output. $output = $characters[ $index ] . $output; // Determine the remainder after removing the applied number. $remainder = floor( $remainder / $length ); // Keep doing it until we have no remainder left. } while ( $remainder ); return $output; } } inc/sitemaps/class-sitemap-image-parser.php 0000644 00000027261 15174712003 0015002 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Parses images from the given post. */ class WPSEO_Sitemap_Image_Parser { /** * Holds the home_url() value to speed up loops. * * @var string */ protected $home_url = ''; /** * Holds site URL hostname. * * @var string */ protected $host = ''; /** * Holds site URL protocol. * * @var string */ protected $scheme = 'http'; /** * Cached set of attachments for multiple posts. * * @var array */ protected $attachments = []; /** * Holds blog charset value for use in DOM parsing. * * @var string */ protected $charset = 'UTF-8'; /** * Set up URL properties for reuse. */ public function __construct() { $this->home_url = home_url(); $parsed_home = wp_parse_url( $this->home_url ); if ( ! empty( $parsed_home['host'] ) ) { $this->host = str_replace( 'www.', '', $parsed_home['host'] ); } if ( ! empty( $parsed_home['scheme'] ) ) { $this->scheme = $parsed_home['scheme']; } $this->charset = esc_attr( get_bloginfo( 'charset' ) ); } /** * Get set of image data sets for the given post. * * @param object $post Post object to get images for. * * @return array */ public function get_images( $post ) { $images = []; if ( ! is_object( $post ) ) { return $images; } $thumbnail_id = get_post_thumbnail_id( $post->ID ); if ( $thumbnail_id ) { $src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) ); $images[] = $this->get_image_item( $post, $src ); } /** * Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content * before it is parsed for images. * * @param string $content The raw/unprocessed post content. */ $content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content ); $unfiltered_images = $this->parse_html_images( $content ); foreach ( $unfiltered_images as $image ) { $images[] = $this->get_image_item( $post, $image['src'] ); } foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) { $src = $this->get_absolute_url( $this->image_url( $attachment->ID ) ); $images[] = $this->get_image_item( $post, $src ); } if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) { $src = $this->get_absolute_url( $this->image_url( $post->ID ) ); $images[] = $this->get_image_item( $post, $src ); } foreach ( $images as $key => $image ) { if ( empty( $image['src'] ) ) { unset( $images[ $key ] ); } } /** * Filter images to be included for the post in XML sitemap. * * @param array $images Array of image items. * @param int $post_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Get the images in the term description. * * @param object $term Term to get images from description for. * * @return array */ public function get_term_images( $term ) { $images = $this->parse_html_images( $term->description ); foreach ( $this->parse_galleries( $term->description ) as $attachment ) { $images[] = [ 'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ), ]; } /** * Filter images to be included for the term in XML sitemap. * * @param array $image_list Array of image items. * @param int $term_id ID of the post. */ $image_list = apply_filters( 'wpseo_sitemap_urlimages_term', $images, $term->term_id ); if ( isset( $image_list ) && is_array( $image_list ) ) { $images = $image_list; } return $images; } /** * Parse `<img />` tags in content. * * @param string $content Content string to parse. * * @return array */ private function parse_html_images( $content ) { $images = []; if ( ! class_exists( 'DOMDocument' ) ) { return $images; } if ( empty( $content ) ) { return $images; } // Prevent DOMDocument from bubbling warnings about invalid HTML. libxml_use_internal_errors( true ); $post_dom = new DOMDocument(); $post_dom->loadHTML( '<?xml encoding="' . $this->charset . '">' . $content ); // Clear the errors, so they don't get kept in memory. libxml_clear_errors(); /** * Image attribute. * * @var DOMElement $img */ foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { $src = $img->getAttribute( 'src' ); if ( empty( $src ) ) { continue; } $class = $img->getAttribute( 'class' ); if ( // This detects WP-inserted images, which we need to upsize. R. ! empty( $class ) && ( strpos( $class, 'size-full' ) === false ) && preg_match( '|wp-image-(?P<id>\d+)|', $class, $matches ) && get_post_status( $matches['id'] ) ) { $query_params = wp_parse_url( $src, PHP_URL_QUERY ); $src = $this->image_url( $matches['id'] ); if ( $query_params ) { $src .= '?' . $query_params; } } $src = $this->get_absolute_url( $src ); if ( strpos( $src, $this->host ) === false ) { continue; } if ( $src !== esc_url( $src, null, 'attribute' ) ) { continue; } $images[] = [ 'src' => $src, ]; } return $images; } /** * Parse gallery shortcodes in a given content. * * @param string $content Content string. * @param int $post_id Optional. ID of post being parsed. * * @return array Set of attachment objects. */ protected function parse_galleries( $content, $post_id = 0 ) { $attachments = []; $galleries = $this->get_content_galleries( $content ); foreach ( $galleries as $gallery ) { $id = $post_id; if ( ! empty( $gallery['id'] ) ) { $id = (int) $gallery['id']; } // Forked from core gallery_shortcode() to have exact same logic. R. if ( ! empty( $gallery['ids'] ) ) { $gallery['include'] = $gallery['ids']; } $gallery_attachments = $this->get_gallery_attachments( $id, $gallery ); $attachments = array_merge( $attachments, $gallery_attachments ); } return array_unique( $attachments, SORT_REGULAR ); } /** * Retrieves galleries from the passed content. * * Forked from core to skip executing shortcodes for performance. * * @param string $content Content to parse for shortcodes. * * @return array A list of arrays, each containing gallery data. */ protected function get_content_galleries( $content ) { $galleries = []; if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) { return $galleries; } foreach ( $matches as $shortcode ) { $attributes = shortcode_parse_atts( $shortcode[3] ); if ( $attributes === '' ) { // Valid shortcode without any attributes. R. $attributes = []; } $galleries[] = $attributes; } return $galleries; } /** * Get image item array with filters applied. * * @param WP_Post $post Post object for the context. * @param string $src Image URL. * * @return array */ protected function get_image_item( $post, $src ) { $image = []; /** * Filter image URL to be included in XML sitemap for the post. * * @param string $src Image URL. * @param object $post Post object. */ $image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post ); /** * Filter image data to be included in XML sitemap for the post. * * @param array $image { * Array of image data. * * @type string $src Image URL. * } * * @param object $post Post object. */ return apply_filters( 'wpseo_xml_sitemap_img', $image, $post ); } /** * Get attached image URL with filters applied. Adapted from core for speed. * * @param int $post_id ID of the post. * * @return string */ private function image_url( $post_id ) { static $uploads; if ( empty( $uploads ) ) { $uploads = wp_upload_dir(); } if ( $uploads['error'] !== false ) { return ''; } $file = get_post_meta( $post_id, '_wp_attached_file', true ); if ( empty( $file ) ) { return ''; } // Check that the upload base exists in the file location. if ( strpos( $file, $uploads['basedir'] ) === 0 ) { $src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); } elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) { $src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) ); } else { // It's a newly uploaded file, therefore $file is relative to the baseurl. $src = $uploads['baseurl'] . '/' . $file; } return apply_filters( 'wp_get_attachment_url', $src, $post_id ); } /** * Make absolute URL for domain or protocol-relative one. * * @param string $src URL to process. * * @return string */ protected function get_absolute_url( $src ) { if ( empty( $src ) || ! is_string( $src ) ) { return $src; } if ( YoastSEO()->helpers->url->is_relative( $src ) === true ) { if ( $src[0] !== '/' ) { return $src; } // The URL is relative, we'll have to make it absolute. return $this->home_url . $src; } if ( strpos( $src, 'http' ) !== 0 ) { // Protocol relative URL, we add the scheme as the standard requires a protocol. return $this->scheme . ':' . $src; } return $src; } /** * Returns the attachments for a gallery. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments( $id, $gallery ) { // When there are attachments to include. if ( ! empty( $gallery['include'] ) ) { return $this->get_gallery_attachments_for_included( $gallery['include'] ); } // When $id is empty, just return empty array. if ( empty( $id ) ) { return []; } return $this->get_gallery_attachments_for_parent( $id, $gallery ); } /** * Returns the attachments for the given ID. * * @param int $id The post ID. * @param array $gallery The gallery config. * * @return array The selected attachments. */ protected function get_gallery_attachments_for_parent( $id, $gallery ) { $query = [ 'posts_per_page' => -1, 'post_parent' => $id, ]; // When there are posts that should be excluded from result set. if ( ! empty( $gallery['exclude'] ) ) { $query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] ); } return $this->get_attachments( $query ); } /** * Returns an array with attachments for the post IDs that will be included. * * @param array $included_ids Array with IDs to include. * * @return array The found attachments. */ protected function get_gallery_attachments_for_included( $included_ids ) { $ids_to_include = wp_parse_id_list( $included_ids ); $attachments = $this->get_attachments( [ 'posts_per_page' => count( $ids_to_include ), 'post__in' => $ids_to_include, ], ); $gallery_attachments = []; foreach ( $attachments as $val ) { $gallery_attachments[ $val->ID ] = $val; } return $gallery_attachments; } /** * Returns the attachments. * * @param array $args Array with query args. * * @return array The found attachments. */ protected function get_attachments( $args ) { $default_args = [ 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', // Defaults taken from function get_posts. 'orderby' => 'date', 'order' => 'DESC', 'meta_key' => '', 'meta_value' => '', 'suppress_filters' => true, 'ignore_sticky_posts' => true, 'no_found_rows' => true, ]; $args = wp_parse_args( $args, $default_args ); $get_attachments = new WP_Query(); return $get_attachments->query( $args ); } } inc/sitemaps/class-author-sitemap-provider.php 0000644 00000013122 15174712003 0015547 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap provider for author archives. */ class WPSEO_Author_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { // If the author archives have been disabled, we don't do anything. if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) { return false; } return $type === 'author'; } /** * Get the links for the sitemap index. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { if ( ! $this->handles_type( 'author' ) ) { return []; } // @todo Consider doing this less often / when necessary. R. $this->update_user_meta(); $has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' ); $query_arguments = []; if ( ! $has_exclude_filter ) { // We only need full users if legacy filter(s) hooked to exclusion logic. R. $query_arguments['fields'] = 'ID'; } $users = $this->get_users( $query_arguments ); if ( $has_exclude_filter ) { $users = $this->exclude_users( $users ); $users = wp_list_pluck( $users, 'ID' ); } if ( empty( $users ) ) { return []; } $index = []; $user_pages = array_chunk( $users, $max_entries ); foreach ( $user_pages as $page_counter => $users_page ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); $user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated. $user = get_user_by( 'id', $user_id ); $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $current_page . '.xml' ), 'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? YoastSEO()->helpers->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null, ]; } return $index; } /** * Retrieve users, taking account of all necessary exclusions. * * @param array $arguments Arguments to add. * * @return array */ protected function get_users( $arguments = [] ) { global $wpdb; $defaults = [ 'capability' => [ 'edit_posts' ], 'meta_key' => '_yoast_wpseo_profile_updated', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'meta_query' => [ 'relation' => 'AND', [ 'key' => $wpdb->get_blog_prefix() . 'user_level', 'value' => '0', 'compare' => '!=', ], [ 'relation' => 'OR', [ 'key' => 'wpseo_noindex_author', 'value' => 'on', 'compare' => '!=', ], [ 'key' => 'wpseo_noindex_author', 'compare' => 'NOT EXISTS', ], ], ], ]; if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) { unset( $defaults['capability'] ); // Otherwise it cancels out next argument. $defaults['has_published_posts'] = YoastSEO()->helpers->author_archive->get_author_archive_post_types(); } return get_users( array_merge( $defaults, $arguments ) ); } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = []; if ( ! $this->handles_type( 'author' ) ) { return $links; } $user_criteria = [ 'offset' => ( ( $current_page - 1 ) * $max_entries ), 'number' => $max_entries, ]; $users = $this->get_users( $user_criteria ); // Throw an exception when there are no users in the sitemap. if ( count( $users ) === 0 ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $users = $this->exclude_users( $users ); if ( empty( $users ) ) { $users = []; } $time = time(); foreach ( $users as $user ) { $author_link = get_author_posts_url( $user->ID ); if ( empty( $author_link ) ) { continue; } $mod = $time; if ( isset( $user->_yoast_wpseo_profile_updated ) ) { $mod = $user->_yoast_wpseo_profile_updated; } $url = [ 'loc' => $author_link, 'mod' => date( DATE_W3C, $mod ), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ]; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'user', $user ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Update any users that don't have last profile update timestamp. * * @return int Count of users updated. */ protected function update_user_meta() { $user_criteria = [ 'capability' => [ 'edit_posts' ], 'meta_query' => [ [ 'key' => '_yoast_wpseo_profile_updated', 'compare' => 'NOT EXISTS', ], ], ]; $users = get_users( $user_criteria ); $time = time(); foreach ( $users as $user ) { update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time ); } return count( $users ); } /** * Wrap legacy filter to deduplicate calls. * * @param array $users Array of user objects to filter. * * @return array */ protected function exclude_users( $users ) { /** * Filter the authors, included in XML sitemap. * * @param array $users Array of user objects to filter. */ return apply_filters( 'wpseo_sitemap_exclude_author', $users ); } } inc/sitemaps/class-sitemaps-renderer.php 0000644 00000022551 15174712003 0014414 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Renders XML output for sitemaps. */ class WPSEO_Sitemaps_Renderer { /** * XSL stylesheet for styling a sitemap for web browsers. * * @var string */ protected $stylesheet = ''; /** * Holds the get_bloginfo( 'charset' ) value to reuse for performance. * * @var string */ protected $charset = 'UTF-8'; /** * Holds charset of output, might be converted. * * @var string */ protected $output_charset = 'UTF-8'; /** * If data encoding needs to be converted for output. * * @var bool */ protected $needs_conversion = false; /** * Set up object properties. */ public function __construct() { $stylesheet_url = preg_replace( '/(^http[s]?:)/', '', $this->get_xsl_url() ); $this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( $stylesheet_url ) . '"?>'; $this->charset = get_bloginfo( 'charset' ); $this->output_charset = $this->charset; if ( $this->charset !== 'UTF-8' && function_exists( 'mb_list_encodings' ) && in_array( $this->charset, mb_list_encodings(), true ) ) { $this->output_charset = 'UTF-8'; } $this->needs_conversion = $this->output_charset !== $this->charset; } /** * Builds the sitemap index. * * @param array<string> $links Set of sitemaps index links. * * @return string */ public function get_index( $links ) { $xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; foreach ( $links as $link ) { $xml .= $this->sitemap_index_url( $link ); } /** * Filter to append sitemaps to the index. * * @param string $index String to append to sitemaps index, defaults to empty. */ $xml .= apply_filters( 'wpseo_sitemap_index', '' ); $xml .= '</sitemapindex>'; return $xml; } /** * Builds the sitemap. * * @param array<string> $links Set of sitemap links. * @param string $type Sitemap type. * @param int $current_page Current sitemap page number. * * @return string */ public function get_sitemap( $links, $type, $current_page ) { $urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" ' . 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd ' . 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" ' . 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; /** * Filters the `urlset` for all sitemaps. * * @param string $urlset The output for the sitemap's `urlset`. */ $urlset = apply_filters( 'wpseo_sitemap_urlset', $urlset ); /** * Filters the `urlset` for a sitemap by type. * * @param string $urlset The output for the sitemap's `urlset`. */ $xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset ); foreach ( $links as $url ) { $xml .= $this->sitemap_url( $url ); } /** * Filter to add extra URLs to the XML sitemap by type. * * Only runs for the first page, not on all. * * @param string $content String content to add, defaults to empty. */ if ( $current_page === 1 ) { $xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' ); } $xml .= '</urlset>'; return $xml; } /** * Produce final XML output with debug information. * * @param string $sitemap Sitemap XML. * * @return string */ public function get_output( $sitemap ) { $output = '<?xml version="1.0" encoding="' . esc_attr( $this->output_charset ) . '"?>'; if ( $this->stylesheet ) { /** * Filter the stylesheet URL for the XML sitemap. * * @param string $stylesheet Stylesheet URL. */ $output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n"; } $output .= $sitemap; $output .= "\n<!-- XML Sitemap generated by Yoast SEO -->"; return $output; } /** * Get charset for the output. * * @return string */ public function get_output_charset() { return $this->output_charset; } /** * Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet. * * @param string $stylesheet Full XML-stylesheet declaration. * * @return void */ public function set_stylesheet( $stylesheet ) { $this->stylesheet = $stylesheet; } /** * Build the `<sitemap>` tag for a given URL. * * @param array<string> $url Array of parts that make up this entry. * * @return string */ protected function sitemap_index_url( $url ) { $date = null; if ( ! empty( $url['lastmod'] ) ) { $date = YoastSEO()->helpers->date->format( $url['lastmod'] ); } $url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false ); $output = "\t<sitemap>\n"; $output .= "\t\t<loc>" . $url['loc'] . "</loc>\n"; $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n"; $output .= "\t</sitemap>\n"; return $output; } /** * Build the `<url>` tag for a given URL. * * Public access for backwards compatibility reasons. * * @param array<string> $url Array of parts that make up this entry. * * @return string */ public function sitemap_url( $url ) { $date = null; if ( ! empty( $url['mod'] ) ) { // Create a DateTime object date in the correct timezone. $date = YoastSEO()->helpers->date->format( $url['mod'] ); } $output = "\t<url>\n"; $output .= "\t\t<loc>" . $this->encode_and_escape( $url['loc'] ) . "</loc>\n"; $output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n"; if ( empty( $url['images'] ) ) { $url['images'] = []; } foreach ( $url['images'] as $img ) { if ( empty( $img['src'] ) ) { continue; } $output .= "\t\t<image:image>\n"; $output .= "\t\t\t<image:loc>" . $this->encode_and_escape( $img['src'] ) . "</image:loc>\n"; $output .= "\t\t</image:image>\n"; } unset( $img ); $output .= "\t</url>\n"; /** * Filters the output for the sitemap URL tag. * * @param string $output The output for the sitemap url tag. * @param array $url The sitemap URL array on which the output is based. */ return apply_filters( 'wpseo_sitemap_url', $output, $url ); } /** * Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap. * * This method works around a two quirks in esc_url(): * 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs, * the URL must always begin with a protocol. * 2. `esc_url()` escapes ampersands as `&` instead of the more common `&`. * According to the specs, `&` should be used, and even though this shouldn't * really make a difference in practice, to quote Jono: "I'd be nervous about & * given how many weird and wonderful things eat sitemaps", so better safe than sorry. * * @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions * @link https://www.sitemaps.org/protocol.html#escaping * @link https://developer.wordpress.org/reference/functions/esc_url/ * * @param string $url URL to encode and escape. * * @return string */ protected function encode_and_escape( $url ) { $url = $this->encode_url_rfc3986( $url ); $url = esc_url( $url ); $url = str_replace( '&', '&', $url ); $url = str_replace( ''', ''', $url ); if ( strpos( $url, '//' ) === 0 ) { // Schema-relative URL for which esc_url() does not add a scheme. $url = 'http:' . $url; } return $url; } /** * Apply some best effort conversion to comply with RFC3986. * * @param string $url URL to encode. * * @return string */ protected function encode_url_rfc3986( $url ) { if ( filter_var( $url, FILTER_VALIDATE_URL ) ) { return $url; } $path = wp_parse_url( $url, PHP_URL_PATH ); if ( ! empty( $path ) && $path !== '/' ) { $encoded_path = explode( '/', $path ); // First decode the path, to prevent double encoding. $encoded_path = array_map( 'rawurldecode', $encoded_path ); $encoded_path = array_map( 'rawurlencode', $encoded_path ); $encoded_path = implode( '/', $encoded_path ); $url = str_replace( $path, $encoded_path, $url ); } $query = wp_parse_url( $url, PHP_URL_QUERY ); if ( ! empty( $query ) ) { parse_str( $query, $parsed_query ); $parsed_query = http_build_query( $parsed_query, '', '&', PHP_QUERY_RFC3986 ); $url = str_replace( $query, $parsed_query, $url ); } return $url; } /** * Retrieves the XSL URL that should be used in the current environment * * When home_url and site_url are not the same, the home_url should be used. * This is because the XSL needs to be served from the same domain, protocol and port * as the XML file that is loading it. * * @return string The XSL URL that needs to be used. */ protected function get_xsl_url() { if ( home_url() !== site_url() ) { return apply_filters( 'wpseo_sitemap_public_url', home_url( 'main-sitemap.xsl' ) ); } /* * Fallback to circumvent a cross-domain security problem when the XLS file is * loaded from a different (sub)domain. */ if ( strpos( plugins_url(), home_url() ) !== 0 ) { return home_url( 'main-sitemap.xsl' ); } return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl'; } } inc/sitemaps/class-taxonomy-sitemap-provider.php 0000644 00000022267 15174712003 0016135 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap provider for author archives. */ class WPSEO_Taxonomy_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Holds image parser instance. * * @var WPSEO_Sitemap_Image_Parser */ protected static $image_parser; /** * Determines whether images should be included in the XML sitemap. * * @var bool */ private $include_images; /** * Set up object properties for data reuse. */ public function __construct() { /** * Filter - Allows excluding images from the XML sitemap. * * @param bool $include True to include, false to exclude. */ $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return bool */ public function handles_type( $type ) { $taxonomy = get_taxonomy( $type ); if ( $taxonomy === false || ! $this->is_valid_taxonomy( $taxonomy->name ) || ! $taxonomy->public ) { return false; } return true; } /** * Retrieves the links for the sitemap. * * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); if ( empty( $taxonomies ) ) { return []; } $taxonomy_names = array_filter( array_keys( $taxonomies ), [ $this, 'is_valid_taxonomy' ] ); $taxonomies = array_intersect_key( $taxonomies, array_flip( $taxonomy_names ) ); // Retrieve all the taxonomies and their terms so we can do a proper count on them. /** * Filter the setting of excluding empty terms from the XML sitemap. * * @param bool $exclude Defaults to true. * @param array $taxonomy_names Array of names for the taxonomies being processed. */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, $taxonomy_names ); $all_taxonomies = []; foreach ( $taxonomy_names as $taxonomy_name ) { /** * Filter the setting of excluding empty terms from the XML sitemap for a specific taxonomy. * * @param bool $exclude Defaults to the sitewide setting. * @param string $taxonomy_name The name of the taxonomy being processed. */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy_name ); $term_args = [ 'taxonomy' => $taxonomy_name, 'hide_empty' => $hide_empty_tax, 'fields' => 'ids', ]; $taxonomy_terms = get_terms( $term_args ); if ( count( $taxonomy_terms ) > 0 ) { $all_taxonomies[ $taxonomy_name ] = $taxonomy_terms; } } $index = []; foreach ( $taxonomies as $tax_name => $tax ) { if ( ! isset( $all_taxonomies[ $tax_name ] ) ) { // No eligible terms found. continue; } $total_count = ( isset( $all_taxonomies[ $tax_name ] ) ) ? count( $all_taxonomies[ $tax_name ] ) : 1; $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $last_modified_gmt = WPSEO_Sitemaps::get_last_modified_gmt( $tax->object_type ); for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { $current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 ); if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) { continue; } $terms = array_splice( $all_taxonomies[ $tax_name ], 0, $max_entries ); if ( ! $terms ) { continue; } $args = [ 'post_type' => $tax->object_type, 'tax_query' => [ [ 'taxonomy' => $tax_name, 'terms' => $terms, ], ], 'orderby' => 'modified', 'order' => 'DESC', 'posts_per_page' => 1, ]; $query = new WP_Query( $args ); if ( $query->have_posts() ) { $date = $query->posts[0]->post_modified_gmt; } else { $date = $last_modified_gmt; } $index[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ]; } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { global $wpdb; $links = []; if ( ! $this->handles_type( $type ) ) { return $links; } $taxonomy = get_taxonomy( $type ); $steps = $max_entries; $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, [ $taxonomy->name ] ); /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy->name ); $terms = get_terms( [ 'taxonomy' => $taxonomy->name, 'hide_empty' => $hide_empty_tax, 'update_term_meta_cache' => false, 'offset' => $offset, 'number' => $steps, ], ); // If there are no terms fetched for this range, we are on an invalid page. if ( empty( $terms ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses() ); $replacements = array_merge( [ 'post_modified_gmt', $wpdb->posts, $wpdb->term_relationships, 'object_id', 'ID', $wpdb->term_taxonomy, 'term_taxonomy_id', 'term_taxonomy_id', 'taxonomy', 'term_id', 'post_status', ], $post_statuses, [ 'post_password' ], ); /** * Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID. * * @param array $terms_to_exclude The terms to exclude. */ $terms_to_exclude = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] ); foreach ( $terms as $term ) { if ( in_array( $term->term_id, $terms_to_exclude, true ) ) { continue; } $url = []; $tax_noindex = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'noindex' ); if ( $tax_noindex === 'noindex' ) { continue; } $canonical = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' ); $url['loc'] = get_term_link( $term, $term->taxonomy ); if ( is_string( $canonical ) && $canonical !== '' && $canonical !== $url['loc'] ) { continue; } $current_replacements = $replacements; array_splice( $current_replacements, 9, 0, $term->taxonomy ); array_splice( $current_replacements, 11, 0, $term->term_id ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $url['mod'] = $wpdb->get_var( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT MAX(p.%i) AS lastmod FROM %i AS p INNER JOIN %i AS term_rel ON term_rel.%i = p.%i INNER JOIN %i AS term_tax ON term_tax.%i = term_rel.%i AND term_tax.%i = %s AND term_tax.%i = %d WHERE p.%i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ") AND p.%i = '' ", $current_replacements, ), ); if ( $this->include_images ) { $url['images'] = $this->get_image_parser()->get_term_images( $term ); } // Deprecated, kept for backwards data compat. R. $url['chf'] = 'daily'; $url['pri'] = 1; /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ $url = apply_filters( 'wpseo_sitemap_entry', $url, 'term', $term ); if ( ! empty( $url ) ) { $links[] = $url; } } return $links; } /** * Check if taxonomy by name is valid to appear in sitemaps. * * @param string $taxonomy_name Taxonomy name to check. * * @return bool */ public function is_valid_taxonomy( $taxonomy_name ) { if ( WPSEO_Options::get( "noindex-tax-{$taxonomy_name}" ) === true ) { return false; } if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu', 'wp_pattern_category' ], true ) ) { return false; } if ( $taxonomy_name === 'post_format' && WPSEO_Options::get( 'disable-post_format', false ) ) { return false; } /** * Filter to exclude the taxonomy from the XML sitemap. * * @param bool $exclude Defaults to false. * @param string $taxonomy_name Name of the taxonomy to exclude.. */ if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $taxonomy_name ) ) { return false; } return true; } /** * Get the Image Parser. * * @return WPSEO_Sitemap_Image_Parser */ protected function get_image_parser() { if ( ! isset( self::$image_parser ) ) { self::$image_parser = new WPSEO_Sitemap_Image_Parser(); } return self::$image_parser; } } inc/sitemaps/class-sitemaps-router.php 0000644 00000010707 15174712003 0014126 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ use Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional; /** * Rewrite setup and handling for sitemaps functionality. */ class WPSEO_Sitemaps_Router { /** * Sets up init logic. */ public function __construct() { // If we add rewrite rules during the plugin's deactivation, the flush_rewrite_rules that we perform afterwards won't properly flush those new rules. if ( YoastSEO()->classes->get( Deactivating_Yoast_Seo_Conditional::class )->is_met() ) { return; } add_action( 'yoast_add_dynamic_rewrite_rules', [ $this, 'add_rewrite_rules' ] ); add_filter( 'query_vars', [ $this, 'add_query_vars' ] ); add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ] ); add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 ); } /** * Adds rewrite routes for sitemaps. * * @param Yoast_Dynamic_Rewrites $dynamic_rewrites Dynamic rewrites handler instance. * * @return void */ public function add_rewrite_rules( $dynamic_rewrites ) { $dynamic_rewrites->add_rule( 'sitemap_index\.xml$', 'index.php?sitemap=1', 'top' ); $dynamic_rewrites->add_rule( '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' ); $dynamic_rewrites->add_rule( '([a-z]+)?-?sitemap\.xsl$', 'index.php?yoast-sitemap-xsl=$matches[1]', 'top' ); } /** * Adds query variables for sitemaps. * * @param array<string> $query_vars List of query variables to filter. * * @return array<string> Filtered query variables. */ public function add_query_vars( $query_vars ) { $query_vars[] = 'sitemap'; $query_vars[] = 'sitemap_n'; $query_vars[] = 'yoast-sitemap-xsl'; return $query_vars; } /** * Sets up rewrite rules. * * @deprecated 21.8 * @codeCoverageIgnore * * @return void */ public function init() { _deprecated_function( __METHOD__, 'Yoast SEO 21.8' ); } /** * Stop trailing slashes on sitemap.xml URLs. * * @param string $redirect The redirect URL currently determined. * * @return bool|string */ public function redirect_canonical( $redirect ) { if ( get_query_var( 'sitemap' ) || get_query_var( 'yoast-sitemap-xsl' ) ) { return false; } return $redirect; } /** * Redirects sitemap.xml to sitemap_index.xml. * * @return void */ public function template_redirect() { if ( ! $this->needs_sitemap_index_redirect() ) { return; } YoastSEO()->helpers->redirect->do_safe_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' ); } /** * Checks whether the current request needs to be redirected to sitemap_index.xml. * * @global WP_Query $wp_query Current query. * * @return bool True if redirect is needed, false otherwise. */ public function needs_sitemap_index_redirect() { global $wp_query; $protocol = 'http://'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) === 'on' ) { $protocol = 'https://'; } $domain = ''; if ( isset( $_SERVER['SERVER_NAME'] ) ) { $domain = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) ); } $path = ''; if ( isset( $_SERVER['REQUEST_URI'] ) ) { $path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } // Due to different environment configurations, we need to check both SERVER_NAME and HTTP_HOST. $check_urls = [ $protocol . $domain . $path ]; if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { $check_urls[] = $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . $path; } return $wp_query->is_404 && in_array( home_url( '/sitemap.xml' ), $check_urls, true ); } /** * Create base URL for the sitemap. * * @param string $page Page to append to the base URL. * * @return string base URL (incl page) */ public static function get_base_url( $page ) { global $wp_rewrite; $base = $wp_rewrite->using_index_permalinks() ? 'index.php/' : '/'; /** * Filter the base URL of the sitemaps. * * @param string $base The string that should be added to home_url() to make the full base URL. */ $base = apply_filters( 'wpseo_sitemaps_base_url', $base ); /* * Get the scheme from the configured home URL instead of letting WordPress * determine the scheme based on the requested URI. */ return home_url( $base . $page, wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ) ); } } inc/sitemaps/class-sitemaps-cache.php 0000644 00000020423 15174712003 0013645 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Handles sitemaps caching and invalidation. * * @since 3.2 */ class WPSEO_Sitemaps_Cache { /** * Holds the options that, when updated, should cause the cache to clear. * * @var array */ protected static $cache_clear = []; /** * Mirror of enabled status for static calls. * * @var bool */ protected static $is_enabled = false; /** * Holds the flag to clear all cache. * * @var bool */ protected static $clear_all = false; /** * Holds the array of types to clear. * * @var array */ protected static $clear_types = []; /** * Hook methods for invalidation on necessary events. */ public function __construct() { add_action( 'init', [ $this, 'init' ] ); add_action( 'deleted_term_relationships', [ self::class, 'invalidate' ] ); add_action( 'update_option', [ self::class, 'clear_on_option_update' ] ); add_action( 'edited_terms', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'clean_term_cache', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'clean_object_term_cache', [ self::class, 'invalidate_helper' ], 10, 2 ); add_action( 'user_register', [ self::class, 'invalidate_author' ] ); add_action( 'delete_user', [ self::class, 'invalidate_author' ] ); add_action( 'shutdown', [ self::class, 'clear_queued' ] ); } /** * Setup context for static calls. * * @return void */ public function init() { self::$is_enabled = $this->is_enabled(); } /** * If cache is enabled. * * @since 3.2 * * @return bool */ public function is_enabled() { /** * Filter if XML sitemap transient cache is enabled. * * @param bool $unsigned Enable cache or not, defaults to true. */ return apply_filters( 'wpseo_enable_xml_sitemap_transient_caching', false ); } /** * Retrieve the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return string|bool */ public function get_sitemap( $type, $page ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } return get_transient( $transient_key ); } /** * Get the sitemap that is cached. * * @param string $type Sitemap type. * @param int $page Page number to retrieve. * * @return WPSEO_Sitemap_Cache_Data|null Null on no cache found otherwise object containing sitemap and meta data. */ public function get_sitemap_data( $type, $page ) { $sitemap = $this->get_sitemap( $type, $page ); if ( empty( $sitemap ) ) { return null; } /* * Unserialize Cache Data object as is_serialized() doesn't recognize classes in C format. * This work-around should no longer be needed once the minimum PHP version has gone up to PHP 7.4, * as the `WPSEO_Sitemap_Cache_Data` class uses O format serialization in PHP 7.4 and higher. * * @link https://wiki.php.net/rfc/custom_object_serialization */ if ( is_string( $sitemap ) && strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) === 0 ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Can't be avoided due to how WP stores options. $sitemap = unserialize( $sitemap ); } // What we expect it to be if it is set. if ( $sitemap instanceof WPSEO_Sitemap_Cache_Data_Interface ) { return $sitemap; } return null; } /** * Store the sitemap page from cache. * * @since 3.2 * * @param string $type Sitemap type. * @param int $page Page number to store. * @param string $sitemap Sitemap body to store. * @param bool $usable Is this a valid sitemap or a cache of an invalid sitemap. * * @return bool */ public function store_sitemap( $type, $page, $sitemap, $usable = true ) { $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); if ( $transient_key === false ) { return false; } $status = ( $usable ) ? WPSEO_Sitemap_Cache_Data::OK : WPSEO_Sitemap_Cache_Data::ERROR; $sitemap_data = new WPSEO_Sitemap_Cache_Data(); $sitemap_data->set_sitemap( $sitemap ); $sitemap_data->set_status( $status ); return set_transient( $transient_key, $sitemap_data, DAY_IN_SECONDS ); } /** * Delete cache transients for index and specific type. * * Always deletes the main index sitemaps cache, as that's always invalidated by any other change. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache() to method in this class. * * @param string $type Sitemap type to invalidate. * * @return void */ public static function invalidate( $type ) { self::clear( [ $type ] ); } /** * Helper to invalidate in hooks where type is passed as second argument. * * @since 3.2 * * @param int $unused Unused term ID value. * @param string $type Taxonomy to invalidate. * * @return void */ public static function invalidate_helper( $unused, $type ) { if ( WPSEO_Options::get( 'noindex-' . $type ) === false || WPSEO_Options::get( 'noindex-tax-' . $type ) === false ) { self::invalidate( $type ); } } /** * Invalidate sitemap cache for authors. * * @param int $user_id User ID. * * @return bool True if the sitemap was properly invalidated. False otherwise. */ public static function invalidate_author( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( $user === false ) { return false; } if ( current_action() === 'user_register' ) { update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() ); } if ( empty( $user->roles ) || in_array( 'subscriber', $user->roles, true ) ) { return false; } self::invalidate( 'author' ); return true; } /** * Invalidate sitemap cache for the post type of a post. * * Don't invalidate for revisions. * * @since 1.5.4 * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache_on_save_post() to method in this class. * * @param int $post_id Post ID to invalidate type for. * * @return void */ public static function invalidate_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) { return; } self::invalidate( get_post_type( $post_id ) ); } /** * Delete cache transients for given sitemaps types or all by default. * * @since 1.8.0 * @since 3.2 Moved from WPSEO_Utils to this class. * * @param array $types Set of sitemap types to delete cache transients for. * * @return void */ public static function clear( $types = [] ) { if ( ! self::$is_enabled ) { return; } // No types provided, clear all. if ( empty( $types ) ) { self::$clear_all = true; return; } // Always invalidate the index sitemap as well. if ( ! in_array( WPSEO_Sitemaps::SITEMAP_INDEX_TYPE, $types, true ) ) { array_unshift( $types, WPSEO_Sitemaps::SITEMAP_INDEX_TYPE ); } foreach ( $types as $type ) { if ( ! in_array( $type, self::$clear_types, true ) ) { self::$clear_types[] = $type; } } } /** * Invalidate storage for cache types queued to clear. * * @return void */ public static function clear_queued() { if ( self::$clear_all ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage(); self::$clear_all = false; self::$clear_types = []; return; } foreach ( self::$clear_types as $type ) { WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type ); } self::$clear_types = []; } /** * Adds a hook that when given option is updated, the cache is cleared. * * @since 3.2 * * @param string $option Option name. * @param string $type Sitemap type. * * @return void */ public static function register_clear_on_option_update( $option, $type = '' ) { self::$cache_clear[ $option ] = $type; } /** * Clears the transient cache when a given option is updated, if that option has been registered before. * * @since 3.2 * * @param string $option The option name that's being updated. * * @return void */ public static function clear_on_option_update( $option ) { if ( array_key_exists( $option, self::$cache_clear ) ) { if ( empty( self::$cache_clear[ $option ] ) ) { // Clear all caches. self::clear(); } else { // Clear specific provided type(s). $types = (array) self::$cache_clear[ $option ]; self::clear( $types ); } } } } inc/sitemaps/class-sitemaps-admin.php 0000644 00000005736 15174712003 0013704 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\XML Sitemaps */ /** * Class that handles the Admin side of XML sitemaps. */ class WPSEO_Sitemaps_Admin { /** * Post_types that are being imported. * * @var array */ private $importing_post_types = []; /** * Class constructor. */ public function __construct() { add_action( 'transition_post_status', [ $this, 'status_transition' ], 10, 3 ); add_action( 'admin_footer', [ $this, 'status_transition_bulk_finished' ] ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo_titles', '' ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo', '' ); } /** * Hooked into transition_post_status. Will initiate search engine pings * if the post is being published, is a post type that a sitemap is built for * and is a post that is included in sitemaps. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * * @return void */ public function status_transition( $new_status, $old_status, $post ) { if ( $new_status !== 'publish' ) { return; } if ( defined( 'WP_IMPORTING' ) ) { $this->status_transition_bulk( $new_status, $old_status, $post ); return; } $post_type = get_post_type( $post ); wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. } /** * Notify Google of the updated sitemap. * * @deprecated 22.0 * @codeCoverageIgnore * * @return void */ public function ping_search_engines() { _deprecated_function( __METHOD__, 'Yoast SEO 22.0' ); } /** * While bulk importing, just save unique post_types. * * When importing is done, if we have a post_type that is saved in the sitemap * try to ping the search engines. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. * * @return void */ private function status_transition_bulk( $new_status, $old_status, $post ) { $this->importing_post_types[] = get_post_type( $post ); $this->importing_post_types = array_unique( $this->importing_post_types ); } /** * After import finished, walk through imported post_types and update info. * * @return void */ public function status_transition_bulk_finished() { if ( ! defined( 'WP_IMPORTING' ) ) { return; } if ( empty( $this->importing_post_types ) ) { return; } $ping_search_engines = false; foreach ( $this->importing_post_types as $post_type ) { wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. // Just have the cache deleted for nav_menu_item. if ( $post_type === 'nav_menu_item' ) { continue; } if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) { $ping_search_engines = true; } } // Nothing to do. if ( $ping_search_engines === false ) { return; } if ( WP_CACHE ) { do_action( 'wpseo_hit_sitemap_index' ); } } } inc/sitemaps/class-sitemaps.php 0000644 00000040477 15174712003 0012617 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Class WPSEO_Sitemaps. * * @todo This class could use a general description with some explanation on sitemaps. OR. */ class WPSEO_Sitemaps { /** * Sitemap index identifier. * * @var string */ public const SITEMAP_INDEX_TYPE = '1'; /** * Content of the sitemap to output. * * @var string */ protected $sitemap = ''; /** * Flag to indicate if this is an invalid or empty sitemap. * * @var bool */ public $bad_sitemap = false; /** * Whether or not the XML sitemap was served from a transient or not. * * @var bool */ private $transient = false; /** * HTTP protocol to use in headers. * * @since 3.2 * * @var string */ protected $http_protocol = 'HTTP/1.1'; /** * Holds the n variable. * * @var int */ private $current_page = 1; /** * The sitemaps router. * * @since 3.2 * * @var WPSEO_Sitemaps_Router */ public $router; /** * The sitemap renderer. * * @since 3.2 * * @var WPSEO_Sitemaps_Renderer */ public $renderer; /** * The sitemap cache. * * @since 3.2 * * @var WPSEO_Sitemaps_Cache */ public $cache; /** * The sitemap providers. * * @since 3.2 * * @var WPSEO_Sitemap_Provider[] */ public $providers; /** * Class constructor. */ public function __construct() { add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] ); add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 ); add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 ); add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] ); $this->router = new WPSEO_Sitemaps_Router(); $this->renderer = new WPSEO_Sitemaps_Renderer(); $this->cache = new WPSEO_Sitemaps_Cache(); if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) { $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); } } /** * Initialize sitemap providers classes. * * @since 5.3 * * @return void */ public function init_sitemaps_providers() { $this->providers = [ new WPSEO_Post_Type_Sitemap_Provider(), new WPSEO_Taxonomy_Sitemap_Provider(), new WPSEO_Author_Sitemap_Provider(), ]; $external_providers = apply_filters( 'wpseo_sitemaps_providers', [] ); foreach ( $external_providers as $provider ) { if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) { $this->providers[] = $provider; } } } /** * Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets. * * @return void */ public function reduce_query_load() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return; } $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); $extension = substr( $request_uri, -4 ); if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) { remove_all_actions( 'widgets_init' ); } } /** * Register your own sitemap. Call this during 'init'. * * @param string $name The name of the sitemap. * @param callback $building_function Function to build your sitemap. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_sitemap( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_do_sitemap_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' ); } } /** * Register your own XSL file. Call this during 'init'. * * @since 1.4.23 * * @param string $name The name of the XSL file. * @param callback $building_function Function to build your XSL file. * @param string $rewrite Optional. Regular expression to match your sitemap with. * * @return void */ public function register_xsl( $name, $building_function, $rewrite = '' ) { add_action( 'wpseo_xsl_' . $name, $building_function ); if ( $rewrite ) { Yoast_Dynamic_Rewrites::instance()->add_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' ); } } /** * Set the sitemap current page to allow creating partial sitemaps with WP-CLI * in a one-off process. * * @param int $current_page The part that should be generated. * * @return void */ public function set_n( $current_page ) { if ( is_scalar( $current_page ) && (int) $current_page > 0 ) { $this->current_page = (int) $current_page; } } /** * Set the sitemap content to display after you have generated it. * * @param string $sitemap The generated sitemap to output. * * @return void */ public function set_sitemap( $sitemap ) { $this->sitemap = $sitemap; } /** * Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests. * * @param bool $is_bad Is this a bad request. True or false. * * @return void */ public function set_bad_sitemap( $is_bad ) { $this->bad_sitemap = (bool) $is_bad; } /** * Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML. * * @since 1.4.16 * * @return void */ public function sitemap_close() { remove_all_actions( 'wp_footer' ); exit(); } /** * Hijack requests for potential sitemaps and XSL files. * * @param WP_Query $query Main query instance. * * @return void */ public function redirect( $query ) { if ( ! $query->is_main_query() ) { return; } $yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' ); if ( ! empty( $yoast_sitemap_xsl ) ) { /* * This is a method to provide the XSL via the home_url. * Needed when the site_url and home_url are not the same. * Loading the XSL needs to come from the same domain, protocol and port as the XML. * * Whenever home_url and site_url are the same, the file can be loaded directly. */ $this->xsl_output( $yoast_sitemap_xsl ); $this->sitemap_close(); return; } $type = get_query_var( 'sitemap' ); if ( empty( $type ) ) { return; } if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) { wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' ); exit(); } $this->set_n( get_query_var( 'sitemap_n' ) ); if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) { $this->build_sitemap( $type ); } if ( $this->bad_sitemap ) { $query->set_404(); status_header( 404 ); return; } $this->output(); $this->sitemap_close(); } /** * Try to get the sitemap from cache. * * @param string $type Sitemap type. * @param int $page_number The page number to retrieve. * * @return bool If the sitemap has been retrieved from cache. */ private function get_sitemap_from_cache( $type, $page_number ) { $this->transient = false; if ( $this->cache->is_enabled() !== true ) { return false; } /** * Fires before the attempt to retrieve XML sitemap from the transient cache. * * @param WPSEO_Sitemaps $sitemaps Sitemaps object. */ do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this ); $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number ); // No cache was found, refresh it because cache is enabled. if ( empty( $sitemap_cache_data ) ) { return $this->refresh_sitemap_cache( $type, $page_number ); } // Cache object was found, parse information. $this->transient = true; $this->sitemap = $sitemap_cache_data->get_sitemap(); $this->bad_sitemap = ! $sitemap_cache_data->is_usable(); return true; } /** * Build and save sitemap to cache. * * @param string $type Sitemap type. * @param int $page_number The page number to save to. * * @return bool */ private function refresh_sitemap_cache( $type, $page_number ) { $this->set_n( $page_number ); $this->build_sitemap( $type ); return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap ); } /** * Attempts to build the requested sitemap. * * Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy. * * @param string $type The requested sitemap's identifier. * * @return void */ public function build_sitemap( $type ) { /** * Filter the type of sitemap to build. * * @param string $type Sitemap type, determined by the request. */ $type = apply_filters( 'wpseo_build_sitemap_post_type', $type ); if ( $type === '1' ) { $this->build_root_map(); return; } $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { if ( ! $provider->handles_type( $type ) ) { continue; } try { $links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page ); } catch ( OutOfBoundsException $exception ) { $this->bad_sitemap = true; return; } $this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page ); return; } if ( has_action( 'wpseo_do_sitemap_' . $type ) ) { /** * Fires custom handler, if hooked to generate sitemap for the type. */ do_action( 'wpseo_do_sitemap_' . $type ); return; } $this->bad_sitemap = true; } /** * Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types. * * @return void */ public function build_root_map() { $links = []; $entries_per_page = $this->get_entries_per_page(); foreach ( $this->providers as $provider ) { $links = array_merge( $links, $provider->get_index_links( $entries_per_page ) ); } /** * Filter the sitemap links array before the index sitemap is built. * * @param array $links Array of sitemap links */ $links = apply_filters( 'wpseo_sitemap_index_links', $links ); if ( empty( $links ) ) { $this->bad_sitemap = true; $this->sitemap = ''; return; } $this->sitemap = $this->renderer->get_index( $links ); } /** * Spits out the XSL for the XML sitemap. * * @since 1.4.13 * * @param string $type Type to output. * * @return void */ public function xsl_output( $type ) { if ( $type !== 'main' ) { /** * Fires for the output of XSL for XML sitemaps, other than type "main". */ do_action( 'wpseo_xsl_' . $type ); return; } header( $this->http_protocol . ' 200 OK', true, 200 ); // Prevent the search engines from indexing the XML Sitemap. header( 'X-Robots-Tag: noindex, follow', true ); header( 'Content-Type: text/xml' ); // Make the browser cache this file properly. $expires = YEAR_IN_SECONDS; header( 'Pragma: public' ); header( 'Cache-Control: max-age=' . $expires ); header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' ); // Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043. readfile( WPSEO_PATH . 'css/main-sitemap.xsl' ); } /** * Spit out the generated sitemap. * * @return void */ public function output() { $this->send_headers(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document. echo $this->renderer->get_output( $this->sitemap ); } /** * Makes a request to the sitemap index to cache it before the arrival of the search engines. * * @return void */ public function hit_sitemap_index() { if ( ! $this->cache->is_enabled() ) { return; } wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ); } /** * Get the GMT modification date for the last modified post in the post type. * * @since 3.2 * * @param string|array $post_types Post type or array of types. * @param bool $return_all Flag to return array of values. * * @return string|array|false */ public static function get_last_modified_gmt( $post_types, $return_all = false ) { global $wpdb; static $post_type_dates = null; if ( ! is_array( $post_types ) ) { $post_types = [ $post_types ]; } foreach ( $post_types as $post_type ) { if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R. $post_type_dates = null; break; } } if ( $post_type_dates === null ) { $post_type_dates = []; $post_type_names = WPSEO_Post_Type::get_accessible_post_types(); if ( ! empty( $post_type_names ) ) { $post_statuses = array_map( 'esc_sql', self::get_post_statuses() ); $replacements = array_merge( [ 'post_type', 'post_modified_gmt', 'date', $wpdb->posts, 'post_status', ], $post_statuses, [ 'post_type' ], array_keys( $post_type_names ), [ 'post_type', 'date', ], ); //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to use a direct query here. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. $dates = $wpdb->get_results( //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. $wpdb->prepare( ' SELECT %i, MAX(%i) AS %i FROM %i WHERE %i IN (' . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND %i IN (' . implode( ', ', array_fill( 0, count( $post_type_names ), '%s' ) ) . ') GROUP BY %i ORDER BY %i DESC ', $replacements, ), ); foreach ( $dates as $obj ) { $post_type_dates[ $obj->post_type ] = $obj->date; } } } $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) ); if ( count( $dates ) > 0 ) { if ( $return_all ) { return $dates; } return max( $dates ); } return false; } /** * Get the modification date for the last modified post in the post type. * * @param array $post_types Post types to get the last modification date for. * * @return string */ public function get_last_modified( $post_types ) { return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) ); } /** * Get the maximum number of entries per XML sitemap. * * @return int The maximum number of entries. */ protected function get_entries_per_page() { /** * Filter the maximum number of entries per XML sitemap. * * After changing the output of the filter, make sure that you disable and enable the * sitemaps to make sure the value is picked up for the sitemap cache. * * @param int $entries The maximum number of entries per XML sitemap. */ $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ); return $entries; } /** * Get post statuses for post_type or the root sitemap. * * @since 10.2 * * @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap. * * @return array List of post statuses. */ public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) { /** * Filter post status list for sitemap query for the post type. * * @param array $post_statuses Post status list, defaults to array( 'publish' ). * @param string $type Post type or SITEMAP_INDEX_TYPE. */ $post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type ); if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) { $post_statuses = [ 'publish' ]; } if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' ) && ! in_array( 'inherit', $post_statuses, true ) ) { $post_statuses[] = 'inherit'; } return $post_statuses; } /** * Sends all the required HTTP Headers. * * @return void */ private function send_headers() { if ( headers_sent() ) { return; } $headers = [ $this->http_protocol . ' 200 OK' => 200, // Prevent the search engines from indexing the XML Sitemap. 'X-Robots-Tag: noindex, follow' => '', 'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '', ]; /** * Filter the HTTP headers we send before an XML sitemap. * * @param array $headers The HTTP headers we're going to send out. */ $headers = apply_filters( 'wpseo_sitemap_http_headers', $headers ); foreach ( $headers as $header => $status ) { if ( is_numeric( $status ) ) { header( $header, true, $status ); continue; } header( $header, true ); } } } inc/options/class-wpseo-option-titles.php 0000644 00000104514 15174712003 0014576 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ use Yoast\WP\SEO\Config\Schema_Types; /** * Option: wpseo_titles. */ class WPSEO_Option_Titles extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_titles'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * {@internal Note: Some of the default values are added via the translate_defaults() method.}} * * @var string[] */ protected $defaults = [ // Form fields. 'forcerewritetitle' => false, 'separator' => 'sc-dash', 'title-home-wpseo' => '%%sitename%% %%page%% %%sep%% %%sitedesc%%', // Text field. 'title-author-wpseo' => '', // Text field. 'title-archive-wpseo' => '%%date%% %%page%% %%sep%% %%sitename%%', // Text field. 'title-search-wpseo' => '', // Text field. 'title-404-wpseo' => '', // Text field. 'social-title-author-wpseo' => '%%name%%', // Text field. 'social-title-archive-wpseo' => '%%date%%', // Text field. 'social-description-author-wpseo' => '', // Text area. 'social-description-archive-wpseo' => '', // Text area. 'social-image-url-author-wpseo' => '', // Hidden input field. 'social-image-url-archive-wpseo' => '', // Hidden input field. 'social-image-id-author-wpseo' => 0, // Hidden input field. 'social-image-id-archive-wpseo' => 0, // Hidden input field. 'metadesc-home-wpseo' => '', // Text area. 'metadesc-author-wpseo' => '', // Text area. 'metadesc-archive-wpseo' => '', // Text area. 'rssbefore' => '', // Text area. 'rssafter' => '', // Text area. 'noindex-author-wpseo' => false, 'noindex-author-noposts-wpseo' => true, 'noindex-archive-wpseo' => true, 'disable-author' => false, 'disable-date' => false, 'disable-post_format' => false, 'disable-attachment' => true, 'breadcrumbs-404crumb' => '', // Text field. 'breadcrumbs-display-blog-page' => true, 'breadcrumbs-boldlast' => false, 'breadcrumbs-archiveprefix' => '', // Text field. 'breadcrumbs-enable' => true, 'breadcrumbs-home' => '', // Text field. 'breadcrumbs-prefix' => '', // Text field. 'breadcrumbs-searchprefix' => '', // Text field. 'breadcrumbs-sep' => '»', // Text field. 'website_name' => '', 'person_name' => '', 'person_logo' => '', 'person_logo_id' => 0, 'alternate_website_name' => '', 'company_logo' => '', 'company_logo_id' => 0, 'company_logo_meta' => false, 'person_logo_meta' => false, 'company_name' => '', 'company_alternate_name' => '', 'company_or_person' => 'company', 'company_or_person_user_id' => false, 'stripcategorybase' => false, 'open_graph_frontpage_title' => '%%sitename%%', // Text field. 'open_graph_frontpage_desc' => '', // Text field. 'open_graph_frontpage_image' => '', // Text field. 'open_graph_frontpage_image_id' => 0, 'publishing_principles_id' => 0, 'ownership_funding_info_id' => 0, 'actionable_feedback_policy_id' => 0, 'corrections_policy_id' => 0, 'ethics_policy_id' => 0, 'diversity_policy_id' => 0, 'diversity_staffing_report_id' => 0, 'org-description' => '', 'org-email' => '', 'org-phone' => '', 'org-legal-name' => '', 'org-founding-date' => '', 'org-number-employees' => '', 'org-vat-id' => '', 'org-tax-id' => '', 'org-iso' => '', 'org-duns' => '', 'org-leicode' => '', 'org-naics' => '', /* * Uses enrich_defaults to add more along the lines of: * - 'title-' . $pt->name => ''; // Text field. * - 'metadesc-' . $pt->name => ''; // Text field. * - 'noindex-' . $pt->name => false; * - 'display-metabox-pt-' . $pt->name => false; * * - 'title-ptarchive-' . $pt->name => ''; // Text field. * - 'metadesc-ptarchive-' . $pt->name => ''; // Text field. * - 'bctitle-ptarchive-' . $pt->name => ''; // Text field. * - 'noindex-ptarchive-' . $pt->name => false; * * - 'title-tax-' . $tax->name => '''; // Text field. * - 'metadesc-tax-' . $tax->name => ''; // Text field. * - 'noindex-tax-' . $tax->name => false; * - 'display-metabox-tax-' . $tax->name => false; * * - 'schema-page-type-' . $pt->name => 'WebPage'; * - 'schema-article-type-' . $pt->name => 'Article'; */ ]; /** * Used for "caching" during pageload. * * @var string[]|null */ protected $enriched_defaults = null; /** * Array of variable option name patterns for the option. * * @var string[] */ protected $variable_array_key_patterns = [ 'title-', 'metadesc-', 'noindex-', 'display-metabox-pt-', 'bctitle-ptarchive-', 'post_types-', 'taxonomy-', 'schema-page-type-', 'schema-article-type-', 'social-title-', 'social-description-', 'social-image-url-', 'social-image-id-', 'org-', ]; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var string[] */ public $ms_exclude = [ 'forcerewritetitle', ]; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_action( 'init', [ $this, 'end_of_init' ], 999 ); add_action( 'registered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'unregistered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'registered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_action( 'unregistered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] ); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } /** * Make sure we can recognize the right action for the double cleaning. * * @return void */ public function end_of_init() { do_action( 'wpseo_double_clean_titles' ); } /** * Get the singleton instance of this class. * * @return self */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Get the available separator options. * * @return string[] */ public function get_separator_options() { $separators = wp_list_pluck( self::get_separator_option_list(), 'option' ); /** * Allow altering the array with separator options. * * @param array $separator_options Array with the separator options. */ $filtered_separators = apply_filters( 'wpseo_separator_options', $separators ); if ( is_array( $filtered_separators ) && $filtered_separators !== [] ) { $separators = array_merge( $separators, $filtered_separators ); } return $separators; } /** * Get the available separator options aria-labels. * * @return string[] Array with the separator options aria-labels. */ public function get_separator_options_for_display() { $separators = $this->get_separator_options(); $separator_list = self::get_separator_option_list(); $separator_options = []; foreach ( $separators as $key => $label ) { $aria_label = ( $separator_list[ $key ]['label'] ?? '' ); $separator_options[ $key ] = [ 'label' => $label, 'aria_label' => $aria_label, ]; } return $separator_options; } /** * Translate strings used in the option defaults. * * @return void */ public function translate_defaults() { /* translators: 1: Author name; 2: Site name. */ $this->defaults['title-author-wpseo'] = sprintf( __( '%1$s, Author at %2$s', 'wordpress-seo' ), '%%name%%', '%%sitename%%' ) . ' %%page%% '; /* translators: %s expands to the search phrase. */ $this->defaults['title-search-wpseo'] = sprintf( __( 'You searched for %s', 'wordpress-seo' ), '%%searchphrase%%' ) . ' %%page%% %%sep%% %%sitename%%'; $this->defaults['title-404-wpseo'] = __( 'Page not found', 'wordpress-seo' ) . ' %%sep%% %%sitename%%'; /* translators: 1: link to post; 2: link to blog. */ $this->defaults['rssafter'] = sprintf( __( 'The post %1$s appeared first on %2$s.', 'wordpress-seo' ), '%%POSTLINK%%', '%%BLOGLINK%%' ); $this->defaults['breadcrumbs-404crumb'] = __( 'Error 404: Page not found', 'wordpress-seo' ); $this->defaults['breadcrumbs-archiveprefix'] = __( 'Archives for', 'wordpress-seo' ); $this->defaults['breadcrumbs-home'] = __( 'Home', 'wordpress-seo' ); $this->defaults['breadcrumbs-searchprefix'] = __( 'You searched for', 'wordpress-seo' ); } /** * Add dynamically created default options based on available post types and taxonomies. * * @return void */ public function enrich_defaults() { $enriched_defaults = $this->enriched_defaults; if ( $enriched_defaults !== null ) { $this->defaults += $enriched_defaults; return; } $enriched_defaults = []; /* * Retrieve all the relevant post type and taxonomy arrays. * * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here. * These are the defaults and can be prepared for any public post type. */ $post_type_objects = get_post_types( [ 'public' => true ], 'objects' ); if ( $post_type_objects ) { /* translators: %s expands to the name of a post type (plural). */ $archive = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' ); foreach ( $post_type_objects as $pt ) { $enriched_defaults[ 'title-' . $pt->name ] = '%%title%% %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'noindex-' . $pt->name ] = false; $enriched_defaults[ 'display-metabox-pt-' . $pt->name ] = true; $enriched_defaults[ 'post_types-' . $pt->name . '-maintax' ] = 0; // Select box. $enriched_defaults[ 'schema-page-type-' . $pt->name ] = 'WebPage'; $enriched_defaults[ 'schema-article-type-' . $pt->name ] = ( $pt->name === 'post' ) ? 'Article' : 'None'; if ( $pt->name !== 'attachment' ) { $enriched_defaults[ 'social-title-' . $pt->name ] = '%%title%%'; // Text field. $enriched_defaults[ 'social-description-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-' . $pt->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-' . $pt->name ] = 0; // Hidden input field. } // Custom post types that have archives. if ( ! $pt->_builtin && WPSEO_Post_Type::has_archive( $pt ) ) { $enriched_defaults[ 'title-ptarchive-' . $pt->name ] = $archive . ' %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-ptarchive-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'bctitle-ptarchive-' . $pt->name ] = ''; // Text field. $enriched_defaults[ 'noindex-ptarchive-' . $pt->name ] = false; $enriched_defaults[ 'social-title-ptarchive-' . $pt->name ] = $archive; // Text field. $enriched_defaults[ 'social-description-ptarchive-' . $pt->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-ptarchive-' . $pt->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-ptarchive-' . $pt->name ] = 0; // Hidden input field. } } } $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' ); if ( $taxonomy_objects ) { /* translators: %s expands to the variable used for term title. */ $archives = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' ); foreach ( $taxonomy_objects as $tax ) { $enriched_defaults[ 'title-tax-' . $tax->name ] = $archives . ' %%page%% %%sep%% %%sitename%%'; // Text field. $enriched_defaults[ 'metadesc-tax-' . $tax->name ] = ''; // Text area. $enriched_defaults[ 'display-metabox-tax-' . $tax->name ] = true; $enriched_defaults[ 'noindex-tax-' . $tax->name ] = ( $tax->name === 'post_format' ); $enriched_defaults[ 'social-title-tax-' . $tax->name ] = $archives; // Text field. $enriched_defaults[ 'social-description-tax-' . $tax->name ] = ''; // Text area. $enriched_defaults[ 'social-image-url-tax-' . $tax->name ] = ''; // Hidden input field. $enriched_defaults[ 'social-image-id-tax-' . $tax->name ] = 0; // Hidden input field. $enriched_defaults[ 'taxonomy-' . $tax->name . '-ptparent' ] = 0; // Select box;. } } $this->enriched_defaults = $enriched_defaults; $this->defaults += $enriched_defaults; } /** * Invalidates enrich_defaults() cache. * * Called from actions: * - (un)registered_post_type * - (un)registered_taxonomy * * @return void */ public function invalidate_enrich_defaults_cache() { $this->enriched_defaults = null; } /** * Validate the option. * * @param string[] $dirty New value for the option. * @param string[] $clean Clean value for the option, normally the defaults. * @param string[] $old Old value of the option. * * @return string[] Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { $allowed_post_types = $this->get_allowed_post_types(); foreach ( $clean as $key => $value ) { $switch_key = $this->get_switch_key( $key ); switch ( $switch_key ) { // Only ever set programmatically, so no reason for intense validation. case 'company_logo_meta': case 'person_logo_meta': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; /* Breadcrumbs text fields. */ case 'breadcrumbs-404crumb': case 'breadcrumbs-archiveprefix': case 'breadcrumbs-home': case 'breadcrumbs-prefix': case 'breadcrumbs-searchprefix': case 'breadcrumbs-sep': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = wp_kses_post( $dirty[ $key ] ); } break; /* * Text fields. */ /* * Covers: * 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo', // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. * 'title-search-wpseo', 'title-404-wpseo' * 'title-' . $pt->name * 'title-ptarchive-' . $pt->name * 'title-tax-' . $tax->name * 'social-title-' . $pt->name * 'social-title-ptarchive-' . $pt->name * 'social-title-tax-' . $tax->name * 'social-title-author-wpseo', 'social-title-archive-wpseo' * 'open_graph_frontpage_title' */ case 'org-': case 'website_name': case 'alternate_website_name': case 'title-': case 'social-title-': case 'open_graph_frontpage_title': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; case 'company_or_person': if ( isset( $dirty[ $key ] ) ) { if ( in_array( $dirty[ $key ], [ 'company', 'person' ], true ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $clean[ $key ] = $defaults['company_or_person']; } } break; /* * Covers: * 'company_logo', 'person_logo' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. */ case 'company_logo': case 'person_logo': case 'open_graph_frontpage_image': // When a logo changes, we need to ditch the caches we have for it. unset( $clean[ $switch_key . '_id' ] ); unset( $clean[ $switch_key . '_meta' ] ); $this->validate_url( $key, $dirty, $old, $clean ); break; /* * Covers: * 'social-image-url-' . $pt->name * 'social-image-url-ptarchive-' . $pt->name * 'social-image-url-tax-' . $tax->name * 'social-image-url-author-wpseo', 'social-image-url-archive-wpseo' */ case 'social-image-url-': $this->validate_url( $key, $dirty, $old, $clean ); break; /* * Covers: * 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo' * 'metadesc-' . $pt->name * 'metadesc-ptarchive-' . $pt->name * 'metadesc-tax-' . $tax->name * and also: * 'bctitle-ptarchive-' . $pt->name * 'social-description-' . $pt->name * 'social-description-ptarchive-' . $pt->name * 'social-description-tax-' . $tax->name * 'social-description-author-wpseo', 'social-description-archive-wpseo' * 'open_graph_frontpage_desc' */ case 'metadesc-': case 'bctitle-ptarchive-': case 'company_name': case 'company_alternate_name': case 'person_name': case 'social-description-': case 'open_graph_frontpage_desc': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; /* * Covers: 'rssbefore', 'rssafter' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code. */ case 'rssbefore': case 'rssafter': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = wp_kses_post( $dirty[ $key ] ); } break; /* 'post_types-' . $pt->name . '-maintax' fields. */ case 'post_types-': $post_type = str_replace( [ 'post_types-', '-maintax' ], '', $key ); $taxonomies = get_object_taxonomies( $post_type, 'names' ); if ( isset( $dirty[ $key ] ) ) { if ( $taxonomies !== [] && in_array( $dirty[ $key ], $taxonomies, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) { $clean[ $key ] = 0; } elseif ( sanitize_title_with_dashes( $dirty[ $key ] ) === $dirty[ $key ] ) { // Allow taxonomies which may not be registered yet. $clean[ $key ] = $dirty[ $key ]; } else { if ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] ); } /* * @todo [JRF => whomever] Maybe change the untranslated $pt name in the * error message to the nicely translated label ? */ add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-id for the error message box. /* translators: %s expands to a post type. */ sprintf( __( 'Please select a valid taxonomy for post type "%s"', 'wordpress-seo' ), $post_type ), // The error message. 'error', // Message type. ); } } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] ); } unset( $taxonomies, $post_type ); break; /* 'taxonomy-' . $tax->name . '-ptparent' fields. */ case 'taxonomy-': if ( isset( $dirty[ $key ] ) ) { if ( $allowed_post_types !== [] && in_array( $dirty[ $key ], $allowed_post_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) { $clean[ $key ] = 0; } elseif ( sanitize_key( $dirty[ $key ] ) === $dirty[ $key ] ) { // Allow taxonomies which may not be registered yet. $clean[ $key ] = $dirty[ $key ]; } else { if ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_key( $old[ $key ] ); } /* * @todo [JRF =? whomever] Maybe change the untranslated $tax name in the * error message to the nicely translated label ? */ $tax = str_replace( [ 'taxonomy-', '-ptparent' ], '', $key ); add_settings_error( $this->group_name, // Slug title of the setting. '_' . $tax, // Suffix-ID for the error message box. /* translators: %s expands to a taxonomy slug. */ sprintf( __( 'Please select a valid post type for taxonomy "%s"', 'wordpress-seo' ), $tax ), // The error message. 'error', // Message type. ); unset( $tax ); } } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = sanitize_key( $old[ $key ] ); } break; /* * Covers: * 'company_or_person_user_id' * 'company_logo_id', 'person_logo_id', 'open_graph_frontpage_image_id' * 'social-image-id-' . $pt->name * 'social-image-id-ptarchive-' . $pt->name * 'social-image-id-tax-' . $tax->name * 'social-image-id-author-wpseo', 'social-image-id-archive-wpseo' */ case 'company_or_person_user_id': case 'company_logo_id': case 'person_logo_id': case 'social-image-id-': case 'open_graph_frontpage_image_id': case 'publishing_principles_id': case 'ownership_funding_info_id': case 'actionable_feedback_policy_id': case 'corrections_policy_id': case 'ethics_policy_id': case 'diversity_policy_id': case 'diversity_staffing_report_id': if ( isset( $dirty[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $dirty[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } elseif ( isset( $old[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $old[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } break; /* Separator field - Radio. */ case 'separator': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { // Get separator fields. $separator_fields = $this->get_separator_options(); // Check if the given separator exists. if ( isset( $separator_fields[ $dirty[ $key ] ] ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'schema-page-type-': if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) { if ( array_key_exists( $dirty[ $key ], Schema_Types::PAGE_TYPES ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $post_type = str_replace( $switch_key, '', $key ); $clean[ $key ] = $defaults[ $switch_key . $post_type ]; } } break; case 'schema-article-type-': if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) { /** * Filter: 'wpseo_schema_article_types' - Allow developers to filter the available article types. * * Make sure when you filter this to also filter `wpseo_schema_article_types_labels`. * * @param array $schema_article_types The available schema article types. */ if ( array_key_exists( $dirty[ $key ], apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ) ) ) { $clean[ $key ] = $dirty[ $key ]; } else { $defaults = $this->get_defaults(); $post_type = str_replace( $switch_key, '', $key ); $clean[ $key ] = $defaults[ $switch_key . $post_type ]; } } break; /* * Boolean fields. */ /* * Covers: * 'noindex-author-wpseo', 'noindex-author-noposts-wpseo', 'noindex-archive-wpseo' * 'noindex-' . $pt->name * 'noindex-ptarchive-' . $pt->name * 'noindex-tax-' . $tax->name * 'forcerewritetitle': * 'noodp': * 'noydir': * 'disable-author': * 'disable-date': * 'disable-post_format'; * 'noindex-' * 'display-metabox-pt-' * 'display-metabox-pt-'. $pt->name * 'display-metabox-tax-' * 'display-metabox-tax-' . $tax->name * 'breadcrumbs-display-blog-page' * 'breadcrumbs-boldlast' * 'breadcrumbs-enable' * 'stripcategorybase' */ default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } /** * Retrieve a list of the allowed post types as breadcrumb parent for a taxonomy. * Helper method for validation. * * {@internal Don't make static as new types may still be registered.}} * * @return string[] */ protected function get_allowed_post_types() { $allowed_post_types = []; /* * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here. */ $post_types = get_post_types( [ 'public' => true ], 'objects' ); if ( get_option( 'show_on_front' ) === 'page' && get_option( 'page_for_posts' ) > 0 ) { $allowed_post_types[] = 'post'; } if ( is_array( $post_types ) && $post_types !== [] ) { foreach ( $post_types as $type ) { if ( WPSEO_Post_Type::has_archive( $type ) ) { $allowed_post_types[] = $type->name; } } } return $allowed_post_types; } /** * Clean a given option value. * * @param string[] $option_value Old (not merged with defaults or filtered) option value to clean according to the rules for this option. * @param string[]|null $current_version Optional. Version from which to upgrade, if not set, version specific upgrades will be disregarded. * @param string[]|null $all_old_option_values Optional. Only used when importing old options to have access to the real old values, in contrast to the saved ones. * * @return string[] Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { static $original = null; // Double-run this function to ensure renaming of the taxonomy options will work. if ( ! isset( $original ) && has_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ) === false ) { add_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ); $original = $option_value; } /* * Move options from very old option to this one. * * {@internal Don't rename to the 'current' names straight away as that would prevent * the rename/unset combi below from working.}} * * @todo [JRF] Maybe figure out a smarter way to deal with this. */ $old_option = null; if ( isset( $all_old_option_values ) ) { // Ok, we have an import. if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) { $old_option = $all_old_option_values['wpseo_indexation']; } } else { $old_option = get_option( 'wpseo_indexation' ); } if ( is_array( $old_option ) && $old_option !== [] ) { $move = [ 'noindexauthor' => 'noindex-author', 'disableauthor' => 'disable-author', 'noindexdate' => 'noindex-archive', 'noindexcat' => 'noindex-category', 'noindextag' => 'noindex-post_tag', 'noindexpostformat' => 'noindex-post_format', ]; foreach ( $move as $old => $new ) { if ( isset( $old_option[ $old ] ) && ! isset( $option_value[ $new ] ) ) { $option_value[ $new ] = $old_option[ $old ]; } } unset( $move, $old, $new ); } unset( $old_option ); // Fix wrongness created by buggy version 1.2.2. if ( isset( $option_value['title-home'] ) && $option_value['title-home'] === '%%sitename%% - %%sitedesc%% - 12345' ) { $option_value['title-home-wpseo'] = '%%sitename%% - %%sitedesc%%'; } /* * Renaming these options to avoid ever overwritting these if a (bloody stupid) user / * programmer would use any of the following as a custom post type or custom taxonomy: * 'home', 'author', 'archive', 'search', '404', 'subpages'. * * Similarly, renaming the tax options to avoid a custom post type and a taxonomy * with the same name occupying the same option. */ $rename = [ 'title-home' => 'title-home-wpseo', 'title-author' => 'title-author-wpseo', 'title-archive' => 'title-archive-wpseo', 'title-search' => 'title-search-wpseo', 'title-404' => 'title-404-wpseo', 'metadesc-home' => 'metadesc-home-wpseo', 'metadesc-author' => 'metadesc-author-wpseo', 'metadesc-archive' => 'metadesc-archive-wpseo', 'noindex-author' => 'noindex-author-wpseo', 'noindex-archive' => 'noindex-archive-wpseo', ]; foreach ( $rename as $old => $new ) { if ( isset( $option_value[ $old ] ) && ! isset( $option_value[ $new ] ) ) { $option_value[ $new ] = $option_value[ $old ]; unset( $option_value[ $old ] ); } } unset( $rename, $old, $new ); /* * {@internal This clean-up action can only be done effectively once the taxonomies * and post_types have been registered, i.e. at the end of the init action.}} */ if ( ( isset( $original ) && current_filter() === 'wpseo_double_clean_titles' ) || did_action( 'wpseo_double_clean_titles' ) > 0 ) { $rename = [ 'title-' => 'title-tax-', 'metadesc-' => 'metadesc-tax-', 'noindex-' => 'noindex-tax-', 'tax-hideeditbox-' => 'hideeditbox-tax-', ]; $taxonomy_names = get_taxonomies( [ 'public' => true ], 'names' ); $post_type_names = get_post_types( [ 'public' => true ], 'names' ); $defaults = $this->get_defaults(); if ( $taxonomy_names !== [] ) { foreach ( $taxonomy_names as $tax ) { foreach ( $rename as $old_prefix => $new_prefix ) { if ( ( isset( $original[ $old_prefix . $tax ] ) && ! isset( $original[ $new_prefix . $tax ] ) ) && ( ! isset( $option_value[ $new_prefix . $tax ] ) || ( isset( $option_value[ $new_prefix . $tax ] ) && $option_value[ $new_prefix . $tax ] === $defaults[ $new_prefix . $tax ] ) ) ) { $option_value[ $new_prefix . $tax ] = $original[ $old_prefix . $tax ]; /* * Check if there is a cpt with the same name as the tax, * if so, we should make sure that the old setting hasn't been removed. */ if ( ! isset( $post_type_names[ $tax ] ) && isset( $option_value[ $old_prefix . $tax ] ) ) { unset( $option_value[ $old_prefix . $tax ] ); } elseif ( isset( $post_type_names[ $tax ] ) && ! isset( $option_value[ $old_prefix . $tax ] ) ) { $option_value[ $old_prefix . $tax ] = $original[ $old_prefix . $tax ]; } if ( $old_prefix === 'tax-hideeditbox-' ) { unset( $option_value[ $old_prefix . $tax ] ); } } } } } unset( $rename, $taxonomy_names, $post_type_names, $defaults, $tax, $old_prefix, $new_prefix ); } return $option_value; } /** * Make sure that any set option values relating to post_types and/or taxonomies are retained, * even when that post_type or taxonomy may not yet have been registered. * * {@internal Overrule the abstract class version of this to make sure one extra renamed * variable key does not get removed. IMPORTANT: keep this method in line with * the parent on which it is based!}} * * @param string[] $dirty Original option as retrieved from the database. * @param string[] $clean Filtered option where any options which shouldn't be in our option * have already been removed and any options which weren't set * have been set to their defaults. * * @return string[] */ protected function retain_variable_keys( $dirty, $clean ) { if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) { // Add the extra pattern. $patterns = $this->variable_array_key_patterns; $patterns[] = 'tax-hideeditbox-'; /** * Allow altering the array with variable array key patterns. * * @param array $patterns Array with the variable array key patterns. */ $patterns = apply_filters( 'wpseo_option_titles_variable_array_key_patterns', $patterns ); foreach ( $dirty as $key => $value ) { // Do nothing if already in filtered option array. if ( isset( $clean[ $key ] ) ) { continue; } foreach ( $patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { $clean[ $key ] = $value; break; } } } } return $clean; } /** * Retrieves a list of separator options. * * @return string[] An array of the separator options. */ protected static function get_separator_option_list() { $separators = [ 'sc-dash' => [ 'option' => '-', 'label' => __( 'Dash', 'wordpress-seo' ), ], 'sc-ndash' => [ 'option' => '–', 'label' => __( 'En dash', 'wordpress-seo' ), ], 'sc-mdash' => [ 'option' => '—', 'label' => __( 'Em dash', 'wordpress-seo' ), ], 'sc-colon' => [ 'option' => ':', 'label' => __( 'Colon', 'wordpress-seo' ), ], 'sc-middot' => [ 'option' => '·', 'label' => __( 'Middle dot', 'wordpress-seo' ), ], 'sc-bull' => [ 'option' => '•', 'label' => __( 'Bullet', 'wordpress-seo' ), ], 'sc-star' => [ 'option' => '*', 'label' => __( 'Asterisk', 'wordpress-seo' ), ], 'sc-smstar' => [ 'option' => '⋆', 'label' => __( 'Low asterisk', 'wordpress-seo' ), ], 'sc-pipe' => [ 'option' => '|', 'label' => __( 'Vertical bar', 'wordpress-seo' ), ], 'sc-tilde' => [ 'option' => '~', 'label' => __( 'Small tilde', 'wordpress-seo' ), ], 'sc-laquo' => [ 'option' => '«', 'label' => __( 'Left angle quotation mark', 'wordpress-seo' ), ], 'sc-raquo' => [ 'option' => '»', 'label' => __( 'Right angle quotation mark', 'wordpress-seo' ), ], 'sc-lt' => [ 'option' => '>', 'label' => __( 'Less than sign', 'wordpress-seo' ), ], 'sc-gt' => [ 'option' => '<', 'label' => __( 'Greater than sign', 'wordpress-seo' ), ], ]; /** * Allows altering the separator options array. * * @param array $separators Array with the separator options. */ $separator_list = apply_filters( 'wpseo_separator_option_list', $separators ); if ( ! is_array( $separator_list ) ) { return $separators; } return $separator_list; } } inc/options/class-wpseo-option.php 0000644 00000070013 15174712003 0013270 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * This abstract class and its concrete classes implement defaults and value validation for * all WPSEO options and subkeys within options. * * Some guidelines: * [Retrieving options] * - Use the normal get_option() to retrieve an option. You will receive a complete array for the option. * Any subkeys which were not set, will have their default values in place. * - In other words, you will normally not have to check whether a subkey isset() as they will *always* be set. * They will also *always* be of the correct variable type. * The only exception to this are the options with variable option names based on post_type or taxonomy * as those will not always be available before the taxonomy/post_type is registered. * (they will be available if a value was set, they won't be if it wasn't as the class won't know * that a default needs to be injected). * * [Updating/Adding options] * - For multisite site_options, please use the WPSEO_Options::update_site_option() method. * - For normal options, use the normal add/update_option() functions. As long as the classes here * are instantiated, validation for all options and their subkeys will be automatic. * - On (successful) update of a couple of options, certain related actions will be run automatically. * Some examples: * - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly * - on change of wpseo and wpseo_title, some caches will be cleared * * [Important information about add/updating/changing these classes] * - Make sure that option array key names are unique across options. The WPSEO_Options::get_all() * method merges most options together. If any of them have non-unique names, even if they * are in a different option, they *will* overwrite each other. * - When you add a new array key in an option: make sure you add proper defaults and add the key * to the validation routine in the proper place or add a new validation case. * You don't need to do any upgrading as any option returned will always be merged with the * defaults, so new options will automatically be available. * If the default value is a string which need translating, add this to the concrete class * translate_defaults() method. * - When you remove an array key from an option: if it's important that the option is really removed, * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run. * This will re-save the option and automatically remove the array key no longer in existence. * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run. * - When you change the default for an option sub-key, make sure you verify that the validation routine will * still work the way it should. * Example: changing a default from '' (empty string) to 'text' with a validation routine with tests * for an empty string will prevent a user from saving an empty string as the real value. So the * test for '' with the validation routine would have to be removed in that case. * - If an option needs specific actions different from defined in this abstract class, you can just overrule * a method by defining it in the concrete class. * * @todo [JRF => testers] Double check that validation will not cause errors when called * from upgrade routine (some of the WP functions may not yet be available). */ abstract class WPSEO_Option { /** * Prefix for override option keys that allow or disallow the option key of the same name. * * @var string */ public const ALLOW_KEY_PREFIX = 'allow_'; /** * Option name - MUST be set in concrete class and set to public. * * @var string */ protected $option_name; /** * Option group name for use in settings forms. * * Will be set automagically if not set in concrete class (i.e. * if it conforms to the normal pattern 'yoast' . $option_name . 'options', * only set in concrete class if it doesn't). * * @var string */ public $group_name; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * Also determines which options are copied over for ms_(re)set_blog(). * * @var bool */ public $include_in_all = true; /** * Whether this option is only for when the install is multisite. * * @var bool */ public $multisite_only = false; /** * Array of defaults for the option - MUST be set in concrete class. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults; /** * Array of variable option name patterns for the option - if any. * * Set this when the option contains array keys which vary based on post_type * or taxonomy. * * @var array */ protected $variable_array_key_patterns; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = []; /** * Name for an option higher in the hierarchy to override setting access. * * @var string */ protected $override_option_name; /** * Instance of this class. * * @var WPSEO_Option */ protected static $instance; /* *********** INSTANTIATION METHODS *********** */ /** * Add all the actions and filters for the option. */ protected function __construct() { /* Add filters which get applied to the get_options() results. */ $this->add_default_filters(); // Return defaults if option not set. $this->add_option_filters(); // Merge with defaults if option *is* set. if ( $this->multisite_only !== true ) { /** * The option validation routines remove the default filters to prevent failing * to insert an option if it's new. Let's add them back afterwards. */ add_action( 'add_option', [ $this, 'add_default_filters_if_same_option' ] ); // Adding back after INSERT. add_action( 'update_option', [ $this, 'add_default_filters_if_same_option' ] ); add_filter( 'pre_update_option', [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 ); // Refills the cache when the option has been updated. add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 ); } elseif ( is_multisite() ) { /* * The option validation routines remove the default filters to prevent failing * to insert an option if it's new. Let's add them back afterwards. * * For site_options, this method is not foolproof as these actions are not fired * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method * for updating site options to make sure the filters are in place. */ add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] ); add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] ); add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 ); // Refills the cache when the option has been updated. add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 ); } /* * Make sure the option will always get validated, independently of register_setting() * (only available on back-end). */ add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] ); /* Register our option for the admin pages */ add_action( 'admin_init', [ $this, 'register_setting' ] ); /* Set option group name if not given */ if ( ! isset( $this->group_name ) || $this->group_name === '' ) { $this->group_name = 'yoast_' . $this->option_name . '_options'; } /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */ if ( method_exists( $this, 'translate_defaults' ) ) { add_action( 'init', [ $this, 'translate_defaults' ], 2 ); } /** * Enrich defaults once custom post types and taxonomies have been registered * which is normally done on the init action. * * @todo [JRF/testers] Verify that none of the options which are only available after * enrichment are used before the enriching. */ if ( method_exists( $this, 'enrich_defaults' ) ) { add_action( 'init', [ $this, 'enrich_defaults' ], 99 ); } } /* * All concrete classes *must* contain the get_instance method. * * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}} * * ``` * abstract protected static function get_instance(); * ``` * --------------- * * Concrete classes *may* contain a translate_defaults method. * ``` * abstract public function translate_defaults(); * ``` * --------------- * * Concrete classes *may* contain an enrich_defaults method to add additional defaults once * all post_types and taxonomies have been registered. * * ``` * abstract public function enrich_defaults(); * ``` */ /* *********** METHODS INFLUENCING get_option() *********** */ /** * Add filters to make sure that the option default is returned if the option is not set. * * @return void */ public function add_default_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) { add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } } /** * Adds back the default filters that were removed during validation if the option was changed. * Checks if this option was changed to prevent constantly checking if filters are present. * * @param string $option_name The option name. * * @return void */ public function add_default_filters_if_same_option( $option_name ) { if ( $option_name === $this->option_name ) { $this->add_default_filters(); } } /** * Adds back the default filters that were removed during validation if the option was not changed. * This is because in that case the latter actions are not called and thus the filters are never * added back. * * @param mixed $value The current value. * @param string $option_name The option name. * @param mixed $old_value The old value. * * @return string The current value. */ public function add_default_filters_if_not_changed( $value, $option_name, $old_value ) { if ( $option_name !== $this->option_name ) { return $value; } if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) { $this->add_default_filters(); } return $value; } /** * Validate webmaster tools & Pinterest verification strings. * * @param string $key Key to check, by type of service. * @param array $dirty Dirty data with the new values. * @param array $old Old data. * @param array $clean Clean data by reference, normally the default values. * * @return void */ public function validate_verification_string( $key, $dirty, $old, &$clean ) { if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $meta = $dirty[ $key ]; if ( strpos( $meta, 'content=' ) ) { // Make sure we only have the real key, not a complete meta tag. preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match ); if ( isset( $match[2] ) ) { $meta = $match[2]; } unset( $match ); } $meta = sanitize_text_field( $meta ); if ( $meta !== '' ) { $regex = '`^[A-Fa-f0-9_-]+$`'; switch ( $key ) { case 'googleverify': case 'ahrefsverify': case 'baiduverify': $regex = '`^[A-Za-z0-9_-]+$`'; break; case 'msverify': case 'pinterestverify': case 'yandexverify': break; } if ( preg_match( $regex, $meta ) ) { $clean[ $key ] = $meta; } else { // Restore the previous value, if any. if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) { $clean[ $key ] = $old[ $key ]; } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta ); } } } } /** * Validates an option as a valid URL. Prints out a WordPress settings error * notice if the URL is invalid. * * @param string $key Key to check, by type of URL setting. * @param array $dirty Dirty data with the new values. * @param array $old Old data. * @param array $clean Clean data by reference, normally the default values. * * @return void */ public function validate_url( $key, $dirty, $old, &$clean ) { if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $submitted_url = trim( $dirty[ $key ] ); $validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL ); if ( $validated_url === false ) { // Restore the previous URL value, if any. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $url = WPSEO_Utils::sanitize_url( $old[ $key ] ); if ( $url !== '' ) { $clean[ $key ] = $url; } } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url ); return; } // The URL format is valid, let's sanitize it. $url = WPSEO_Utils::sanitize_url( $validated_url ); if ( $url !== '' ) { $clean[ $key ] = $url; } } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { remove_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } /** * Get the enriched default value for an option. * * Checks if the concrete class contains an enrich_defaults() method and if so, runs it. * * {@internal The enrich_defaults method is used to set defaults for variable array keys * in an option, such as array keys depending on post_types and/or taxonomies.}} * * @return array */ public function get_defaults() { if ( method_exists( $this, 'translate_defaults' ) ) { $this->translate_defaults(); } if ( method_exists( $this, 'enrich_defaults' ) ) { $this->enrich_defaults(); } return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name ); } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) { add_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { remove_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ); } /** * Merge an option with its default values. * * This method should *not* be called directly!!! It is only meant to filter the get_option() results. * * @param mixed $options Option value. * * @return mixed Option merged with the defaults for that option. */ public function get_option( $options = null ) { $filtered = $this->array_filter_merge( $options ); /* * If the option contains variable option keys, make sure we don't remove those settings * - even if the defaults are not complete yet. * Unfortunately this means we also won't be removing the settings for post types or taxonomies * which are no longer in the WP install, but rather that than the other way around. */ if ( isset( $this->variable_array_key_patterns ) ) { $filtered = $this->retain_variable_keys( $options, $filtered ); } return $filtered; } /* *********** METHODS influencing add_option(), update_option() and saving from admin pages. *********** */ /** * Register (whitelist) the option for the configuration pages. * The validation callback is already registered separately on the sanitize_option hook, * so no need to double register. * * @return void */ public function register_setting() { if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { return; } if ( $this->multisite_only === true ) { $network_settings_api = Yoast_Network_Settings_API::get(); if ( $network_settings_api->meets_requirements() ) { $network_settings_api->register_setting( $this->group_name, $this->option_name ); } return; } register_setting( $this->group_name, $this->option_name ); } /** * Validate the option. * * @param mixed $option_value The unvalidated new value for the option. * * @return array Validated new value for the option. */ public function validate( $option_value ) { $clean = $this->get_defaults(); /* Return the defaults if the new value is empty. */ if ( ! is_array( $option_value ) || $option_value === [] ) { return $clean; } $option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value ); $old = $this->get_original_option(); if ( ! is_array( $old ) ) { $old = []; } $old = array_merge( $clean, $old ); $clean = $this->validate_option( $option_value, $clean, $old ); // Prevent updates to variables that are disabled via the override option. $clean = $this->prevent_disabled_options_update( $clean, $old ); /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */ if ( isset( $this->variable_array_key_patterns ) ) { $clean = $this->retain_variable_keys( $option_value, $clean ); } $this->remove_default_filters(); return $clean; } /** * Checks whether a specific option key is disabled. * * This is determined by whether an override option is available with a key that equals the given key prefixed * with 'allow_'. * * @param string $key Option key. * * @return bool True if option key is disabled, false otherwise. */ public function is_disabled( $key ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return false; } return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ]; } /** * All concrete classes must contain a validate_option() method which validates all * values within the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. */ abstract protected function validate_option( $dirty, $clean, $old ); /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */ /** * Retrieve the real old value (unmerged with defaults). * * @return array|bool The original option value (which can be false if the option doesn't exist). */ protected function get_original_option() { $this->remove_default_filters(); $this->remove_option_filters(); // Get (unvalidated) array, NOT merged with defaults. if ( $this->multisite_only !== true ) { $option_value = get_option( $this->option_name ); } else { $option_value = get_site_option( $this->option_name ); } $this->add_option_filters(); $this->add_default_filters(); return $option_value; } /** * Add the option if it doesn't exist for some strange reason. * * @uses WPSEO_Option::get_original_option() * * @return void */ public function maybe_add_option() { if ( $this->get_original_option() === false ) { if ( $this->multisite_only !== true ) { update_option( $this->option_name, $this->get_defaults() ); } else { $this->update_site_option( $this->get_defaults() ); } } } /** * Update a site_option. * * {@internal This special method is only needed for multisite options, but very needed indeed there. * The order in which certain functions and hooks are run is different between * get_option() and get_site_option() which means in practice that the removing * of the default filters would be done too late and the re-adding of the default * filters might not be done at all. * Aka: use the WPSEO_Options::update_site_option() method (which calls this method) * for safely adding/updating multisite options.}} * * @param mixed $value The new value for the option. * * @return bool Whether the update was successful. */ public function update_site_option( $value ) { if ( $this->multisite_only === true && is_multisite() ) { $this->remove_default_filters(); $result = update_site_option( $this->option_name, $value ); $this->add_default_filters(); return $result; } else { return false; } } /** * Retrieve the real old value (unmerged with defaults), clean and re-save the option. * * @uses WPSEO_Option::get_original_option() * @uses WPSEO_Option::import() * * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version-specific upgrades will be disregarded. * * @return void */ public function clean( $current_version = null ) { $option_value = $this->get_original_option(); $this->import( $option_value, $current_version ); } /** * Clean and re-save the option. * * @uses clean_option() method from concrete class if it exists. * * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe * something along the lines of: * -> add them to a property in this class * -> if that property isset at the end of the routine and add_settings_error function does not exist, * save as transient (or update the transient if one already exists) * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it * once the admin has dismissed the message (add ajax function) * Important: all validation routines which add_settings_errors would need to be changed for this to work * * @param array $option_value Option value to be imported. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version-specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to * have access to the real old values, in contrast to * the saved ones. * * @return void */ public function import( $option_value, $current_version = null, $all_old_option_values = null ) { if ( $option_value === false ) { $option_value = $this->get_defaults(); } elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) { $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values ); } /* * Save the cleaned value - validation will take care of cleaning out array keys which * should no longer be there. */ if ( $this->multisite_only !== true ) { update_option( $this->option_name, $option_value ); } else { $this->update_site_option( $this->option_name, $option_value ); } } /** * Returns the variable array key patterns for an options class. * * @return array */ public function get_patterns() { return (array) $this->variable_array_key_patterns; } /** * Retrieves the option name. * * @return string The set option name. */ public function get_option_name() { return $this->option_name; } /* * Concrete classes *may* contain a clean_option method which will clean out old/renamed * values within the option. * * ``` * abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null ); * ``` */ /* *********** HELPER METHODS for internal use. *********** */ /** * Helper method - Combines a fixed array of default values with an options array * while filtering out any keys which are not in the defaults array. * * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation * removes any invalid keys on save. * * @param array|null $options Optional. Current options. If not set, the option defaults * for the $option_key will be returned. * * @return array Combined and filtered options array. */ protected function array_filter_merge( $options = null ) { $defaults = $this->get_defaults(); if ( ! isset( $options ) || $options === false || $options === [] ) { return $defaults; } $options = (array) $options; /* $filtered = array(); if ( $defaults !== array() ) { foreach ( $defaults as $key => $default_value ) { // @todo should this walk through array subkeys ? $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value ); } } */ $filtered = array_merge( $defaults, $options ); return $filtered; } /** * Sets updated values for variables that are disabled via the override option back to their previous values. * * @param array $updated Updated option value. * @param array $old Old option value. * * @return array Updated option value, with all disabled variables set to their old values. */ protected function prevent_disabled_options_update( $updated, $old ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return $updated; } /* * This loop could as well call `is_disabled( $key )` for each iteration, * however this would be worse performance-wise. */ foreach ( $old as $key => $value ) { if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) { $updated[ $key ] = $old[ $key ]; } } return $updated; } /** * Retrieves the value of the override option, if available. * * An override option contains values that may determine access to certain sub-variables * of this option. * * Only regular options in multisite can have override options, which in that case * would be network options. * * @return array Override option value, or empty array if unavailable. */ protected function get_override_option() { if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) { return []; } return get_site_option( $this->override_option_name, [] ); } /** * Make sure that any set option values relating to post_types and/or taxonomies are retained, * even when that post_type or taxonomy may not yet have been registered. * * {@internal The wpseo_titles concrete class overrules this method. Make sure that any * changes applied here, also get ported to that version.}} * * @param array $dirty Original option as retrieved from the database. * @param array $clean Filtered option where any options which shouldn't be in our option * have already been removed and any options which weren't set * have been set to their defaults. * * @return array */ protected function retain_variable_keys( $dirty, $clean ) { if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) { foreach ( $dirty as $key => $value ) { // Do nothing if already in filtered options. if ( isset( $clean[ $key ] ) ) { continue; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { $clean[ $key ] = $value; break; } } } } return $clean; } /** * Check whether a given array key conforms to one of the variable array key patterns for this option. * * @used-by validate_option() methods for options with variable array keys. * * @param string $key Array key to check. * * @return string Pattern if it conforms, original array key if it doesn't or if the option * does not have variable array keys. */ protected function get_switch_key( $key ) { if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === [] ) ) { return $key; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { return $pattern; } } return $key; } } inc/options/class-wpseo-option-llmstxt.php 0000644 00000005671 15174712003 0015005 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_llmstxt. */ class WPSEO_Option_Llmstxt extends WPSEO_Option { private const OTHER_INCLUDED_PAGES_LIMIT = 100; /** * Option name. * * @var string */ public $option_name = 'wpseo_llmstxt'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array<string, int|string|array<int>> */ protected $defaults = [ 'llms_txt_selection_mode' => 'auto', 'about_us_page' => 0, 'contact_page' => 0, 'terms_page' => 0, 'privacy_policy_page' => 0, 'shop_page' => 0, 'other_included_pages' => [], ]; /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * All concrete classes must contain a validate_option() method which validates all * values within the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array The clean option with the saved value. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'other_included_pages': if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( ! is_array( $items ) ) { $items = json_decode( $dirty[ $key ], true ); } if ( is_array( $items ) ) { $items = array_slice( $items, 0, $this->get_other_included_pages_limit() ); foreach ( $items as $item ) { $validated_id = WPSEO_Utils::validate_int( $item ); if ( $validated_id === false || $validated_id === 0 ) { continue; } $clean[ $key ][] = $validated_id; } } } break; case 'about_us_page': case 'contact_page': case 'terms_page': case 'privacy_policy_page': case 'shop_page': if ( isset( $dirty[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $dirty[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } elseif ( isset( $old[ $key ] ) ) { $int = WPSEO_Utils::validate_int( $old[ $key ] ); if ( $int !== false && $int >= 0 ) { $clean[ $key ] = $int; } } break; case 'llms_txt_selection_mode': if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], [ 'auto', 'manual' ], true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; } } return $clean; } /** * Gets the limit for the other included pages. * * @return int The limit for the other included pages. */ public function get_other_included_pages_limit() { return self::OTHER_INCLUDED_PAGES_LIMIT; } } inc/options/class-wpseo-option-wpseo.php 0000644 00000061601 15174712003 0014426 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo. */ class WPSEO_Option_Wpseo extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo'; /** * Array of defaults for the option. * * {@internal Shouldn't be requested directly, use $this->get_defaults();}} * * @var array */ protected $defaults = [ // Non-form fields, set via (ajax) function. 'tracking' => null, 'toggled_tracking' => false, 'license_server_version' => false, 'ms_defaults_set' => false, 'ignore_search_engines_discouraged_notice' => false, 'indexing_first_time' => true, 'indexing_started' => null, 'indexing_reason' => '', 'indexables_indexing_completed' => false, 'index_now_key' => '', // Non-form field, should only be set via validation routine. 'version' => '', // Leave default as empty to ensure activation/upgrade works. 'previous_version' => '', // Form fields. 'disableadvanced_meta' => true, 'enable_headless_rest_endpoints' => true, 'ryte_indexability' => false, 'baiduverify' => '', // Text field. 'googleverify' => '', // Text field. 'msverify' => '', // Text field. 'yandexverify' => '', 'ahrefsverify' => '', 'site_type' => '', // List of options. 'has_multiple_authors' => '', 'environment_type' => '', 'content_analysis_active' => true, 'keyword_analysis_active' => true, 'inclusive_language_analysis_active' => false, 'enable_admin_bar_menu' => true, 'enable_cornerstone_content' => true, 'enable_xml_sitemap' => true, 'enable_text_link_counter' => true, 'enable_index_now' => true, 'enable_ai_generator' => true, 'ai_enabled_pre_default' => false, 'show_onboarding_notice' => false, 'first_activated_on' => false, 'myyoast-oauth' => [ 'config' => [ 'clientId' => null, 'secret' => null, ], 'access_tokens' => [], ], 'semrush_integration_active' => true, 'semrush_tokens' => [], 'semrush_country_code' => 'us', 'permalink_structure' => '', 'home_url' => '', 'dynamic_permalinks' => false, 'category_base_url' => '', 'tag_base_url' => '', 'custom_taxonomy_slugs' => [], 'enable_enhanced_slack_sharing' => true, 'enable_metabox_insights' => true, 'enable_link_suggestions' => true, 'algolia_integration_active' => false, 'import_cursors' => [], 'workouts_data' => [ 'configuration' => [ 'finishedSteps' => [] ] ], 'configuration_finished_steps' => [], 'dismiss_configuration_workout_notice' => false, 'dismiss_premium_deactivated_notice' => false, 'importing_completed' => [], 'wincher_integration_active' => true, 'wincher_tokens' => [], 'wincher_automatically_add_keyphrases' => false, 'wincher_website_id' => '', 'first_time_install' => false, 'should_redirect_after_install_free' => false, 'activation_redirect_timestamp_free' => 0, 'remove_feed_global' => false, 'remove_feed_global_comments' => false, 'remove_feed_post_comments' => false, 'remove_feed_authors' => false, 'remove_feed_categories' => false, 'remove_feed_tags' => false, 'remove_feed_custom_taxonomies' => false, 'remove_feed_post_types' => false, 'remove_feed_search' => false, 'remove_atom_rdf_feeds' => false, 'remove_shortlinks' => false, 'remove_rest_api_links' => false, 'remove_rsd_wlw_links' => false, 'remove_oembed_links' => false, 'remove_generator' => false, 'remove_emoji_scripts' => false, 'remove_powered_by_header' => false, 'remove_pingback_header' => false, 'clean_campaign_tracking_urls' => false, 'clean_permalinks' => false, 'clean_permalinks_extra_variables' => '', 'search_cleanup' => false, 'search_cleanup_emoji' => false, 'search_cleanup_patterns' => false, 'search_character_limit' => 50, 'deny_search_crawling' => false, 'deny_wp_json_crawling' => false, 'deny_adsbot_crawling' => false, 'deny_ccbot_crawling' => false, 'deny_google_extended_crawling' => false, 'deny_gptbot_crawling' => false, 'redirect_search_pretty_urls' => false, 'least_readability_ignore_list' => [], 'least_seo_score_ignore_list' => [], 'most_linked_ignore_list' => [], 'least_linked_ignore_list' => [], 'indexables_page_reading_list' => [ false, false, false, false, false ], 'indexables_overview_state' => 'dashboard-not-visited', 'last_known_public_post_types' => [], 'last_known_public_taxonomies' => [], 'last_known_no_unindexed' => [], 'new_post_types' => [], 'new_taxonomies' => [], 'show_new_content_type_notification' => false, 'site_kit_configuration_permanently_dismissed' => false, 'site_kit_connected' => false, 'site_kit_tracking_setup_widget_loaded' => 'no', 'site_kit_tracking_first_interaction_stage' => '', 'site_kit_tracking_last_interaction_stage' => '', 'site_kit_tracking_setup_widget_temporarily_dismissed' => 'no', 'site_kit_tracking_setup_widget_permanently_dismissed' => 'no', 'google_site_kit_feature_enabled' => false, // No longer used. 'ai_free_sparks_started_on' => null, 'enable_llms_txt' => false, 'last_updated_on' => false, 'default_seo_title' => [], 'default_seo_meta_desc' => [], 'first_activated_by' => 0, 'enable_schema_aggregation_endpoint' => false, 'schema_aggregation_endpoint_enabled_on' => null, 'enable_task_list' => true, 'enable_schema' => true, ]; /** * Sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = [ 'ignore_search_engines_discouraged_notice', /* Privacy. */ 'baiduverify', 'googleverify', 'msverify', 'yandexverify', 'ahrefsverify', ]; /** * Possible values for the site_type option. * * @var array */ protected $site_types = [ '', 'blog', 'shop', 'news', 'smallBusiness', 'corporateOther', 'personalOther', ]; /** * Possible environment types. * * @var array */ protected $environment_types = [ '', 'local', 'production', 'staging', 'development', ]; /** * Possible has_multiple_authors options. * * @var array */ protected $has_multiple_authors_options = [ '', true, false, ]; /** * Name for an option higher in the hierarchy to override setting access. * * @var string */ protected $override_option_name = 'wpseo_ms'; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); /** * Filter: 'wpseo_enable_tracking' - Enables the data tracking of Yoast SEO Premium. * * @param string|false $is_enabled The enabled state. Default is false. */ $this->defaults['tracking'] = apply_filters( 'wpseo_enable_tracking', false ); /* Clear the cache on update/add. */ add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] ); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); /** * Filter the `wpseo` option defaults. * * @param array $defaults Array the defaults for the `wpseo` option attributes. */ $this->defaults = apply_filters( 'wpseo_option_wpseo_defaults', $this->defaults ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { parent::add_option_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook(); if ( has_filter( $hookname, $callback ) === false ) { add_filter( $hookname, $callback, $priority ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { parent::remove_option_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook(); remove_filter( $hookname, $callback, $priority ); } /** * Add filters to make sure that the option default is returned if the option is not set. * * @return void */ public function add_default_filters() { parent::add_default_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook(); if ( has_filter( $hookname, $callback ) === false ) { add_filter( $hookname, $callback, $priority ); } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { parent::remove_default_filters(); list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook(); remove_filter( $hookname, $callback, $priority ); } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'version': $clean[ $key ] = WPSEO_VERSION; break; case 'previous_version': case 'semrush_country_code': case 'license_server_version': case 'home_url': case 'index_now_key': case 'wincher_website_id': case 'clean_permalinks_extra_variables': case 'indexables_overview_state': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'indexing_reason': case 'site_kit_tracking_setup_widget_loaded': case 'site_kit_tracking_first_interaction_stage': case 'site_kit_tracking_last_interaction_stage': case 'site_kit_tracking_setup_widget_temporarily_dismissed': case 'site_kit_tracking_setup_widget_permanently_dismissed': case 'ai_free_sparks_started_on': case 'schema_aggregation_endpoint_enabled_on': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = sanitize_text_field( $dirty[ $key ] ); } break; /* Verification strings. */ case 'baiduverify': case 'googleverify': case 'msverify': case 'yandexverify': case 'ahrefsverify': $this->validate_verification_string( $key, $dirty, $old, $clean ); break; /* * Boolean dismiss warnings - not fields - may not be in form * (and don't need to be either as long as the default is false). */ case 'ignore_search_engines_discouraged_notice': case 'ms_defaults_set': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] ); } elseif ( isset( $old[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::validate_bool( $old[ $key ] ); } break; case 'site_type': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->site_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'environment_type': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->environment_types, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'has_multiple_authors': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->has_multiple_authors_options, true ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'first_activated_on': case 'indexing_started': case 'activation_redirect_timestamp_free': case 'last_updated_on': $clean[ $key ] = false; if ( isset( $dirty[ $key ] ) ) { if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'first_activated_by': // A slight change from the other integer fields, as we want to allow '0' here, but don't want to have much impact elsewhere. $clean[ $key ] = false; if ( isset( $dirty[ $key ] ) ) { if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) !== false ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'tracking': $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : null ); break; case 'myyoast_oauth': case 'semrush_tokens': case 'custom_taxonomy_slugs': case 'wincher_tokens': case 'workouts_data': case 'configuration_finished_steps': case 'least_readability_ignore_list': case 'least_seo_score_ignore_list': case 'most_linked_ignore_list': case 'least_linked_ignore_list': case 'indexables_page_reading_list': case 'last_known_public_post_types': case 'last_known_public_taxonomies': case 'new_post_types': case 'new_taxonomies': case 'default_seo_title': case 'default_seo_meta_desc': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( ! is_array( $items ) ) { $items = json_decode( $dirty[ $key ], true ); } if ( is_array( $items ) ) { $clean[ $key ] = $dirty[ $key ]; } } break; case 'permalink_structure': case 'category_base_url': case 'tag_base_url': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = sanitize_option( $key, $dirty[ $key ] ); } break; case 'search_character_limit': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = (int) $dirty[ $key ]; } break; case 'import_cursors': case 'importing_completed': if ( isset( $dirty[ $key ] ) && is_array( $dirty[ $key ] ) ) { $clean[ $key ] = $dirty[ $key ]; } break; case 'last_known_no_unindexed': $clean[ $key ] = $old[ $key ]; if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( is_array( $items ) ) { foreach ( $items as $item_key => $item ) { if ( ! is_string( $item_key ) || ! is_numeric( $item ) ) { unset( $items[ $item_key ] ); } } $clean[ $key ] = $items; } } break; /* * Boolean (checkbox) fields. * * Covers: * 'disableadvanced_meta' * 'enable_headless_rest_endpoints' * 'yoast_tracking' * 'dynamic_permalinks' * 'indexing_first_time' * 'first_time_install' * 'remove_feed_global' * 'remove_feed_global_comments' * 'remove_feed_post_comments' * 'remove_feed_authors' * 'remove_feed_categories' * 'remove_feed_tags' * 'remove_feed_custom_taxonomies' * 'remove_feed_post_types' * 'remove_feed_search' * 'remove_atom_rdf_feeds' * 'remove_shortlinks' * 'remove_rest_api_links' * 'remove_rsd_wlw_links' * 'remove_oembed_links' * 'remove_generator' * 'remove_emoji_scripts' * 'remove_powered_by_header' * 'remove_pingback_header' * 'clean_campaign_tracking_urls' * 'clean_permalinks' * 'clean_permalinks_extra_variables' * 'search_cleanup' * 'search_cleanup_emoji' * 'search_cleanup_patterns' * 'deny_wp_json_crawling' * 'deny_adsbot_crawling' * 'deny_ccbot_crawling' * 'deny_google_extended_crawling' * 'deny_gptbot_crawling' * 'redirect_search_pretty_urls' * 'should_redirect_after_install_free' * 'show_new_content_type_notification' * 'site_kit_configuration_permanently_dismissed', * 'site_kit_connected', * 'google_site_kit_feature_enabled', * 'enable_llms_txt', * 'enable_schema_aggregation_endpoint' * 'enable_task_list', * 'enable_schema', * and most of the feature variables. */ default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } /** * Verifies that the feature variables are turned off if the network is configured so. * * @param mixed $options Value of the option to be returned. Typically an array. * * @return mixed Filtered $options value. */ public function verify_features_against_network( $options = [] ) { if ( ! is_array( $options ) || empty( $options ) ) { return $options; } // For the feature variables, set their values to off in case they are disabled. $feature_vars = [ 'disableadvanced_meta' => false, 'ryte_indexability' => false, 'content_analysis_active' => false, 'keyword_analysis_active' => false, 'inclusive_language_analysis_active' => false, 'enable_admin_bar_menu' => false, 'enable_cornerstone_content' => false, 'enable_xml_sitemap' => false, 'enable_text_link_counter' => false, 'enable_metabox_insights' => false, 'enable_link_suggestions' => false, 'enable_headless_rest_endpoints' => false, 'tracking' => false, 'enable_enhanced_slack_sharing' => false, 'semrush_integration_active' => false, 'wincher_integration_active' => false, 'remove_feed_global' => false, 'remove_feed_global_comments' => false, 'remove_feed_post_comments' => false, 'enable_index_now' => false, 'enable_ai_generator' => false, 'remove_feed_authors' => false, 'remove_feed_categories' => false, 'remove_feed_tags' => false, 'remove_feed_custom_taxonomies' => false, 'remove_feed_post_types' => false, 'remove_feed_search' => false, 'remove_atom_rdf_feeds' => false, 'remove_shortlinks' => false, 'remove_rest_api_links' => false, 'remove_rsd_wlw_links' => false, 'remove_oembed_links' => false, 'remove_generator' => false, 'remove_emoji_scripts' => false, 'remove_powered_by_header' => false, 'remove_pingback_header' => false, 'clean_campaign_tracking_urls' => false, 'clean_permalinks' => false, 'search_cleanup' => false, 'search_cleanup_emoji' => false, 'search_cleanup_patterns' => false, 'redirect_search_pretty_urls' => false, 'algolia_integration_active' => false, 'google_site_kit_feature_enabled' => false, 'enable_llms_txt' => false, 'enable_task_list' => false, 'enable_schema_aggregation_endpoint' => false, 'enable_schema' => false, ]; // We can reuse this logic from the base class with the above defaults to parse with the correct feature values. $options = $this->prevent_disabled_options_update( $options, $feature_vars ); return $options; } /** * Gets the filter hook name and callback for adjusting the retrieved option value * against the network-allowed features. * * @return array Array where the first item is the hook name, the second is the hook callback, * and the third is the hook priority. */ protected function get_verify_features_option_filter_hook() { return [ "option_{$this->option_name}", [ $this, 'verify_features_against_network' ], 11, ]; } /** * Gets the filter hook name and callback for adjusting the default option value against the network-allowed features. * * @return array Array where the first item is the hook name, the second is the hook callback, * and the third is the hook priority. */ protected function get_verify_features_default_option_filter_hook() { return [ "default_option_{$this->option_name}", [ $this, 'verify_features_against_network' ], 11, ]; } /** * Clean a given option value. * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { // Deal with value change from text string to boolean. $value_change = [ 'ignore_search_engines_discouraged_notice', ]; $target_values = [ 'ignore', 'done', ]; foreach ( $value_change as $key ) { if ( isset( $option_value[ $key ] ) && in_array( $option_value[ $key ], $target_values, true ) ) { $option_value[ $key ] = true; } } return $option_value; } } inc/options/class-wpseo-options.php 0000644 00000042147 15174712003 0013462 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Overall Option Management class. * * Instantiates all the options and offers a number of utility methods to work with the options. */ class WPSEO_Options { /** * The option values. * * @var array|null */ protected static $option_values = null; /** * Options this class uses. * * @var array Array format: (string) option_name => (string) name of concrete class for the option. */ public static $options = [ 'wpseo' => 'WPSEO_Option_Wpseo', 'wpseo_titles' => 'WPSEO_Option_Titles', 'wpseo_social' => 'WPSEO_Option_Social', 'wpseo_ms' => 'WPSEO_Option_MS', 'wpseo_taxonomy_meta' => 'WPSEO_Taxonomy_Meta', 'wpseo_llmstxt' => 'WPSEO_Option_Llmstxt', 'wpseo_tracking_only' => 'WPSEO_Option_Tracking_Only', ]; /** * Array of instantiated option objects. * * @var array */ protected static $option_instances = []; /** * Array with the option names. * * @var array */ protected static $option_names = []; /** * Instance of this class. * * @var WPSEO_Options */ protected static $instance; /** * Instantiate all the WPSEO option management classes. */ protected function __construct() { $this->register_hooks(); foreach ( static::$options as $option_class ) { static::register_option( call_user_func( [ $option_class, 'get_instance' ] ) ); } } /** * Register our hooks. * * @return void */ public function register_hooks() { add_action( 'registered_taxonomy', [ $this, 'clear_cache' ] ); add_action( 'unregistered_taxonomy', [ $this, 'clear_cache' ] ); add_action( 'registered_post_type', [ $this, 'clear_cache' ] ); add_action( 'unregistered_post_type', [ $this, 'clear_cache' ] ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( static::$instance instanceof self ) ) { static::$instance = new self(); } return static::$instance; } /** * Registers an option to the options list. * * @param WPSEO_Option $option_instance Instance of the option. * * @return void */ public static function register_option( WPSEO_Option $option_instance ) { $option_name = $option_instance->get_option_name(); if ( $option_instance->multisite_only && ! static::is_multisite() ) { unset( static::$options[ $option_name ], static::$option_names[ $option_name ] ); return; } $is_already_registered = array_key_exists( $option_name, static::$options ); if ( ! $is_already_registered ) { static::$options[ $option_name ] = get_class( $option_instance ); } if ( $option_instance->include_in_all === true ) { static::$option_names[ $option_name ] = $option_name; } static::$option_instances[ $option_name ] = $option_instance; if ( ! $is_already_registered ) { static::clear_cache(); } } /** * Get the group name of an option for use in the settings form. * * @param string $option_name The option for which you want to retrieve the option group name. * * @return string|bool */ public static function get_group_name( $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]->group_name; } return false; } /** * Get a specific default value for an option. * * @param string $option_name The option for which you want to retrieve a default. * @param string $key The key within the option who's default you want. * * @return mixed */ public static function get_default( $option_name, $key ) { if ( isset( static::$option_instances[ $option_name ] ) ) { $defaults = static::$option_instances[ $option_name ]->get_defaults(); if ( isset( $defaults[ $key ] ) ) { return $defaults[ $key ]; } } return null; } /** * Update a site_option. * * @param string $option_name The option name of the option to save. * @param mixed $value The new value for the option. * * @return bool */ public static function update_site_option( $option_name, $value ) { if ( is_multisite() && isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]->update_site_option( $value ); } return false; } /** * Get the instantiated option instance. * * @param string $option_name The option for which you want to retrieve the instance. * * @return object|bool */ public static function get_option_instance( $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { return static::$option_instances[ $option_name ]; } return false; } /** * Retrieve an array of the options which should be included in get_all() and reset(). * * @return array Array of option names. */ public static function get_option_names() { $option_names = array_values( static::$option_names ); if ( $option_names === [] ) { foreach ( static::$option_instances as $option_name => $option_object ) { if ( $option_object->include_in_all === true ) { $option_names[] = $option_name; } } } /** * Filter: wpseo_options - Allow developers to change the option name to include. * * @param array $option_names The option names to include in get_all and reset(). */ return apply_filters( 'wpseo_options', $option_names ); } /** * Retrieve all the options for the SEO plugin in one go. * * @param array<string> $specific_options The option groups of the option you want to get. * * @return array Array combining the values of all the options. */ public static function get_all( $specific_options = [] ) { $option_names = ( empty( $specific_options ) ) ? static::get_option_names() : $specific_options; static::$option_values = static::get_options( $option_names ); return static::$option_values; } /** * Retrieve one or more options for the SEO plugin. * * @param array $option_names An array of option names of the options you want to get. * * @return array Array combining the values of the requested options. */ public static function get_options( array $option_names ) { $options = []; $option_names = array_filter( $option_names, 'is_string' ); foreach ( $option_names as $option_name ) { if ( isset( static::$option_instances[ $option_name ] ) ) { $option = static::get_option( $option_name ); if ( $option !== null ) { $options = array_merge( $options, $option ); } } } return $options; } /** * Retrieve a single option for the SEO plugin. * * @param string $option_name The name of the option you want to get. * * @return array Array containing the requested option. */ public static function get_option( $option_name ) { $option = null; if ( is_string( $option_name ) && ! empty( $option_name ) ) { if ( isset( static::$option_instances[ $option_name ] ) ) { if ( static::$option_instances[ $option_name ]->multisite_only !== true ) { $option = get_option( $option_name ); } else { $option = get_site_option( $option_name ); } } } return $option; } /** * Retrieve a single field from any option for the SEO plugin. Keys are always unique. * * @param string $key The key it should return. * @param mixed $default_value The default value that should be returned if the key isn't set. * @param array<string> $option_groups The option groups to retrieve the option from. * * @return mixed Returns value if found, $default_value if not. */ public static function get( $key, $default_value = null, $option_groups = [] ) { if ( ! isset( static::$option_values[ $key ] ) ) { static::prime_cache( $option_groups ); } if ( isset( static::$option_values[ $key ] ) ) { return static::$option_values[ $key ]; } return $default_value; } /** * Resets the cache to null. * * @return void */ public static function clear_cache() { static::$option_values = null; } /** * Primes our cache. * * @param array<string> $option_groups The option groups to prime the cache with. * * @return void */ private static function prime_cache( $option_groups = [] ) { static::$option_values = static::get_all( $option_groups ); static::$option_values = static::add_ms_option( static::$option_values ); } /** * Retrieve a single field from an option for the SEO plugin. * * @param string $key The key to set. * @param mixed $value The value to set. * @param string $option_group The lookup table which represents the option_group where the key is stored. * * @return mixed|null Returns value if found, $default if not. */ public static function set( $key, $value, $option_group = '' ) { $lookup_table = static::get_lookup_table( $option_group ); if ( isset( $lookup_table[ $key ] ) ) { return static::save_option( $lookup_table[ $key ], $key, $value ); } $patterns = static::get_pattern_table(); foreach ( $patterns as $pattern => $option ) { if ( strpos( $key, $pattern ) === 0 ) { return static::save_option( $option, $key, $value ); } } static::$option_values[ $key ] = $value; } /** * Get an option only if it's been auto-loaded. * * @param string $option The option to retrieve. * @param mixed $default_value A default value to return. * * @return mixed */ public static function get_autoloaded_option( $option, $default_value = false ) { $value = wp_cache_get( $option, 'options' ); if ( $value === false ) { $passed_default = func_num_args() > 1; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default ); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option ); } /** * Run the clean up routine for one or all options. * * @param array|string|null $option_name Optional. the option you want to clean or an array of * option names for the options you want to clean. * If not set, all options will be cleaned. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * * @return void */ public static function clean_up( $option_name = null, $current_version = null ) { if ( isset( $option_name ) && is_string( $option_name ) && $option_name !== '' ) { if ( isset( static::$option_instances[ $option_name ] ) ) { static::$option_instances[ $option_name ]->clean( $current_version ); } } elseif ( isset( $option_name ) && is_array( $option_name ) && $option_name !== [] ) { foreach ( $option_name as $option ) { if ( isset( static::$option_instances[ $option ] ) ) { static::$option_instances[ $option ]->clean( $current_version ); } } unset( $option ); } else { foreach ( static::$option_instances as $instance ) { $instance->clean( $current_version ); } unset( $instance ); // If we've done a full clean-up, we can safely remove this really old option. delete_option( 'wpseo_indexation' ); } } /** * Check that all options exist in the database and add any which don't. * * @return void */ public static function ensure_options_exist() { foreach ( static::$option_instances as $instance ) { $instance->maybe_add_option(); } } /** * Initialize some options on first install/activate/reset. * * @return void */ public static function initialize() { /* Force WooThemes to use Yoast SEO data. */ if ( function_exists( 'woo_version_init' ) ) { update_option( 'seo_woo_use_third_party_data', 'true' ); } } /** * Reset all options to their default values and rerun some tests. * * @return void */ public static function reset() { if ( ! is_multisite() ) { $option_names = static::get_option_names(); if ( is_array( $option_names ) && $option_names !== [] ) { foreach ( $option_names as $option_name ) { delete_option( $option_name ); update_option( $option_name, get_option( $option_name ) ); } } unset( $option_names ); } else { // Reset MS blog based on network default blog setting. static::reset_ms_blog( get_current_blog_id() ); } static::initialize(); } /** * Initialize default values for a new multisite blog. * * @param bool $force_init Whether to always do the initialization routine (title/desc test). * * @return void */ public static function maybe_set_multisite_defaults( $force_init = false ) { $option = get_option( 'wpseo' ); if ( is_multisite() ) { if ( $option['ms_defaults_set'] === false ) { static::reset_ms_blog( get_current_blog_id() ); static::initialize(); } elseif ( $force_init === true ) { static::initialize(); } } } /** * Reset all options for a specific multisite blog to their default values based upon a * specified default blog if one was chosen on the network page or the plugin defaults if it was not. * * @param int|string $blog_id Blog id of the blog for which to reset the options. * * @return void */ public static function reset_ms_blog( $blog_id ) { if ( is_multisite() ) { $options = get_site_option( 'wpseo_ms' ); $option_names = static::get_option_names(); if ( is_array( $option_names ) && $option_names !== [] ) { $base_blog_id = $blog_id; if ( $options['defaultblog'] !== '' && $options['defaultblog'] !== 0 ) { $base_blog_id = $options['defaultblog']; } foreach ( $option_names as $option_name ) { delete_blog_option( $blog_id, $option_name ); $new_option = get_blog_option( $base_blog_id, $option_name ); /* Remove sensitive, theme dependent and site dependent info. */ if ( isset( static::$option_instances[ $option_name ] ) && static::$option_instances[ $option_name ]->ms_exclude !== [] ) { foreach ( static::$option_instances[ $option_name ]->ms_exclude as $key ) { unset( $new_option[ $key ] ); } } if ( $option_name === 'wpseo' ) { $new_option['ms_defaults_set'] = true; } update_blog_option( $blog_id, $option_name, $new_option ); } } } } /** * Saves the option to the database. * * @param string $wpseo_options_group_name The name for the wpseo option group in the database. * @param string $option_name The name for the option to set. * @param mixed $option_value The value for the option. * * @return bool Returns true if the option is successfully saved in the database. */ public static function save_option( $wpseo_options_group_name, $option_name, $option_value ) { $options = static::get_option( $wpseo_options_group_name ); $options[ $option_name ] = $option_value; if ( isset( static::$option_instances[ $wpseo_options_group_name ] ) && static::$option_instances[ $wpseo_options_group_name ]->multisite_only === true ) { static::update_site_option( $wpseo_options_group_name, $options ); } else { update_option( $wpseo_options_group_name, $options ); } // Check if everything got saved properly. $saved_option = static::get_option( $wpseo_options_group_name ); // Clear our cache. static::clear_cache(); return $saved_option[ $option_name ] === $options[ $option_name ]; } /** * Adds the multisite options to the option stack if relevant. * * @param array $option The currently present options settings. * * @return array Options possibly including multisite. */ protected static function add_ms_option( $option ) { if ( ! is_multisite() ) { return $option; } $ms_option = static::get_option( 'wpseo_ms' ); if ( $ms_option === null ) { return $option; } return array_merge( $option, $ms_option ); } /** * Checks if installation is multisite. * * @return bool True when is multisite. */ protected static function is_multisite() { static $is_multisite; $is_multisite ??= is_multisite(); return $is_multisite; } /** * Retrieves a lookup table to find in which option_group a key is stored. * * @param string $option_group The option_group where the key is stored. * * @return array The lookup table. */ private static function get_lookup_table( $option_group = '' ) { $lookup_table = []; $option_groups = ( $option_group === '' ) ? static::$options : [ $option_group => static::$options[ $option_group ] ]; foreach ( array_keys( $option_groups ) as $option_name ) { $full_option = static::get_option( $option_name ); foreach ( $full_option as $key => $value ) { $lookup_table[ $key ] = $option_name; } } return $lookup_table; } /** * Retrieves a lookup table to find in which option_group a key is stored. * * @return array The lookup table. */ private static function get_pattern_table() { $pattern_table = []; foreach ( static::$options as $option_name => $option_class ) { $instance = call_user_func( [ $option_class, 'get_instance' ] ); foreach ( $instance->get_patterns() as $key ) { $pattern_table[ $key ] = $option_name; } } return $pattern_table; } } inc/options/class-wpseo-taxonomy-meta.php 0000644 00000042543 15174712003 0014571 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_taxonomy_meta. */ class WPSEO_Taxonomy_Meta extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_taxonomy_meta'; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * @var bool */ public $include_in_all = false; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * {@internal Important: in contrast to most defaults, the below array format is * very bare. The real option is in the format [taxonomy_name][term_id][...] * where [...] is any of the $defaults_per_term options shown below. * This is of course taken into account in the below methods.}} * * @var array */ protected $defaults = []; /** * Option name - same as $option_name property, but now also available to static methods. * * @var string */ public static $name; /** * Array of defaults for individual taxonomy meta entries. * * @var array */ public static $defaults_per_term = [ 'wpseo_title' => '', 'wpseo_desc' => '', 'wpseo_canonical' => '', 'wpseo_bctitle' => '', 'wpseo_noindex' => 'default', 'wpseo_focuskw' => '', 'wpseo_linkdex' => '', 'wpseo_content_score' => '', 'wpseo_inclusive_language_score' => '', 'wpseo_focuskeywords' => '[]', 'wpseo_keywordsynonyms' => '[]', 'wpseo_is_cornerstone' => '0', // Social fields. 'wpseo_opengraph-title' => '', 'wpseo_opengraph-description' => '', 'wpseo_opengraph-image' => '', 'wpseo_opengraph-image-id' => '', 'wpseo_twitter-title' => '', 'wpseo_twitter-description' => '', 'wpseo_twitter-image' => '', 'wpseo_twitter-image-id' => '', ]; /** * Available index options. * * Used for form generation and input validation. * * {@internal Labels (translation) added on admin_init via WPSEO_Taxonomy::translate_meta_options().}} * * @var array */ public static $no_index_options = [ 'default' => '', 'index' => '', 'noindex' => '', ]; /** * Add the actions and filters for the option. * * @todo [JRF => testers] Check if the extra actions below would run into problems if an option * is updated early on and if so, change the call to schedule these for a later action on add/update * instead of running them straight away. */ protected function __construct() { parent::__construct(); self::$name = $this->option_name; } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); self::$name = self::$instance->option_name; } return self::$instance; } /** * Add extra default options received from a filter. * * @return void */ public function enrich_defaults() { $extra_defaults_per_term = apply_filters( 'wpseo_add_extra_taxmeta_term_defaults', [] ); if ( is_array( $extra_defaults_per_term ) ) { self::$defaults_per_term = array_merge( $extra_defaults_per_term, self::$defaults_per_term ); } } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { /* * Prevent complete validation (which can be expensive when there are lots of terms) * if only one item has changed and has already been validated. */ if ( isset( $dirty['wpseo_already_validated'] ) && $dirty['wpseo_already_validated'] === true ) { unset( $dirty['wpseo_already_validated'] ); return $dirty; } foreach ( $dirty as $taxonomy => $terms ) { /* Don't validate taxonomy - may not be registered yet and we don't want to remove valid ones. */ if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term_id => $meta_data ) { /* Only validate term if the taxonomy exists. */ if ( taxonomy_exists( $taxonomy ) && get_term_by( 'id', $term_id, $taxonomy ) === false ) { /* Is this term id a special case ? */ if ( has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) { $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id ); } continue; } if ( is_array( $meta_data ) && $meta_data !== [] ) { /* Validate meta data. */ $old_meta = self::get_term_meta( $term_id, $taxonomy ); $meta_data = self::validate_term_meta_data( $meta_data, $old_meta ); if ( $meta_data !== [] ) { $clean[ $taxonomy ][ $term_id ] = $meta_data; } } // Deal with special cases (for when taxonomy doesn't exist yet). if ( ! isset( $clean[ $taxonomy ][ $term_id ] ) && has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) { $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id ); } } } } return $clean; } /** * Validate the meta data for one individual term and removes default values (no need to save those). * * @param array $meta_data New values. * @param array $old_meta The original values. * * @return array Validated and filtered value. */ public static function validate_term_meta_data( $meta_data, $old_meta ) { $clean = self::$defaults_per_term; $meta_data = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $meta_data ); if ( ! is_array( $meta_data ) || $meta_data === [] ) { return $clean; } foreach ( $clean as $key => $value ) { switch ( $key ) { case 'wpseo_noindex': if ( isset( $meta_data[ $key ] ) ) { if ( isset( self::$no_index_options[ $meta_data[ $key ] ] ) ) { $clean[ $key ] = $meta_data[ $key ]; } } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_canonical': if ( isset( $meta_data[ $key ] ) && $meta_data[ $key ] !== '' ) { $url = WPSEO_Utils::sanitize_url( $meta_data[ $key ] ); if ( $url !== '' ) { $clean[ $key ] = $url; } unset( $url ); } break; case 'wpseo_bctitle': if ( isset( $meta_data[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_keywordsynonyms': if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation. $input = json_decode( $meta_data[ $key ], true ); $sanitized = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $input ); $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_focuskeywords': if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation. $input = json_decode( $meta_data[ $key ], true ); // This data has two known keys: `keyword` and `score`. $sanitized = []; foreach ( $input as $entry ) { $sanitized[] = [ 'keyword' => WPSEO_Utils::sanitize_text_field( $entry['keyword'] ), 'score' => WPSEO_Utils::sanitize_text_field( $entry['score'] ), ]; } $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized ); } elseif ( isset( $old_meta[ $key ] ) ) { // Retain old value if field currently not in use. $clean[ $key ] = $old_meta[ $key ]; } break; case 'wpseo_focuskw': case 'wpseo_title': case 'wpseo_desc': case 'wpseo_linkdex': default: if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] ); } if ( $key === 'wpseo_focuskw' ) { $search = [ '<', '>', '`', '<', '>', '`', ]; $clean[ $key ] = str_replace( $search, '', $clean[ $key ] ); } break; } $clean[ $key ] = apply_filters( 'wpseo_sanitize_tax_meta_' . $key, $clean[ $key ], ( $meta_data[ $key ] ?? null ), ( $old_meta[ $key ] ?? null ) ); } // Only save the non-default values. return array_diff_assoc( $clean, self::$defaults_per_term ); } /** * Clean a given option value. * - Convert old option values to new * - Fixes strings which were escaped (should have been sanitized - escaping is for output) * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { /* Clean up old values and remove empty arrays. */ if ( is_array( $option_value ) && $option_value !== [] ) { foreach ( $option_value as $taxonomy => $terms ) { if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term_id => $meta_data ) { if ( ! is_array( $meta_data ) || $meta_data === [] ) { // Remove empty term arrays. unset( $option_value[ $taxonomy ][ $term_id ] ); } else { foreach ( $meta_data as $key => $value ) { switch ( $key ) { case 'noindex': if ( $value === 'on' ) { // Convert 'on' to 'noindex'. $option_value[ $taxonomy ][ $term_id ][ $key ] = 'noindex'; } break; case 'canonical': case 'wpseo_bctitle': case 'wpseo_title': case 'wpseo_desc': case 'wpseo_linkdex': // @todo [JRF => whomever] Needs checking, I don't have example data [JRF]. if ( $value !== '' ) { // Fix incorrectly saved (encoded) canonical urls and texts. $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( stripslashes( $value ), ENT_QUOTES ); } break; default: // @todo [JRF => whomever] Needs checking, I don't have example data [JRF]. if ( $value !== '' ) { // Fix incorrectly saved (escaped) text strings. $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( $value, ENT_QUOTES ); } break; } } } } } else { // Remove empty taxonomy arrays. unset( $option_value[ $taxonomy ] ); } } } return $option_value; } /** * Retrieve a taxonomy term's meta value(s). * * @param mixed $term Term to get the meta value for * either (string) term name, (int) term id or (object) term. * @param string $taxonomy Name of the taxonomy to which the term is attached. * @param string|null $meta Optional. Meta value to get (without prefix). * * @return mixed Value for the $meta if one is given, might be the default. * If no meta is given, an array of all the meta data for the term. * False if the term does not exist or the $meta provided is invalid. */ public static function get_term_meta( $term, $taxonomy, $meta = null ) { /* Figure out the term id. */ if ( is_int( $term ) ) { $term = get_term_by( 'id', $term, $taxonomy ); } elseif ( is_string( $term ) ) { $term = get_term_by( 'slug', $term, $taxonomy ); } if ( is_object( $term ) && isset( $term->term_id ) ) { $term_id = $term->term_id; } else { return false; } $tax_meta = self::get_term_tax_meta( $term_id, $taxonomy ); /* * Either return the complete array or a single value from it or false if the value does not exist * (shouldn't happen after merge with defaults, indicates typo in request). */ if ( ! isset( $meta ) ) { return $tax_meta; } if ( isset( $tax_meta[ 'wpseo_' . $meta ] ) ) { return $tax_meta[ 'wpseo_' . $meta ]; } return false; } /** * Get the current queried object and return the meta value. * * @param string $meta The meta field that is needed. * * @return mixed */ public static function get_meta_without_term( $meta ) { $term = $GLOBALS['wp_query']->get_queried_object(); if ( ! $term || empty( $term->taxonomy ) ) { return false; } return self::get_term_meta( $term, $term->taxonomy, $meta ); } /** * Saving the values for the given term_id. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param array $meta_values The values that will be saved. * * @return void */ public static function set_values( $term_id, $taxonomy, array $meta_values ) { /* Validate the post values */ $old = self::get_term_meta( $term_id, $taxonomy ); $clean = self::validate_term_meta_data( $meta_values, $old ); self::save_clean_values( $term_id, $taxonomy, $clean ); } /** * Setting a single value to the term meta. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param string $meta_key The target meta key to store the value in. * @param string $meta_value The value of the target meta key. * * @return void */ public static function set_value( $term_id, $taxonomy, $meta_key, $meta_value ) { if ( substr( strtolower( $meta_key ), 0, 6 ) !== 'wpseo_' ) { $meta_key = 'wpseo_' . $meta_key; } self::set_values( $term_id, $taxonomy, [ $meta_key => $meta_value ] ); } /** * Find the keyword usages in the metas for the taxonomies/terms. * * @param string $keyword The keyword to look for. * @param string $current_term_id The current term id. * @param string $current_taxonomy The current taxonomy name. * * @return array */ public static function get_keyword_usage( $keyword, $current_term_id, $current_taxonomy ) { $tax_meta = self::get_tax_meta(); $found = []; // @todo Check for terms of all taxonomies, not only the current taxonomy. foreach ( $tax_meta as $taxonomy_name => $terms ) { foreach ( $terms as $term_id => $meta_values ) { $is_current = ( $current_taxonomy === $taxonomy_name && (string) $current_term_id === (string) $term_id ); if ( ! $is_current && ! empty( $meta_values['wpseo_focuskw'] ) && $meta_values['wpseo_focuskw'] === $keyword ) { $found[] = $term_id; } } } return [ $keyword => $found ]; } /** * Saving the values for the given term_id. * * @param int $term_id ID of the term to save data for. * @param string $taxonomy The taxonomy the term belongs to. * @param array $clean Array with clean values. * * @return void */ private static function save_clean_values( $term_id, $taxonomy, array $clean ) { $tax_meta = self::get_tax_meta(); /* Add/remove the result to/from the original option value. */ if ( $clean !== [] ) { $tax_meta[ $taxonomy ][ $term_id ] = $clean; } else { unset( $tax_meta[ $taxonomy ][ $term_id ] ); if ( isset( $tax_meta[ $taxonomy ] ) && $tax_meta[ $taxonomy ] === [] ) { unset( $tax_meta[ $taxonomy ] ); } } // Prevent complete array validation. $tax_meta['wpseo_already_validated'] = true; self::save_tax_meta( $tax_meta ); } /** * Getting the meta from the options. * * @return array|void */ private static function get_tax_meta() { return get_option( self::$name ); } /** * Saving the tax meta values to the database. * * @param array $tax_meta Array with the meta values for taxonomy. * * @return void */ private static function save_tax_meta( $tax_meta ) { update_option( self::$name, $tax_meta ); } /** * Getting the taxonomy meta for the given term_id and taxonomy. * * @param int $term_id The id of the term. * @param string $taxonomy Name of the taxonomy to which the term is attached. * * @return array */ private static function get_term_tax_meta( $term_id, $taxonomy ) { $tax_meta = self::get_tax_meta(); /* If we have data for the term, merge with defaults for complete array, otherwise set defaults. */ if ( isset( $tax_meta[ $taxonomy ][ $term_id ] ) ) { return array_merge( self::$defaults_per_term, $tax_meta[ $taxonomy ][ $term_id ] ); } return self::$defaults_per_term; } } inc/options/class-wpseo-option-social.php 0000644 00000022153 15174712003 0014542 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_social. */ class WPSEO_Option_Social extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_social'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults = [ // Form fields. 'facebook_site' => '', // Text field. 'instagram_url' => '', 'linkedin_url' => '', 'myspace_url' => '', 'og_default_image' => '', // Text field. 'og_default_image_id' => '', 'og_frontpage_title' => '', // Text field. 'og_frontpage_desc' => '', // Text field. 'og_frontpage_image' => '', // Text field. 'og_frontpage_image_id' => '', 'opengraph' => true, 'pinterest_url' => '', 'pinterestverify' => '', 'twitter' => true, 'twitter_site' => '', // Text field. 'twitter_card_type' => 'summary_large_image', 'youtube_url' => '', 'wikipedia_url' => '', 'other_social_urls' => [], 'mastodon_url' => '', ]; /** * Array of sub-options which should not be overloaded with multi-site defaults. * * @var array */ public $ms_exclude = [ /* Privacy. */ 'pinterestverify', ]; /** * Array of allowed twitter card types. * * While we only have the options summary and summary_large_image in the * interface now, we might change that at some point. * * {@internal Uncomment any of these to allow them in validation *and* automatically * add them as a choice in the options page.}} * * @var array */ public static $twitter_card_types = [ 'summary_large_image' => '', // 'summary' => '', // 'photo' => '', // 'gallery' => '', // 'app' => '', // 'player' => '', // 'product' => '', ]; /** * Add the actions and filters for the option. */ protected function __construct() { parent::__construct(); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Translate/set strings used in the option defaults. * * @return void */ public function translate_defaults() { self::$twitter_card_types['summary_large_image'] = 'Summary with large image'; } /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { /* Text fields. */ case 'og_frontpage_desc': case 'og_frontpage_title': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] ); } break; case 'og_default_image_id': case 'og_frontpage_image_id': if ( isset( $dirty[ $key ] ) ) { $clean[ $key ] = (int) $dirty[ $key ]; if ( $dirty[ $key ] === '' ) { $clean[ $key ] = $dirty[ $key ]; } } break; /* URL text fields - no ftp allowed. */ case 'facebook_site': case 'instagram_url': case 'linkedin_url': case 'myspace_url': case 'pinterest_url': case 'og_default_image': case 'og_frontpage_image': case 'youtube_url': case 'wikipedia_url': case 'mastodon_url': $this->validate_url( $key, $dirty, $old, $clean ); break; case 'pinterestverify': $this->validate_verification_string( $key, $dirty, $old, $clean ); break; /* Twitter user name. */ case 'twitter_site': if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $twitter_id = $this->validate_twitter_id( $dirty[ $key ] ); if ( $twitter_id ) { $clean[ $key ] = $twitter_id; } elseif ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $twitter_id = sanitize_text_field( ltrim( $old[ $key ], '@' ) ); if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) { $clean[ $key ] = $twitter_id; } } unset( $twitter_id ); Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] ); } break; case 'twitter_card_type': if ( isset( $dirty[ $key ], self::$twitter_card_types[ $dirty[ $key ] ] ) && $dirty[ $key ] !== '' ) { $clean[ $key ] = $dirty[ $key ]; } break; /* Boolean fields. */ case 'opengraph': case 'twitter': $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; /* Array fields. */ case 'other_social_urls': if ( isset( $dirty[ $key ] ) ) { $items = $dirty[ $key ]; if ( ! is_array( $items ) ) { $items = json_decode( $dirty[ $key ], true ); } if ( is_array( $items ) ) { foreach ( $items as $item_key => $item ) { $validated_url = $this->validate_social_url( $item ); if ( $validated_url === false ) { // Restore the previous URL values, if any. $old_urls = ( isset( $old[ $key ] ) ) ? $old[ $key ] : []; foreach ( $old_urls as $old_item_key => $old_url ) { if ( $old_url !== '' ) { $url = WPSEO_Utils::sanitize_url( $old_url ); if ( $url !== '' ) { $clean[ $key ][ $old_item_key ] = $url; } } } break; } // The URL format is valid, let's sanitize it. $url = WPSEO_Utils::sanitize_url( $validated_url ); if ( $url !== '' ) { $clean[ $key ][ $item_key ] = $url; } } } } break; } } return $clean; } /** * Validates a social URL. * * @param string $url The url to be validated. * * @return string|false The validated URL or false if the URL is not valid. */ public function validate_social_url( $url ) { $validated_url = filter_var( WPSEO_Utils::sanitize_url( trim( $url ) ), FILTER_VALIDATE_URL ); return $validated_url; } /** * Validates a twitter id. * * @param string $twitter_id The twitter id to be validated. * @param bool $strip_at_sign Whether or not to strip the `@` sign. * * @return string|false The validated twitter id or false if it is not valid. */ public function validate_twitter_id( $twitter_id, $strip_at_sign = true ) { $twitter_id = ( $strip_at_sign ) ? sanitize_text_field( ltrim( $twitter_id, '@' ) ) : sanitize_text_field( $twitter_id ); /* * From the Twitter documentation about twitter screen names: * Typically a maximum of 15 characters long, but some historical accounts * may exist with longer names. * A username can only contain alphanumeric characters (letters A-Z, numbers 0-9) * with the exception of underscores. * * @link https://support.twitter.com/articles/101299-why-can-t-i-register-certain-usernames */ if ( preg_match( '`^[A-Za-z0-9_]{1,25}$`', $twitter_id ) ) { return $twitter_id; } if ( preg_match( '`^http(?:s)?://(?:www\.)?(?:twitter|x)\.com/(?P<handle>[A-Za-z0-9_]{1,25})/?$`', $twitter_id, $matches ) ) { return $matches['handle']; } return false; } /** * Clean a given option value. * * @param array $option_value Old (not merged with defaults or filtered) option value to * clean according to the rules for this option. * @param string|null $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to have * access to the real old values, in contrast to the saved ones. * * @return array Cleaned option. */ protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) { /* Move options from very old option to this one. */ $old_option = null; if ( isset( $all_old_option_values ) ) { // Ok, we have an import. if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) { $old_option = $all_old_option_values['wpseo_indexation']; } } else { $old_option = get_option( 'wpseo_indexation' ); } if ( is_array( $old_option ) && $old_option !== [] ) { $move = [ 'opengraph', ]; foreach ( $move as $key ) { if ( isset( $old_option[ $key ] ) && ! isset( $option_value[ $key ] ) ) { $option_value[ $key ] = $old_option[ $key ]; } } unset( $move, $key ); } unset( $old_option ); return $option_value; } } inc/options/class-wpseo-option-tracking-only.php 0000644 00000004444 15174712003 0016054 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Option: wpseo_tracking_only. * * For stuff that their only purpose is tracking. */ class WPSEO_Option_Tracking_Only extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_tracking_only'; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array<string, int|string|array<int>> */ protected $defaults = [ 'task_list_first_opened_on' => '', 'task_first_actioned_on' => '', 'frontend_inspector_first_actioned_on' => '', ]; /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * All concrete classes must contain a validate_option() method which validates all * values within the option. * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Needed because the function is called with the parameter $old. * * @param array<string, string> $dirty New value for the option. * @param array<string, string> $clean Clean value for the option, normally the defaults. * @param array<string, string> $old Old value of the option. * * @return array<string, string> The clean option with the saved value. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'task_list_first_opened_on': case 'task_first_actioned_on': case 'frontend_inspector_first_actioned_on': // These should be set only once and never changed again (unless completely reset to default). if ( isset( $dirty[ $key ] ) && $old[ $key ] === $this->get_defaults()[ $key ] ) { // Allow setting it for the first time. $clean[ $key ] = sanitize_text_field( $dirty[ $key ] ); } elseif ( isset( $dirty[ $key ] ) && $dirty[ $key ] === $this->get_defaults()[ $key ] ) { // Allow resetting to default. $clean[ $key ] = $dirty[ $key ]; } else { // Otherwise keep old value. $clean[ $key ] = $old[ $key ]; } break; } } return $clean; } } inc/options/class-wpseo-option-ms.php 0000644 00000022351 15174712003 0013707 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals\Options */ /** * Site option for Multisite installs only * * Overloads a number of methods of the abstract class to ensure the use of the correct site_option * WP functions. */ class WPSEO_Option_MS extends WPSEO_Option { /** * Option name. * * @var string */ public $option_name = 'wpseo_ms'; /** * Option group name for use in settings forms. * * @var string */ public $group_name = 'yoast_wpseo_multisite_options'; /** * Whether to include the option in the return for WPSEO_Options::get_all(). * * @var bool */ public $include_in_all = false; /** * Whether this option is only for when the install is multisite. * * @var bool */ public $multisite_only = true; /** * Array of defaults for the option. * * Shouldn't be requested directly, use $this->get_defaults(); * * @var array */ protected $defaults = []; /** * Available options for the 'access' setting. Used for input validation. * * {@internal Important: Make sure the options added to the array here are in line * with the keys for the options set for the select box in the * admin/pages/network.php file.}} * * @var array */ public static $allowed_access_options = [ 'admin', 'superadmin', ]; /** * Get the singleton instance of this class. * * @return object */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Only run parent constructor in multisite context. */ public function __construct() { $allow_prefix = self::ALLOW_KEY_PREFIX; $this->defaults = [ 'access' => 'admin', 'defaultblog' => '', // Numeric blog ID or empty. "{$allow_prefix}disableadvanced_meta" => true, "{$allow_prefix}ryte_indexability" => false, "{$allow_prefix}content_analysis_active" => true, "{$allow_prefix}keyword_analysis_active" => true, "{$allow_prefix}inclusive_language_analysis_active" => true, "{$allow_prefix}enable_admin_bar_menu" => true, "{$allow_prefix}enable_cornerstone_content" => true, "{$allow_prefix}enable_xml_sitemap" => true, "{$allow_prefix}enable_text_link_counter" => true, "{$allow_prefix}enable_headless_rest_endpoints" => true, "{$allow_prefix}enable_metabox_insights" => true, "{$allow_prefix}enable_link_suggestions" => true, "{$allow_prefix}tracking" => true, "{$allow_prefix}enable_enhanced_slack_sharing" => true, "{$allow_prefix}semrush_integration_active" => true, "{$allow_prefix}wincher_integration_active" => false, "{$allow_prefix}remove_feed_global" => true, "{$allow_prefix}remove_feed_global_comments" => true, "{$allow_prefix}remove_feed_post_comments" => true, "{$allow_prefix}enable_index_now" => true, "{$allow_prefix}enable_ai_generator" => true, "{$allow_prefix}remove_feed_authors" => true, "{$allow_prefix}remove_feed_categories" => true, "{$allow_prefix}remove_feed_tags" => true, "{$allow_prefix}remove_feed_custom_taxonomies" => true, "{$allow_prefix}remove_feed_post_types" => true, "{$allow_prefix}remove_feed_search" => true, "{$allow_prefix}remove_atom_rdf_feeds" => true, "{$allow_prefix}remove_shortlinks" => true, "{$allow_prefix}remove_rest_api_links" => true, "{$allow_prefix}remove_rsd_wlw_links" => true, "{$allow_prefix}remove_oembed_links" => true, "{$allow_prefix}remove_generator" => true, "{$allow_prefix}remove_emoji_scripts" => true, "{$allow_prefix}remove_powered_by_header" => true, "{$allow_prefix}remove_pingback_header" => true, "{$allow_prefix}clean_campaign_tracking_urls" => true, "{$allow_prefix}clean_permalinks" => true, "{$allow_prefix}search_cleanup" => true, "{$allow_prefix}search_cleanup_emoji" => true, "{$allow_prefix}search_cleanup_patterns" => true, "{$allow_prefix}redirect_search_pretty_urls" => true, "{$allow_prefix}algolia_integration_active" => true, ]; if ( is_multisite() ) { parent::__construct(); add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] ); } } /** * Add filters to make sure that the option default is returned if the option is not set * * @return void */ public function add_default_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) { add_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { remove_filter( 'default_site_option_' . $this->option_name, [ $this, 'get_defaults' ] ); } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) { add_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { remove_filter( 'site_option_' . $this->option_name, [ $this, 'get_option' ] ); } /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages *********** */ /** * Validate the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. * * @return array Validated clean value for the option to be saved to the database. */ protected function validate_option( $dirty, $clean, $old ) { foreach ( $clean as $key => $value ) { switch ( $key ) { case 'access': if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], self::$allowed_access_options, true ) ) { $clean[ $key ] = $dirty[ $key ]; } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. /* translators: %1$s expands to the option name and %2$sexpands to Yoast SEO */ sprintf( __( '%1$s is not a valid choice for who should be allowed access to the %2$s settings. Value reset to the default.', 'wordpress-seo' ), esc_html( sanitize_text_field( $dirty[ $key ] ) ), 'Yoast SEO' ), // The error message. 'error', // Message type. ); } break; case 'defaultblog': if ( isset( $dirty[ $key ] ) && ( $dirty[ $key ] !== '' && $dirty[ $key ] !== '-' ) ) { $int = WPSEO_Utils::validate_int( $dirty[ $key ] ); if ( $int !== false && $int > 0 ) { // Check if a valid blog number has been received. $exists = get_blog_details( $int, false ); if ( $exists && $exists->deleted === '0' ) { $clean[ $key ] = $int; } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' ) . '<br>' . sprintf( /* translators: %s is the ID number of a blog. */ esc_html__( 'This must be an existing blog. Blog %s does not exist or has been marked as deleted.', 'wordpress-seo' ), '<strong>' . esc_html( sanitize_text_field( $dirty[ $key ] ) ) . '</strong>', ), // The error message. 'error', // Message type. ); } unset( $exists ); } elseif ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. esc_html__( 'The default blog setting must be the numeric blog id of the blog you want to use as default.', 'wordpress-seo' ) . '<br>' . esc_html__( 'No numeric value was received.', 'wordpress-seo' ), // The error message. 'error', // Message type. ); } unset( $int ); } break; default: $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false ); break; } } return $clean; } } inc/date-helper.php 0000644 00000003330 15174712003 0010217 0 ustar 00 <?php /** * Date helper class. * * @package WPSEO\Internals */ /** * Class WPSEO_Date_Helper * * Note: Move this class with namespace to the src/helpers directory and add a class_alias for BC. */ class WPSEO_Date_Helper { /** * Formats a given date in UTC TimeZone format. * * @param string $date String representing the date / time. * @param string $format The format that the passed date should be in. * * @return string The formatted date. */ public function format( $date, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format( $date, $format ); } /** * Formats the given timestamp to the needed format. * * @param int $timestamp The timestamp to use for the formatting. * @param string $format The format that the passed date should be in. * * @return string The formatted date. */ public function format_timestamp( $timestamp, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format_timestamp( $timestamp, $format ); } /** * Formats a given date in UTC TimeZone format and translate it to the set language. * * @param string $date String representing the date / time. * @param string $format The format that the passed date should be in. * * @return string The formatted and translated date. */ public function format_translated( $date, $format = DATE_W3C ) { return YoastSEO()->helpers->date->format_translated( $date, $format ); } /** * Check if a string is a valid datetime. * * @param string $datetime String input to check as valid input for DateTime class. * * @return bool True when datatime is valid. */ public function is_valid_datetime( $datetime ) { return YoastSEO()->helpers->date->is_valid_datetime( $datetime ); } } inc/class-upgrade-history.php 0000644 00000006056 15174712003 0012266 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internal */ /** * This class handles storing the current options for future reference. * * This should only be used during an upgrade routine. */ class WPSEO_Upgrade_History { /** * Option to use to store/retrieve data from. * * @var string */ protected $option_name = 'wpseo_upgrade_history'; /** * WPSEO_Upgrade_History constructor. * * @param string|null $option_name Optional. Custom option to use to store/retrieve history from. */ public function __construct( $option_name = null ) { if ( $option_name !== null ) { $this->option_name = $option_name; } } /** * Retrieves the content of the history items currently stored. * * @return array<array<string>> The contents of the history option. */ public function get() { $data = get_option( $this->get_option_name(), [] ); if ( ! is_array( $data ) ) { return []; } return $data; } /** * Adds a new history entry in the storage. * * @param string $old_version The version we are upgrading from. * @param string $new_version The version we are upgrading to. * @param array<string> $option_names The options that need to be stored. * * @return void */ public function add( $old_version, $new_version, array $option_names ) { $option_data = []; if ( $option_names !== [] ) { $option_data = $this->get_options_data( $option_names ); } // Retrieve current history. $data = $this->get(); // Add new entry. $data[ time() ] = [ 'options' => $option_data, 'old_version' => $old_version, 'new_version' => $new_version, ]; // Store the data. $this->set( $data ); } /** * Retrieves the data for the specified option names from the database. * * @param array<string> $option_names The option names to retrieve. * * @return array<int|string, array<string|int|bool|float>> The retrieved data. */ protected function get_options_data( array $option_names ) { $wpdb = $this->get_wpdb(); $results = $wpdb->get_results( $wpdb->prepare( ' SELECT %i, %i FROM ' . $wpdb->options . ' WHERE %i IN ( ' . implode( ',', array_fill( 0, count( $option_names ), '%s' ) ) . ' ) ', array_merge( [ 'option_value', 'option_name', 'option_name' ], $option_names ), ), ARRAY_A, ); $data = []; foreach ( $results as $result ) { $data[ $result['option_name'] ] = maybe_unserialize( $result['option_value'] ); } return $data; } /** * Stores the new history state. * * @param array<array<string>> $data The data to store. * * @return void */ protected function set( array $data ) { // This should not be autoloaded! update_option( $this->get_option_name(), $data, false ); } /** * Retrieves the WPDB object. * * @return wpdb The WPDB object to use. */ protected function get_wpdb() { global $wpdb; return $wpdb; } /** * Retrieves the option name to store the history in. * * @return string The option name to store the history in. */ protected function get_option_name() { return $this->option_name; } } inc/class-yoast-dynamic-rewrites.php 0000644 00000012361 15174712003 0013557 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Class containing an alternative rewrite rules API for handling them dynamically without requiring flushing rules. */ class Yoast_Dynamic_Rewrites implements WPSEO_WordPress_Integration { /** * Additional rewrite rules with high priority. * * @var array */ protected $extra_rules_top = []; /** * Additional rewrite rules with low priority. * * @var array */ protected $extra_rules_bottom = []; /** * Main instance holder. * * @var self|null */ protected static $instance = null; /** * WP_Rewrite instance to use. * * @var WP_Rewrite */ public $wp_rewrite; /** * Gets the main instance of the class. * * @return self Dynamic rewrites main instance. */ public static function instance() { if ( self::$instance === null ) { self::$instance = new self(); self::$instance->register_hooks(); } return self::$instance; } /** * Constructor. * * Sets the WP_Rewrite instance to use. * * @param WP_Rewrite|null $rewrite Optional. WP_Rewrite instance to use. Default is the $wp_rewrite global. * @throws RuntimeException Throws an exception if the $wp_rewrite global is not set. */ public function __construct( $rewrite = null ) { if ( ! $rewrite ) { if ( empty( $GLOBALS['wp_rewrite'] ) ) { /* translators: 1: PHP class name, 2: PHP variable name */ throw new RuntimeException( sprintf( __( 'The %1$s class must not be instantiated before the %2$s global is set.', 'wordpress-seo' ), self::class, '$wp_rewrite' ) ); } $rewrite = $GLOBALS['wp_rewrite']; } $this->wp_rewrite = $rewrite; } /** * Registers all necessary hooks with WordPress. * * @return void */ public function register_hooks() { add_action( 'init', [ $this, 'trigger_dynamic_rewrite_rules_hook' ], 1 ); add_filter( 'option_rewrite_rules', [ $this, 'filter_rewrite_rules_option' ] ); add_filter( 'sanitize_option_rewrite_rules', [ $this, 'sanitize_rewrite_rules_option' ] ); } /** * Adds a dynamic rewrite rule that transforms a URL structure to a set of query vars. * * Rules registered with this method are applied dynamically and do not require the rewrite rules * to be flushed in order to become active, which is a benefit over the regular WordPress core API. * Note however that the dynamic application only works for rules that correspond to index.php. * Non-WordPress rewrite rules still require flushing. * * Any value in the $after parameter that isn't 'bottom' will result in the rule * being placed at the top of the rewrite rules. * * @param string $regex Regular expression to match request against. * @param string|array $query The corresponding query vars for this rewrite rule. * @param string $priority Optional. Priority of the new rule. Accepts 'top' * or 'bottom'. Default 'bottom'. * * @return void */ public function add_rule( $regex, $query, $priority = 'bottom' ) { if ( is_array( $query ) ) { $query = add_query_arg( $query, 'index.php' ); } $this->wp_rewrite->add_rule( $regex, $query, $priority ); // Do not further handle external rules. if ( substr( $query, 0, strlen( $this->wp_rewrite->index . '?' ) ) !== $this->wp_rewrite->index . '?' ) { return; } if ( $priority === 'bottom' ) { $this->extra_rules_bottom[ $regex ] = $query; return; } $this->extra_rules_top[ $regex ] = $query; } /** * Triggers the hook on which rewrite rules should be added. * * This allows for a more specific point in time from the generic `init` hook where this is * otherwise handled. * * @return void */ public function trigger_dynamic_rewrite_rules_hook() { /** * Fires when the plugin's dynamic rewrite rules should be added. * * @param self $dynamic_rewrites Dynamic rewrites handler instance. Use its `add_rule()` method * to add dynamic rewrite rules. */ do_action( 'yoast_add_dynamic_rewrite_rules', $this ); } /** * Filters the rewrite rules option to dynamically add additional rewrite rules. * * @param array|string $rewrite_rules Array of rewrite rule $regex => $query pairs, or empty string * if currently not set. * * @return array|string Filtered value of $rewrite_rules. */ public function filter_rewrite_rules_option( $rewrite_rules ) { // Do not add extra rewrite rules if the rules need to be flushed. if ( empty( $rewrite_rules ) ) { return $rewrite_rules; } return array_merge( $this->extra_rules_top, $rewrite_rules, $this->extra_rules_bottom ); } /** * Sanitizes the rewrite rules option prior to writing it to the database. * * This method ensures that the dynamic rewrite rules do not become part of the actual option. * * @param array|string $rewrite_rules Array pf rewrite rule $regex => $query pairs, or empty string * in order to unset. * * @return array|string Filtered value of $rewrite_rules before writing the option. */ public function sanitize_rewrite_rules_option( $rewrite_rules ) { if ( empty( $rewrite_rules ) ) { return $rewrite_rules; } return array_diff_key( $rewrite_rules, $this->extra_rules_top, $this->extra_rules_bottom ); } } inc/class-addon-manager.php 0000644 00000065054 15174712003 0011640 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration; use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; /** * Represents the addon manager. */ class WPSEO_Addon_Manager { /** * Holds the name of the transient. * * @var string */ public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information'; /** * Holds the name of the transient. * * @var string */ public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick'; /** * Holds the slug for YoastSEO free. * * @var string */ public const FREE_SLUG = 'yoast-seo-wordpress'; /** * Holds the slug for YoastSEO Premium. * * @var string */ public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium'; /** * Holds the slug for Yoast News. * * @var string */ public const NEWS_SLUG = 'yoast-seo-news'; /** * Holds the slug for Video. * * @var string */ public const VIDEO_SLUG = 'yoast-seo-video'; /** * Holds the slug for WooCommerce. * * @var string */ public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce'; /** * Holds the slug for Local. * * @var string */ public const LOCAL_SLUG = 'yoast-seo-local'; /** * The expected addon data. * * @var array<string, string> */ protected static $addons = [ 'wp-seo-premium.php' => self::PREMIUM_SLUG, 'wpseo-news.php' => self::NEWS_SLUG, 'video-seo.php' => self::VIDEO_SLUG, 'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG, 'local-seo.php' => self::LOCAL_SLUG, ]; /** * The addon data for the shortlinks. * * @var array<string, array<string, string>> */ private $addon_details = [ self::PREMIUM_SLUG => [ 'name' => 'Yoast SEO Premium', 'short_link_activation' => 'https://yoa.st/13j', 'short_link_renewal' => 'https://yoa.st/4ey', ], self::NEWS_SLUG => [ 'name' => 'Yoast News SEO', 'short_link_activation' => 'https://yoa.st/4xq', 'short_link_renewal' => 'https://yoa.st/4xv', ], self::WOOCOMMERCE_SLUG => [ 'name' => 'Yoast WooCommerce SEO', 'short_link_activation' => 'https://yoa.st/4xs', 'short_link_renewal' => 'https://yoa.st/4xx', ], self::VIDEO_SLUG => [ 'name' => 'Yoast Video SEO', 'short_link_activation' => 'https://yoa.st/4xr', 'short_link_renewal' => 'https://yoa.st/4xw', ], self::LOCAL_SLUG => [ 'name' => 'Yoast Local SEO', 'short_link_activation' => 'https://yoa.st/4xp', 'short_link_renewal' => 'https://yoa.st/4xu', ], ]; /** * Holds the site information data. * * @var stdClass */ private $site_information; /** * Hooks into WordPress. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'validate_addons' ], 15 ); add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] ); add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 ); add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 ); } /** * Registers "expired subscription" warnings to the update messages of our addons. * * @return void */ public function register_expired_messages() { foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) { add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 ); } } /** * Gets the subscriptions for current site. * * @return stdClass The subscriptions. */ public function get_subscriptions() { return $this->get_site_information()->subscriptions; } /** * Provides a list of addon filenames. * * @return string[] List of addon filenames with their slugs. */ public function get_addon_filenames() { return self::$addons; } /** * Finds the plugin file. * * @param string $plugin_slug The plugin slug to search. * * @return bool|string Plugin file when installed, False when plugin isn't installed. */ public function get_plugin_file( $plugin_slug ) { $plugins = $this->get_plugins(); $plugin_files = array_keys( $plugins ); $target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true ); if ( ! $target_plugin_file ) { return false; } foreach ( $plugin_files as $plugin_file ) { if ( strpos( $plugin_file, $target_plugin_file ) !== false ) { return $plugin_file; } } return false; } /** * Retrieves the subscription for the given slug. * * @param string $slug The plugin slug to retrieve. * * @return stdClass|false Subscription data when found, false when not found. */ public function get_subscription( $slug ) { foreach ( $this->get_subscriptions() as $subscription ) { if ( $subscription->product->slug === $slug ) { return $subscription; } } return false; } /** * Retrieves a list of (subscription) slugs by the active addons. * * @return array<string, stdClass> The slugs. */ public function get_subscriptions_for_active_addons() { $active_addons = array_keys( $this->get_active_addons() ); $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons ); $subscriptions = []; foreach ( $subscription_slugs as $subscription_slug ) { $subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug ); } return $subscriptions; } /** * Retrieves a list of versions for each addon. * * @return array<string, string> The addon versions. */ public function get_installed_addons_versions() { $addon_versions = []; foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) { $addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version']; } return $addon_versions; } /** * Retrieves the plugin information from the subscriptions. * * @param stdClass|false $data The result object. Default false. * @param string $action The type of information being requested from the Plugin Installation API. * @param stdClass $args Plugin API arguments. * * @return object Extended plugin data. */ public function get_plugin_information( $data, $action, $args ) { if ( $action !== 'plugin_information' ) { return $data; } if ( ! isset( $args->slug ) ) { return $data; } $subscription = $this->get_subscription( $args->slug ); if ( ! $subscription ) { return $data; } $data = $this->convert_subscription_to_plugin( $subscription, null, true ); if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->package, $data->download_link ); } return $data; } /** * Retrieves information from MyYoast about which addons are connected to the current site. * * @return stdClass The list of addons activated for this site. */ public function get_myyoast_site_information() { $this->site_information ??= $this->get_site_information_transient(); if ( $this->site_information ) { return $this->site_information; } $this->site_information = $this->request_current_sites(); if ( $this->site_information ) { $this->site_information = $this->map_site_information( $this->site_information ); $this->set_site_information_transient( $this->site_information ); return $this->site_information; } return $this->get_site_information_default(); } /** * Checks if the subscription for the given slug is valid. * * @param string $slug The plugin slug to retrieve. * * @return bool True when the subscription is valid. */ public function has_valid_subscription( $slug ) { $subscription = $this->get_subscription( $slug ); // An non-existing subscription is never valid. if ( ! $subscription ) { return false; } return ! $this->has_subscription_expired( $subscription ); } /** * Checks if there are addon updates. * * @param stdClass $data The current data for update_plugins. * * @return stdClass Extended data for update_plugins. */ public function check_for_updates( $data ) { global $wp_version; if ( empty( $data ) ) { return $data; } // We have to figure out if we're safe to upgrade the add-ons, based on what the latest Yoast Free requirements for the WP version is. $yoast_free_data = $this->extract_yoast_data( $data ); foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) { $subscription_slug = $this->get_slug_by_plugin_file( $plugin_file ); $subscription = $this->get_subscription( $subscription_slug ); if ( ! $subscription ) { continue; } $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file ); // Let's assume for now that it will get added in the 'no_update' key that we'll return to the WP API. $is_no_update = true; // If the add-on's version is the latest, we have to do no further checks. if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) { // If we haven't retrieved the Yoast Free requirements for the WP version yet, do nothing. The next run will probably get us that information. if ( $plugin_data->requires === null ) { continue; } if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) { // The add-on has an available update *and* the Yoast Free requirements for the WP version are also met, so go ahead and show the upgrade info to the user. $is_no_update = false; $data->response[ $plugin_file ] = $plugin_data; if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link ); } } } if ( $is_no_update ) { // Still convert subscription when no updates is available. $data->no_update[ $plugin_file ] = $plugin_data; if ( $this->has_subscription_expired( $subscription ) ) { unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link ); } } } return $data; } /** * Extracts Yoast SEO Free's data from the wp.org API response. * * @param object $data The wp.org API response. * * @return object Yoast Free's data from wp.org. */ protected function extract_yoast_data( $data ) { if ( isset( $data->response[ WPSEO_BASENAME ] ) ) { return $data->response[ WPSEO_BASENAME ]; } if ( isset( $data->no_update[ WPSEO_BASENAME ] ) ) { return $data->no_update[ WPSEO_BASENAME ]; } return (object) []; } /** * If the plugin is lacking an active subscription, throw a warning. * * @param array $plugin_data The data for the plugin in this row. * * @return void */ public function expired_subscription_warning( $plugin_data ) { $subscription = $this->get_subscription( $plugin_data['slug'] ); if ( $subscription && $this->has_subscription_expired( $subscription ) ) { $addon_link = ( isset( $this->addon_details[ $plugin_data['slug'] ] ) ) ? $this->addon_details[ $plugin_data['slug'] ]['short_link_renewal'] : $this->addon_details[ self::PREMIUM_SLUG ]['short_link_renewal']; $sale_copy = ''; if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { $sale_copy = sprintf( /* translators: %1$s and %2$s are a <span> opening and closing tag. */ esc_html__( '%1$s30%% OFF - Black Friday %2$s', 'wordpress-seo' ), '<span class="yoast-update-plugin-bf-sale-badge">', '</span>', ); } echo '<br><br>'; echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> ' . sprintf( /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */ esc_html__( 'Your %1$s plugin cannot be updated as your subscription has expired. %2$sRenew your product subscription%3$s to restore updates and full feature access.', 'wordpress-seo' ), esc_html( $plugin_data['name'] ), '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">', '</a>', ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above. . $sale_copy . '</strong>'; } } /** * Checks if there are any installed addons. * * @return bool True when there are installed Yoast addons. */ public function has_installed_addons() { $installed_addons = $this->get_installed_addons(); return ! empty( $installed_addons ); } /** * Checks if the plugin is installed and activated in WordPress. * * @param string $slug The class' slug. * * @return bool True when installed and activated. */ public function is_installed( $slug ) { $slug_to_class_map = [ static::PREMIUM_SLUG => 'WPSEO_Premium', static::NEWS_SLUG => 'WPSEO_News', static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO', static::VIDEO_SLUG => 'WPSEO_Video_Sitemap', static::LOCAL_SLUG => 'WPSEO_Local_Core', ]; if ( ! isset( $slug_to_class_map[ $slug ] ) ) { return false; } return class_exists( $slug_to_class_map[ $slug ] ); } /** * Validates the addons and show a notice for the ones that are invalid. * * @return void */ public function validate_addons() { $notification_center = Yoast_Notification_Center::get(); if ( $notification_center === null ) { return; } foreach ( $this->addon_details as $slug => $addon_info ) { $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] ); // Add a notification when the installed plugin isn't activated in My Yoast. if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) { $notification_center->add_notification( $notification ); continue; } $notification_center->remove_notification( $notification ); } } /** * Checks if the user has any active addons. * * @return bool Whether there are active addons. */ public function has_active_addons() { $active_addons = $this->get_active_addons(); return ! empty( $active_addons ); } /** * Removes the site information transients. * * @codeCoverageIgnore * * @return void */ public function remove_site_information_transients() { delete_transient( self::SITE_INFORMATION_TRANSIENT ); delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK ); } /** * Creates an instance of Yoast_Notification. * * @param string $product_name The product to create the notification for. * @param string $short_link The short link for the addon notification. * * @return Yoast_Notification The created notification. */ protected function create_notification( $product_name, $short_link ) { $notification_options = [ 'type' => Yoast_Notification::ERROR, 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ), 'capabilities' => 'wpseo_manage_options', ]; return new Yoast_Notification( sprintf( /* translators: %1$s expands to a strong tag, %2$s expands to the product name, %3$s expands to a closing strong tag, %4$s expands to an a tag. %5$s expands to MyYoast, %6$s expands to a closing a tag, %7$s expands to the product name */ __( '%1$s %2$s isn\'t working as expected %3$s and you are not receiving updates or support! Make sure to %4$s activate your product subscription in %5$s%6$s to unlock all the features of %7$s.', 'wordpress-seo' ), '<strong>', $product_name, '</strong>', '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">', 'MyYoast', '</a>', $product_name, ), $notification_options, ); } /** * Checks whether a plugin expiry date has been passed. * * @param stdClass $subscription Plugin subscription. * * @return bool Has the plugin expired. */ protected function has_subscription_expired( $subscription ) { return ( strtotime( $subscription->expiry_date ) - time() ) < 0; } /** * Converts a subscription to plugin based format. * * @param stdClass $subscription The subscription to convert. * @param stdClass|null $yoast_free_data The Yoast Free's data. * @param bool $plugin_info Whether we're in the plugin information modal. * @param string $plugin_file The plugin filename. * * @return stdClass The converted subscription. */ protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) { $changelog = ''; if ( isset( $subscription->product->changelog ) ) { // We need to replace h2's and h3's with h4's because the styling expects that. $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) ); $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) ); } // If we're running this because we want to just show the plugin info in the version details modal, we can fallback to the Yoast Free constants, since that modal will not be accessible anyway in the event that the new Free version increases those constants. $defaults = [ // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future. 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null, ]; return (object) [ 'new_version' => ( $subscription->product->version ?? '' ), 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'plugin' => $plugin_file, 'url' => $subscription->product->store_url, 'last_update' => $subscription->product->last_updated, 'homepage' => $subscription->product->store_url, 'download_link' => $subscription->product->download, 'package' => $subscription->product->download, 'sections' => [ 'changelog' => $changelog, 'support' => $this->get_support_section(), ], 'icons' => [ '2x' => $this->get_icon( $subscription->product->slug ), ], 'update_supported' => true, 'banners' => $this->get_banners( $subscription->product->slug ), // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults. 'tested' => YOAST_SEO_WP_TESTED, 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ), 'requires_php' => YOAST_SEO_PHP_REQUIRED, ]; } /** * Returns the plugin's icon URL. * * @param string $slug The plugin slug. * * @return string The icon URL for this plugin. */ protected function get_icon( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return 'https://yoa.st/local-seo-icon'; case self::NEWS_SLUG: return 'https://yoa.st/news-seo-icon'; case self::PREMIUM_SLUG: return 'https://yoa.st/yoast-seo-icon'; case self::VIDEO_SLUG: return 'https://yoa.st/video-seo-icon'; case self::WOOCOMMERCE_SLUG: return 'https://yoa.st/woo-seo-icon'; } } /** * Return an array of plugin banner URLs. * * @param string $slug The plugin slug. * * @return string[] */ protected function get_banners( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-local', 'low' => 'https://yoa.st/yoast-seo-banner-low-local', ]; case self::NEWS_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-news', 'low' => 'https://yoa.st/yoast-seo-banner-low-news', ]; case self::PREMIUM_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-premium', 'low' => 'https://yoa.st/yoast-seo-banner-low-premium', ]; case self::VIDEO_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-video', 'low' => 'https://yoa.st/yoast-seo-banner-low-video', ]; case self::WOOCOMMERCE_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-woo', 'low' => 'https://yoa.st/yoast-seo-banner-low-woo', ]; } } /** * Checks if the given plugin_file belongs to a Yoast addon. * * @param string $plugin_file Path to the plugin. * * @return bool True when plugin file is for a Yoast addon. */ protected function is_yoast_addon( $plugin_file ) { return $this->get_slug_by_plugin_file( $plugin_file ) !== ''; } /** * Retrieves the addon slug by given plugin file path. * * @param string $plugin_file The file path to the plugin. * * @return string The slug when found or empty string when not. */ protected function get_slug_by_plugin_file( $plugin_file ) { $addons = self::$addons; // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations. if ( YoastSEO()->helpers->product->is_premium() ) { $addons['wp-seo.php'] = self::FREE_SLUG; } foreach ( $addons as $addon => $addon_slug ) { if ( strpos( $plugin_file, $addon ) !== false ) { return $addon_slug; } } return ''; } /** * Retrieves the installed Yoast addons. * * @return array The installed plugins. */ protected function get_installed_addons() { return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves a list of active addons. * * @return array The active addons. */ protected function get_active_addons() { return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves the current sites from the API. * * @codeCoverageIgnore * * @return bool|stdClass Object when request is successful. False if not. */ protected function request_current_sites() { $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' ); if ( $api_request->fire() ) { return $api_request->get_response(); } return $this->get_site_information_default(); } /** * Retrieves the transient value with the site information. * * @codeCoverageIgnore * * @return stdClass|false The transient value. */ protected function get_site_information_transient() { global $pagenow; // Force re-check on license & dashboard pages. $current_page = null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize. $current_page = wp_unslash( $_GET['page'] ); } // Check whether the licenses are valid or whether we need to show notifications. $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE ); // Also do a fresh request on Plugins & Core Update pages. $quick = $quick || $pagenow === 'plugins.php'; $quick = $quick || $pagenow === 'update-core.php'; if ( $quick ) { return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK ); } return get_transient( self::SITE_INFORMATION_TRANSIENT ); } /** * Sets the site information transient. * * @codeCoverageIgnore * * @param stdClass $site_information The site information to save. * * @return void */ protected function set_site_information_transient( $site_information ) { set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS ); set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 ); } /** * Retrieves all installed WordPress plugins. * * @codeCoverageIgnore * * @return array The plugins. */ protected function get_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugins(); } /** * Checks if the given plugin file belongs to an active plugin. * * @codeCoverageIgnore * * @param string $plugin_file The file path to the plugin. * * @return bool True when plugin is active. */ protected function is_plugin_active( $plugin_file ) { return is_plugin_active( $plugin_file ); } /** * Returns an object with no subscriptions. * * @codeCoverageIgnore * * @return stdClass Site information. */ protected function get_site_information_default() { return (object) [ 'url' => WPSEO_Utils::get_home_url(), 'subscriptions' => [], ]; } /** * Maps the plugin API response. * * @param object $site_information Site information as received from the API. * * @return stdClass Mapped site information. */ protected function map_site_information( $site_information ) { return (object) [ 'url' => $site_information->url, 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ), ]; } /** * Maps a plugin subscription. * * @param object $subscription Subscription information as received from the API. * * @return stdClass Mapped subscription. */ protected function map_subscription( $subscription ) { // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties. return (object) [ 'renewal_url' => $subscription->renewalUrl, 'expiry_date' => $subscription->expiryDate, 'product' => (object) [ 'version' => $subscription->product->version, 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'last_updated' => $subscription->product->lastUpdated, 'store_url' => $subscription->product->storeUrl, // Ternary operator is necessary because download can be undefined. 'download' => ( $subscription->product->download ?? null ), 'changelog' => $subscription->product->changelog, ], ]; // phpcs:enable } /** * Retrieves the site information. * * @return stdClass The site information. */ private function get_site_information() { if ( ! $this->has_installed_addons() ) { return $this->get_site_information_default(); } return $this->get_myyoast_site_information(); } /** * Retrieves the contents for the support section. * * @return string The support section content. */ protected function get_support_section() { return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>' . '<p>' /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */ . sprintf( __( 'You can probably find an answer to your question in our %1$shelp center%2$s.', 'wordpress-seo' ), '<a href="https://yoast.com/help/">', '</a>' ) . ' ' /* translators: %s expands to a mailto support link. */ . sprintf( __( 'If you still need support and have an active subscription for this product, please email %s.', 'wordpress-seo' ), '<a href="mailto:support@yoast.com">support@yoast.com</a>' ) . '</p>'; } } inc/class-wpseo-image-utils.php 0000644 00000036037 15174712003 0012515 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Image_Utils. */ class WPSEO_Image_Utils { /** * Find an attachment ID for a given URL. * * @param string $url The URL to find the attachment for. * * @return int The found attachment ID, or 0 if none was found. */ public static function get_attachment_by_url( $url ) { /* * As get_attachment_by_url won't work on resized versions of images, * we strip out the size part of an image URL. */ $url = preg_replace( '/(.*)-\d+x\d+\.(jpg|png|gif)$/', '$1.$2', $url ); static $uploads; $uploads ??= wp_get_upload_dir(); // Don't try to do this for external URLs. if ( strpos( $url, $uploads['baseurl'] ) !== 0 ) { return 0; } if ( function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) { // @codeCoverageIgnoreStart -- We can't test this properly. return (int) wpcom_vip_attachment_url_to_postid( $url ); // @codeCoverageIgnoreEnd -- The rest we _can_ test. } return self::attachment_url_to_postid( $url ); } /** * Implements the attachment_url_to_postid with use of WP Cache. * * @param string $url The attachment URL for which we want to know the Post ID. * * @return int The Post ID belonging to the attachment, 0 if not found. */ protected static function attachment_url_to_postid( $url ) { $cache_key = sprintf( 'yoast_attachment_url_post_id_%s', md5( $url ) ); // Set the ID based on the hashed URL in the cache. $id = wp_cache_get( $cache_key ); if ( $id === 'not_found' ) { return 0; } // ID is found in cache, return. if ( $id !== false ) { return $id; } // Note: We use the WP COM version if we can, see above. $id = attachment_url_to_postid( $url ); if ( empty( $id ) ) { /** * If no ID was found, maybe we're dealing with a scaled big image. So, let's try that. * * @see https://core.trac.wordpress.org/ticket/51058 */ $id = self::get_scaled_image_id( $url ); } if ( empty( $id ) ) { wp_cache_set( $cache_key, 'not_found', '', ( 12 * HOUR_IN_SECONDS + wp_rand( 0, ( 4 * HOUR_IN_SECONDS ) ) ) ); return 0; } // We have the Post ID, but it's not in the cache yet. We do that here and return. wp_cache_set( $cache_key, $id, '', ( 24 * HOUR_IN_SECONDS + wp_rand( 0, ( 12 * HOUR_IN_SECONDS ) ) ) ); return $id; } /** * Tries getting the ID of a potentially scaled image. * * @param string $url The URL of the image. * * @return int|false The ID of the image or false for failure. */ protected static function get_scaled_image_id( $url ) { $path_parts = pathinfo( $url ); if ( isset( $path_parts['dirname'], $path_parts['filename'], $path_parts['extension'] ) ) { $scaled_url = trailingslashit( $path_parts['dirname'] ) . $path_parts['filename'] . '-scaled.' . $path_parts['extension']; return attachment_url_to_postid( $scaled_url ); } return false; } /** * Retrieves the image data. * * @param array $image Image array with URL and metadata. * @param int $attachment_id Attachment ID. * * @return array|false { * Array of image data * * @type string $alt Image's alt text. * @type string $path Path of image. * @type int $width Width of image. * @type int $height Height of image. * @type string $type Image's MIME type. * @type string $size Image's size. * @type string $url Image's URL. * @type int $filesize The file size in bytes, if already set. * } */ public static function get_data( $image, $attachment_id ) { if ( ! is_array( $image ) ) { return false; } // Deals with non-set keys and values being null or false. if ( empty( $image['width'] ) || empty( $image['height'] ) ) { return false; } $image['id'] = $attachment_id; $image['alt'] = self::get_alt_tag( $attachment_id ); $image['pixels'] = ( (int) $image['width'] * (int) $image['height'] ); if ( ! isset( $image['type'] ) ) { $image['type'] = get_post_mime_type( $attachment_id ); } /** * Filter: 'wpseo_image_data' - Filter image data. * * Elements with keys not listed in the section will be discarded. * * @param array $image_data { * Array of image data * * @type int id Image's ID as an attachment. * @type string alt Image's alt text. * @type string path Image's path. * @type int width Width of image. * @type int height Height of image. * @type int pixels Number of pixels in the image. * @type string type Image's MIME type. * @type string size Image's size. * @type string url Image's URL. * @type int filesize The file size in bytes, if already set. * } * @param int $attachment_id Attachment ID. */ $image = apply_filters( 'wpseo_image_data', $image, $attachment_id ); // Keep only the keys we need, and nothing else. return array_intersect_key( $image, array_flip( [ 'id', 'alt', 'path', 'width', 'height', 'pixels', 'type', 'size', 'url', 'filesize' ] ) ); } /** * Checks a size version of an image to see if it's not too heavy. * * @param array $image Image to check the file size of. * * @return bool True when the image is within limits, false if not. */ public static function has_usable_file_size( $image ) { if ( ! is_array( $image ) || $image === [] ) { return false; } /** * Filter: 'wpseo_image_image_weight_limit' - Determines what the maximum weight * (in bytes) of an image is allowed to be, default is 2 MB. * * @param int $max_bytes The maximum weight (in bytes) of an image. */ $max_size = apply_filters( 'wpseo_image_image_weight_limit', 2_097_152 ); // We cannot check without a path, so assume it's fine. if ( ! isset( $image['path'] ) ) { return true; } return ( self::get_file_size( $image ) <= $max_size ); } /** * Find the right version of an image based on size. * * @param int $attachment_id Attachment ID. * @param string|array $size Size name, or array of width and height in pixels (e.g [800,400]). * * @return array|false Returns an array with image data on success, false on failure. */ public static function get_image( $attachment_id, $size ) { $image = false; if ( $size === 'full' ) { $image = self::get_full_size_image_data( $attachment_id ); } if ( ! $image ) { $image = image_get_intermediate_size( $attachment_id, $size ); } if ( ! is_array( $image ) ) { $image_src = wp_get_attachment_image_src( $attachment_id, $size ); if ( is_array( $image_src ) && isset( $image_src[1] ) && isset( $image_src[2] ) ) { $image = []; $image['url'] = $image_src[0]; $image['width'] = $image_src[1]; $image['height'] = $image_src[2]; $image['size'] = 'full'; } } if ( ! $image ) { return false; } if ( ! isset( $image['size'] ) ) { $image['size'] = $size; } return self::get_data( $image, $attachment_id ); } /** * Returns the image data for the full size image. * * @param int $attachment_id Attachment ID. * * @return array|false Array when there is a full size image. False if not. */ protected static function get_full_size_image_data( $attachment_id ) { $image = wp_get_attachment_metadata( $attachment_id ); if ( ! is_array( $image ) ) { return false; } $image['url'] = wp_get_attachment_image_url( $attachment_id, 'full' ); $image['path'] = get_attached_file( $attachment_id ); $image['size'] = 'full'; return $image; } /** * Finds the full file path for a given image file. * * @param string $path The relative file path. * * @return string The full file path. */ public static function get_absolute_path( $path ) { static $uploads; $uploads ??= wp_get_upload_dir(); // Add the uploads basedir if the path does not start with it. if ( empty( $uploads['error'] ) && strpos( $path, $uploads['basedir'] ) !== 0 ) { return $uploads['basedir'] . DIRECTORY_SEPARATOR . ltrim( $path, DIRECTORY_SEPARATOR ); } return $path; } /** * Get the relative path of the image. * * @param string $img Image URL. * * @return string The expanded image URL. */ public static function get_relative_path( $img ) { if ( $img[0] !== '/' ) { return $img; } // If it's a relative URL, it's relative to the domain, not necessarily to the WordPress install, we // want to preserve domain name and URL scheme (http / https) though. $parsed_url = wp_parse_url( home_url() ); $img = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $img; return $img; } /** * Get the image file size. * * @param array $image An image array object. * * @return int The file size in bytes. */ public static function get_file_size( $image ) { if ( isset( $image['filesize'] ) ) { return $image['filesize']; } if ( ! isset( $image['path'] ) ) { return 0; } // If the file size for the file is over our limit, we're going to go for a smaller version. if ( function_exists( 'wp_filesize' ) ) { return wp_filesize( self::get_absolute_path( $image['path'] ) ); } return file_exists( $image['path'] ) ? (int) filesize( $image['path'] ) : 0; } /** * Returns the different image variations for consideration. * * @param int $attachment_id The attachment to return the variations for. * * @return array The different variations possible for this attachment ID. */ public static function get_variations( $attachment_id ) { $variations = []; foreach ( self::get_sizes() as $size ) { $variation = self::get_image( $attachment_id, $size ); // The get_image function returns false if the size doesn't exist for this attachment. if ( $variation ) { $variations[] = $variation; } } return $variations; } /** * Check original size of image. If original image is too small, return false, else return true. * * Filters a list of variations by a certain set of usable dimensions. * * @param array $usable_dimensions { * The parameters to check against. * * @type int $min_width Minimum width of image. * @type int $max_width Maximum width of image. * @type int $min_height Minimum height of image. * @type int $max_height Maximum height of image. * } * @param array $variations The variations that should be considered. * * @return array Whether a variation is fit for display or not. */ public static function filter_usable_dimensions( $usable_dimensions, $variations ) { $filtered = []; foreach ( $variations as $variation ) { $dimensions = $variation; if ( self::has_usable_dimensions( $dimensions, $usable_dimensions ) ) { $filtered[] = $variation; } } return $filtered; } /** * Filters a list of variations by (disk) file size. * * @param array $variations The variations to consider. * * @return array The validations that pass the required file size limits. */ public static function filter_usable_file_size( $variations ) { foreach ( $variations as $variation ) { // We return early to prevent measuring the file size of all the variations. if ( self::has_usable_file_size( $variation ) ) { return [ $variation ]; } } return []; } /** * Retrieve the internal WP image file sizes. * * @return array An array of image sizes. */ public static function get_sizes() { /** * Filter: 'wpseo_image_sizes' - Determines which image sizes we'll loop through to get an appropriate image. * * @param array<string> $sizes The array of image sizes to loop through. */ return apply_filters( 'wpseo_image_sizes', [ 'full', 'large', 'medium_large' ] ); } /** * Grabs an image alt text. * * @param int $attachment_id The attachment ID. * * @return string The image alt text. */ public static function get_alt_tag( $attachment_id ) { return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); } /** * Checks whether an img sizes up to the parameters. * * @param array $dimensions The image values. * @param array $usable_dimensions The parameters to check against. * * @return bool True if the image has usable measurements, false if not. */ private static function has_usable_dimensions( $dimensions, $usable_dimensions ) { foreach ( [ 'width', 'height' ] as $param ) { $minimum = $usable_dimensions[ 'min_' . $param ]; $maximum = $usable_dimensions[ 'max_' . $param ]; $current = $dimensions[ $param ]; if ( ( $current < $minimum ) || ( $current > $maximum ) ) { return false; } } return true; } /** * Gets the post's first usable content image. Null if none is available. * * @param int|null $post_id The post id. * * @return string|null The image URL. */ public static function get_first_usable_content_image_for_post( $post_id = null ) { $post = get_post( $post_id ); // We know get_post() returns the post or null. if ( ! $post ) { return null; } $image_finder = new WPSEO_Content_Images(); $images = $image_finder->get_images( $post->ID, $post ); return self::get_first_image( $images ); } /** * Gets the term's first usable content image. Null if none is available. * * @param int $term_id The term id. * * @return string|null The image URL. */ public static function get_first_content_image_for_term( $term_id ) { $term_description = term_description( $term_id ); // We know term_description() returns a string which may be empty. if ( $term_description === '' ) { return null; } $image_finder = new WPSEO_Content_Images(); $images = $image_finder->get_images_from_content( $term_description ); return self::get_first_image( $images ); } /** * Retrieves an attachment ID for an image uploaded in the settings. * * Due to self::get_attachment_by_url returning 0 instead of false. * 0 is also a possibility when no ID is available. * * @param string $setting The setting the image is stored in. * * @return int|bool The attachment id, or false or 0 if no ID is available. */ public static function get_attachment_id_from_settings( $setting ) { $image_id = WPSEO_Options::get( $setting . '_id', false ); if ( $image_id ) { return $image_id; } $image = WPSEO_Options::get( $setting, false ); if ( $image ) { // There is not an option to put a URL in an image field in the settings anymore, only to upload it through the media manager. // This means an attachment always exists, so doing this is only needed once. $image_id = self::get_attachment_by_url( $image ); } // Only store a new ID if it is not 0, to prevent an update loop. if ( $image_id ) { WPSEO_Options::set( $setting . '_id', $image_id ); } return $image_id; } /** * Retrieves the first possible image url from an array of images. * * @param array $images The array to extract image url from. * * @return string|null The extracted image url when found, null when not found. */ protected static function get_first_image( $images ) { if ( ! is_array( $images ) ) { return null; } $images = array_filter( $images ); if ( empty( $images ) ) { return null; } return reset( $images ); } } inc/class-wpseo-utils.php 0000644 00000070747 15174712003 0011443 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.8.0 */ use Yoast\WP\SEO\Integrations\Feature_Flag_Integration; /** * Group of utility methods for use by WPSEO. * All methods are static, this is just a sort of namespacing class wrapper. */ class WPSEO_Utils { /** * Whether the PHP filter extension is enabled. * * @since 1.8.0 * * @var bool */ public static $has_filters; /** * Check whether file editing is allowed for the .htaccess and robots.txt files. * * {@internal current_user_can() checks internally whether a user is on wp-ms and adjusts accordingly.}} * * @since 1.8.0 * * @return bool */ public static function allow_system_file_edit() { $allowed = true; if ( current_user_can( 'edit_files' ) === false ) { $allowed = false; } /** * Filter: 'wpseo_allow_system_file_edit' - Allow developers to change whether the editing of * .htaccess and robots.txt is allowed. * * @param bool $allowed Whether file editing is allowed. */ return apply_filters( 'wpseo_allow_system_file_edit', $allowed ); } /** * Check if the web server is running on Apache or compatible (LiteSpeed). * * @since 1.8.0 * * @return bool */ public static function is_apache() { if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) { return false; } $software = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ); return stripos( $software, 'apache' ) !== false || stripos( $software, 'litespeed' ) !== false; } /** * Check if the web server is running on Nginx. * * @since 1.8.0 * * @return bool */ public static function is_nginx() { if ( ! isset( $_SERVER['SERVER_SOFTWARE'] ) ) { return false; } $software = sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ); return stripos( $software, 'nginx' ) !== false; } /** * Check whether a url is relative. * * @since 1.8.0 * * @param string $url URL string to check. * * @return bool */ public static function is_url_relative( $url ) { return YoastSEO()->helpers->url->is_relative( $url ); } /** * Recursively trim whitespace round a string value or of string values within an array. * Only trims strings to avoid typecasting a variable (to string). * * @since 1.8.0 * * @param mixed $value Value to trim or array of values to trim. * * @return mixed Trimmed value or array of trimmed values. */ public static function trim_recursive( $value ) { if ( is_string( $value ) ) { $value = trim( $value ); } elseif ( is_array( $value ) ) { $value = array_map( [ self::class, 'trim_recursive' ], $value ); } return $value; } /** * Emulate the WP native sanitize_text_field function in a %%variable%% safe way. * * Sanitize a string from user input or from the db. * * - Check for invalid UTF-8; * - Convert single < characters to entity; * - Strip all tags; * - Remove line breaks, tabs and extra white space; * - Strip octets - BUT DO NOT REMOVE (part of) VARIABLES WHICH WILL BE REPLACED. * * @link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/formatting.php for the original. * * @since 1.8.0 * * @param string $value String value to sanitize. * * @return string */ public static function sanitize_text_field( $value ) { $filtered = wp_check_invalid_utf8( $value ); if ( strpos( $filtered, '<' ) !== false ) { $filtered = wp_pre_kses_less_than( $filtered ); // This will strip extra whitespace for us. $filtered = wp_strip_all_tags( $filtered, true ); } else { $filtered = trim( preg_replace( '`[\r\n\t ]+`', ' ', $filtered ) ); } $found = false; while ( preg_match( '`[^%](%[a-f0-9]{2})`i', $filtered, $match ) ) { $filtered = str_replace( $match[1], '', $filtered ); $found = true; } unset( $match ); if ( $found ) { // Strip out the whitespace that may now exist after removing the octets. $filtered = trim( preg_replace( '` +`', ' ', $filtered ) ); } /** * Filter a sanitized text field string. * * @since WP 2.9.0 * * @param string $filtered The sanitized string. * @param string $str The string prior to being sanitized. */ return apply_filters( 'sanitize_text_field', $filtered, $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Using WP native filter. } /** * Sanitize a url for saving to the database. * Not to be confused with the old native WP function. * * @since 1.8.0 * * @param string $value String URL value to sanitize. * @param array $allowed_protocols Optional set of allowed protocols. * * @return string */ public static function sanitize_url( $value, $allowed_protocols = [ 'http', 'https' ] ) { $url = ''; $parts = wp_parse_url( $value ); if ( isset( $parts['scheme'], $parts['host'] ) ) { $url = $parts['scheme'] . '://'; if ( isset( $parts['user'] ) ) { $url .= rawurlencode( $parts['user'] ); $url .= isset( $parts['pass'] ) ? ':' . rawurlencode( $parts['pass'] ) : ''; $url .= '@'; } $parts['host'] = preg_replace( '`[^a-z0-9-.:\[\]\\x80-\\xff]`', '', strtolower( $parts['host'] ), ); $url .= $parts['host'] . ( isset( $parts['port'] ) ? ':' . (int) $parts['port'] : '' ); } if ( isset( $parts['path'] ) && strpos( $parts['path'], '/' ) === 0 ) { $path = explode( '/', wp_strip_all_tags( $parts['path'] ) ); $path = self::sanitize_encoded_text_field( $path ); $url .= str_replace( '%40', '@', implode( '/', $path ) ); } if ( ! $url ) { return ''; } if ( isset( $parts['query'] ) ) { wp_parse_str( $parts['query'], $parsed_query ); $parsed_query = array_combine( self::sanitize_encoded_text_field( array_keys( $parsed_query ) ), self::sanitize_encoded_text_field( array_values( $parsed_query ) ), ); $url = add_query_arg( $parsed_query, $url ); } if ( isset( $parts['fragment'] ) ) { $url .= '#' . self::sanitize_encoded_text_field( $parts['fragment'] ); } if ( strpos( $url, '%' ) !== false ) { $url = preg_replace_callback( '`%[a-fA-F0-9]{2}`', static function ( $octects ) { return strtolower( $octects[0] ); }, $url, ); } return esc_url_raw( $url, $allowed_protocols ); } /** * Decode, sanitize and encode the array of strings or the string. * * @since 13.3 * * @param array|string $value The value to sanitize and encode. * * @return array|string The sanitized value. */ public static function sanitize_encoded_text_field( $value ) { if ( is_array( $value ) ) { return array_map( [ self::class, 'sanitize_encoded_text_field' ], $value ); } return rawurlencode( sanitize_text_field( rawurldecode( $value ) ) ); } /** * Validate a value as boolean. * * @since 1.8.0 * * @param mixed $value Value to validate. * * @return bool */ public static function validate_bool( $value ) { if ( ! isset( self::$has_filters ) ) { self::$has_filters = extension_loaded( 'filter' ); } if ( self::$has_filters ) { return filter_var( $value, FILTER_VALIDATE_BOOLEAN ); } else { return self::emulate_filter_bool( $value ); } } /** * Cast a value to bool. * * @since 1.8.0 * * @param mixed $value Value to cast. * * @return bool */ public static function emulate_filter_bool( $value ) { $true = [ '1', 'true', 'True', 'TRUE', 'y', 'Y', 'yes', 'Yes', 'YES', 'on', 'On', 'ON', ]; $false = [ '0', 'false', 'False', 'FALSE', 'n', 'N', 'no', 'No', 'NO', 'off', 'Off', 'OFF', ]; if ( is_bool( $value ) ) { return $value; } elseif ( is_int( $value ) && ( $value === 0 || $value === 1 ) ) { return (bool) $value; } elseif ( ( is_float( $value ) && ! is_nan( $value ) ) && ( $value === (float) 0 || $value === (float) 1 ) ) { return (bool) $value; } elseif ( is_string( $value ) ) { $value = trim( $value ); if ( in_array( $value, $true, true ) ) { return true; } elseif ( in_array( $value, $false, true ) ) { return false; } else { return false; } } return false; } /** * Validate a value as integer. * * @since 1.8.0 * * @param mixed $value Value to validate. * * @return int|bool Int or false in case of failure to convert to int. */ public static function validate_int( $value ) { if ( ! isset( self::$has_filters ) ) { self::$has_filters = extension_loaded( 'filter' ); } if ( self::$has_filters ) { return filter_var( $value, FILTER_VALIDATE_INT ); } else { return self::emulate_filter_int( $value ); } } /** * Cast a value to integer. * * @since 1.8.0 * * @param mixed $value Value to cast. * * @return int|bool */ public static function emulate_filter_int( $value ) { if ( is_int( $value ) ) { return $value; } elseif ( is_float( $value ) ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. if ( (int) $value == $value && ! is_nan( $value ) ) { return (int) $value; } else { return false; } } elseif ( is_string( $value ) ) { $value = trim( $value ); if ( $value === '' ) { return false; } elseif ( ctype_digit( $value ) ) { return (int) $value; } elseif ( strpos( $value, '-' ) === 0 && ctype_digit( substr( $value, 1 ) ) ) { return (int) $value; } else { return false; } } return false; } /** * Clears the WP or W3TC cache depending on which is used. * * @since 1.8.0 * * @return void */ public static function clear_cache() { if ( function_exists( 'w3tc_flush_posts' ) ) { w3tc_flush_posts(); } elseif ( function_exists( 'wp_cache_clear_cache' ) ) { wp_cache_clear_cache(); } } /** * Clear rewrite rules. * * @since 1.8.0 * * @return void */ public static function clear_rewrites() { update_option( 'rewrite_rules', '' ); } /** * Do simple reliable math calculations without the risk of wrong results. * * In the rare case that the bcmath extension would not be loaded, it will return the normal calculation results. * * @link http://floating-point-gui.de/ * @link http://php.net/language.types.float.php See the big red warning. * * @since 1.5.0 * @since 1.8.0 Moved from stand-alone function to this class. * * @param mixed $number1 Scalar (string/int/float/bool). * @param string $action Calculation action to execute. Valid input: * '+' or 'add' or 'addition', * '-' or 'sub' or 'subtract', * '*' or 'mul' or 'multiply', * '/' or 'div' or 'divide', * '%' or 'mod' or 'modulus' * '=' or 'comp' or 'compare'. * @param mixed $number2 Scalar (string/int/float/bool). * @param bool $round Whether or not to round the result. Defaults to false. * Will be disregarded for a compare operation. * @param int $decimals Decimals for rounding operation. Defaults to 0. * @param int $precision Calculation precision. Defaults to 10. * * @return mixed Calculation Result or false if either or the numbers isn't scalar or * an invalid operation was passed. * - For compare the result will always be an integer. * - For all other operations, the result will either be an integer (preferred) * or a float. */ public static function calc( $number1, $action, $number2, $round = false, $decimals = 0, $precision = 10 ) { static $bc; if ( ! is_scalar( $number1 ) || ! is_scalar( $number2 ) ) { return false; } if ( ! isset( $bc ) ) { $bc = extension_loaded( 'bcmath' ); } if ( $bc ) { $number1 = number_format( $number1, 10, '.', '' ); $number2 = number_format( $number2, 10, '.', '' ); } $result = null; $compare = false; switch ( $action ) { case '+': case 'add': case 'addition': $result = ( $bc ) ? bcadd( $number1, $number2, $precision ) /* string */ : ( $number1 + $number2 ); break; case '-': case 'sub': case 'subtract': $result = ( $bc ) ? bcsub( $number1, $number2, $precision ) /* string */ : ( $number1 - $number2 ); break; case '*': case 'mul': case 'multiply': $result = ( $bc ) ? bcmul( $number1, $number2, $precision ) /* string */ : ( $number1 * $number2 ); break; case '/': case 'div': case 'divide': if ( $bc ) { $result = bcdiv( $number1, $number2, $precision ); // String, or NULL if right_operand is 0. } elseif ( $number2 != 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 / $number2 ); } if ( ! isset( $result ) ) { $result = 0; } break; case '%': case 'mod': case 'modulus': if ( $bc ) { $result = bcmod( $number1, $number2 ); // String, or NULL if modulus is 0. } elseif ( $number2 != 0 ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 % $number2 ); } if ( ! isset( $result ) ) { $result = 0; } break; case '=': case 'comp': case 'compare': $compare = true; if ( $bc ) { $result = bccomp( $number1, $number2, $precision ); // Returns int 0, 1 or -1. } else { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( $number1 == $number2 ) ? 0 : ( ( $number1 > $number2 ) ? 1 : -1 ); } break; } if ( isset( $result ) ) { if ( $compare === false ) { if ( $round === true ) { $result = round( (float) $result, $decimals ); if ( $decimals === 0 ) { $result = (int) $result; } } else { // phpcs:ignore Universal.Operators.StrictComparisons -- Purposeful loose comparison. $result = ( intval( $result ) == $result ) ? (int) $result : (float) $result; } } return $result; } return false; } /** * Trim whitespace and NBSP (Non-breaking space) from string. * * @since 2.0.0 * * @param string $text String input to trim. * * @return string */ public static function trim_nbsp_from_string( $text ) { $find = [ ' ', chr( 0xC2 ) . chr( 0xA0 ) ]; $text = str_replace( $find, ' ', $text ); $text = trim( $text ); return $text; } /** * Check if a string is a valid datetime. * * @since 2.0.0 * * @param string $datetime String input to check as valid input for DateTime class. * * @return bool */ public static function is_valid_datetime( $datetime ) { return YoastSEO()->helpers->date->is_valid_datetime( $datetime ); } /** * Format the URL to be sure it is okay for using as a redirect url. * * This method will parse the URL and combine them in one string. * * @since 2.3.0 * * @param string $url URL string. * * @return mixed */ public static function format_url( $url ) { $parsed_url = wp_parse_url( $url ); $formatted_url = ''; if ( ! empty( $parsed_url['path'] ) ) { $formatted_url = $parsed_url['path']; } // Prepend a slash if first char != slash. if ( stripos( $formatted_url, '/' ) !== 0 ) { $formatted_url = '/' . $formatted_url; } // Append 'query' string if it exists. if ( ! empty( $parsed_url['query'] ) ) { $formatted_url .= '?' . $parsed_url['query']; } return apply_filters( 'wpseo_format_admin_url', $formatted_url ); } /** * Retrieves the sitename. * * @since 3.0.0 * * @return string */ public static function get_site_name() { return YoastSEO()->helpers->site->get_site_name(); } /** * Check if the current opened page is a Yoast SEO page. * * @since 3.0.0 * * @return bool */ public static function is_yoast_seo_page() { return YoastSEO()->helpers->current_page->is_yoast_seo_page(); } /** * Check if the current opened page belongs to Yoast SEO Free. * * @since 3.3.0 * * @param string $current_page The current page the user is on. * * @return bool */ public static function is_yoast_seo_free_page( $current_page ) { $yoast_seo_free_pages = [ 'wpseo_tools', 'wpseo_search_console', ]; return in_array( $current_page, $yoast_seo_free_pages, true ); } /** * Determine if Yoast SEO is in development mode? * * Inspired by JetPack (https://github.com/Automattic/jetpack/blob/master/class.jetpack.php#L1383-L1406). * * @since 3.0.0 * * @return bool */ public static function is_development_mode() { $development_mode = false; if ( defined( 'YOAST_ENVIRONMENT' ) && YOAST_ENVIRONMENT === 'development' ) { $development_mode = true; } elseif ( defined( 'WPSEO_DEBUG' ) ) { $development_mode = WPSEO_DEBUG; } elseif ( site_url() && strpos( site_url(), '.' ) === false ) { $development_mode = true; } /** * Filter the Yoast SEO development mode. * * @since 3.0 * * @param bool $development_mode Is Yoast SEOs development mode active. */ return apply_filters( 'yoast_seo_development_mode', $development_mode ); } /** * Retrieve home URL with proper trailing slash. * * @since 3.3.0 * * @param string $path Path relative to home URL. * @param string|null $scheme Scheme to apply. * * @return string Home URL with optional path, appropriately slashed if not. */ public static function home_url( $path = '', $scheme = null ) { return YoastSEO()->helpers->url->home( $path, $scheme ); } /** * Checks if the WP-REST-API is available. * * @since 3.6 * @since 3.7 Introduced the $minimum_version parameter. * * @param string $minimum_version The minimum version the API should be. * * @return bool Returns true if the API is available. */ public static function is_api_available( $minimum_version = '2.0' ) { return ( defined( 'REST_API_VERSION' ) && version_compare( REST_API_VERSION, $minimum_version, '>=' ) ); } /** * Determine whether or not the metabox should be displayed for a post type. * * @param string|null $post_type Optional. The post type to check the visibility of the metabox for. * * @return bool Whether or not the metabox should be displayed. */ protected static function display_post_type_metabox( $post_type = null ) { if ( ! isset( $post_type ) ) { $post_type = get_post_type(); } if ( ! isset( $post_type ) || ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) ) { return false; } if ( $post_type === 'attachment' && WPSEO_Options::get( 'disable-attachment' ) ) { return false; } return apply_filters( 'wpseo_enable_editor_features_' . $post_type, WPSEO_Options::get( 'display-metabox-pt-' . $post_type ) ); } /** * Determine whether or not the metabox should be displayed for a taxonomy. * * @param string|null $taxonomy Optional. The post type to check the visibility of the metabox for. * * @return bool Whether or not the metabox should be displayed. */ protected static function display_taxonomy_metabox( $taxonomy = null ) { if ( ! isset( $taxonomy ) || ! in_array( $taxonomy, get_taxonomies( [ 'public' => true ], 'names' ), true ) ) { return false; } return WPSEO_Options::get( 'display-metabox-tax-' . $taxonomy ); } /** * Determines whether the metabox is active for the given identifier and type. * * @param string $identifier The identifier to check for. * @param string $type The type to check for. * * @return bool Whether or not the metabox is active. */ public static function is_metabox_active( $identifier, $type ) { if ( $type === 'post_type' ) { return self::display_post_type_metabox( $identifier ); } if ( $type === 'taxonomy' ) { return self::display_taxonomy_metabox( $identifier ); } return false; } /** * Determines whether the plugin is active for the entire network. * * @return bool Whether the plugin is network-active. */ public static function is_plugin_network_active() { return YoastSEO()->helpers->url->is_plugin_network_active(); } /** * Gets the type of the current post. * * @return string The post type, or an empty string. */ public static function get_post_type() { $wp_screen = get_current_screen(); if ( $wp_screen !== null && ! empty( $wp_screen->post_type ) ) { return $wp_screen->post_type; } return ''; } /** * Gets the type of the current page. * * @return string Returns 'post' if the current page is a post edit page. Taxonomy in other cases. */ public static function get_page_type() { global $pagenow; if ( WPSEO_Metabox::is_post_edit( $pagenow ) ) { return 'post'; } return 'taxonomy'; } /** * Getter for the Adminl10n array. Applies the wpseo_admin_l10n filter. * * @return array The Adminl10n array. */ public static function get_admin_l10n() { $post_type = self::get_post_type(); $page_type = self::get_page_type(); $label_object = false; $no_index = false; if ( $page_type === 'post' ) { $label_object = get_post_type_object( $post_type ); $no_index = WPSEO_Options::get( 'noindex-' . $post_type, false ); } else { $label_object = WPSEO_Taxonomy::get_labels(); $wp_screen = get_current_screen(); if ( $wp_screen !== null && ! empty( $wp_screen->taxonomy ) ) { $taxonomy_slug = $wp_screen->taxonomy; $no_index = WPSEO_Options::get( 'noindex-tax-' . $taxonomy_slug, false ); } } $wpseo_admin_l10n = [ 'displayAdvancedTab' => WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) || ! WPSEO_Options::get( 'disableadvanced_meta' ), 'noIndex' => (bool) $no_index, 'isPostType' => (bool) get_post_type(), 'postType' => get_post_type(), 'postTypeNamePlural' => ( $page_type === 'post' ) ? $label_object->label : $label_object->name, 'postTypeNameSingular' => ( $page_type === 'post' ) ? $label_object->labels->singular_name : $label_object->singular_name, 'isBreadcrumbsDisabled' => WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ), 'isAiFeatureActive' => (bool) WPSEO_Options::get( 'enable_ai_generator' ), ]; $additional_entries = apply_filters( 'wpseo_admin_l10n', [] ); if ( is_array( $additional_entries ) ) { $wpseo_admin_l10n = array_merge( $wpseo_admin_l10n, $additional_entries ); } return $wpseo_admin_l10n; } /** * Retrieves the analysis worker log level. Defaults to errors only. * * Uses bool YOAST_SEO_DEBUG as flag to enable logging. Off equals ERROR. * Uses string YOAST_SEO_DEBUG_ANALYSIS_WORKER as log level for the Analysis * Worker. Defaults to INFO. * Can be: TRACE, DEBUG, INFO, WARN or ERROR. * * @return string The log level to use. */ public static function get_analysis_worker_log_level() { if ( defined( 'YOAST_SEO_DEBUG' ) && YOAST_SEO_DEBUG ) { return defined( 'YOAST_SEO_DEBUG_ANALYSIS_WORKER' ) ? YOAST_SEO_DEBUG_ANALYSIS_WORKER : 'INFO'; } return 'ERROR'; } /** * Returns the unfiltered home URL. * * In case WPML is installed, returns the original home_url and not the WPML version. * In case of a multisite setup we return the network_home_url. * * @codeCoverageIgnore * * @return string The home url. */ public static function get_home_url() { return YoastSEO()->helpers->url->network_safe_home_url(); } /** * Prepares data for outputting as JSON. * * @param array $data The data to format. * * @return string|false The prepared JSON string. */ public static function format_json_encode( $data ) { $flags = ( JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( self::is_development_mode() ) { $flags = ( $flags | JSON_PRETTY_PRINT ); /** * Filter the Yoast SEO development mode. * * @param array $data Allows filtering of the JSON data for debug purposes. */ $data = apply_filters( 'wpseo_debug_json_data', $data ); } // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.FoundWithAdditionalParams -- This is the definition of format_json_encode. return wp_json_encode( $data, $flags ); } /** * Extends the allowed post tags with accessibility-related attributes. * * @codeCoverageIgnore * * @param array $allowed_post_tags The allowed post tags. * * @return array The allowed tags including post tags, input tags and select tags. */ public static function extend_kses_post_with_a11y( $allowed_post_tags ) { static $a11y_tags; if ( isset( $a11y_tags ) === false ) { $a11y_tags = [ 'button' => [ 'aria-expanded' => true, 'aria-controls' => true, ], 'div' => [ 'tabindex' => true, ], // Below are attributes that are needed for backwards compatibility (WP < 5.1). 'span' => [ 'aria-hidden' => true, ], 'input' => [ 'aria-describedby' => true, ], 'select' => [ 'aria-describedby' => true, ], 'textarea' => [ 'aria-describedby' => true, ], ]; // Add the global allowed attributes to each html element. $a11y_tags = array_map( '_wp_add_global_attributes', $a11y_tags ); } return array_merge_recursive( $allowed_post_tags, $a11y_tags ); } /** * Extends the allowed post tags with input, select and option tags. * * @codeCoverageIgnore * * @param array $allowed_post_tags The allowed post tags. * * @return array The allowed tags including post tags, input tags, select tags and option tags. */ public static function extend_kses_post_with_forms( $allowed_post_tags ) { static $input_tags; if ( isset( $input_tags ) === false ) { $input_tags = [ 'input' => [ 'accept' => true, 'accesskey' => true, 'align' => true, 'alt' => true, 'autocomplete' => true, 'autofocus' => true, 'checked' => true, 'contenteditable' => true, 'dirname' => true, 'disabled' => true, 'draggable' => true, 'dropzone' => true, 'form' => true, 'formaction' => true, 'formenctype' => true, 'formmethod' => true, 'formnovalidate' => true, 'formtarget' => true, 'height' => true, 'hidden' => true, 'lang' => true, 'list' => true, 'max' => true, 'maxlength' => true, 'min' => true, 'multiple' => true, 'name' => true, 'pattern' => true, 'placeholder' => true, 'readonly' => true, 'required' => true, 'size' => true, 'spellcheck' => true, 'src' => true, 'step' => true, 'tabindex' => true, 'translate' => true, 'type' => true, 'value' => true, 'width' => true, /* * Below are attributes that are needed for backwards compatibility (WP < 5.1). * They are used for the social media image in the metabox. * These can be removed once we move to the React versions of the social previews. */ 'data-target' => true, 'data-target-id' => true, ], 'select' => [ 'accesskey' => true, 'autofocus' => true, 'contenteditable' => true, 'disabled' => true, 'draggable' => true, 'dropzone' => true, 'form' => true, 'hidden' => true, 'lang' => true, 'multiple' => true, 'name' => true, 'onblur' => true, 'onchange' => true, 'oncontextmenu' => true, 'onfocus' => true, 'oninput' => true, 'oninvalid' => true, 'onreset' => true, 'onsearch' => true, 'onselect' => true, 'onsubmit' => true, 'required' => true, 'size' => true, 'spellcheck' => true, 'tabindex' => true, 'translate' => true, ], 'option' => [ 'class' => true, 'disabled' => true, 'id' => true, 'label' => true, 'selected' => true, 'value' => true, ], ]; // Add the global allowed attributes to each html element. $input_tags = array_map( '_wp_add_global_attributes', $input_tags ); } return array_merge_recursive( $allowed_post_tags, $input_tags ); } /** * Gets an array of enabled features. * * @return string[] The array of enabled features. */ public static function retrieve_enabled_features() { /** * The feature flag integration. * * @var Feature_Flag_Integration $feature_flag_integration */ $feature_flag_integration = YoastSEO()->classes->get( Feature_Flag_Integration::class ); return $feature_flag_integration->get_enabled_features(); } } inc/class-my-yoast-api-request.php 0000644 00000012616 15174712003 0013156 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ /** * Handles requests to MyYoast. */ class WPSEO_MyYoast_Api_Request { /** * The Request URL. * * @var string */ protected $url; /** * The request parameters. * * @var array */ protected $args = [ 'method' => 'GET', 'timeout' => 5, 'headers' => [ 'Accept-Encoding' => '*', 'Expect' => '', ], ]; /** * Contains the fetched response. * * @var stdClass */ protected $response; /** * Contains the error message when request went wrong. * * @var string */ protected $error_message = ''; /** * Constructor. * * @codeCoverageIgnore * * @param string $url The request url. * @param array $args The request arguments. */ public function __construct( $url, array $args = [] ) { $this->url = 'https://my.yoast.com/api/' . $url; $this->args = wp_parse_args( $args, $this->args ); } /** * Fires the request. * * @return bool True when request is successful. */ public function fire() { try { $response = $this->do_request( $this->url, $this->args ); $response = $this->decode_response( $response ); $this->response = $this->validate_response( $response ); return true; } catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) { $this->error_message = $bad_request_exception->getMessage(); return false; } } /** * Retrieves the error message. * * @return string The set error message. */ public function get_error_message() { return $this->error_message; } /** * Retrieves the response. * * @return stdClass The response object. */ public function get_response() { return $this->response; } /** * Performs the request using WordPress internals. * * @codeCoverageIgnore * * @param string $url The request URL. * @param array $request_arguments The request arguments. * * @return string The retrieved body. * @throws WPSEO_MyYoast_Bad_Request_Exception When request is invalid. */ protected function do_request( $url, $request_arguments ) { $request_arguments = $this->enrich_request_arguments( $request_arguments ); $response = wp_remote_request( $url, $request_arguments ); if ( is_wp_error( $response ) ) { throw new WPSEO_MyYoast_Bad_Request_Exception( $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $response_message = wp_remote_retrieve_response_message( $response ); // Do nothing, response code is okay. if ( $response_code === 200 ) { return wp_remote_retrieve_body( $response ); } throw new WPSEO_MyYoast_Bad_Request_Exception( esc_html( $response_message ), (int) $response_code ); } /** * Decodes the JSON encoded response. * * @param string $response The response to decode. * * @return stdClass The json decoded response. * @throws WPSEO_MyYoast_Invalid_JSON_Exception When decoded string is not a JSON object. */ protected function decode_response( $response ) { $response = json_decode( $response ); if ( ! is_object( $response ) ) { throw new WPSEO_MyYoast_Invalid_JSON_Exception( esc_html__( 'No JSON object was returned.', 'wordpress-seo' ), ); } return $response; } /** * Validates that all the needed fields are in de decoded response. * * @param stdClass $response The response to validate. * * @return stdClass The json decoded response. * @throws WPSEO_MyYoast_Invalid_JSON_Exception When not all needed fields are found. */ private function validate_response( $response ) { if ( isset( $response->url, $response->subscriptions ) && is_array( $response->subscriptions ) ) { return $response; } throw new WPSEO_MyYoast_Invalid_JSON_Exception( esc_html__( 'Not all needed fields are present.', 'wordpress-seo' ), ); } /** * Checks if MyYoast tokens are allowed and adds the token to the request body. * * When tokens are disallowed it will add the url to the request body. * * @param array $request_arguments The arguments to enrich. * * @return array The enriched arguments. */ protected function enrich_request_arguments( array $request_arguments ) { $request_arguments = wp_parse_args( $request_arguments, [ 'headers' => [] ] ); $addon_version_headers = $this->get_installed_addon_versions(); foreach ( $addon_version_headers as $addon => $version ) { $request_arguments['headers'][ $addon . '-version' ] = $version; } $request_body = $this->get_request_body(); if ( $request_body !== [] ) { $request_arguments['body'] = $request_body; } return $request_arguments; } /** * Retrieves the request body based on URL or access token support. * * @codeCoverageIgnore * * @return array The request body. */ public function get_request_body() { return [ 'url' => WPSEO_Utils::get_home_url() ]; } /** * Wraps the get current user id function. * * @codeCoverageIgnore * * @return int The user id. */ protected function get_current_user_id() { return get_current_user_id(); } /** * Retrieves the installed addons as http headers. * * @codeCoverageIgnore * * @return array The installed addon versions. */ protected function get_installed_addon_versions() { $addon_manager = new WPSEO_Addon_Manager(); return $addon_manager->get_installed_addons_versions(); } } inc/class-wpseo-meta.php 0000644 00000104263 15174712003 0011220 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.5.0 */ use Yoast\WP\SEO\Config\Schema_Types; use Yoast\WP\SEO\Helpers\Schema\Article_Helper; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * This class implements defaults and value validation for all WPSEO Post Meta values. * * Some guidelines: * - To update a meta value, you can just use update_post_meta() with the full (prefixed) meta key * or the convenience method WPSEO_Meta::set_value() with the internal key. * All updates will be automatically validated. * Meta values will only be saved to the database if they are *not* the same as the default to * keep database load low. * - To retrieve a WPSEO meta value, you **must** use WPSEO_Meta::get_value() which will always return a * string value, either the saved value or the default. * This method can also retrieve a complete set of WPSEO meta values for one specific post, see * the method documentation for the parameters. * * {@internal Unfortunately there isn't a filter available to hook into before returning the results * for get_post_meta(), get_post_custom() and the likes. That would have been the * preferred solution.}} * * {@internal All WP native get_meta() results get cached internally, so no need to cache locally.}} * {@internal Use $key when the key is the WPSEO internal name (without prefix), $meta_key when it * includes the prefix.}} */ class WPSEO_Meta { /** * Prefix for all WPSEO meta values in the database. * * {@internal If at any point this would change, quite apart from an upgrade routine, * this also will need to be changed in the wpml-config.xml file.}} * * @var string */ public static $meta_prefix = '_yoast_wpseo_'; /** * Prefix for all WPSEO meta value form field names and ids. * * @var string */ public static $form_prefix = 'yoast_wpseo_'; /** * Allowed length of the meta description. * * @var int */ public static $meta_length = 156; /** * Reason the meta description is not the default length. * * @var string */ public static $meta_length_reason = ''; /** * Meta box field definitions for the meta box form. * * {@internal * - Titles, help texts, description text and option labels are added via a translate_meta_boxes() method * in the relevant child classes (WPSEO_Metabox and WPSEO_Social_admin) as they are only needed there. * - Beware: even though the meta keys are divided into subsets, they still have to be uniquely named!}} * * @var array * Array format: * (required) 'type' => (string) field type. i.e. text / textarea / checkbox / * radio / select / multiselect / upload etc. * (recommended) 'default_value' => (string|array) default value for the field. * IMPORTANT: * - if the field has options, the default has to be the * key of one of the options. * - if the field is a text field, the default **has** to be * an empty string as otherwise the user can't save * an empty value/delete the meta value. * - if the field is a checkbox, the only valid values * are 'on' or 'off'. * (semi-required) 'options' => (array) options for used with (multi-)select and radio * fields, required if that's the field type. * key = (string) value which will be saved to db. * value = (string) text label for the option. * (optional) 'autocomplete' => (bool) whether autocomplete is on for text fields, * defaults to true. * (optional) 'class' => (string) classname(s) to add to the actual <input> tag. * (optional) 'rows' => (int) number of rows for a textarea, defaults to 3. * (optional) 'serialized' => (bool) whether the value is expected to be serialized, * i.e. an array or object, defaults to false. * Currently only used by add-on plugins. */ public static $meta_fields = [ 'general' => [ 'focuskw' => [ 'type' => 'hidden', 'title' => '', ], 'title' => [ 'type' => 'hidden', 'default_value' => '', ], 'metadesc' => [ 'type' => 'hidden', 'default_value' => '', 'class' => 'metadesc', 'rows' => 2, ], 'linkdex' => [ 'type' => 'hidden', 'default_value' => '0', ], 'content_score' => [ 'type' => 'hidden', 'default_value' => '0', ], 'inclusive_language_score' => [ 'type' => 'hidden', 'default_value' => '0', ], 'is_cornerstone' => [ 'type' => 'hidden', 'default_value' => 'false', ], ], 'advanced' => [ 'meta-robots-noindex' => [ 'type' => 'hidden', 'default_value' => '0', // = post-type default. 'options' => [ '0' => '', // Post type default. '2' => '', // Index. '1' => '', // No-index. ], ], 'meta-robots-nofollow' => [ 'type' => 'hidden', 'default_value' => '0', // = follow. 'options' => [ '0' => '', // Follow. '1' => '', // No-follow. ], ], 'meta-robots-adv' => [ 'type' => 'hidden', 'default_value' => '', 'options' => [ 'noimageindex' => '', 'noarchive' => '', 'nosnippet' => '', ], ], 'bctitle' => [ 'type' => 'hidden', 'default_value' => '', ], 'canonical' => [ 'type' => 'hidden', 'default_value' => '', ], 'redirect' => [ 'type' => 'url', 'default_value' => '', ], ], 'social' => [], 'schema' => [ 'schema_page_type' => [ 'type' => 'hidden', 'options' => Schema_Types::PAGE_TYPES, ], 'schema_article_type' => [ 'type' => 'hidden', 'hide_on_pages' => true, 'options' => Schema_Types::ARTICLE_TYPES, ], ], /* Fields we should validate & save, but not show on any form. */ 'non_form' => [ 'linkdex' => [ 'type' => null, 'default_value' => '0', ], ], ]; /** * Helper property - reverse index of the definition array. * * Format: [full meta key including prefix] => array * ['subset'] => (string) primary index * ['key'] => (string) internal key * * @var array */ public static $fields_index = []; /** * Helper property - array containing only the defaults in the format: * [full meta key including prefix] => (string) default value * * @var array */ public static $defaults = []; /** * Helper property to define the social network meta field definitions - networks. * * @var array */ private static $social_networks = [ 'opengraph' => 'opengraph', 'twitter' => 'twitter', ]; /** * Helper property to define the social network meta field definitions - fields and their type. * * @var array */ private static $social_fields = [ 'title' => 'hidden', 'description' => 'hidden', 'image' => 'hidden', 'image-id' => 'hidden', ]; /** * Register our actions and filters. * * @return void */ public static function init() { foreach ( self::$social_networks as $option => $network ) { if ( WPSEO_Options::get( $option, false, [ 'wpseo_social' ] ) === true ) { foreach ( self::$social_fields as $box => $type ) { self::$meta_fields['social'][ $network . '-' . $box ] = [ 'type' => $type, 'default_value' => '', ]; } } } unset( $option, $network, $box, $type ); /** * Allow add-on plugins to register their meta fields for management by this class. * Calls to add_filter() must be made before plugins_loaded prio 14. */ $extra_fields = apply_filters( 'add_extra_wpseo_meta_fields', [] ); if ( is_array( $extra_fields ) ) { self::$meta_fields = self::array_merge_recursive_distinct( $extra_fields, self::$meta_fields ); } unset( $extra_fields ); foreach ( self::$meta_fields as $subset => $field_group ) { foreach ( $field_group as $key => $field_def ) { register_meta( 'post', self::$meta_prefix . $key, [ 'sanitize_callback' => [ self::class, 'sanitize_post_meta' ] ], ); // Set the $fields_index property for efficiency. self::$fields_index[ self::$meta_prefix . $key ] = [ 'subset' => $subset, 'key' => $key, ]; // Set the $defaults property for efficiency. if ( isset( $field_def['default_value'] ) ) { self::$defaults[ self::$meta_prefix . $key ] = $field_def['default_value']; } else { // Meta will always be a string, so let's make the meta meta default also a string. self::$defaults[ self::$meta_prefix . $key ] = ''; } } } unset( $subset, $field_group, $key, $field_def ); self::filter_schema_article_types(); add_filter( 'update_post_metadata', [ self::class, 'remove_meta_if_default' ], 10, 5 ); add_filter( 'add_post_metadata', [ self::class, 'dont_save_meta_if_default' ], 10, 4 ); } /** * Retrieve the meta box form field definitions for the given tab and post type. * * @param string $tab Tab for which to retrieve the field definitions. * @param string $post_type Post type of the current post. * * @return array Array containing the meta box field definitions. */ public static function get_meta_field_defs( $tab, $post_type = 'post' ) { if ( ! isset( self::$meta_fields[ $tab ] ) ) { return []; } $field_defs = self::$meta_fields[ $tab ]; switch ( $tab ) { case 'non-form': // Prevent non-form fields from being passed to forms. $field_defs = []; break; case 'advanced': global $post; if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) { return []; } $post_type = ''; if ( isset( $post->post_type ) ) { $post_type = $post->post_type; } elseif ( ! isset( $post->post_type ) && isset( $_GET['post_type'] ) ) { $post_type = sanitize_text_field( $_GET['post_type'] ); } if ( $post_type === '' ) { return []; } /* Don't show the breadcrumb title field if breadcrumbs aren't enabled. */ if ( WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ) ) { unset( $field_defs['bctitle'] ); } if ( empty( $post->ID ) || ( ! empty( $post->ID ) && self::get_value( 'redirect', $post->ID ) === '' ) ) { unset( $field_defs['redirect'] ); } break; case 'schema': if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) { return []; } $field_defs['schema_page_type']['default'] = WPSEO_Options::get( 'schema-page-type-' . $post_type ); $article_helper = new Article_Helper(); if ( $article_helper->is_article_post_type( $post_type ) ) { $default_schema_article_type = WPSEO_Options::get( 'schema-article-type-' . $post_type ); /** This filter is documented in inc/options/class-wpseo-option-titles.php */ $allowed_article_types = apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ); if ( ! array_key_exists( $default_schema_article_type, $allowed_article_types ) ) { $default_schema_article_type = WPSEO_Options::get_default( 'wpseo_titles', 'schema-article-type-' . $post_type ); } $field_defs['schema_article_type']['default'] = $default_schema_article_type; } else { unset( $field_defs['schema_article_type'] ); } break; } /** * Filter the WPSEO metabox form field definitions for a tab. * {tab} can be 'general', 'advanced' or 'social'. * * @param array $field_defs Metabox form field definitions. * @param string $post_type Post type of the post the metabox is for, defaults to 'post'. * * @return array */ return apply_filters( 'wpseo_metabox_entries_' . $tab, $field_defs, $post_type ); } /** * Validate the post meta values. * * @param mixed $meta_value The new value. * @param string $meta_key The full meta key (including prefix). * * @return string Validated meta value. */ public static function sanitize_post_meta( $meta_value, $meta_key ) { $field_def = self::$meta_fields[ self::$fields_index[ $meta_key ]['subset'] ][ self::$fields_index[ $meta_key ]['key'] ]; $clean = self::$defaults[ $meta_key ]; switch ( true ) { case ( $meta_key === self::$meta_prefix . 'linkdex' ): $int = WPSEO_Utils::validate_int( $meta_value ); if ( $int !== false && $int >= 0 ) { $clean = (string) $int; // Convert to string to make sure default check works. } break; case ( $field_def['type'] === 'checkbox' ): // Only allow value if it's one of the predefined options. if ( in_array( $meta_value, [ 'on', 'off' ], true ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'select' || $field_def['type'] === 'radio' ): // Only allow value if it's one of the predefined options. if ( isset( $field_def['options'][ $meta_value ] ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'meta-robots-adv' ): $clean = self::validate_meta_robots_adv( $meta_value ); break; case ( $field_def['type'] === 'url' || $meta_key === self::$meta_prefix . 'canonical' ): // Validate as url(-part). $url = WPSEO_Utils::sanitize_url( $meta_value ); if ( $url !== '' ) { $clean = $url; } break; case ( $field_def['type'] === 'upload' && in_array( $meta_key, [ self::$meta_prefix . 'opengraph-image', self::$meta_prefix . 'twitter-image' ], true ) ): // Validate as url. $url = WPSEO_Utils::sanitize_url( $meta_value, [ 'http', 'https', 'ftp', 'ftps' ] ); if ( $url !== '' ) { $clean = $url; } break; case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'is_cornerstone' ): $clean = $meta_value; /* * This used to be a checkbox, then became a hidden input. * To make sure the value remains consistent, we cast 'true' to '1'. */ if ( $meta_value === 'true' ) { $clean = '1'; } break; case ( $field_def['type'] === 'hidden' && isset( $field_def['options'] ) ): // Only allow value if it's one of the predefined options. if ( isset( $field_def['options'][ $meta_value ] ) ) { $clean = $meta_value; } break; case ( $field_def['type'] === 'textarea' ): if ( is_string( $meta_value ) ) { // Remove line breaks and tabs. // @todo [JRF => Yoast] Verify that line breaks and the likes aren't allowed/recommended in meta header fields. $meta_value = str_replace( [ "\n", "\r", "\t", ' ' ], ' ', $meta_value ); $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) ); } break; case ( $field_def['type'] === 'multiselect' ): $clean = $meta_value; break; case ( $field_def['type'] === 'text' ): default: if ( is_string( $meta_value ) ) { $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) ); } break; } $clean = apply_filters( 'wpseo_sanitize_post_meta_' . $meta_key, $clean, $meta_value, $field_def, $meta_key ); return $clean; } /** * Validate a meta-robots-adv meta value. * * @todo [JRF => Yoast] Verify that this logic for the prioritisation is correct. * * @param array|string $meta_value The value to validate. * * @return string Clean value. */ public static function validate_meta_robots_adv( $meta_value ) { $clean = self::$meta_fields['advanced']['meta-robots-adv']['default_value']; $options = self::$meta_fields['advanced']['meta-robots-adv']['options']; if ( is_string( $meta_value ) ) { $meta_value = explode( ',', $meta_value ); } if ( is_array( $meta_value ) && $meta_value !== [] ) { $meta_value = array_map( 'trim', $meta_value ); // Individual selected entries. $cleaning = []; foreach ( $meta_value as $value ) { if ( isset( $options[ $value ] ) ) { $cleaning[] = $value; } } if ( $cleaning !== [] ) { $clean = implode( ',', $cleaning ); } unset( $cleaning, $value ); } return $clean; } /** * Prevent saving of default values and remove potential old value from the database if replaced by a default. * * @param bool $check The current status to allow updating metadata for the given type. * @param int $object_id ID of the current object for which the meta is being updated. * @param string $meta_key The full meta key (including prefix). * @param string $meta_value New meta value. * @param string $prev_value The old meta value. * * @return bool|null True = stop saving, null = continue saving. */ public static function remove_meta_if_default( $check, $object_id, $meta_key, $meta_value, $prev_value = '' ) { /* If it's one of our meta fields, check against default. */ if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) { if ( $prev_value !== '' ) { delete_post_meta( $object_id, $meta_key, $prev_value ); } else { delete_post_meta( $object_id, $meta_key ); } return true; // Stop saving the value. } return $check; // Go on with the normal execution (update) in meta.php. } /** * Prevent adding of default values to the database. * * @param bool $check The current status to allow adding metadata for the given type. * @param int $object_id ID of the current object for which the meta is being added. * @param string $meta_key The full meta key (including prefix). * @param string $meta_value New meta value. * * @return bool|null True = stop saving, null = continue saving. */ public static function dont_save_meta_if_default( $check, $object_id, $meta_key, $meta_value ) { /* If it's one of our meta fields, check against default. */ if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) { return true; // Stop saving the value. } return $check; // Go on with the normal execution (add) in meta.php. } /** * Is the given meta value the same as the default value ? * * @param string $meta_key The full meta key (including prefix). * @param mixed $meta_value The value to check. * * @return bool */ public static function meta_value_is_default( $meta_key, $meta_value ) { return ( isset( self::$defaults[ $meta_key ] ) && $meta_value === self::$defaults[ $meta_key ] ); } /** * Get a custom post meta value. * * Returns the default value if the meta value has not been set. * * {@internal Unfortunately there isn't a filter available to hook into before returning * the results for get_post_meta(), get_post_custom() and the likes. That * would have been the preferred solution.}} * * @param string $key Internal key of the value to get (without prefix). * @param int $postid Post ID of the post to get the value for. * * @return string All 'normal' values returned from get_post_meta() are strings. * Objects and arrays are possible, but not used by this plugin * and therefore discarted (except when the special 'serialized' field def * value is set to true - only used by add-on plugins for now). * Will return the default value if no value was found. * Will return empty string if no default was found (not one of our keys) or * if the post does not exist. */ public static function get_value( $key, $postid = 0 ) { global $post; $postid = absint( $postid ); if ( $postid === 0 ) { if ( ( isset( $post ) && is_object( $post ) ) && ( isset( $post->post_status ) && $post->post_status !== 'auto-draft' ) ) { $postid = $post->ID; } else { return ''; } } $custom = get_post_custom( $postid ); // Array of strings or empty array. $table_key = self::$meta_prefix . $key; // Populate the field_def using the field_index lookup array. $field_def = []; if ( isset( self::$fields_index[ $table_key ] ) ) { $field_def = self::$meta_fields[ self::$fields_index[ $table_key ]['subset'] ][ self::$fields_index[ $table_key ]['key'] ]; } // Check if we have a custom post meta entry. if ( isset( $custom[ $table_key ][0] ) ) { $unserialized = maybe_unserialize( $custom[ $table_key ][0] ); // Check if it is already unserialized. if ( $custom[ $table_key ][0] === $unserialized ) { return $custom[ $table_key ][0]; } // Check whether we need to unserialize it. if ( isset( $field_def['serialized'] ) && $field_def['serialized'] === true ) { // Ok, serialize value expected/allowed. return $unserialized; } } // Meta was either not found or found, but object/array while not allowed to be. if ( isset( self::$defaults[ self::$meta_prefix . $key ] ) ) { // Update the default value to the current post type. switch ( $key ) { case 'schema_page_type': case 'schema_article_type': return ''; } return self::$defaults[ self::$meta_prefix . $key ]; } /* * Shouldn't ever happen, means not one of our keys as there will always be a default available * for all our keys. */ return ''; } /** * Update a meta value for a post. * * @param string $key The internal key of the meta value to change (without prefix). * @param mixed $meta_value The value to set the meta to. * @param int $post_id The ID of the post to change the meta for. * * @return bool Whether the value was changed. */ public static function set_value( $key, $meta_value, $post_id ) { /* * Slash the data, because `update_metadata` will unslash it and we have already unslashed it. * Related issue: https://github.com/Yoast/YoastSEO.js/issues/2158 */ $meta_value = wp_slash( $meta_value ); return update_post_meta( $post_id, self::$meta_prefix . $key, $meta_value ); } /** * Deletes a meta value for a post. * * @param string $key The internal key of the meta value to change (without prefix). * @param int $post_id The ID of the post to delete the meta for. * * @return bool Whether the delete was successful or not. */ public static function delete( $key, $post_id ) { return delete_post_meta( $post_id, self::$meta_prefix . $key ); } /** * Used for imports, this functions imports the value of $old_metakey into $new_metakey for those post * where no WPSEO meta data has been set. * Optionally deletes the $old_metakey values. * * @param string $old_metakey The old key of the meta value. * @param string $new_metakey The new key, usually the WPSEO meta key (including prefix). * @param bool $delete_old Whether to delete the old meta key/value-sets. * * @return void */ public static function replace_meta( $old_metakey, $new_metakey, $delete_old = false ) { global $wpdb; /* * Get only those rows where no wpseo meta values exist for the same post * (with the exception of linkdex as that will be set independently of whether the post has been edited). * * {@internal Query is pretty well optimized this way.}} */ $query = $wpdb->prepare( " SELECT `a`.* FROM {$wpdb->postmeta} AS a WHERE `a`.`meta_key` = %s AND NOT EXISTS ( SELECT DISTINCT `post_id` , count( `meta_id` ) AS count FROM {$wpdb->postmeta} AS b WHERE `a`.`post_id` = `b`.`post_id` AND `meta_key` LIKE %s AND `meta_key` <> %s GROUP BY `post_id` ) ;", $old_metakey, $wpdb->esc_like( self::$meta_prefix . '%' ), self::$meta_prefix . 'linkdex', ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { update_post_meta( $old->post_id, $new_metakey, $old->meta_value ); } } // Delete old keys. if ( $delete_old === true ) { delete_post_meta_by_key( $old_metakey ); } } /** * General clean-up of the saved meta values. * - Remove potentially lingering old meta keys; * - Remove all default and invalid values. * * @return void */ public static function clean_up() { global $wpdb; /* * Clean up '_yoast_wpseo_meta-robots'. * * Retrieve all '_yoast_wpseo_meta-robots' meta values and convert if no new values found. * * {@internal Query is pretty well optimized this way.}} * * @todo [JRF => Yoast] Find out all possible values which the old '_yoast_wpseo_meta-robots' could contain * to convert the data correctly. */ $query = $wpdb->prepare( " SELECT `a`.* FROM {$wpdb->postmeta} AS a WHERE `a`.`meta_key` = %s AND NOT EXISTS ( SELECT DISTINCT `post_id` , count( `meta_id` ) AS count FROM {$wpdb->postmeta} AS b WHERE `a`.`post_id` = `b`.`post_id` AND ( `meta_key` = %s OR `meta_key` = %s ) GROUP BY `post_id` ) ;", self::$meta_prefix . 'meta-robots', self::$meta_prefix . 'meta-robots-noindex', self::$meta_prefix . 'meta-robots-nofollow', ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { $old_values = explode( ',', $old->meta_value ); foreach ( $old_values as $value ) { if ( $value === 'noindex' ) { update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-noindex', 1 ); } elseif ( $value === 'nofollow' ) { update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-nofollow', 1 ); } } } } unset( $query, $oldies, $old, $old_values, $value ); // Delete old keys. delete_post_meta_by_key( self::$meta_prefix . 'meta-robots' ); /* * Remove all default values and (most) invalid option values. * Invalid option values for the multiselect (meta-robots-adv) field will be dealt with seperately. * * {@internal Some of the defaults have changed in v1.5, but as the defaults will * be removed and new defaults will now automatically be passed when no * data found, this update is automatic (as long as we remove the old * values which we do in the below routine).}} * * {@internal Unfortunately we can't use the normal delete_meta() with key/value combination * as '' (empty string) values will be ignored and would result in all metas * with that key being deleted, not just the empty fields. * Still, the below implementation is largely based on the delete_meta() function.}} */ $query = []; foreach ( self::$meta_fields as $subset => $field_group ) { foreach ( $field_group as $key => $field_def ) { if ( ! isset( $field_def['default_value'] ) ) { continue; } if ( isset( $field_def['options'] ) && is_array( $field_def['options'] ) && $field_def['options'] !== [] ) { $valid = $field_def['options']; // Remove the default value from the valid options. unset( $valid[ $field_def['default_value'] ] ); $valid = array_keys( $valid ); $query[] = $wpdb->prepare( "( meta_key = %s AND meta_value NOT IN ( '" . implode( "','", esc_sql( $valid ) ) . "' ) )", self::$meta_prefix . $key, ); unset( $valid ); } elseif ( is_string( $field_def['default_value'] ) && $field_def['default_value'] !== '' ) { $query[] = $wpdb->prepare( '( meta_key = %s AND meta_value = %s )', self::$meta_prefix . $key, $field_def['default_value'], ); } else { $query[] = $wpdb->prepare( "( meta_key = %s AND meta_value = '' )", self::$meta_prefix . $key, ); } } } unset( $subset, $field_group, $key, $field_def ); $query = "SELECT meta_id FROM {$wpdb->postmeta} WHERE " . implode( ' OR ', $query ) . ';'; $meta_ids = $wpdb->get_col( $query ); if ( is_array( $meta_ids ) && $meta_ids !== [] ) { // WP native action. do_action( 'delete_post_meta', $meta_ids, null, null, null ); $query = "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN( " . implode( ',', $meta_ids ) . ' )'; $count = $wpdb->query( $query ); if ( $count ) { foreach ( $meta_ids as $object_id ) { wp_cache_delete( $object_id, 'post_meta' ); } // WP native action. do_action( 'deleted_post_meta', $meta_ids, null, null, null ); } } unset( $query, $meta_ids, $count, $object_id ); /* * Deal with the multiselect (meta-robots-adv) field. * * Removes invalid option combinations, such as 'none,noarchive'. * * Default values have already been removed, so we should have a small result set and * (hopefully) even smaller set of invalid results. */ $query = $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s", self::$meta_prefix . 'meta-robots-adv', ); $oldies = $wpdb->get_results( $query ); if ( is_array( $oldies ) && $oldies !== [] ) { foreach ( $oldies as $old ) { $clean = self::validate_meta_robots_adv( $old->meta_value ); if ( $clean !== $old->meta_value ) { if ( $clean !== self::$meta_fields['advanced']['meta-robots-adv']['default_value'] ) { update_metadata_by_mid( 'post', $old->meta_id, $clean ); } else { delete_metadata_by_mid( 'post', $old->meta_id ); } } } } unset( $query, $oldies, $old, $clean ); do_action( 'wpseo_meta_clean_up' ); } /** * Recursively merge a variable number of arrays, using the left array as base, * giving priority to the right array. * * Difference with native array_merge_recursive(): * array_merge_recursive converts values with duplicate keys to arrays rather than * overwriting the value in the first array with the duplicate value in the second array. * * array_merge_recursive_distinct does not change the data types of the values in the arrays. * Matching keys' values in the second array overwrite those in the first array, as is the * case with array_merge. * * Freely based on information found on http://www.php.net/manual/en/function.array-merge-recursive.php * * {@internal Should be moved to a general utility class.}} * * @return array */ public static function array_merge_recursive_distinct() { $arrays = func_get_args(); if ( count( $arrays ) < 2 ) { if ( $arrays === [] ) { return []; } else { return $arrays[0]; } } $merged = array_shift( $arrays ); foreach ( $arrays as $array ) { foreach ( $array as $key => $value ) { if ( is_array( $value ) && ( isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) ) { $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value ); } else { $merged[ $key ] = $value; } } unset( $key, $value ); } return $merged; } /** * Counts the total of all the keywords being used for posts except the given one. * * @param string $keyword The keyword to be counted. * @param int $post_id The id of the post to which the keyword belongs. * * @return array */ public static function keyword_usage( $keyword, $post_id ) { if ( empty( $keyword ) ) { return []; } /** * The indexable repository. * * @var Indexable_Repository $repository */ $repository = YoastSEO()->classes->get( Indexable_Repository::class ); $post_ids = $repository->query() ->select( 'object_id' ) ->where( 'primary_focus_keyword', $keyword ) ->where( 'object_type', 'post' ) ->where_not_equal( 'object_id', $post_id ) ->where_not_equal( 'post_status', 'trash' ) ->limit( 2 ) // Limit to 2 results to save time and resources. ->find_array(); // Get object_id from each subarray in $post_ids. $post_ids = ( is_array( $post_ids ) ) ? array_column( $post_ids, 'object_id' ) : []; /* * If Premium is installed, get the additional keywords as well. * We only check for the additional keywords if we've not already found two. * In that case there's no use for an additional query as we already know * that the keyword has been used multiple times before. */ if ( count( $post_ids ) < 2 ) { /** * Allows enhancing the array of posts' that share their focus keywords with the post's focus keywords. * * @param array $post_ids The array of posts' ids that share their related keywords with the post. * @param string $keyword The keyword to search for. * @param int $post_id The id of the post the keyword is associated to. */ $post_ids = apply_filters( 'wpseo_posts_for_focus_keyword', $post_ids, $keyword, $post_id ); } return $post_ids; } /** * Returns the post types for the given post ids. * * @param array $post_ids The post ids to get the post types for. * * @return array The post types. */ public static function post_types_for_ids( $post_ids ) { // Check if post ids is not empty. if ( ! empty( $post_ids ) ) { /** * The indexable repository. * * @var Indexable_Repository $repository */ $repository = YoastSEO()->classes->get( Indexable_Repository::class ); // Get the post subtypes for the posts that share the keyword. $post_types = $repository->query() ->select( 'object_sub_type' ) ->where_in( 'object_id', $post_ids ) ->find_array(); // Get object_sub_type from each subarray in $post_ids. $post_types = array_column( $post_types, 'object_sub_type' ); } else { $post_types = []; } return $post_types; } /** * Filter the schema article types. * * @return void */ public static function filter_schema_article_types() { /** This filter is documented in inc/options/class-wpseo-option-titles.php */ self::$meta_fields['schema']['schema_article_type']['options'] = apply_filters( 'wpseo_schema_article_types', self::$meta_fields['schema']['schema_article_type']['options'] ); } } inc/interface-wpseo-wordpress-integration.php 0000644 00000000534 15174712003 0015472 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ if ( ! interface_exists( 'WPSEO_WordPress_Integration' ) ) { /** * An interface for registering integrations with WordPress. */ interface WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks(); } } inc/class-rewrite.php 0000644 00000017123 15174712003 0010616 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Frontend */ /** * This code handles the category rewrites. */ class WPSEO_Rewrite { /** * Class constructor. */ public function __construct() { add_filter( 'query_vars', [ $this, 'query_vars' ] ); add_filter( 'term_link', [ $this, 'no_category_base' ], 10, 3 ); add_filter( 'request', [ $this, 'request' ] ); add_filter( 'category_rewrite_rules', [ $this, 'category_rewrite_rules_wrapper' ] ); add_action( 'created_category', [ $this, 'schedule_flush' ] ); add_action( 'edited_category', [ $this, 'schedule_flush' ] ); add_action( 'delete_category', [ $this, 'schedule_flush' ] ); } /** * Trigger a rewrite_rule flush on shutdown. * * @since 1.2.8 * * @return void */ public function schedule_flush() { if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) { add_action( 'shutdown', 'flush_rewrite_rules' ); } } /** * Override the category link to remove the category base. * * @param string $link Term link, overridden by the function for categories. * @param WP_Term $term Unused, term object. * @param string $taxonomy Taxonomy slug. * * @return string */ public function no_category_base( $link, $term, $taxonomy ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $link; } if ( $taxonomy !== 'category' ) { return $link; } $category_base = get_option( 'category_base' ); if ( empty( $category_base ) ) { $category_base = 'category'; } /* * Remove initial slash, if there is one (we remove the trailing slash * in the regex replacement and don't want to end up short a slash). */ if ( substr( $category_base, 0, 1 ) === '/' ) { $category_base = substr( $category_base, 1 ); } $category_base .= '/'; return preg_replace( '`' . preg_quote( $category_base, '`' ) . '`u', '', $link, 1 ); } /** * Update the query vars with the redirect var when stripcategorybase is active. * * @param array<string> $query_vars Main query vars to filter. * * @return array<string> The query vars. */ public function query_vars( $query_vars ) { if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) { $query_vars[] = 'wpseo_category_redirect'; } return $query_vars; } /** * Checks whether the redirect needs to be created. * * @param array<string> $query_vars Query vars to check for existence of redirect var. * * @return array<string> The query vars. */ public function request( $query_vars ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $query_vars; } if ( ! isset( $query_vars['wpseo_category_redirect'] ) ) { return $query_vars; } $this->redirect( $query_vars['wpseo_category_redirect'] ); return []; } /** * Wrapper for the category_rewrite_rules() below, so we can add the $rules param in a BC way. * * @param array<string> $rules Rewrite rules generated for the current permastruct, keyed by their regex pattern. * * @return array<string> The category rewrite rules. */ public function category_rewrite_rules_wrapper( $rules ) { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return $rules; } return $this->category_rewrite_rules(); } /** * This function taken and only slightly adapted from WP No Category Base plugin by Saurabh Gupta. * * @return array<string> The category rewrite rules. */ public function category_rewrite_rules() { global $wp_rewrite; $category_rewrite = []; $taxonomy = get_taxonomy( 'category' ); $permalink_structure = get_option( 'permalink_structure' ); $blog_prefix = ''; if ( strpos( $permalink_structure, '/blog/' ) === 0 ) { if ( ( is_multisite() && ! is_subdomain_install() ) || is_main_site() || is_main_network() ) { $blog_prefix = 'blog/'; } } $categories = get_categories( [ 'hide_empty' => false ] ); if ( is_array( $categories ) && $categories !== [] ) { foreach ( $categories as $category ) { $category_nicename = $category->slug; if ( $category->parent === $category->cat_ID ) { // Recursive recursion. $category->parent = 0; } elseif ( $taxonomy->rewrite['hierarchical'] !== false && $category->parent !== 0 ) { $parents = get_category_parents( $category->parent, false, '/', true ); if ( ! is_wp_error( $parents ) ) { $category_nicename = $parents . $category_nicename; } unset( $parents ); } $category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename, $blog_prefix, $wp_rewrite->pagination_base ); // Adds rules for the uppercase encoded URIs. $category_nicename_filtered = $this->convert_encoded_to_upper( $category_nicename ); if ( $category_nicename_filtered !== $category_nicename ) { $category_rewrite = $this->add_category_rewrites( $category_rewrite, $category_nicename_filtered, $blog_prefix, $wp_rewrite->pagination_base ); } } unset( $categories, $category, $category_nicename, $category_nicename_filtered ); } // Redirect support from Old Category Base. $old_base = $wp_rewrite->get_category_permastruct(); $old_base = str_replace( '%category%', '(.+)', $old_base ); $old_base = trim( $old_base, '/' ); $category_rewrite[ $old_base . '$' ] = 'index.php?wpseo_category_redirect=$matches[1]'; return $category_rewrite; } /** * Adds required category rewrites rules. * * @param array<string> $rewrites The current set of rules. * @param string $category_name Category nicename. * @param string $blog_prefix Multisite blog prefix. * @param string $pagination_base WP_Query pagination base. * * @return array<string> The added set of rules. */ protected function add_category_rewrites( $rewrites, $category_name, $blog_prefix, $pagination_base ) { $rewrite_name = $blog_prefix . '(' . $category_name . ')'; global $wp_rewrite; $feed_regex = '(' . implode( '|', $wp_rewrite->feeds ) . ')'; $rewrites[ $rewrite_name . '/(?:feed/)?' . $feed_regex . '/?$' ] = 'index.php?category_name=$matches[1]&feed=$matches[2]'; $rewrites[ $rewrite_name . '/' . $pagination_base . '/?([0-9]{1,})/?$' ] = 'index.php?category_name=$matches[1]&paged=$matches[2]'; $rewrites[ $rewrite_name . '/?$' ] = 'index.php?category_name=$matches[1]'; return $rewrites; } /** * Walks through category nicename and convert encoded parts * into uppercase using $this->encode_to_upper(). * * @param string $name The encoded category URI string. * * @return string The convered URI string. */ protected function convert_encoded_to_upper( $name ) { // Checks if name has any encoding in it. if ( strpos( $name, '%' ) === false ) { return $name; } $names = explode( '/', $name ); $names = array_map( [ $this, 'encode_to_upper' ], $names ); return implode( '/', $names ); } /** * Converts the encoded URI string to uppercase. * * @param string $encoded The encoded string. * * @return string The uppercased string. */ public function encode_to_upper( $encoded ) { if ( strpos( $encoded, '%' ) === false ) { return $encoded; } return strtoupper( $encoded ); } /** * Redirect the "old" category URL to the new one. * * @codeCoverageIgnore * * @param string $category_redirect The category page to redirect to. * @return void */ protected function redirect( $category_redirect ) { $catlink = trailingslashit( get_option( 'home' ) ) . user_trailingslashit( $category_redirect, 'category' ); wp_safe_redirect( $catlink, 301, 'Yoast SEO' ); exit(); } } inc/index.php 0000644 00000000046 15174712003 0007135 0 ustar 00 <?php /** * Nothing to see here. */ inc/wpseo-functions.php 0000644 00000022002 15174712003 0011165 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } if ( ! function_exists( 'yoast_breadcrumb' ) ) { /** * Template tag for breadcrumbs. * * @param string $before What to show before the breadcrumb. * @param string $after What to show after the breadcrumb. * @param bool $display Whether to display the breadcrumb (true) or return it (false). * * @return string */ function yoast_breadcrumb( $before = '', $after = '', $display = true ) { $breadcrumbs_enabled = current_theme_supports( 'yoast-seo-breadcrumbs' ); if ( ! $breadcrumbs_enabled ) { $breadcrumbs_enabled = WPSEO_Options::get( 'breadcrumbs-enable', false ); } if ( $breadcrumbs_enabled ) { return WPSEO_Breadcrumbs::breadcrumb( $before, $after, $display ); } } } if ( ! function_exists( 'yoast_get_primary_term_id' ) ) { /** * Get the primary term ID. * * @param string $taxonomy Optional. The taxonomy to get the primary term ID for. Defaults to category. * @param int|WP_Post|null $post Optional. Post to get the primary term ID for. * * @return bool|int */ function yoast_get_primary_term_id( $taxonomy = 'category', $post = null ) { $post = get_post( $post ); $primary_term = new WPSEO_Primary_Term( $taxonomy, $post->ID ); return $primary_term->get_primary_term(); } } if ( ! function_exists( 'yoast_get_primary_term' ) ) { /** * Get the primary term name. * * @param string $taxonomy Optional. The taxonomy to get the primary term for. Defaults to category. * @param int|WP_Post|null $post Optional. Post to get the primary term for. * * @return string Name of the primary term. */ function yoast_get_primary_term( $taxonomy = 'category', $post = null ) { $primary_term_id = yoast_get_primary_term_id( $taxonomy, $post ); $term = get_term( $primary_term_id ); if ( ! is_wp_error( $term ) && ! empty( $term ) ) { return $term->name; } return ''; } } /** * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt. * * @param string $text The string to replace the variables in. * @param object $args The object some of the replacement values might come from, * could be a post, taxonomy or term. * @param array $omit Variables that should not be replaced by this function. * * @return string */ function wpseo_replace_vars( $text, $args, $omit = [] ) { $replacer = new WPSEO_Replace_Vars(); return $replacer->replace( $text, $args, $omit ); } /** * Register a new variable replacement. * * This function is for use by other plugins/themes to easily add their own additional variables to replace. * This function should be called from a function on the 'wpseo_register_extra_replacements' action hook. * The use of this function is preferred over the older 'wpseo_replacements' filter as a way to add new replacements. * The 'wpseo_replacements' filter should still be used to adjust standard WPSEO replacement values. * The function can not be used to replace standard WPSEO replacement value functions and will thrown a warning * if you accidently try. * To avoid conflicts with variables registered by WPSEO and other themes/plugins, try and make the * name of your variable unique. Variable names also can not start with "%%cf_" or "%%ct_" as these are reserved * for the standard WPSEO variable variables 'cf_<custom-field-name>', 'ct_<custom-tax-name>' and * 'ct_desc_<custom-tax-name>'. * The replacement function will be passed the undelimited name (i.e. stripped of the %%) of the variable * to replace in case you need it. * * Example code: * <code> * <?php * function retrieve_var1_replacement( $var1 ) { * return 'your replacement value'; * } * * function register_my_plugin_extra_replacements() { * wpseo_register_var_replacement( '%%myvar1%%', 'retrieve_var1_replacement', 'advanced', 'this is a help text for myvar1' ); * wpseo_register_var_replacement( 'myvar2', array( 'class', 'method_name' ), 'basic', 'this is a help text for myvar2' ); * } * add_action( 'wpseo_register_extra_replacements', 'register_my_plugin_extra_replacements' ); * ?> * </code> * * @since 1.5.4 * * @param string $replacevar_name The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional, name can only contain [A-Za-z0-9_-]. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool Whether the replacement function was successfully registered. */ function wpseo_register_var_replacement( $replacevar_name, $replace_function, $type = 'advanced', $help_text = '' ) { return WPSEO_Replace_Vars::register_replacement( $replacevar_name, $replace_function, $type, $help_text ); } /** * WPML plugin support: Set titles for custom types / taxonomies as translatable. * * It adds new keys to a wpml-config.xml file for a custom post type title, metadesc, * title-ptarchive and metadesc-ptarchive fields translation. * Documentation: http://wpml.org/documentation/support/language-configuration-files/ * * @global $sitepress * * @param array $config WPML configuration data to filter. * * @return array */ function wpseo_wpml_config( $config ) { global $sitepress; if ( ( is_array( $config ) && isset( $config['wpml-config']['admin-texts']['key'] ) ) && ( is_array( $config['wpml-config']['admin-texts']['key'] ) && $config['wpml-config']['admin-texts']['key'] !== [] ) ) { $admin_texts = $config['wpml-config']['admin-texts']['key']; foreach ( $admin_texts as $k => $val ) { if ( $val['attr']['name'] === 'wpseo_titles' ) { $translate_cp = array_keys( $sitepress->get_translatable_documents() ); if ( is_array( $translate_cp ) && $translate_cp !== [] ) { foreach ( $translate_cp as $post_type ) { $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-ptarchive-' . $post_type; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-ptarchive-' . $post_type; $translate_tax = $sitepress->get_translatable_taxonomies( false, $post_type ); if ( is_array( $translate_tax ) && $translate_tax !== [] ) { foreach ( $translate_tax as $taxonomy ) { $admin_texts[ $k ]['key'][]['attr']['name'] = 'title-tax-' . $taxonomy; $admin_texts[ $k ]['key'][]['attr']['name'] = 'metadesc-tax-' . $taxonomy; } } } } break; } } $config['wpml-config']['admin-texts']['key'] = $admin_texts; } return $config; } add_filter( 'icl_wpml_config_array', 'wpseo_wpml_config' ); if ( ! function_exists( 'ctype_digit' ) ) { /** * Emulate PHP native ctype_digit() function for when the ctype extension would be disabled *sigh*. * Only emulates the behaviour for when the input is a string, does not handle integer input as ascii value. * * @param string $text String input to validate. * * @return bool */ function ctype_digit( $text ) { $return = false; if ( ( is_string( $text ) && $text !== '' ) && preg_match( '`^\d+$`', $text ) === 1 ) { $return = true; } return $return; } } /** * Makes sure the taxonomy meta is updated when a taxonomy term is split. * * @link https://make.wordpress.org/core/2015/02/16/taxonomy-term-splitting-in-4-2-a-developer-guide/ Article explaining the taxonomy term splitting in WP 4.2. * * @param string $old_term_id Old term id of the taxonomy term that was splitted. * @param string $new_term_id New term id of the taxonomy term that was splitted. * @param string $term_taxonomy_id Term taxonomy id for the taxonomy that was affected. * @param string $taxonomy The taxonomy that the taxonomy term was splitted for. * * @return void */ function wpseo_split_shared_term( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { $tax_meta = get_option( 'wpseo_taxonomy_meta', [] ); if ( ! empty( $tax_meta[ $taxonomy ][ $old_term_id ] ) ) { $tax_meta[ $taxonomy ][ $new_term_id ] = $tax_meta[ $taxonomy ][ $old_term_id ]; unset( $tax_meta[ $taxonomy ][ $old_term_id ] ); update_option( 'wpseo_taxonomy_meta', $tax_meta ); } } add_action( 'split_shared_term', 'wpseo_split_shared_term', 10, 4 ); /** * Get all WPSEO related capabilities. * * @since 8.3 * @return array */ function wpseo_get_capabilities() { if ( ! did_action( 'wpseo_register_capabilities' ) ) { do_action( 'wpseo_register_capabilities' ); } return WPSEO_Capability_Manager_Factory::get()->get_capabilities(); } inc/language-utils.php 0000644 00000002441 15174712003 0010750 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 5.9.0 */ /** * Group of language utility methods for use by WPSEO. * All methods are static, this is just a sort of namespacing class wrapper. */ class WPSEO_Language_Utils { /** * Returns the language part of a given locale, defaults to english when the $locale is empty. * * @param string|null $locale The locale to get the language of. * * @return string The language part of the locale. */ public static function get_language( $locale = null ) { $language = 'en'; if ( empty( $locale ) || ! is_string( $locale ) ) { return $language; } $locale_parts = explode( '_', $locale ); if ( ! empty( $locale_parts[0] ) && ( strlen( $locale_parts[0] ) === 2 || strlen( $locale_parts[0] ) === 3 ) ) { $language = $locale_parts[0]; } return $language; } /** * Returns the full name for the sites' language. * * @return string The language name. */ public static function get_site_language_name() { require_once ABSPATH . 'wp-admin/includes/translation-install.php'; $translations = wp_get_available_translations(); $locale = get_locale(); $language = isset( $translations[ $locale ] ) ? $translations[ $locale ]['native_name'] : 'English (US)'; return $language; } } inc/interface-wpseo-wordpress-ajax-integration.php 0000644 00000000446 15174712003 0016415 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * An interface for registering AJAX integrations with WordPress. */ interface WPSEO_WordPress_AJAX_Integration { /** * Registers all AJAX hooks to WordPress. * * @return void */ public function register_ajax_hooks(); } inc/wpseo-non-ajax-functions.php 0000644 00000003071 15174712003 0012703 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Initializes the admin bar. * * @return void */ function wpseo_initialize_admin_bar() { $admin_bar_menu = new WPSEO_Admin_Bar_Menu(); $admin_bar_menu->register_hooks(); } add_action( 'wp_loaded', 'wpseo_initialize_admin_bar' ); /** * Allows editing of the meta fields through weblog editors like Marsedit. * * @param array $required_capabilities Capabilities that must all be true to allow action. * @param array $capabilities Array of capabilities to be checked, unused here. * @param array $args List of arguments for the specific capabilities to be checked. * * @return array Filtered capabilities. */ function allow_custom_field_edits( $required_capabilities, $capabilities, $args ) { if ( ! in_array( $args[0], [ 'edit_post_meta', 'add_post_meta' ], true ) ) { return $required_capabilities; } // If this is provided, it is the post ID. if ( empty( $args[2] ) ) { return $required_capabilities; } // If this is provided, it is the custom field. if ( empty( $args[3] ) ) { return $required_capabilities; } // If the meta key is part of the plugin, grant capabilities accordingly. if ( strpos( $args[3], WPSEO_Meta::$meta_prefix ) === 0 && current_user_can( 'edit_post', $args[2] ) ) { $required_capabilities[ $args[0] ] = true; } return $required_capabilities; } add_filter( 'user_has_cap', 'allow_custom_field_edits', 0, 3 ); inc/class-wpseo-installation.php 0000644 00000002261 15174712003 0012766 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 3.6 */ /** * This class checks if the wpseo option doesn't exists. In the case it doesn't it will set a property that is * accessible via a method to check if the installation is fresh. */ class WPSEO_Installation { /** * Checks if Yoast SEO is installed for the first time. */ public function __construct() { $is_first_install = $this->is_first_install(); if ( $is_first_install && WPSEO_Utils::is_api_available() ) { add_action( 'wpseo_activate', [ $this, 'set_first_install_options' ] ); } } /** * When the option doesn't exist, it should be a new install. * * @return bool */ private function is_first_install() { return ( get_option( 'wpseo' ) === false ); } /** * Sets the options on first install for showing the installation notice and disabling of the settings pages. * * @return void */ public function set_first_install_options() { $options = get_option( 'wpseo' ); $options['show_onboarding_notice'] = true; $options['first_activated_on'] = time(); $options['first_activated_by'] = get_current_user_id(); update_option( 'wpseo', $options ); } } inc/wpseo-functions-deprecated.php 0000644 00000000104 15174712003 0013262 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Deprecated */ inc/class-wpseo-primary-term.php 0000644 00000003313 15174712003 0012714 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * Represents a post's primary term. */ class WPSEO_Primary_Term { /** * Taxonomy name for the term. * * @var string */ protected $taxonomy_name; /** * Post ID for the term. * * @var int */ protected $post_ID; /** * The taxonomy this term is part of. * * @param string $taxonomy_name Taxonomy name for the term. * @param int $post_id Post ID for the term. */ public function __construct( $taxonomy_name, $post_id ) { $this->taxonomy_name = $taxonomy_name; $this->post_ID = $post_id; } /** * Returns the primary term ID. * * @return int|bool */ public function get_primary_term() { $primary_term = get_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, true ); if ( ! $primary_term ) { return false; } $terms = $this->get_terms(); if ( ! in_array( (int) $primary_term, wp_list_pluck( $terms, 'term_id' ), true ) ) { $primary_term = false; } $primary_term = (int) $primary_term; return ( $primary_term ) ? ( $primary_term ) : false; } /** * Sets the new primary term ID. * * @param int $new_primary_term New primary term ID. * * @return void */ public function set_primary_term( $new_primary_term ) { update_post_meta( $this->post_ID, WPSEO_Meta::$meta_prefix . 'primary_' . $this->taxonomy_name, $new_primary_term ); } /** * Get the terms for the current post ID. * When $terms is not an array, set $terms to an array. * * @return array */ protected function get_terms() { $terms = get_the_terms( $this->post_ID, $this->taxonomy_name ); if ( ! is_array( $terms ) ) { $terms = []; } return $terms; } } inc/class-post-type.php 0000644 00000007622 15174712003 0011104 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Inc */ /** * Represents the post type utils. */ class WPSEO_Post_Type { /** * Returns an array with the accessible post types. * * An accessible post type is a post type that is public and isn't set as no-index (robots). * * @return array Array with all the accessible post_types. */ public static function get_accessible_post_types() { return YoastSEO()->helpers->post_type->get_accessible_post_types(); } /** * Returns whether the passed post type is considered accessible. * * @param string $post_type The post type to check. * * @return bool Whether or not the post type is considered accessible. */ public static function is_post_type_accessible( $post_type ) { return in_array( $post_type, self::get_accessible_post_types(), true ); } /** * Checks if the request post type is public and indexable. * * @param string $post_type_name The name of the post type to lookup. * * @return bool True when post type is set to index. */ public static function is_post_type_indexable( $post_type_name ) { return YoastSEO()->helpers->post_type->is_indexable( $post_type_name ); } /** * Filters the attachment post type from an array with post_types. * * @param array $post_types The array to filter the attachment post type from. * * @return array The filtered array. */ public static function filter_attachment_post_type( array $post_types ) { if ( WPSEO_Options::get( 'disable-attachment' ) === true ) { unset( $post_types['attachment'] ); } return $post_types; } /** * Checks if the post type is enabled in the REST API. * * @param string $post_type The post type to check. * * @return bool Whether or not the post type is available in the REST API. */ public static function is_rest_enabled( $post_type ) { $post_type_object = get_post_type_object( $post_type ); if ( $post_type_object === null ) { return false; } return $post_type_object->show_in_rest === true; } /** * Checks if the current post type has an archive. * * Context: The has_archive value can be a string or a boolean. In most case it will be a boolean, * but it can be defined as a string. When it is a string the archive_slug will be overwritten to * define another endpoint. * * @param WP_Post_Type $post_type The post type object. * * @return bool True whether the post type has an archive. */ public static function has_archive( $post_type ) { return YoastSEO()->helpers->post_type->has_archive( $post_type ); } /** * Checks if the Yoast Metabox has been enabled for the post type. * * @param string $post_type The post type name. * * @return bool True whether the metabox is enabled. */ public static function has_metabox_enabled( $post_type ) { return WPSEO_Options::get( 'display-metabox-pt-' . $post_type, false ); } /* ********************* DEPRECATED METHODS ********************* */ /** * Removes the notification related to the post types which have been made public. * * @deprecated 20.10 * @codeCoverageIgnore * * @return void */ public static function remove_post_types_made_public_notification() { _deprecated_function( __METHOD__, 'Yoast SEO 20.10', 'Content_Type_Visibility_Dismiss_Notifications::dismiss_notifications' ); $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'post-types-made-public' ); } /** * Removes the notification related to the taxonomies which have been made public. * * @deprecated 20.10 * @codeCoverageIgnore * * @return void */ public static function remove_taxonomies_made_public_notification() { _deprecated_function( __METHOD__, 'Yoast SEO 20.10', 'Content_Type_Visibility_Dismiss_Notifications::dismiss_notifications' ); $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'taxonomies-made-public' ); } } inc/class-wpseo-replace-vars.php 0000644 00000146733 15174712003 0012666 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 1.5.4 */ // Avoid direct calls to this file. if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Class: WPSEO_Replace_Vars. * * This class implements the replacing of `%%variable_placeholders%%` with their real value based on the current * requested page/post/cpt/etc in text strings. */ class WPSEO_Replace_Vars { /** * Default post/page/cpt information. * * @var array */ protected $defaults = [ 'ID' => '', 'name' => '', 'post_author' => '', 'post_content' => '', 'post_date' => '', 'post_excerpt' => '', 'post_modified' => '', 'post_title' => '', 'taxonomy' => '', 'term_id' => '', 'term404' => '', ]; /** * Current post/page/cpt information. * * @var stdClass */ protected $args; /** * Help texts for use in WPSEO -> Search appearance tabs. * * @var array */ protected static $help_texts = []; /** * Register of additional variable replacements registered by other plugins/themes. * * @var array */ protected static $external_replacements = []; /** * Setup the help texts and external replacements as statics so they will be available to all instances. * * @return void */ public static function setup_statics_once() { if ( self::$help_texts === [] ) { self::set_basic_help_texts(); self::set_advanced_help_texts(); } if ( self::$external_replacements === [] ) { /** * Action: 'wpseo_register_extra_replacements' - Allows for registration of additional * variables to replace. */ do_action( 'wpseo_register_extra_replacements' ); } } /** * Register new replacement %%variables%%. * For use by other plugins/themes to register extra variables. * * @see wpseo_register_var_replacement() for a usage example. * * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool Whether the replacement function was succesfully registered. */ public static function register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) { $success = false; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $var_to_replace = self::remove_var_delimiter( $var_to_replace ); if ( preg_match( '`^[A-Z0-9_-]+$`i', $var_to_replace ) === false ) { trigger_error( esc_html__( 'A replacement variable can only contain alphanumeric characters, an underscore or a dash. Try renaming your variable.', 'wordpress-seo' ), E_USER_WARNING ); } elseif ( strpos( $var_to_replace, 'cf_' ) === 0 || strpos( $var_to_replace, 'ct_' ) === 0 ) { trigger_error( esc_html__( 'A replacement variable can not start with "%%cf_" or "%%ct_" as these are reserved for the WPSEO standard variable variables for custom fields and custom taxonomies. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING ); } elseif ( ! method_exists( self::class, 'retrieve_' . $var_to_replace ) ) { if ( $var_to_replace !== '' && ! isset( self::$external_replacements[ $var_to_replace ] ) ) { self::$external_replacements[ $var_to_replace ] = $replace_function; $replacement_variable = new WPSEO_Replacement_Variable( $var_to_replace, $var_to_replace, $help_text ); self::register_help_text( $type, $replacement_variable ); $success = true; } else { trigger_error( esc_html__( 'A replacement variable with the same name has already been registered. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING ); } } else { trigger_error( esc_html__( 'You cannot overrule a WPSEO standard variable replacement by registering a variable with the same name. Use the "wpseo_replacements" filter instead to adjust the replacement value.', 'wordpress-seo' ), E_USER_WARNING ); } } return $success; } /** * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt/etc. * * @param string $text The string to replace the variables in. * @param array $args The object some of the replacement values might come from, * could be a post, taxonomy or term. * @param array $omit Variables that should not be replaced by this function. * * @return string */ public function replace( $text, $args, $omit = [] ) { $text = wp_strip_all_tags( $text ); // Let's see if we can bail super early. if ( strpos( $text, '%%' ) === false ) { return YoastSEO()->helpers->string->standardize_whitespace( $text ); } $args = (array) $args; if ( isset( $args['post_content'] ) && ! empty( $args['post_content'] ) ) { $args['post_content'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_content'] ); } if ( isset( $args['post_excerpt'] ) && ! empty( $args['post_excerpt'] ) ) { $args['post_excerpt'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_excerpt'] ); } $this->args = (object) wp_parse_args( $args, $this->defaults ); // Clean $omit array. if ( is_array( $omit ) && $omit !== [] ) { $omit = array_map( [ self::class, 'remove_var_delimiter' ], $omit ); } $replacements = []; if ( preg_match_all( '`%%([^%]+(%%single)?)%%?`iu', $text, $matches ) ) { $replacements = $this->set_up_replacements( $matches, $omit ); } /** * Filter: 'wpseo_replacements' - Allow customization of the replacements before they are applied. * * @param array $replacements The replacements. * @param array $args The object some of the replacement values might come from, * could be a post, taxonomy or term. */ $replacements = apply_filters( 'wpseo_replacements', $replacements, $this->args ); // Do the actual replacements. if ( is_array( $replacements ) && $replacements !== [] ) { $text = str_replace( array_keys( $replacements ), // Make sure to exclude replacement values that are arrays e.g. coming from a custom field serialized value. array_filter( array_values( $replacements ), 'is_scalar' ), $text, ); } /** * Filter: 'wpseo_replacements_final' - Allow overruling of whether or not to remove placeholders * which didn't yield a replacement. * * @example <code>add_filter( 'wpseo_replacements_final', '__return_false' );</code> * * @param bool $final */ if ( apply_filters( 'wpseo_replacements_final', true ) === true && ( isset( $matches[1] ) && is_array( $matches[1] ) ) ) { // Remove non-replaced variables. $remove = array_diff( $matches[1], $omit ); // Make sure the $omit variables do not get removed. $remove = array_map( [ self::class, 'add_var_delimiter' ], $remove ); $text = str_replace( $remove, '', $text ); } // Undouble separators which have nothing between them, i.e. where a non-replaced variable was removed. if ( isset( $replacements['%%sep%%'] ) && ( is_string( $replacements['%%sep%%'] ) && $replacements['%%sep%%'] !== '' ) ) { $q_sep = preg_quote( $replacements['%%sep%%'], '`' ); $text = preg_replace( '`' . $q_sep . '(?:\s*' . $q_sep . ')*`u', $replacements['%%sep%%'], $text ); } // Remove superfluous whitespace. $text = YoastSEO()->helpers->string->standardize_whitespace( $text ); return $text; } /** * Register a new replacement variable if it has not been registered already. * * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'. * Note: the surrounding %% are optional. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable. * Uses the same format as add_filter/add_action function parameter and * should *return* the replacement value. DON'T echo it. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'. * @param string $help_text Help text to be added to the help tab for this variable. * * @return bool `true` if the replace var has been registered, `false` if not. */ public function safe_register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) { if ( ! $this->has_been_registered( $var_to_replace ) ) { return self::register_replacement( $var_to_replace, $replace_function, $type, $help_text ); } return false; } /** * Checks whether the given replacement variable has already been registered or not. * * @param string $replacement_variable The replacement variable to check, including the variable delimiter (e.g. `%%var%%`). * * @return bool `true` if the replacement variable has already been registered. */ public function has_been_registered( $replacement_variable ) { $replacement_variable = self::remove_var_delimiter( $replacement_variable ); return isset( self::$external_replacements[ $replacement_variable ] ); } /** * Returns the list of hidden replace vars. * * E.g. the replace vars that should work, but are not advertised. * * @return string[] The list of hidden replace vars. */ public function get_hidden_replace_vars() { return [ 'currentdate', 'currentyear', 'currentmonth', 'currentday', 'post_year', 'post_month', 'post_day', 'author_first_name', 'author_last_name', 'permalink', 'post_content', 'category_title', ]; } /** * Retrieve the replacements for the variables found. * * @param array $matches Variables found in the original string - regex result. * @param array $omit Variables that should not be replaced by this function. * * @return array Retrieved replacements - this might be a smaller array as some variables * may not yield a replacement in certain contexts. */ private function set_up_replacements( $matches, $omit ) { $replacements = []; // @todo Figure out a way to deal with external functions starting with cf_/ct_. foreach ( $matches[1] as $k => $var ) { // Don't set up replacements which should be omitted. if ( in_array( $var, $omit, true ) ) { continue; } // Deal with variable variable names first. if ( strpos( $var, 'cf_' ) === 0 ) { $replacement = $this->retrieve_cf_custom_field_name( $var ); } elseif ( strpos( $var, 'ct_desc_' ) === 0 ) { $replacement = $this->retrieve_ct_desc_custom_tax_name( $var ); } elseif ( strpos( $var, 'ct_' ) === 0 ) { $single = ( isset( $matches[2][ $k ] ) && $matches[2][ $k ] !== '' ); $replacement = $this->retrieve_ct_custom_tax_name( $var, $single ); } // Deal with non-variable variable names. elseif ( method_exists( $this, 'retrieve_' . $var ) ) { $method_name = 'retrieve_' . $var; $replacement = $this->$method_name(); } // Deal with externally defined variable names. elseif ( isset( self::$external_replacements[ $var ] ) && is_callable( self::$external_replacements[ $var ] ) ) { $replacement = call_user_func( self::$external_replacements[ $var ], $var, $this->args ); } // Replacement retrievals can return null if no replacement can be determined, root those outs. if ( isset( $replacement ) ) { $var = self::add_var_delimiter( $var ); $replacements[ $var ] = $replacement; } unset( $replacement, $single, $method_name ); } return $replacements; } /* *********************** BASIC VARIABLES ************************** */ /** * Retrieve the post/cpt categories (comma separated) for use as replacement string. * * @return string|null */ private function retrieve_category() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $cat = $this->get_terms( $this->args->ID, 'category' ); if ( $cat !== '' ) { return $cat; } } if ( isset( $this->args->cat_name ) && ! empty( $this->args->cat_name ) ) { $replacement = $this->args->cat_name; } return $replacement; } /** * Retrieve the category description for use as replacement string. * * @return string|null */ private function retrieve_category_description() { return $this->retrieve_term_description(); } /** * Retrieve the date of the post/page/cpt for use as replacement string. * * @return string|null */ private function retrieve_date() { $replacement = null; if ( $this->args->post_date !== '' ) { // Returns a string. $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_date, get_option( 'date_format' ) ); } elseif ( get_query_var( 'day' ) && get_query_var( 'day' ) !== '' ) { // Returns a string. $replacement = get_the_date(); } elseif ( single_month_title( ' ', false ) ) { // Returns a string. $replacement = single_month_title( ' ', false ); } elseif ( get_query_var( 'year' ) !== '' ) { // Returns an integer, let's cast to string. $replacement = (string) get_query_var( 'year' ); } return $replacement; } /** * Retrieve the post/page/cpt excerpt for use as replacement string. * The excerpt will be auto-generated if it does not exist. * * @return string|null */ private function retrieve_excerpt() { $replacement = null; $locale = get_locale(); // Japanese doesn't have a jp_JP variant in WP. $limit = ( $locale === 'ja' ) ? 80 : 156; // The check `post_password_required` is because excerpt must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && ! post_password_required( $this->args->ID ) ) { if ( $this->args->post_excerpt !== '' ) { $replacement = wp_strip_all_tags( $this->args->post_excerpt ); } elseif ( $this->args->post_content !== '' ) { $content = strip_shortcodes( $this->args->post_content ); $content = wp_strip_all_tags( $content ); if ( mb_strlen( $content ) <= $limit ) { return $content; } $replacement = wp_html_excerpt( $content, $limit ); // Check if the description has space and trim the auto-generated string to a word boundary. if ( strrpos( $replacement, ' ' ) ) { $replacement = substr( $replacement, 0, strrpos( $replacement, ' ' ) ); } } } return $replacement; } /** * Retrieve the post/page/cpt excerpt for use as replacement string (without auto-generation). * * @return string|null */ private function retrieve_excerpt_only() { $replacement = null; // The check `post_password_required` is because excerpt must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && $this->args->post_excerpt !== '' && ! post_password_required( $this->args->ID ) ) { $replacement = wp_strip_all_tags( $this->args->post_excerpt ); } return $replacement; } /** * Retrieve the title of the parent page of the current page/cpt for use as replacement string. * Only applicable for hierarchical post types. * * @todo Check: shouldn't this use $this->args as well ? * * @return string|null */ private function retrieve_parent_title() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $parent_id = wp_get_post_parent_id( $this->args->ID ); if ( $parent_id ) { $replacement = get_the_title( $parent_id ); } } return $replacement; } /** * Retrieve the current search phrase for use as replacement string. * * @return string|null */ private function retrieve_searchphrase() { $replacement = null; $search = get_query_var( 's' ); if ( $search !== '' ) { $replacement = esc_html( $search ); } return $replacement; } /** * Retrieve the separator for use as replacement string. * * @return string Retrieves the title separator. */ private function retrieve_sep() { return YoastSEO()->helpers->options->get_title_separator(); } /** * Retrieve the site's tag line / description for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string|null */ private function retrieve_sitedesc() { static $replacement; if ( ! isset( $replacement ) ) { $description = wp_strip_all_tags( get_bloginfo( 'description' ) ); if ( $description !== '' ) { $replacement = $description; } } return $replacement; } /** * Retrieve the site's name for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string|null */ private function retrieve_sitename() { static $replacement; if ( ! isset( $replacement ) ) { $sitename = YoastSEO()->helpers->site->get_site_name(); if ( $sitename !== '' ) { $replacement = $sitename; } } return $replacement; } /** * Retrieve the current tag/tags for use as replacement string. * * @return string|null */ private function retrieve_tag() { $replacement = null; if ( ! empty( $this->args->ID ) ) { $tags = $this->get_terms( $this->args->ID, 'post_tag' ); if ( $tags !== '' ) { $replacement = $tags; } } return $replacement; } /** * Retrieve the tag description for use as replacement string. * * @return string|null */ private function retrieve_tag_description() { return $this->retrieve_term_description(); } /** * Retrieve the term description for use as replacement string. * * @return string|null */ private function retrieve_term_description() { $replacement = null; if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) { $term_desc = get_term_field( 'description', $this->args->term_id, $this->args->taxonomy ); if ( $term_desc !== '' ) { $replacement = wp_strip_all_tags( $term_desc ); } } return $replacement; } /** * Retrieve the term name for use as replacement string. * * @return string|null */ private function retrieve_term_title() { $replacement = null; if ( ! empty( $this->args->taxonomy ) && ! empty( $this->args->name ) ) { $replacement = $this->args->name; } return $replacement; } /** * Retrieve the title of the post/page/cpt for use as replacement string. * * @return string|null */ private function retrieve_title() { $replacement = null; if ( is_string( $this->args->post_title ) && $this->args->post_title !== '' ) { $replacement = $this->args->post_title; } return $replacement; } /** * Retrieve primary category for use as replacement string. * * @return bool|int|null */ private function retrieve_primary_category() { $primary_category = null; if ( ! empty( $this->args->ID ) ) { $wpseo_primary_category = new WPSEO_Primary_Term( 'category', $this->args->ID ); $term_id = $wpseo_primary_category->get_primary_term(); $term = get_term( $term_id ); if ( ! is_wp_error( $term ) && ! empty( $term ) ) { $primary_category = $term->name; } } return $primary_category; } /** * Retrieve the string generated by get_the_archive_title(). * * @return string|null */ private function retrieve_archive_title() { return get_the_archive_title(); } /* *********************** ADVANCED VARIABLES ************************** */ /** * Determine the page numbering of the current post/page/cpt. * * @param string $request Either 'nr'|'max' - whether to return the page number or the max number of pages. * * @return int|null */ private function determine_pagenumbering( $request = 'nr' ) { global $wp_query, $post; $max_num_pages = null; $page_number = null; $max_num_pages = 1; if ( ! is_singular() ) { $page_number = get_query_var( 'paged' ); if ( $page_number === 0 || $page_number === '' ) { $page_number = 1; } if ( ! empty( $wp_query->max_num_pages ) ) { $max_num_pages = $wp_query->max_num_pages; } } else { $page_number = get_query_var( 'page' ); if ( $page_number === 0 || $page_number === '' ) { $page_number = 1; } if ( isset( $post->post_content ) ) { $max_num_pages = ( substr_count( $post->post_content, '<!--nextpage-->' ) + 1 ); } } $return = null; switch ( $request ) { case 'nr': $return = $page_number; break; case 'max': $return = $max_num_pages; break; } return $return; } /** * Determine the post type names for the current post/page/cpt. * * @param string $request Either 'single'|'plural' - whether to return the single or plural form. * * @return string|null */ private function determine_pt_names( $request = 'single' ) { global $wp_query; $pt_single = null; $pt_plural = null; $post_type = ''; if ( isset( $wp_query->query_vars['post_type'] ) && ( ( is_string( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== '' ) || ( is_array( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== [] ) ) ) { $post_type = $wp_query->query_vars['post_type']; } elseif ( isset( $this->args->post_type ) && ( is_string( $this->args->post_type ) && $this->args->post_type !== '' ) ) { $post_type = $this->args->post_type; } else { // Make it work in preview mode. $post = $wp_query->get_queried_object(); if ( $post instanceof WP_Post ) { $post_type = $post->post_type; } } if ( is_array( $post_type ) ) { $post_type = reset( $post_type ); } if ( $post_type !== '' ) { $pt = get_post_type_object( $post_type ); $pt_single = $pt->name; $pt_plural = $pt->name; if ( isset( $pt->labels->singular_name ) ) { $pt_single = $pt->labels->singular_name; } if ( isset( $pt->labels->name ) ) { $pt_plural = $pt->labels->name; } } $return = null; switch ( $request ) { case 'single': $return = $pt_single; break; case 'plural': $return = $pt_plural; break; } return $return; } /** * Retrieve the attachment caption for use as replacement string. * * @return string|null */ private function retrieve_caption() { return $this->retrieve_excerpt_only(); } /** * Retrieve a post/page/cpt's custom field value for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom field which value is to be retrieved. * * @return string|null */ private function retrieve_cf_custom_field_name( $var_to_replace ) { $replacement = null; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $field = substr( $var_to_replace, 3 ); if ( ! empty( $this->args->ID ) ) { // Post meta can be arrays and in this case we need to exclude them. $name = get_post_meta( $this->args->ID, $field, true ); if ( $name !== '' && ! is_array( $name ) ) { $replacement = sanitize_text_field( $name ); } } elseif ( ! empty( $this->args->term_id ) ) { $name = get_term_meta( $this->args->term_id, $field, true ); if ( $name !== '' ) { $replacement = sanitize_text_field( $name ); } } } return $replacement; } /** * Retrieve a post/page/cpt's custom taxonomies for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom taxonomy which value(s) is to be retrieved. * @param bool $single Whether to retrieve only the first or all values for the taxonomy. * * @return string|null */ private function retrieve_ct_custom_tax_name( $var_to_replace, $single = false ) { $replacement = null; if ( ( is_string( $var_to_replace ) && $var_to_replace !== '' ) && ! empty( $this->args->ID ) ) { $tax = substr( $var_to_replace, 3 ); $name = $this->get_terms( $this->args->ID, $tax, $single ); if ( $name !== '' ) { $replacement = $name; } } return $replacement; } /** * Retrieve a post/page/cpt's custom taxonomies description for use as replacement string. * * @param string $var_to_replace The complete variable to replace which includes the name of * the custom taxonomy which description is to be retrieved. * * @return string|null */ private function retrieve_ct_desc_custom_tax_name( $var_to_replace ) { $replacement = null; if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) { $tax = substr( $var_to_replace, 8 ); if ( ! empty( $this->args->ID ) ) { $terms = get_the_terms( $this->args->ID, $tax ); if ( is_array( $terms ) && $terms !== [] ) { $term = current( $terms ); $term_desc = get_term_field( 'description', $term->term_id, $tax ); if ( $term_desc !== '' ) { $replacement = wp_strip_all_tags( $term_desc ); } } } } return $replacement; } /** * Retrieve the current date for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The formatted current date. */ private function retrieve_currentdate() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( get_option( 'date_format' ) ); } return $replacement; } /** * Retrieve the current day for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current day. */ private function retrieve_currentday() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'j' ); } return $replacement; } /** * Retrieve the current month for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current month. */ private function retrieve_currentmonth() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'F' ); } return $replacement; } /** * Retrieve the current time for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The formatted current time. */ private function retrieve_currenttime() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( get_option( 'time_format' ) ); } return $replacement; } /** * Retrieve the current year for use as replacement string. * * The `$replacement` variable is static because it doesn't change depending * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482. * * @return string The current year. */ private function retrieve_currentyear() { static $replacement; if ( ! isset( $replacement ) ) { $replacement = date_i18n( 'Y' ); } return $replacement; } /** * Retrieve the post/page/cpt's focus keyword for use as replacement string. * * @return string|null */ private function retrieve_focuskw() { // Retrieve focuskw from a Post. if ( ! empty( $this->args->ID ) ) { $focus_kw = WPSEO_Meta::get_value( 'focuskw', $this->args->ID ); if ( $focus_kw !== '' ) { return $focus_kw; } return null; } // Retrieve focuskw from a Term. if ( ! empty( $this->args->term_id ) ) { $focus_kw = WPSEO_Taxonomy_Meta::get_term_meta( $this->args->term_id, $this->args->taxonomy, 'focuskw' ); if ( $focus_kw !== '' ) { return $focus_kw; } } return null; } /** * Retrieve the post/page/cpt ID for use as replacement string. * * @return string|null */ private function retrieve_id() { $replacement = null; if ( ! empty( $this->args->ID ) ) { // The post/page/cpt ID is an integer, let's cast to string. $replacement = (string) $this->args->ID; } return $replacement; } /** * Retrieve the post/page/cpt modified time for use as replacement string. * * @return string|null */ private function retrieve_modified() { $replacement = null; if ( ! empty( $this->args->post_modified ) ) { $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_modified, get_option( 'date_format' ) ); } return $replacement; } /** * Retrieve the post/page/cpt author's "nice name" for use as replacement string. * * @return string|null */ private function retrieve_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'display_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt author's users description for use as a replacement string. * * @return string|null */ private function retrieve_user_description() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $description = get_the_author_meta( 'description', $user_id ); if ( $description !== '' ) { $replacement = $description; } return $replacement; } /** * Retrieve the current page number with context (i.e. 'page 2 of 4') for use as replacement string. * * @return string */ private function retrieve_page() { $replacement = null; $max = $this->determine_pagenumbering( 'max' ); $nr = $this->determine_pagenumbering( 'nr' ); $sep = $this->retrieve_sep(); if ( $max > 1 && $nr > 1 ) { /* translators: 1: current page number, 2: total number of pages. */ $replacement = sprintf( $sep . ' ' . __( 'Page %1$d of %2$d', 'wordpress-seo' ), $nr, $max ); } return $replacement; } /** * Retrieve the current page number for use as replacement string. * * @return string|null */ private function retrieve_pagenumber() { $replacement = null; $nr = $this->determine_pagenumbering( 'nr' ); if ( isset( $nr ) && $nr > 0 ) { $replacement = (string) $nr; } return $replacement; } /** * Retrieve the current page total for use as replacement string. * * @return string|null */ private function retrieve_pagetotal() { $replacement = null; $max = $this->determine_pagenumbering( 'max' ); if ( isset( $max ) && $max > 0 ) { $replacement = (string) $max; } return $replacement; } /** * Retrieve the post type plural label for use as replacement string. * * @return string|null */ private function retrieve_pt_plural() { $replacement = null; $name = $this->determine_pt_names( 'plural' ); if ( isset( $name ) && $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post type single label for use as replacement string. * * @return string|null */ private function retrieve_pt_single() { $replacement = null; $name = $this->determine_pt_names( 'single' ); if ( isset( $name ) && $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the slug which caused the 404 for use as replacement string. * * @return string|null */ private function retrieve_term404() { $replacement = null; if ( $this->args->term404 !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $this->args->term404 ) ); } else { $error_request = get_query_var( 'pagename' ); if ( $error_request !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) ); } else { $error_request = get_query_var( 'name' ); if ( $error_request !== '' ) { $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) ); } } } return $replacement; } /** * Retrieve the post/page/cpt author's user id for use as replacement string. * * @return string */ private function retrieve_userid() { // The user ID is an integer, let's cast to string. $replacement = ! empty( $this->args->post_author ) ? (string) $this->args->post_author : (string) get_query_var( 'author' ); return $replacement; } /** * Retrieve the post/page/cpt's published year for use as replacement string. * * @return string|null */ private function retrieve_post_year() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'Y', $this->args->ID ); } /** * Retrieve the post/page/cpt's published month for use as replacement string. * * @return string|null */ private function retrieve_post_month() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'F', $this->args->ID ); } /** * Retrieve the post/page/cpt's published day for use as replacement string. * * @return string|null */ private function retrieve_post_day() { if ( empty( $this->args->ID ) ) { return null; } return get_the_date( 'd', $this->args->ID ); } /** * Retrieve the post/page/cpt author's first name for use as replacement string. * * @return string|null */ private function retrieve_author_first_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'first_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt author's last name for use as replacement string. * * @return string|null */ private function retrieve_author_last_name() { $replacement = null; $user_id = (int) $this->retrieve_userid(); $name = get_the_author_meta( 'last_name', $user_id ); if ( $name !== '' ) { $replacement = $name; } return $replacement; } /** * Retrieve the post/page/cpt permalink for use as replacement string. * * @return string|null */ private function retrieve_permalink() { if ( empty( $this->args->ID ) ) { return null; } return get_permalink( $this->args->ID ); } /** * Retrieve the post/page/cpt content for use as replacement string. * * @return string|null */ private function retrieve_post_content() { $replacement = null; // The check `post_password_required` is because content must be hidden for a post with a password. if ( ! empty( $this->args->ID ) && $this->args->post_content !== '' && ! post_password_required( $this->args->ID ) ) { $content = strip_shortcodes( $this->args->post_content ); $replacement = wp_strip_all_tags( $content ); } return $replacement; } /** * Retrieve the current or first category title. To be used for import data from AIOSEO. * The code derives from AIOSEO's way of dealing with that var, so we can ensure 100% seamless transition. * * @return string|null */ private function retrieve_category_title() { if ( empty( $this->args ) || empty( $this->args->ID ) ) { return null; } $post_id = $this->args->ID; $post = get_post( $post_id ); $taxonomies = get_object_taxonomies( $post, 'objects' ); foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) { if ( ! $taxonomy->hierarchical ) { continue; } $post_terms = get_the_terms( $post_id, $taxonomy_slug ); if ( is_array( $post_terms ) && count( $post_terms ) > 0 ) { // AiOSEO takes the name of whatever the first hierarchical taxonomy is. $term = reset( $post_terms ); if ( $term ) { return $term->name; } } } return null; } /* *********************** HELP TEXT RELATED ************************** */ /** * Set the help text for a user/plugin/theme defined extra variable. * * @param string $type Type of variable: 'basic' or 'advanced'. * @param WPSEO_Replacement_Variable $replacement_variable The replacement variable to register. * * @return void */ private static function register_help_text( $type, WPSEO_Replacement_Variable $replacement_variable ) { $identifier = $replacement_variable->get_variable(); if ( ( is_string( $type ) && in_array( $type, [ 'basic', 'advanced' ], true ) ) && ( $identifier !== '' && ! isset( self::$help_texts[ $type ][ $identifier ] ) ) ) { self::$help_texts[ $type ][ $identifier ] = $replacement_variable; } } /** * Generates a list of replacement variables based on the help texts. * * @return array List of replace vars. */ public function get_replacement_variables_with_labels() { self::setup_statics_once(); $custom_variables = []; foreach ( array_merge( WPSEO_Custom_Fields::get_custom_fields(), WPSEO_Custom_Taxonomies::get_custom_taxonomies() ) as $custom_variable ) { $custom_variables[ $custom_variable ] = new WPSEO_Replacement_Variable( $custom_variable, $this->get_label( $custom_variable ), '' ); } $replacement_variables = array_filter( array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] ), [ $this, 'is_not_prefixed' ], ARRAY_FILTER_USE_KEY, ); $hidden = $this->get_hidden_replace_vars(); return array_values( array_map( static function ( WPSEO_Replacement_Variable $replacement_variable ) use ( $hidden ) { $name = $replacement_variable->get_variable(); return [ 'name' => $name, 'value' => '', 'label' => $replacement_variable->get_label(), 'hidden' => in_array( $name, $hidden, true ), ]; }, array_merge( $replacement_variables, $custom_variables ), ), ); } /** * Generates a list of replacement variables based on the help texts. * * @return array List of replace vars. */ public function get_replacement_variables_list() { self::setup_statics_once(); $replacement_variables = array_merge( $this->get_replacement_variables(), WPSEO_Custom_Fields::get_custom_fields(), WPSEO_Custom_Taxonomies::get_custom_taxonomies(), ); return array_map( [ $this, 'format_replacement_variable' ], $replacement_variables ); } /** * Creates a merged associative array of both the basic and advanced help texts. * * @return array Array with the replacement variables. */ private function get_replacement_variables() { $help_texts = array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] ); return array_filter( array_keys( $help_texts ), [ $this, 'is_not_prefixed' ] ); } /** * Checks whether the replacement variable contains a `ct_` or `cf_` prefix, because they follow different logic. * * @param string $replacement_variable The replacement variable. * * @return bool True when the replacement variable is not prefixed. */ private function is_not_prefixed( $replacement_variable ) { $prefixes = [ 'cf_', 'ct_' ]; $prefix = $this->get_prefix( $replacement_variable ); return ! in_array( $prefix, $prefixes, true ); } /** * Strip the prefix from a replacement variable name. * * @param string $replacement_variable The replacement variable. * * @return string The replacement variable name without the prefix. */ private function strip_prefix( $replacement_variable ) { return substr( $replacement_variable, 3 ); } /** * Gets the prefix from a replacement variable name. * * @param string $replacement_variable The replacement variable. * * @return string The prefix of the replacement variable. */ private function get_prefix( $replacement_variable ) { return substr( $replacement_variable, 0, 3 ); } /** * Strips 'desc_' if present, and appends ' description' at the end. * * @param string $label The replacement variable. * * @return string The altered replacement variable name. */ private function handle_description( $label ) { if ( strpos( $label, 'desc_' ) === 0 ) { return substr( $label, 5 ) . ' description'; } return $label; } /** * Creates a label for prefixed replacement variables that matches the format in the editors. * * @param string $replacement_variable The replacement variable. * * @return string The replacement variable label. */ private function get_label( $replacement_variable ) { $prefix = $this->get_prefix( $replacement_variable ); if ( $prefix === 'cf_' ) { return $this->strip_prefix( $replacement_variable ) . ' (custom field)'; } if ( $prefix === 'ct_' ) { $label = $this->strip_prefix( $replacement_variable ); $label = $this->handle_description( $label ); return ucfirst( $label . ' (custom taxonomy)' ); } if ( $prefix === 'pt_' ) { if ( $replacement_variable === 'pt_single' ) { return 'Post type (singular)'; } return 'Post type (plural)'; } return ''; } /** * Formats the replacement variables. * * @param string $replacement_variable The replacement variable to format. * * @return array The formatted replacement variable. */ private function format_replacement_variable( $replacement_variable ) { return [ 'name' => $replacement_variable, 'value' => '', 'label' => $this->get_label( $replacement_variable ), ]; } /** * Set/translate the help texts for the WPSEO standard basic variables. * * @return void */ private static function set_basic_help_texts() { /* translators: %s: wp_title() function. */ $separator_description = __( 'The separator defined in your theme\'s %s tag.', 'wordpress-seo' ); $separator_description = sprintf( $separator_description, // '<code>wp_title()</code>' 'wp_title()', ); $replacement_variables = [ new WPSEO_Replacement_Variable( 'date', __( 'Date', 'wordpress-seo' ), __( 'Replaced with the date of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'title', __( 'Title', 'wordpress-seo' ), __( 'Replaced with the title of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'parent_title', __( 'Parent title', 'wordpress-seo' ), __( 'Replaced with the title of the parent page of the current page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'archive_title', __( 'Archive title', 'wordpress-seo' ), __( 'Replaced with the normal title for an archive generated by WordPress', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sitename', __( 'Site title', 'wordpress-seo' ), __( 'The site\'s name', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sitedesc', __( 'Tagline', 'wordpress-seo' ), __( 'The site\'s tagline', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'excerpt', __( 'Excerpt', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (or auto-generated if it does not exist)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'excerpt_only', __( 'Excerpt only', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (without auto-generation)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'tag', __( 'Tag', 'wordpress-seo' ), __( 'Replaced with the current tag/tags', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category', __( 'Category', 'wordpress-seo' ), __( 'Replaced with the post categories (comma separated)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'primary_category', __( 'Primary category', 'wordpress-seo' ), __( 'Replaced with the primary category of the post/page', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category_description', __( 'Category description', 'wordpress-seo' ), __( 'Replaced with the category description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'tag_description', __( 'Tag description', 'wordpress-seo' ), __( 'Replaced with the tag description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_description', __( 'Term description', 'wordpress-seo' ), __( 'Replaced with the term description', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_title', __( 'Term title', 'wordpress-seo' ), __( 'Replaced with the term name', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'searchphrase', __( 'Search phrase', 'wordpress-seo' ), __( 'Replaced with the current search phrase', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term_hierarchy', __( 'Term hierarchy', 'wordpress-seo' ), __( 'Replaced with the term ancestors hierarchy', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'sep', __( 'Separator', 'wordpress-seo' ), $separator_description ), new WPSEO_Replacement_Variable( 'currentdate', __( 'Current date', 'wordpress-seo' ), __( 'Replaced with the current date', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentyear', __( 'Current year', 'wordpress-seo' ), __( 'Replaced with the current year', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentmonth', __( 'Current month', 'wordpress-seo' ), __( 'Replaced with the current month', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'currentday', __( 'Current day', 'wordpress-seo' ), __( 'Replaced with the current day', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_year', __( 'Post year', 'wordpress-seo' ), __( 'Replaced with the year the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_month', __( 'Post month', 'wordpress-seo' ), __( 'Replaced with the month the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_day', __( 'Post day', 'wordpress-seo' ), __( 'Replaced with the day the post was published', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'author_first_name', __( 'Author first name', 'wordpress-seo' ), __( 'Replaced with the first name of the author', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'author_last_name', __( 'Author last name', 'wordpress-seo' ), __( 'Replaced with the last name of the author', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'permalink', __( 'Permalink', 'wordpress-seo' ), __( 'Replaced with the permalink', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'post_content', __( 'Post Content', 'wordpress-seo' ), __( 'Replaced with the post content', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'category_title', __( 'Category Title', 'wordpress-seo' ), __( 'Current or first category title', 'wordpress-seo' ) ), ]; foreach ( $replacement_variables as $replacement_variable ) { self::register_help_text( 'basic', $replacement_variable ); } } /** * Set/translate the help texts for the WPSEO standard advanced variables. * * @return void */ private static function set_advanced_help_texts() { $replacement_variables = [ new WPSEO_Replacement_Variable( 'pt_single', __( 'Post type (singular)', 'wordpress-seo' ), __( 'Replaced with the content type single label', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pt_plural', __( 'Post type (plural)', 'wordpress-seo' ), __( 'Replaced with the content type plural label', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'modified', __( 'Modified', 'wordpress-seo' ), __( 'Replaced with the post/page modified time', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'id', __( 'ID', 'wordpress-seo' ), __( 'Replaced with the post/page ID', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'name', __( 'Name', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'nicename\'', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'user_description', __( 'User description', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'Biographical Info\'', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'page', __( 'Page', 'wordpress-seo' ), __( 'Replaced with the current page number with context (i.e. page 2 of 4)', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pagetotal', __( 'Pagetotal', 'wordpress-seo' ), __( 'Replaced with the current page total', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'pagenumber', __( 'Pagenumber', 'wordpress-seo' ), __( 'Replaced with the current page number', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'caption', __( 'Caption', 'wordpress-seo' ), __( 'Attachment caption', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'focuskw', __( 'Focus keyword', 'wordpress-seo' ), __( 'Replaced with the posts focus keyphrase', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'term404', __( 'Term404', 'wordpress-seo' ), __( 'Replaced with the slug which caused the 404', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'cf_<custom-field-name>', '<custom-field-name> ' . __( '(custom field)', 'wordpress-seo' ), __( 'Replaced with a posts custom field value', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'ct_<custom-tax-name>', '<custom-tax-name> ' . __( '(custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a posts custom taxonomies, comma separated.', 'wordpress-seo' ) ), new WPSEO_Replacement_Variable( 'ct_desc_<custom-tax-name>', '<custom-tax-name> ' . __( 'description (custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a custom taxonomies description', 'wordpress-seo' ) ), ]; foreach ( $replacement_variables as $replacement_variable ) { self::register_help_text( 'advanced', $replacement_variable ); } } /* *********************** GENERAL HELPER METHODS ************************** */ /** * Remove the '%%' delimiters from a variable string. * * @param string $text Variable string to be cleaned. * * @return string */ private static function remove_var_delimiter( $text ) { return trim( $text, '%' ); } /** * Add the '%%' delimiters to a variable string. * * @param string $text Variable string to be delimited. * * @return string */ private static function add_var_delimiter( $text ) { return '%%' . $text . '%%'; } /** * Retrieve a post's terms, comma delimited. * * @param int $id ID of the post to get the terms for. * @param string $taxonomy The taxonomy to get the terms for this post from. * @param bool $return_single If true, return the first term. * * @return string Either a single term or a comma delimited string of terms. */ public function get_terms( $id, $taxonomy, $return_single = false ) { $output = ''; // If we're on a specific tag, category or taxonomy page, use that. if ( ! empty( $this->args->term_id ) ) { $output = $this->args->name; } elseif ( ! empty( $id ) && ! empty( $taxonomy ) ) { $terms = get_the_terms( $id, $taxonomy ); if ( is_array( $terms ) && $terms !== [] ) { foreach ( $terms as $term ) { if ( $return_single ) { $output = $term->name; break; } else { $output .= $term->name . ', '; } } $output = rtrim( trim( $output ), ',' ); } } unset( $terms, $term ); /** * Allows filtering of the terms list used to replace %%category%%, %%tag%% * and %%ct_<custom-tax-name>%% variables. * * @param string $output Comma-delimited string containing the terms. * @param string $taxonomy The taxonomy of the terms. */ return apply_filters( 'wpseo_terms', $output, $taxonomy ); } /** * Gets a taxonomy term hierarchy including the term to get the parents for. * * @return string */ private function get_term_hierarchy() { if ( ! is_taxonomy_hierarchical( $this->args->taxonomy ) ) { return ''; } $separator = ' ' . $this->retrieve_sep() . ' '; $args = [ 'format' => 'name', 'separator' => $separator, 'link' => false, 'inclusive' => true, ]; return rtrim( get_term_parents_list( $this->args->term_id, $this->args->taxonomy, $args ), $separator, ); } /** * Retrieves the term ancestors hierarchy. * * @return string|null The term ancestors hierarchy. */ private function retrieve_term_hierarchy() { $replacement = null; if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) { $hierarchy = $this->get_term_hierarchy(); if ( $hierarchy !== '' ) { $replacement = esc_html( $hierarchy ); } } return $replacement; } } inc/class-wpseo-shortlinker.php 0000644 00000002154 15174712003 0012632 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * Helps with creating shortlinks in the plugin. */ class WPSEO_Shortlinker { /** * Builds a URL to use in the plugin as shortlink. * * @param string $url The URL to build upon. * * @return string The final URL. */ public function build_shortlink( $url ) { return YoastSEO()->helpers->short_link->build( $url ); } /** * Returns a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return string The final URL. */ public static function get( $url ) { return YoastSEO()->helpers->short_link->get( $url ); } /** * Echoes a version of the URL with a utm_content with the current version. * * @param string $url The URL to build upon. * * @return void */ public static function show( $url ) { YoastSEO()->helpers->short_link->show( $url ); } /** * Gets the shortlink's query params. * * @return array The shortlink's query params. */ public static function get_query_params() { return YoastSEO()->helpers->short_link->get_query_params(); } } inc/class-wpseo-replacement-variable.php 0000644 00000002537 15174712003 0014355 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals * @since 7.7 */ /** * Class WPSEO_Replacement_Variable. * * This class stores the data of a single snippet variable. */ class WPSEO_Replacement_Variable { /** * The variable to use. * * @var string */ protected $variable; /** * The label of the replacement variable. * * @var string */ protected $label; /** * The description of the replacement variable. * * @var string */ protected $description; /** * WPSEO_Replacement_Variable constructor. * * @param string $variable The variable that is replaced. * @param string $label The label of the replacement variable. * @param string $description The description of the replacement variable. */ public function __construct( $variable, $label, $description ) { $this->variable = $variable; $this->label = $label; $this->description = $description; } /** * Returns the variable to use. * * @return string */ public function get_variable() { return $this->variable; } /** * Returns the label of the replacement variable. * * @return string */ public function get_label() { return $this->label; } /** * Returns the description of the replacement variable. * * @return string */ public function get_description() { return $this->description; } } inc/class-wpseo-content-images.php 0000644 00000005137 15174712003 0013207 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO */ /** * WPSEO_Content_Images. */ class WPSEO_Content_Images { /** * Retrieves images from the post content. * * @param int $post_id The post ID. * @param WP_Post|null $post The post object. * * @return array An array of images found in this post. */ public function get_images( $post_id, $post = null ) { return $this->get_images_from_content( $this->get_post_content( $post_id, $post ) ); } /** * Grabs the images from the content. * * @param string $content The post content string. * * @return array An array of image URLs. */ public function get_images_from_content( $content ) { if ( ! is_string( $content ) ) { return []; } $content_images = $this->get_img_tags_from_content( $content ); $images = array_map( [ $this, 'get_img_tag_source' ], $content_images ); $images = array_filter( $images ); $images = array_unique( $images ); $images = array_values( $images ); // Reset the array keys. return $images; } /** * Gets the image tags from a given content string. * * @param string $content The content to search for image tags. * * @return array An array of `<img>` tags. */ private function get_img_tags_from_content( $content ) { if ( strpos( $content, '<img' ) === false ) { return []; } preg_match_all( '`<img [^>]+>`', $content, $matches ); if ( isset( $matches[0] ) ) { return $matches[0]; } return []; } /** * Retrieves the image URL from an image tag. * * @param string $image Image HTML element. * * @return string|bool The image URL on success, false on failure. */ private function get_img_tag_source( $image ) { preg_match( '`src=(["\'])(.*?)\1`', $image, $matches ); if ( isset( $matches[2] ) && filter_var( $matches[2], FILTER_VALIDATE_URL ) ) { return $matches[2]; } return false; } /** * Retrieves the post content we want to work with. * * @param int $post_id The post ID. * @param WP_Post|array|null $post The post. * * @return string The content of the supplied post. */ private function get_post_content( $post_id, $post ) { $post ??= get_post( $post_id ); if ( $post === null ) { return ''; } /** * Filter: 'wpseo_pre_analysis_post_content' - Allow filtering the content before analysis. * * @param string $post_content The Post content string. * @param WP_Post $post The current post. */ $content = apply_filters( 'wpseo_pre_analysis_post_content', $post->post_content, $post ); if ( ! is_string( $content ) ) { $content = ''; } return $content; } } license.txt 0000644 00000101047 15174712003 0006732 0 ustar 00 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 18. Additional terms In the light of Article 7 of the GPL license, the following additional terms apply: a) You are prohibited to make misrepresentations of the origin of that material, or to require that modified versions of such material be marked in reasonable ways as different from the original version; b) You are limited in the use for publicity purposes of names of licensors or authors of the material; c) You are declined any grant of rights under trademark law for use of the trade names, trademarks, or service marks of YOAST B.V.; d) You are required to indemnify licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. END OF TERMS AND CONDITIONS admin/class-yoast-notification-center.php 0000644 00000064030 15174712003 0014554 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Notifications */ use Yoast\WP\SEO\Presenters\Abstract_Presenter; /** * Handles notifications storage and display. */ class Yoast_Notification_Center { /** * Option name to store notifications on. * * @var string */ public const STORAGE_KEY = 'yoast_notifications'; /** * The singleton instance of this object. * * @var Yoast_Notification_Center|null */ private static $instance = null; /** * Holds the notifications. * * @var Yoast_Notification[][] */ private $notifications = []; /** * Notifications there are newly added. * * @var array */ private $new = []; /** * Notifications that were resolved this execution. * * @var int */ private $resolved = 0; /** * Internal storage for transaction before notifications have been retrieved from storage. * * @var array */ private $queued_transactions = []; /** * Internal flag for whether notifications have been retrieved from storage. * * @var bool */ private $notifications_retrieved = false; /** * Internal flag for whether notifications need to be updated in storage. * * @var bool */ private $notifications_need_storage = false; /** * Construct. */ private function __construct() { add_action( 'init', [ $this, 'setup_current_notifications' ], 1 ); add_action( 'all_admin_notices', [ $this, 'display_notifications' ] ); add_action( 'wp_ajax_yoast_get_notifications', [ $this, 'ajax_get_notifications' ] ); add_action( 'wpseo_deactivate', [ $this, 'deactivate_hook' ] ); add_action( 'shutdown', [ $this, 'update_storage' ] ); } /** * Singleton getter. * * @return Yoast_Notification_Center */ public static function get() { self::$instance ??= new self(); return self::$instance; } /** * Dismiss a notification. * * @return void */ public static function ajax_dismiss_notification() { $notification_center = self::get(); if ( ! isset( $_POST['notification'] ) || ! is_string( $_POST['notification'] ) ) { exit( '-1' ); } $notification_id = sanitize_text_field( wp_unslash( $_POST['notification'] ) ); if ( empty( $notification_id ) ) { exit( '-1' ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a nonce. if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), $notification_id ) ) { exit( '-1' ); } $notification = $notification_center->get_notification_by_id( $notification_id ); if ( ( $notification instanceof Yoast_Notification ) === false ) { // Permit legacy. $options = [ 'id' => $notification_id, 'dismissal_key' => $notification_id, ]; $notification = new Yoast_Notification( '', $options ); } if ( self::maybe_dismiss_notification( $notification ) ) { exit( '1' ); } exit( '-1' ); } /** * Check if the user has dismissed a notification. * * @param Yoast_Notification $notification The notification to check for dismissal. * @param int|null $user_id User ID to check on. * * @return bool */ public static function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) { $user_id = self::get_user_id( $user_id ); $dismissal_key = $notification->get_dismissal_key(); // This checks both the site-specific user option and the meta value. $current_value = get_user_option( $dismissal_key, $user_id ); // Migrate old user meta to user option on-the-fly. if ( ! empty( $current_value ) && metadata_exists( 'user', $user_id, $dismissal_key ) && update_user_option( $user_id, $dismissal_key, $current_value ) ) { delete_user_meta( $user_id, $dismissal_key ); } return ! empty( $current_value ); } /** * Checks if the notification is being dismissed. * * @param Yoast_Notification $notification Notification to check dismissal of. * @param string $meta_value Value to set the meta value to if dismissed. * * @return bool True if dismissed. */ public static function maybe_dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) { // Only persistent notifications are dismissible. if ( ! $notification->is_persistent() ) { return false; } // If notification is already dismissed, we're done. if ( self::is_notification_dismissed( $notification ) ) { return true; } $dismissal_key = $notification->get_dismissal_key(); $notification_id = $notification->get_id(); $is_dismissing = ( $dismissal_key === self::get_user_input( 'notification' ) ); if ( ! $is_dismissing ) { $is_dismissing = ( $notification_id === self::get_user_input( 'notification' ) ); } // Fallback to ?dismissal_key=1&nonce=bla when JavaScript fails. if ( ! $is_dismissing ) { $is_dismissing = ( self::get_user_input( $dismissal_key ) === '1' ); } if ( ! $is_dismissing ) { return false; } $user_nonce = self::get_user_input( 'nonce' ); if ( wp_verify_nonce( $user_nonce, $notification_id ) === false ) { return false; } return self::dismiss_notification( $notification, $meta_value ); } /** * Dismisses a notification. * * @param Yoast_Notification $notification Notification to dismiss. * @param string $meta_value Value to save in the dismissal. * * @return bool True if dismissed, false otherwise. */ public static function dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) { // Dismiss notification. return update_user_option( get_current_user_id(), $notification->get_dismissal_key(), $meta_value ) !== false; } /** * Restores a notification. * * @param Yoast_Notification $notification Notification to restore. * * @return bool True if restored, false otherwise. */ public static function restore_notification( Yoast_Notification $notification ) { $user_id = get_current_user_id(); $dismissal_key = $notification->get_dismissal_key(); // Restore notification. $restored = delete_user_option( $user_id, $dismissal_key ); // Delete unprefixed user meta too for backward-compatibility. if ( metadata_exists( 'user', $user_id, $dismissal_key ) ) { $restored = delete_user_meta( $user_id, $dismissal_key ) && $restored; } return $restored; } /** * Clear dismissal information for the specified Notification. * * When a cause is resolved, the next time it is present we want to show * the message again. * * @param string|Yoast_Notification $notification Notification to clear the dismissal of. * * @return bool */ public function clear_dismissal( $notification ) { global $wpdb; if ( $notification instanceof Yoast_Notification ) { $dismissal_key = $notification->get_dismissal_key(); } if ( is_string( $notification ) ) { $dismissal_key = $notification; } if ( empty( $dismissal_key ) ) { return false; } // Remove notification dismissal for all users. $deleted = delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . $dismissal_key, '', true ); // Delete unprefixed user meta too for backward-compatibility. $deleted = delete_metadata( 'user', 0, $dismissal_key, '', true ) || $deleted; return $deleted; } /** * Retrieves notifications from the storage and merges in previous notification changes. * * The current user in WordPress is not loaded shortly before the 'init' hook, but the plugin * sometimes needs to add or remove notifications before that. In such cases, the transactions * are not actually executed, but added to a queue. That queue is then handled in this method, * after notifications for the current user have been set up. * * @return void */ public function setup_current_notifications() { $this->retrieve_notifications_from_storage( get_current_user_id() ); foreach ( $this->queued_transactions as $transaction ) { list( $callback, $args ) = $transaction; call_user_func_array( $callback, $args ); } $this->queued_transactions = []; } /** * Add notification to the cookie. * * @param Yoast_Notification $notification Notification object instance. * * @return void */ public function add_notification( Yoast_Notification $notification ) { $callback = [ $this, __FUNCTION__ ]; $args = func_get_args(); if ( $this->queue_transaction( $callback, $args ) ) { return; } // Don't add if the user can't see it. if ( ! $notification->display_for_current_user() ) { return; } $notification_id = $notification->get_id(); $user_id = $notification->get_user_id(); // Empty notifications are always added. if ( $notification_id !== '' ) { // If notification ID exists in notifications, don't add again. $present_notification = $this->get_notification_by_id( $notification_id, $user_id ); if ( $present_notification !== null ) { $this->remove_notification( $present_notification, false ); } if ( $present_notification === null ) { $this->new[] = $notification_id; } } // Add to list. $this->notifications[ $user_id ][] = $notification; $this->notifications_need_storage = true; } /** * Get the notification by ID and user ID. * * @param string $notification_id The ID of the notification to search for. * @param int|null $user_id The ID of the user. * * @return Yoast_Notification|null */ public function get_notification_by_id( $notification_id, $user_id = null ) { $user_id = self::get_user_id( $user_id ); $notifications = $this->get_notifications_for_user( $user_id ); foreach ( $notifications as $notification ) { if ( $notification_id === $notification->get_id() ) { return $notification; } } return null; } /** * Display the notifications. * * @param bool $echo_as_json True when notifications should be printed directly. * * @return void */ public function display_notifications( $echo_as_json = false ) { // Never display notifications for network admin. if ( is_network_admin() ) { return; } $sorted_notifications = $this->get_sorted_notifications(); $notifications = array_filter( $sorted_notifications, [ $this, 'is_notification_persistent' ] ); if ( empty( $notifications ) ) { return; } array_walk( $notifications, [ $this, 'remove_notification' ] ); $notifications = array_unique( $notifications ); if ( $echo_as_json ) { $notification_json = []; foreach ( $notifications as $notification ) { $notification_json[] = $notification->render(); } // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. echo WPSEO_Utils::format_json_encode( $notification_json ); return; } foreach ( $notifications as $notification ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Temporarily disabled, see: https://github.com/Yoast/wordpress-seo-premium/issues/2510 and https://github.com/Yoast/wordpress-seo-premium/issues/2511. echo $notification; } } /** * Remove notification after it has been displayed. * * @param Yoast_Notification $notification Notification to remove. * @param bool $resolve Resolve as fixed. * * @return void */ public function remove_notification( Yoast_Notification $notification, $resolve = true ) { $callback = [ $this, __FUNCTION__ ]; $args = func_get_args(); if ( $this->queue_transaction( $callback, $args ) ) { return; } $index = false; // ID of the user to show the notification for, defaults to current user id. $user_id = $notification->get_user_id(); $notifications = $this->get_notifications_for_user( $user_id ); // Match persistent Notifications by ID, non persistent by item in the array. if ( $notification->is_persistent() ) { foreach ( $notifications as $current_index => $present_notification ) { if ( $present_notification->get_id() === $notification->get_id() ) { $index = $current_index; break; } } } else { $index = array_search( $notification, $notifications, true ); } if ( $index === false ) { return; } if ( $notification->is_persistent() && $resolve ) { ++$this->resolved; $this->clear_dismissal( $notification ); } unset( $notifications[ $index ] ); $this->notifications[ $user_id ] = array_values( $notifications ); $this->notifications_need_storage = true; } /** * Removes a notification by its ID. * * @param string $notification_id The notification id. * @param bool $resolve Resolve as fixed. * * @return void */ public function remove_notification_by_id( $notification_id, $resolve = true ) { $notification = $this->get_notification_by_id( $notification_id ); if ( $notification === null ) { return; } $this->remove_notification( $notification, $resolve ); $this->notifications_need_storage = true; } /** * Get the notification count. * * @param bool $dismissed Count dismissed notifications. * * @return int Number of notifications */ public function get_notification_count( $dismissed = false ) { $notifications = $this->get_notifications_for_user( get_current_user_id() ); $notifications = array_filter( $notifications, [ $this, 'filter_persistent_notifications' ] ); if ( ! $dismissed ) { $notifications = array_filter( $notifications, [ $this, 'filter_dismissed_notifications' ] ); } return count( $notifications ); } /** * Get the number of notifications resolved this execution. * * These notifications have been resolved and should be counted when active again. * * @return int */ public function get_resolved_notification_count() { return $this->resolved; } /** * Return the notifications sorted on type and priority. * * @return Yoast_Notification[] Sorted Notifications */ public function get_sorted_notifications() { $notifications = $this->get_notifications_for_user( get_current_user_id() ); if ( empty( $notifications ) ) { return []; } // Sort by severity, error first. usort( $notifications, [ $this, 'sort_notifications' ] ); return $notifications; } /** * AJAX display notifications. * * @return void */ public function ajax_get_notifications() { $echo = false; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form data. if ( isset( $_POST['version'] ) && is_string( $_POST['version'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable in a condition. $echo = wp_unslash( $_POST['version'] ) === '2'; } // Display the notices. $this->display_notifications( $echo ); // AJAX die. exit(); } /** * Remove storage when the plugin is deactivated. * * @return void */ public function deactivate_hook() { $this->clear_notifications(); } /** * Returns the given user ID if it exists. * Otherwise, this function returns the ID of the current user. * * @param int $user_id The user ID to check. * * @return int The user ID to use. */ private static function get_user_id( $user_id ) { if ( $user_id ) { return $user_id; } return get_current_user_id(); } /** * Splits the notifications on user ID. * * In other terms, it returns an associative array, * mapping user ID to a list of notifications for this user. * * @param Yoast_Notification[] $notifications The notifications to split. * * @return array The notifications, split on user ID. */ private function split_on_user_id( $notifications ) { $split_notifications = []; foreach ( $notifications as $notification ) { $split_notifications[ $notification->get_user_id() ][] = $notification; } return $split_notifications; } /** * Save persistent notifications to storage. * * We need to be able to retrieve these so they can be dismissed at any time during the execution. * * @since 3.2 * * @return void */ public function update_storage() { /** * Plugins might exit on the plugins_loaded hook. * This prevents the pluggable.php file from loading, as it's loaded after the plugins_loaded hook. * As we need functions defined in pluggable.php, make sure it's loaded. */ require_once ABSPATH . WPINC . '/pluggable.php'; $notifications = $this->notifications; /** * One array of Yoast_Notifications, merged from multiple arrays. * * @var Yoast_Notification[] $merged_notifications */ $merged_notifications = []; if ( ! empty( $notifications ) ) { $merged_notifications = array_merge( ...$notifications ); } /** * Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them. * * @param Yoast_Notification[] $notifications */ $filtered_merged_notifications = apply_filters( 'yoast_notifications_before_storage', $merged_notifications ); // The notifications were filtered and therefore need to be stored. if ( $merged_notifications !== $filtered_merged_notifications ) { $merged_notifications = $filtered_merged_notifications; $this->notifications_need_storage = true; } $notifications = $this->split_on_user_id( $merged_notifications ); // No notifications to store, clear storage if it was previously present. if ( empty( $notifications ) ) { $this->remove_storage(); return; } // Only store notifications if changes are made. if ( $this->notifications_need_storage ) { array_walk( $notifications, [ $this, 'store_notifications_for_user' ] ); } } /** * Stores the notifications to its respective user's storage. * * @param Yoast_Notification[] $notifications The notifications to store. * @param int $user_id The ID of the user for which to store the notifications. * * @return void */ private function store_notifications_for_user( $notifications, $user_id ) { $notifications_as_arrays = array_map( [ $this, 'notification_to_array' ], $notifications ); update_user_option( $user_id, self::STORAGE_KEY, $notifications_as_arrays ); } /** * Provide a way to verify present notifications. * * @return Yoast_Notification[] Registered notifications. */ public function get_notifications() { if ( ! $this->notifications ) { return []; } return array_merge( ...$this->notifications ); } /** * Returns the notifications for the given user. * * @param int $user_id The id of the user to check. * * @return Yoast_Notification[] The notifications for the user with the given ID. */ public function get_notifications_for_user( $user_id ) { if ( array_key_exists( $user_id, $this->notifications ) ) { return $this->notifications[ $user_id ]; } return []; } /** * Get newly added notifications. * * @return array */ public function get_new_notifications() { return array_map( [ $this, 'get_notification_by_id' ], $this->new ); } /** * Get information from the User input. * * Note that this function does not handle nonce verification. * * @param string $key Key to retrieve. * * @return string non-sanitized value of key if set, an empty string otherwise. */ private static function get_user_input( $key ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information and only using this variable in a comparison. $request_method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: This function does not sanitize variables. // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Reason: This function does not verify a nonce. if ( $request_method === 'POST' ) { if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) { return wp_unslash( $_POST[ $key ] ); } } elseif ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) { return wp_unslash( $_GET[ $key ] ); } // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return ''; } /** * Retrieve the notifications from storage and fill the relevant property. * * @param int $user_id The ID of the user to retrieve notifications for. * * @return void */ private function retrieve_notifications_from_storage( $user_id ) { if ( $this->notifications_retrieved ) { return; } $this->notifications_retrieved = true; $stored_notifications = get_user_option( self::STORAGE_KEY, $user_id ); // Check if notifications are stored. if ( empty( $stored_notifications ) ) { return; } if ( is_array( $stored_notifications ) ) { $notifications = array_map( [ $this, 'array_to_notification' ], $stored_notifications ); // Apply array_values to ensure we get a 0-indexed array. $notifications = array_values( array_filter( $notifications, [ $this, 'filter_notification_current_user' ] ) ); $this->notifications[ $user_id ] = $notifications; } } /** * Sort on type then priority. * * @param Yoast_Notification $a Compare with B. * @param Yoast_Notification $b Compare with A. * * @return int 1, 0 or -1 for sorting offset. */ private function sort_notifications( Yoast_Notification $a, Yoast_Notification $b ) { $a_type = $a->get_type(); $b_type = $b->get_type(); if ( $a_type === $b_type ) { return WPSEO_Utils::calc( $b->get_priority(), 'compare', $a->get_priority() ); } if ( $a_type === 'error' ) { return -1; } if ( $b_type === 'error' ) { return 1; } return 0; } /** * Clear local stored notifications. * * @return void */ private function clear_notifications() { $this->notifications = []; $this->notifications_retrieved = false; } /** * Filter out non-persistent notifications. * * @since 3.2 * * @param Yoast_Notification $notification Notification to test for persistent. * * @return bool */ private function filter_persistent_notifications( Yoast_Notification $notification ) { return $notification->is_persistent(); } /** * Filter out dismissed notifications. * * @param Yoast_Notification $notification Notification to check. * * @return bool */ private function filter_dismissed_notifications( Yoast_Notification $notification ) { return ! self::maybe_dismiss_notification( $notification ); } /** * Convert Notification to array representation. * * @since 3.2 * * @param Yoast_Notification $notification Notification to convert. * * @return array */ private function notification_to_array( Yoast_Notification $notification ) { $notification_data = $notification->to_array(); if ( isset( $notification_data['nonce'] ) ) { unset( $notification_data['nonce'] ); } return $notification_data; } /** * Convert stored array to Notification. * * @param array $notification_data Array to convert to Notification. * * @return Yoast_Notification */ private function array_to_notification( $notification_data ) { if ( isset( $notification_data['options']['nonce'] ) ) { unset( $notification_data['options']['nonce'] ); } if ( isset( $notification_data['message'] ) && is_subclass_of( $notification_data['message'], Abstract_Presenter::class, false ) ) { $notification_data['message'] = $notification_data['message']->present(); } if ( isset( $notification_data['options']['user'] ) ) { $notification_data['options']['user_id'] = $notification_data['options']['user']->ID; unset( $notification_data['options']['user'] ); $this->notifications_need_storage = true; } return new Yoast_Notification( $notification_data['message'], $notification_data['options'], ); } /** * Filter notifications that should not be displayed for the current user. * * @param Yoast_Notification $notification Notification to test. * * @return bool */ private function filter_notification_current_user( Yoast_Notification $notification ) { return $notification->display_for_current_user(); } /** * Checks if given notification is persistent. * * @param Yoast_Notification $notification The notification to check. * * @return bool True when notification is not persistent. */ private function is_notification_persistent( Yoast_Notification $notification ) { return ! $notification->is_persistent(); } /** * Queues a notification transaction for later execution if notifications are not yet set up. * * @param callable $callback Callback that performs the transaction. * @param array $args Arguments to pass to the callback. * * @return bool True if transaction was queued, false if it can be performed immediately. */ private function queue_transaction( $callback, $args ) { if ( $this->notifications_retrieved ) { return false; } $this->add_transaction_to_queue( $callback, $args ); return true; } /** * Adds a notification transaction to the queue for later execution. * * @param callable $callback Callback that performs the transaction. * @param array $args Arguments to pass to the callback. * * @return void */ private function add_transaction_to_queue( $callback, $args ) { $this->queued_transactions[] = [ $callback, $args ]; } /** * Removes all notifications from storage. * * @return bool True when notifications got removed. */ protected function remove_storage() { if ( ! $this->has_stored_notifications() ) { return false; } delete_user_option( get_current_user_id(), self::STORAGE_KEY ); return true; } /** * Checks if there are stored notifications. * * @return bool True when there are stored notifications. */ protected function has_stored_notifications() { $stored_notifications = $this->get_stored_notifications(); return ! empty( $stored_notifications ); } /** * Retrieves the stored notifications. * * @codeCoverageIgnore * * @return array|false Array with notifications or false when not set. */ protected function get_stored_notifications() { return get_user_option( self::STORAGE_KEY, get_current_user_id() ); } } admin/class-admin-recommended-replace-vars.php 0000644 00000013760 15174712003 0015411 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Determines the recommended replacement variables based on the context. */ class WPSEO_Admin_Recommended_Replace_Vars { /** * The recommended replacement variables. * * @var array */ protected $recommended_replace_vars = [ // Posts types. 'page' => [ 'sitename', 'title', 'sep', 'primary_category' ], 'post' => [ 'sitename', 'title', 'sep', 'primary_category' ], // Homepage. 'homepage' => [ 'sitename', 'sitedesc', 'sep' ], // Custom post type. 'custom_post_type' => [ 'sitename', 'title', 'sep' ], // Taxonomies. 'category' => [ 'sitename', 'term_title', 'sep', 'term_hierarchy' ], 'post_tag' => [ 'sitename', 'term_title', 'sep' ], 'post_format' => [ 'sitename', 'term_title', 'sep', 'page' ], // Custom taxonomy. 'term-in-custom-taxonomy' => [ 'sitename', 'term_title', 'sep', 'term_hierarchy' ], // Settings - archive pages. 'author_archive' => [ 'sitename', 'title', 'sep', 'page' ], 'date_archive' => [ 'sitename', 'sep', 'date', 'page' ], 'custom-post-type_archive' => [ 'sitename', 'title', 'sep', 'page' ], // Settings - special pages. 'search' => [ 'sitename', 'searchphrase', 'sep', 'page' ], '404' => [ 'sitename', 'sep' ], ]; /** * Determines the page type of the current term. * * @param string $taxonomy The taxonomy name. * * @return string The page type. */ public function determine_for_term( $taxonomy ) { $recommended_replace_vars = $this->get_recommended_replacevars(); if ( array_key_exists( $taxonomy, $recommended_replace_vars ) ) { return $taxonomy; } return 'term-in-custom-taxonomy'; } /** * Determines the page type of the current post. * * @param WP_Post $post A WordPress post instance. * * @return string The page type. */ public function determine_for_post( $post ) { if ( $post instanceof WP_Post === false ) { return 'post'; } if ( $post->post_type === 'page' && $this->is_homepage( $post ) ) { return 'homepage'; } $recommended_replace_vars = $this->get_recommended_replacevars(); if ( array_key_exists( $post->post_type, $recommended_replace_vars ) ) { return $post->post_type; } return 'custom_post_type'; } /** * Determines the page type for a post type. * * @param string $post_type The name of the post_type. * @param string $fallback The page type to fall back to. * * @return string The page type. */ public function determine_for_post_type( $post_type, $fallback = 'custom_post_type' ) { $page_type = $post_type; $recommended_replace_vars = $this->get_recommended_replacevars(); $has_recommended_replacevars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type ); if ( ! $has_recommended_replacevars ) { return $fallback; } return $page_type; } /** * Determines the page type for an archive page. * * @param string $name The name of the archive. * @param string $fallback The page type to fall back to. * * @return string The page type. */ public function determine_for_archive( $name, $fallback = 'custom-post-type_archive' ) { $page_type = $name . '_archive'; $recommended_replace_vars = $this->get_recommended_replacevars(); $has_recommended_replacevars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type ); if ( ! $has_recommended_replacevars ) { return $fallback; } return $page_type; } /** * Retrieves the recommended replacement variables for the given page type. * * @param string $page_type The page type. * * @return array The recommended replacement variables. */ public function get_recommended_replacevars_for( $page_type ) { $recommended_replace_vars = $this->get_recommended_replacevars(); $has_recommended_replace_vars = $this->has_recommended_replace_vars( $recommended_replace_vars, $page_type ); if ( ! $has_recommended_replace_vars ) { return []; } return $recommended_replace_vars[ $page_type ]; } /** * Retrieves the recommended replacement variables. * * @return array The recommended replacement variables. */ public function get_recommended_replacevars() { /** * Filter: Adds the possibility to add extra recommended replacement variables. * * @param array $additional_replace_vars Empty array to add the replacevars to. */ $recommended_replace_vars = apply_filters( 'wpseo_recommended_replace_vars', $this->recommended_replace_vars ); if ( ! is_array( $recommended_replace_vars ) ) { return $this->recommended_replace_vars; } return $recommended_replace_vars; } /** * Returns whether the given page type has recommended replace vars. * * @param array $recommended_replace_vars The recommended replace vars * to check in. * @param string $page_type The page type to check. * * @return bool True if there are associated recommended replace vars. */ private function has_recommended_replace_vars( $recommended_replace_vars, $page_type ) { if ( ! isset( $recommended_replace_vars[ $page_type ] ) ) { return false; } if ( ! is_array( $recommended_replace_vars[ $page_type ] ) ) { return false; } return true; } /** * Determines whether or not a post is the homepage. * * @param WP_Post $post The WordPress global post object. * * @return bool True if the given post is the homepage. */ private function is_homepage( $post ) { if ( $post instanceof WP_Post === false ) { return false; } /* * The page on front returns a string with normal WordPress interaction, while the post ID is an int. * This way we make sure we always compare strings. */ $post_id = (int) $post->ID; $page_on_front = (int) get_option( 'page_on_front' ); return get_option( 'show_on_front' ) === 'page' && $page_on_front === $post_id; } } admin/endpoints/class-endpoint-file-size.php 0000644 00000003341 15174712003 0015161 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Endpoints */ /** * Represents an implementation of the WPSEO_Endpoint interface to register one or multiple endpoints. */ class WPSEO_Endpoint_File_Size implements WPSEO_Endpoint { /** * The namespace of the REST route. * * @var string */ public const REST_NAMESPACE = 'yoast/v1'; /** * The route of the endpoint to retrieve the file size. * * @var string */ public const ENDPOINT_SINGULAR = 'file_size'; /** * The name of the capability needed to retrieve data using the endpoints. * * @var string */ public const CAPABILITY_RETRIEVE = 'manage_options'; /** * The service provider. * * @var WPSEO_File_Size_Service */ private $service; /** * Sets the service provider. * * @param WPSEO_File_Size_Service $service The service provider. */ public function __construct( WPSEO_File_Size_Service $service ) { $this->service = $service; } /** * Registers the routes for the endpoints. * * @return void */ public function register() { $route_args = [ 'methods' => 'GET', 'args' => [ 'url' => [ 'required' => true, 'type' => 'string', 'description' => 'The url to retrieve', ], ], 'callback' => [ $this->service, 'get', ], 'permission_callback' => [ $this, 'can_retrieve_data', ], ]; register_rest_route( self::REST_NAMESPACE, self::ENDPOINT_SINGULAR, $route_args ); } /** * Determines whether or not data can be retrieved for the registered endpoints. * * @return bool Whether or not data can be retrieved. */ public function can_retrieve_data() { return current_user_can( self::CAPABILITY_RETRIEVE ); } } admin/endpoints/class-endpoint-statistics.php 0000644 00000003167 15174712003 0015472 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Statistics */ /** * Represents an implementation of the WPSEO_Endpoint interface to register one or multiple endpoints. */ class WPSEO_Endpoint_Statistics implements WPSEO_Endpoint { /** * The namespace of the REST route. * * @var string */ public const REST_NAMESPACE = 'yoast/v1'; /** * The route of the statistics endpoint. * * @var string */ public const ENDPOINT_RETRIEVE = 'statistics'; /** * The name of the capability needed to retrieve data using the endpoints. * * @var string */ public const CAPABILITY_RETRIEVE = 'read'; /** * Service to use. * * @var WPSEO_Statistics_Service */ protected $service; /** * Constructs the WPSEO_Endpoint_Statistics class and sets the service to use. * * @param WPSEO_Statistics_Service $service Service to use. */ public function __construct( WPSEO_Statistics_Service $service ) { $this->service = $service; } /** * Registers the REST routes that are available on the endpoint. * * @return void */ public function register() { // Register fetch config. $route_args = [ 'methods' => 'GET', 'callback' => [ $this->service, 'get_statistics' ], 'permission_callback' => [ $this, 'can_retrieve_data' ], ]; register_rest_route( self::REST_NAMESPACE, self::ENDPOINT_RETRIEVE, $route_args ); } /** * Determines whether or not data can be retrieved for the registered endpoints. * * @return bool Whether or not data can be retrieved. */ public function can_retrieve_data() { return current_user_can( self::CAPABILITY_RETRIEVE ); } } admin/endpoints/class-endpoint.php 0000644 00000000727 15174712003 0013301 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Endpoints */ /** * Dictates the required methods for an Endpoint implementation. */ interface WPSEO_Endpoint { /** * Registers the routes for the endpoints. * * @return void */ public function register(); /** * Determines whether or not data can be retrieved for the registered endpoints. * * @return bool Whether or not data can be retrieved. */ public function can_retrieve_data(); } admin/class-yoast-notification.php 0000644 00000024144 15174712003 0013300 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Notifications * @since 1.5.3 */ /** * Implements individual notification. */ class Yoast_Notification { /** * Type of capability check. * * @var string */ public const MATCH_ALL = 'all'; /** * Type of capability check. * * @var string */ public const MATCH_ANY = 'any'; /** * Notification type. * * @var string */ public const ERROR = 'error'; /** * Notification type. * * @var string */ public const WARNING = 'warning'; /** * Notification type. * * @var string */ public const UPDATED = 'updated'; /** * Options of this Notification. * * Contains optional arguments: * * - type: The notification type, i.e. 'updated' or 'error' * - id: The ID of the notification * - nonce: Security nonce to use in case of dismissible notice. * - priority: From 0 to 1, determines the order of Notifications. * - dismissal_key: Option name to save dismissal information in, ID will be used if not supplied. * - capabilities: Capabilities that a user must have for this Notification to show. * - capability_check: How to check capability pass: all or any. * - wpseo_page_only: Only display on wpseo page or on every page. * - yoast_branding: Whether to show the Yoast SEO branding in the notification. * - resolve_nonce: Security nonce to use in case of resolving the notification. * * @var array */ private $options = []; /** * Contains default values for the optional arguments. * * @var array */ private $defaults = [ 'type' => self::UPDATED, 'id' => '', 'user_id' => null, 'nonce' => null, 'priority' => 0.5, 'data_json' => [], 'dismissal_key' => null, 'capabilities' => [], 'capability_check' => self::MATCH_ALL, 'yoast_branding' => false, 'resolve_nonce' => '', ]; /** * The message for the notification. * * @var string */ private $message; /** * Notification class constructor. * * @param string $message Message string. * @param array $options Set of options. */ public function __construct( $message, $options = [] ) { $this->message = $message; $this->options = $this->normalize_options( $options ); } /** * Retrieve notification ID string. * * @return string */ public function get_id() { return $this->options['id']; } /** * Retrieve the user to show the notification for. * * @deprecated 21.6 * @codeCoverageIgnore * * @return WP_User|null The user to show this notification for. */ public function get_user() { _deprecated_function( __METHOD__, 'Yoast SEO 21.6' ); return null; } /** * Retrieve the id of the user to show the notification for. * * Returns the id of the current user if not user has been sent. * * @return int The user id */ public function get_user_id() { return ( $this->options['user_id'] ?? get_current_user_id() ); } /** * Retrieve nonce identifier. * * @return string|null Nonce for this Notification. */ public function get_nonce() { if ( $this->options['id'] && empty( $this->options['nonce'] ) ) { $this->options['nonce'] = wp_create_nonce( $this->options['id'] ); } return $this->options['nonce']; } /** * Make sure the nonce is up to date. * * @return void */ public function refresh_nonce() { if ( $this->options['id'] ) { $this->options['nonce'] = wp_create_nonce( $this->options['id'] ); } } /** * Get the type of the notification. * * @return string */ public function get_type() { return $this->options['type']; } /** * Priority of the notification. * * Relative to the type. * * @return float Returns the priority between 0 and 1. */ public function get_priority() { return $this->options['priority']; } /** * Get the nonce to resolve the alert. * * @return string */ public function get_resolve_nonce() { return $this->options['resolve_nonce']; } /** * Get the User Meta key to check for dismissal of notification. * * @return string User Meta Option key that registers dismissal. */ public function get_dismissal_key() { if ( empty( $this->options['dismissal_key'] ) ) { return $this->options['id']; } return $this->options['dismissal_key']; } /** * Is this Notification persistent. * * @return bool True if persistent, False if fire and forget. */ public function is_persistent() { $id = $this->get_id(); return ! empty( $id ); } /** * Check if the notification is relevant for the current user. * * @return bool True if a user needs to see this notification, false if not. */ public function display_for_current_user() { // If the notification is for the current page only, always show. if ( ! $this->is_persistent() ) { return true; } // If the current user doesn't match capabilities. return $this->match_capabilities(); } /** * Does the current user match required capabilities. * * @return bool */ public function match_capabilities() { // Super Admin can do anything. if ( is_multisite() && is_super_admin( $this->options['user_id'] ) ) { return true; } /** * Filter capabilities that enable the displaying of this notification. * * @param array $capabilities The capabilities that must be present for this notification. * @param Yoast_Notification $notification The notification object. * * @return array Array of capabilities or empty for no restrictions. * * @since 3.2 */ $capabilities = apply_filters( 'wpseo_notification_capabilities', $this->options['capabilities'], $this ); // Should be an array. if ( ! is_array( $capabilities ) ) { $capabilities = (array) $capabilities; } /** * Filter capability check to enable all or any capabilities. * * @param string $capability_check The type of check that will be used to determine if an capability is present. * @param Yoast_Notification $notification The notification object. * * @return string self::MATCH_ALL or self::MATCH_ANY. * * @since 3.2 */ $capability_check = apply_filters( 'wpseo_notification_capability_check', $this->options['capability_check'], $this ); if ( ! in_array( $capability_check, [ self::MATCH_ALL, self::MATCH_ANY ], true ) ) { $capability_check = self::MATCH_ALL; } if ( ! empty( $capabilities ) ) { $has_capabilities = array_filter( $capabilities, [ $this, 'has_capability' ] ); switch ( $capability_check ) { case self::MATCH_ALL: return $has_capabilities === $capabilities; case self::MATCH_ANY: return ! empty( $has_capabilities ); } } return true; } /** * Array filter function to find matched capabilities. * * @param string $capability Capability to test. * * @return bool */ private function has_capability( $capability ) { $user_id = $this->options['user_id']; if ( ! is_numeric( $user_id ) ) { return false; } $user = get_user_by( 'id', $user_id ); if ( ! $user ) { return false; } return $user->has_cap( $capability ); } /** * Return the object properties as an array. * * @return array */ public function to_array() { return [ 'message' => $this->message, 'options' => $this->options, ]; } /** * Adds string (view) behaviour to the notification. * * @return string */ public function __toString() { return $this->render(); } /** * Renders the notification as a string. * * @return string The rendered notification. */ public function render() { $attributes = []; // Default notification classes. $classes = [ 'yoast-notification', ]; // Maintain WordPress visualisation of notifications when they are not persistent. if ( ! $this->is_persistent() ) { $classes[] = 'notice'; $classes[] = $this->get_type(); } if ( ! empty( $classes ) ) { $attributes['class'] = implode( ' ', $classes ); } // Combined attribute key and value into a string. array_walk( $attributes, [ $this, 'parse_attributes' ] ); $message = null; if ( $this->options['yoast_branding'] ) { $message = $this->wrap_yoast_seo_icon( $this->message ); } $message ??= wpautop( $this->message ); // Build the output DIV. return '<div ' . implode( ' ', $attributes ) . '>' . $message . '</div>' . PHP_EOL; } /** * Get the message for the notification. * * @return string The message. */ public function get_message() { return wpautop( $this->message ); } /** * Wraps the message with a Yoast SEO icon. * * @param string $message The message to wrap. * * @return string The wrapped message. */ private function wrap_yoast_seo_icon( $message ) { $out = sprintf( '<img src="%1$s" height="%2$d" width="%3$d" class="yoast-seo-icon" />', esc_url( plugin_dir_url( WPSEO_FILE ) . 'packages/js/images/Yoast_SEO_Icon.svg' ), 60, 60, ); $out .= '<div class="yoast-seo-icon-wrap">'; $out .= $message; $out .= '</div>'; return $out; } /** * Get the JSON if provided. * * @return string|false */ public function get_json() { if ( empty( $this->options['data_json'] ) ) { return ''; } return WPSEO_Utils::format_json_encode( $this->options['data_json'] ); } /** * Make sure we only have values that we can work with. * * @param array $options Options to normalize. * * @return array */ private function normalize_options( $options ) { $options = wp_parse_args( $options, $this->defaults ); // Should not exceed 0 or 1. $options['priority'] = min( 1, max( 0, $options['priority'] ) ); // Set default capabilities when not supplied. if ( empty( $options['capabilities'] ) || $options['capabilities'] === [] ) { $options['capabilities'] = [ 'wpseo_manage_options' ]; } // Set to the id of the current user if not supplied. $options['user_id'] ??= get_current_user_id(); return $options; } /** * Format HTML element attributes. * * @param string $value Attribute value. * @param string $key Attribute name. * * @return void */ private function parse_attributes( &$value, $key ) { $value = sprintf( '%s="%s"', sanitize_key( $key ), esc_attr( $value ) ); } } admin/class-admin-asset-location.php 0000644 00000000750 15174712003 0013465 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents a way to determine an assets location. */ interface WPSEO_Admin_Asset_Location { /** * Determines the URL of the asset on the dev server. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. Usually JS or CSS. * * @return string The URL of the asset. */ public function get_url( WPSEO_Admin_Asset $asset, $type ); } admin/class-yoast-columns.php 0000644 00000007026 15174712003 0012272 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents the yoast columns. */ class WPSEO_Yoast_Columns implements WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_action( 'load-edit.php', [ $this, 'add_help_tab' ] ); } /** * Adds the help tab to the help center for current screen. * * @return void */ public function add_help_tab() { $link_columns_present = $this->display_links(); $meta_columns_present = $this->display_meta_columns(); if ( ! ( $link_columns_present || $meta_columns_present ) ) { return; } $help_tab_content = sprintf( /* translators: %1$s: Yoast SEO */ __( '%1$s adds several columns to this page.', 'wordpress-seo' ), 'Yoast SEO', ); if ( $meta_columns_present ) { $help_tab_content .= ' ' . sprintf( /* translators: %1$s: Link to article about content analysis, %2$s: Anchor closing */ __( 'We\'ve written an article about %1$show to use the SEO score and Readability score%2$s.', 'wordpress-seo' ), '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/16p' ) . '">', '</a>', ); } if ( $link_columns_present ) { $help_tab_content .= ' ' . sprintf( /* translators: %1$s: Link to article about text links, %2$s: Anchor closing tag, %3$s: Emphasis open tag, %4$s: Emphasis close tag */ __( 'The links columns show the number of articles on this site linking %3$sto%4$s this article and the number of URLs linked %3$sfrom%4$s this article. Learn more about %1$show to use these features to improve your internal linking%2$s, which greatly enhances your SEO.', 'wordpress-seo' ), '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/16p' ) . '">', '</a>', '<em>', '</em>', ); } $screen = get_current_screen(); $screen->add_help_tab( [ /* translators: %s expands to Yoast */ 'title' => sprintf( __( '%s Columns', 'wordpress-seo' ), 'Yoast' ), 'id' => 'yst-columns', 'content' => '<p>' . $help_tab_content . '</p>', 'priority' => 15, ], ); } /** * Retrieves the post type from the $_GET variable. * * @return string The current post type. */ private function get_current_post_type() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post_type'] ) && is_string( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); } return ''; } /** * Whether we are showing link columns on this overview page. * This depends on the post being accessible or not. * * @return bool Whether the linking columns are shown */ private function display_links() { $current_post_type = $this->get_current_post_type(); if ( empty( $current_post_type ) ) { return false; } return WPSEO_Post_Type::is_post_type_accessible( $current_post_type ); } /** * Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by * choice of the admin or because the post type is not a public post type. * * @return bool Whether the meta box (and associated columns etc) should be hidden. */ private function display_meta_columns() { $current_post_type = $this->get_current_post_type(); if ( empty( $current_post_type ) ) { return false; } return WPSEO_Utils::is_metabox_active( $current_post_type, 'post_type' ); } } admin/class-admin-help-panel.php 0000644 00000005312 15174712003 0012564 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates the HTML for an inline Help Button and Panel. */ class WPSEO_Admin_Help_Panel { /** * Unique identifier of the element the inline help refers to, used as an identifier in the html. * * @var string */ private $id; /** * The Help Button text. Needs a properly escaped string. * * @var string */ private $help_button_text; /** * The Help Panel content. Needs a properly escaped string (might contain HTML). * * @var string */ private $help_content; /** * Optional Whether to print out a container div element for the Help Panel, used for styling. * * @var string */ private $wrapper; /** * Constructor. * * @param string $id Unique identifier of the element the inline help refers to, used as * an identifier in the html. * @param string $help_button_text The Help Button text. Needs a properly escaped string. * @param string $help_content The Help Panel content. Needs a properly escaped string (might contain HTML). * @param string $wrapper Optional Whether to print out a container div element for the Help Panel, * used for styling. * Pass a `has-wrapper` value to print out the container. Default: no container. */ public function __construct( $id, $help_button_text, $help_content, $wrapper = '' ) { $this->id = $id; $this->help_button_text = $help_button_text; $this->help_content = $help_content; $this->wrapper = $wrapper; } /** * Returns the html for the Help Button. * * @return string */ public function get_button_html() { if ( ! $this->id || ! $this->help_button_text || ! $this->help_content ) { return ''; } return sprintf( ' <button type="button" class="yoast_help yoast-help-button dashicons" id="%1$s-help-toggle" aria-expanded="false" aria-controls="%1$s-help"><span class="yoast-help-icon" aria-hidden="true"></span><span class="screen-reader-text">%2$s</span></button>', esc_attr( $this->id ), $this->help_button_text, ); } /** * Returns the html for the Help Panel. * * @return string */ public function get_panel_html() { if ( ! $this->id || ! $this->help_button_text || ! $this->help_content ) { return ''; } $wrapper_start = ''; $wrapper_end = ''; if ( $this->wrapper === 'has-wrapper' ) { $wrapper_start = '<div class="yoast-seo-help-container">'; $wrapper_end = '</div>'; } return sprintf( '%1$s<p id="%2$s-help" class="yoast-help-panel">%3$s</p>%4$s', $wrapper_start, esc_attr( $this->id ), $this->help_content, $wrapper_end, ); } } admin/class-admin-user-profile.php 0000644 00000006073 15174712003 0013160 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 1.8.0 */ /** * Customizes user profile. */ class WPSEO_Admin_User_Profile { /** * Class constructor. */ public function __construct() { add_action( 'update_user_meta', [ $this, 'clear_author_sitemap_cache' ], 10, 3 ); } /** * Clear author sitemap cache when settings are changed. * * @since 3.1 * * @param int $meta_id The ID of the meta option changed. * @param int $object_id The ID of the user. * @param string $meta_key The key of the meta field changed. * * @return void */ public function clear_author_sitemap_cache( $meta_id, $object_id, $meta_key ) { if ( $meta_key === '_yoast_wpseo_profile_updated' ) { WPSEO_Sitemaps_Cache::clear( [ 'author' ] ); } } /** * Updates the user metas that (might) have been set on the user profile page. * * @deprecated 22.6 * @codeCoverageIgnore * * @param int $user_id User ID of the updated user. * * @return void */ public function process_user_option_update( $user_id ) { _deprecated_function( __METHOD__, 'Yoast SEO 22.6' ); update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() ); if ( ! check_admin_referer( 'wpseo_user_profile_update', 'wpseo_nonce' ) ) { return; } $wpseo_author_title = isset( $_POST['wpseo_author_title'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_author_title'] ) ) : ''; $wpseo_author_metadesc = isset( $_POST['wpseo_author_metadesc'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_author_metadesc'] ) ) : ''; $wpseo_author_pronouns = isset( $_POST['wpseo_author_pronouns'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_author_pronouns'] ) ) : ''; $wpseo_noindex_author = isset( $_POST['wpseo_noindex_author'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_noindex_author'] ) ) : ''; $wpseo_content_analysis_disable = isset( $_POST['wpseo_content_analysis_disable'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_content_analysis_disable'] ) ) : ''; $wpseo_keyword_analysis_disable = isset( $_POST['wpseo_keyword_analysis_disable'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_keyword_analysis_disable'] ) ) : ''; $wpseo_inclusive_language_analysis_disable = isset( $_POST['wpseo_inclusive_language_analysis_disable'] ) ? sanitize_text_field( wp_unslash( $_POST['wpseo_inclusive_language_analysis_disable'] ) ) : ''; update_user_meta( $user_id, 'wpseo_title', $wpseo_author_title ); update_user_meta( $user_id, 'wpseo_metadesc', $wpseo_author_metadesc ); update_user_meta( $user_id, 'wpseo_pronouns', $wpseo_author_pronouns ); update_user_meta( $user_id, 'wpseo_noindex_author', $wpseo_noindex_author ); update_user_meta( $user_id, 'wpseo_content_analysis_disable', $wpseo_content_analysis_disable ); update_user_meta( $user_id, 'wpseo_keyword_analysis_disable', $wpseo_keyword_analysis_disable ); update_user_meta( $user_id, 'wpseo_inclusive_language_analysis_disable', $wpseo_inclusive_language_analysis_disable ); } } admin/class-admin-init.php 0000644 00000025336 15174712003 0011512 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Performs the load on admin side. */ class WPSEO_Admin_Init { /** * Holds the global `$pagenow` variable's value. * * @var string */ private $pagenow; /** * Holds the asset manager. * * @var WPSEO_Admin_Asset_Manager */ private $asset_manager; /** * Class constructor. */ public function __construct() { $GLOBALS['wpseo_admin'] = new WPSEO_Admin(); $this->pagenow = $GLOBALS['pagenow']; $this->asset_manager = new WPSEO_Admin_Asset_Manager(); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_dismissible' ] ); add_action( 'admin_init', [ $this, 'unsupported_php_notice' ], 15 ); add_action( 'admin_init', [ $this, 'remove_translations_notification' ], 15 ); add_action( 'admin_init', [ $this->asset_manager, 'register_assets' ] ); add_action( 'admin_init', [ $this, 'show_hook_deprecation_warnings' ] ); add_action( 'admin_init', [ 'WPSEO_Plugin_Conflict', 'hook_check_for_plugin_conflicts' ] ); add_action( 'admin_notices', [ $this, 'permalink_settings_notice' ] ); add_action( 'post_submitbox_misc_actions', [ $this, 'add_publish_box_section' ] ); $this->load_meta_boxes(); $this->load_taxonomy_class(); $this->load_admin_page_class(); $this->load_admin_user_class(); $this->load_xml_sitemaps_admin(); $this->load_plugin_suggestions(); } /** * Enqueue our styling for dismissible yoast notifications. * * @return void */ public function enqueue_dismissible() { $this->asset_manager->enqueue_style( 'dismissible' ); } /** * Removes any notification for incomplete translations. * * @return void */ public function remove_translations_notification() { $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'i18nModuleTranslationAssistance' ); } /** * Creates an unsupported PHP version notification in the notification center. * * @return void */ public function unsupported_php_notice() { $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'wpseo-dismiss-unsupported-php' ); } /** * Gets the latest released major WordPress version from the WordPress stable-check api. * * @return float|int The latest released major WordPress version. 0 when the stable-check API doesn't respond. */ private function get_latest_major_wordpress_version() { $core_updates = get_core_updates( [ 'dismissed' => true ] ); if ( $core_updates === false ) { return 0; } $wp_version_latest = get_bloginfo( 'version' ); foreach ( $core_updates as $update ) { if ( $update->response === 'upgrade' && version_compare( $update->version, $wp_version_latest, '>' ) ) { $wp_version_latest = $update->version; } } // Strip the patch version and convert to a float. return (float) $wp_version_latest; } /** * Helper to verify if the user is currently visiting one of our admin pages. * * @return bool */ private function on_wpseo_admin_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( ! isset( $_GET['page'] ) || ! is_string( $_GET['page'] ) ) { return false; } if ( $this->pagenow !== 'admin.php' ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $current_page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); return strpos( $current_page, 'wpseo' ) === 0; } /** * Whether we should load the meta box classes. * * @return bool true if we should load the meta box classes, false otherwise. */ private function should_load_meta_boxes() { /** * Filter: 'wpseo_always_register_metaboxes_on_admin' - Allow developers to change whether * the WPSEO metaboxes are only registered on the typical pages (lean loading) or always * registered when in admin. * * @param bool $register_metaboxes Whether to always register the metaboxes or not. Defaults to false. */ if ( apply_filters( 'wpseo_always_register_metaboxes_on_admin', false ) ) { return true; } // If we are in a post editor. if ( WPSEO_Metabox::is_post_overview( $this->pagenow ) || WPSEO_Metabox::is_post_edit( $this->pagenow ) ) { return true; } // If we are doing an inline save. if ( check_ajax_referer( 'inlineeditnonce', '_inline_edit', false ) && isset( $_POST['action'] ) && sanitize_text_field( wp_unslash( $_POST['action'] ) ) === 'inline-save' ) { return true; } return false; } /** * Determine whether we should load the meta box class and if so, load it. * * @return void */ private function load_meta_boxes() { if ( $this->should_load_meta_boxes() ) { $GLOBALS['wpseo_metabox'] = new WPSEO_Metabox(); $GLOBALS['wpseo_meta_columns'] = new WPSEO_Meta_Columns(); } } /** * Determine if we should load our taxonomy edit class and if so, load it. * * @return void */ private function load_taxonomy_class() { if ( WPSEO_Taxonomy::is_term_edit( $this->pagenow ) || WPSEO_Taxonomy::is_term_overview( $this->pagenow ) ) { new WPSEO_Taxonomy(); } } /** * Determine if we should load our admin pages class and if so, load it. * * Loads admin page class for all admin pages starting with `wpseo_`. * * @return void */ private function load_admin_user_class() { if ( in_array( $this->pagenow, [ 'user-edit.php', 'profile.php' ], true ) && current_user_can( 'edit_users' ) ) { new WPSEO_Admin_User_Profile(); } } /** * Determine if we should load our admin pages class and if so, load it. * * Loads admin page class for all admin pages starting with `wpseo_`. * * @return void */ private function load_admin_page_class() { if ( $this->on_wpseo_admin_page() ) { // For backwards compatabilty, this still needs a global, for now... $GLOBALS['wpseo_admin_pages'] = new WPSEO_Admin_Pages(); $page = null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); } // Only renders Yoast SEO Premium upsells when the page is a Yoast SEO page. if ( $page !== null && WPSEO_Utils::is_yoast_seo_free_page( $page ) ) { $this->register_premium_upsell_admin_block(); } } } /** * Loads the plugin suggestions. * * @return void */ private function load_plugin_suggestions() { $suggestions = new WPSEO_Suggested_Plugins( new WPSEO_Plugin_Availability(), Yoast_Notification_Center::get() ); $suggestions->register_hooks(); } /** * Registers the Premium Upsell Admin Block. * * @return void */ private function register_premium_upsell_admin_block() { if ( ! YoastSEO()->helpers->product->is_premium() ) { $upsell_block = new WPSEO_Premium_Upsell_Admin_Block( 'wpseo_admin_promo_footer' ); $upsell_block->register_hooks(); } } /** * See if we should start our XML Sitemaps Admin class. * * @return void */ private function load_xml_sitemaps_admin() { if ( WPSEO_Options::get( 'enable_xml_sitemap', false, [ 'wpseo' ] ) ) { new WPSEO_Sitemaps_Admin(); } } /** * Shows deprecation warnings to the user if a plugin has registered a filter we have deprecated. * * @return void */ public function show_hook_deprecation_warnings() { global $wp_filter; if ( wp_doing_ajax() ) { return; } // WordPress hooks that have been deprecated since a Yoast SEO version. $deprecated_filters = [ 'wpseo_genesis_force_adjacent_rel_home' => [ 'version' => '9.4', 'alternative' => null, ], 'wpseo_opengraph' => [ 'version' => '14.0', 'alternative' => null, ], 'wpseo_twitter' => [ 'version' => '14.0', 'alternative' => null, ], 'wpseo_twitter_taxonomy_image' => [ 'version' => '14.0', 'alternative' => null, ], 'wpseo_twitter_metatag_key' => [ 'version' => '14.0', 'alternative' => null, ], 'wp_seo_get_bc_ancestors' => [ 'version' => '14.0', 'alternative' => 'wpseo_breadcrumb_links', ], 'validate_facebook_app_id_api_response_code' => [ 'version' => '15.5', 'alternative' => null, ], 'validate_facebook_app_id_api_response_body' => [ 'version' => '15.5', 'alternative' => null, ], ]; // Determine which filters have been registered. $deprecated_notices = array_intersect( array_keys( $deprecated_filters ), array_keys( $wp_filter ), ); // Show notice for each deprecated filter or action that has been registered. foreach ( $deprecated_notices as $deprecated_filter ) { $deprecation_info = $deprecated_filters[ $deprecated_filter ]; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Only uses the hardcoded values from above. _deprecated_hook( $deprecated_filter, 'WPSEO ' . $deprecation_info['version'], $deprecation_info['alternative'], ); // phpcs:enable } } /** * Check if the permalink uses %postname%. * * @return bool */ private function has_postname_in_permalink() { return ( strpos( get_option( 'permalink_structure' ), '%postname%' ) !== false ); } /** * Shows a notice on the permalink settings page. * * @return void */ public function permalink_settings_notice() { global $pagenow; if ( $pagenow === 'options-permalink.php' ) { printf( '<div class="notice notice-warning"><p><strong>%1$s</strong><br>%2$s<br><a href="%3$s" target="_blank">%4$s</a></p></div>', esc_html__( 'WARNING:', 'wordpress-seo' ), sprintf( /* translators: %1$s and %2$s expand to <em> items to emphasize the word in the middle. */ esc_html__( 'Changing your permalinks settings can seriously impact your search engine visibility. It should almost %1$s never %2$s be done on a live website.', 'wordpress-seo' ), '<em>', '</em>', ), esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/why-permalinks/' ) ), // The link's content. esc_html__( 'Learn about why permalinks are important for SEO.', 'wordpress-seo' ), ); } } /** * Adds a custom Yoast section within the Classic Editor publish box. * * @param WP_Post $post The current post object. * * @return void */ public function add_publish_box_section( $post ) { if ( in_array( $this->pagenow, [ 'post.php', 'post-new.php' ], true ) ) { ?> <div id="yoast-seo-publishbox-section"></div> <?php /** * Fires after the post time/date setting in the Publish meta box. * * @param WP_Post $post The current post object. */ do_action( 'wpseo_publishbox_misc_actions', $post ); } } } admin/class-export.php 0000644 00000006731 15174712003 0011000 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Export */ /** * Class WPSEO_Export. * * Class with functionality to export the WP SEO settings. */ class WPSEO_Export { /** * Holds the nonce action. * * @var string */ public const NONCE_ACTION = 'wpseo_export'; /** * Holds the export data. * * @var string */ private $export = ''; /** * Holds whether the export was a success. * * @var bool */ public $success; /** * Handles the export request. * * @return void */ public function export() { check_admin_referer( self::NONCE_ACTION ); $this->export_settings(); $this->output(); } /** * Outputs the export. * * @return void */ public function output() { if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { esc_html_e( 'You do not have the required rights to export settings.', 'wordpress-seo' ); return; } echo '<p id="wpseo-settings-export-desc">'; printf( /* translators: %1$s expands to Import settings */ esc_html__( 'Copy all these settings to another site\'s %1$s tab and click "%1$s" there.', 'wordpress-seo', ), esc_html__( 'Import settings', 'wordpress-seo', ), ); echo '</p>'; /* translators: %1$s expands to Yoast SEO */ echo '<label for="wpseo-settings-export" class="yoast-inline-label">' . sprintf( __( 'Your %1$s settings:', 'wordpress-seo' ), 'Yoast SEO' ) . '</label><br />'; echo '<textarea id="wpseo-settings-export" rows="20" cols="100" aria-describedby="wpseo-settings-export-desc">' . esc_textarea( $this->export ) . '</textarea>'; } /** * Exports the current site's WP SEO settings. * * @return void */ private function export_settings() { $this->export_header(); foreach ( WPSEO_Options::get_option_names() as $opt_group ) { $this->write_opt_group( $opt_group ); } } /** * Writes the header of the export. * * @return void */ private function export_header() { $header = sprintf( /* translators: %1$s expands to Yoast SEO, %2$s expands to Yoast.com */ esc_html__( 'These are settings for the %1$s plugin by %2$s', 'wordpress-seo' ), 'Yoast SEO', 'Yoast.com', ); $this->write_line( '; ' . $header ); } /** * Writes a line to the export. * * @param string $line Line string. * @param bool $newline_first Boolean flag whether to prepend with new line. * * @return void */ private function write_line( $line, $newline_first = false ) { if ( $newline_first ) { $this->export .= PHP_EOL; } $this->export .= $line . PHP_EOL; } /** * Writes an entire option group to the export. * * @param string $opt_group Option group name. * * @return void */ private function write_opt_group( $opt_group ) { $this->write_line( '[' . $opt_group . ']', true ); $options = get_option( $opt_group ); if ( ! is_array( $options ) ) { return; } foreach ( $options as $key => $elem ) { if ( is_array( $elem ) ) { $count = count( $elem ); for ( $i = 0; $i < $count; $i++ ) { $elem_check = ( $elem[ $i ] ?? null ); $this->write_setting( $key . '[]', $elem_check ); } } else { $this->write_setting( $key, $elem ); } } } /** * Writes a settings line to the export. * * @param string $key Key string. * @param string $val Value string. * * @return void */ private function write_setting( $key, $val ) { if ( is_string( $val ) ) { $val = '"' . $val . '"'; } $this->write_line( $key . ' = ' . $val ); } } admin/class-paper-presenter.php 0000644 00000007020 15174712003 0012563 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class WPSEO_presenter_paper. */ class WPSEO_Paper_Presenter { /** * Title of the paper. * * @var string */ private $title; /** * The view variables. * * @var array */ private $settings; /** * The path to the view file. * * @var string */ private $view_file; /** * WPSEO_presenter_paper constructor. * * @param string $title The title of the paper. * @param string|null $view_file Optional. The path to the view file. Use the content setting * if do not wish to use a view file. * @param array $settings Optional. Settings for the paper. */ public function __construct( $title, $view_file = null, array $settings = [] ) { $defaults = [ 'paper_id' => null, 'paper_id_prefix' => 'wpseo-', 'collapsible' => false, 'collapsible_header_class' => '', 'expanded' => false, 'help_text' => '', 'title_after' => '', 'class' => '', 'content' => '', 'view_data' => [], ]; $this->settings = wp_parse_args( $settings, $defaults ); $this->title = $title; $this->view_file = $view_file; } /** * Renders the collapsible paper and returns it as a string. * * @return string The rendered paper. */ public function get_output() { $view_variables = $this->get_view_variables(); extract( $view_variables, EXTR_SKIP ); $content = $this->settings['content']; if ( $this->view_file !== null ) { ob_start(); require $this->view_file; $content = ob_get_clean(); } ob_start(); require WPSEO_PATH . 'admin/views/paper-collapsible.php'; $rendered_output = ob_get_clean(); return $rendered_output; } /** * Retrieves the view variables. * * @return array The view variables. */ private function get_view_variables() { if ( $this->settings['help_text'] instanceof WPSEO_Admin_Help_Panel === false ) { $this->settings['help_text'] = new WPSEO_Admin_Help_Panel( '', '', '' ); } $view_variables = [ 'class' => $this->settings['class'], 'collapsible' => $this->settings['collapsible'], 'collapsible_config' => $this->collapsible_config(), 'collapsible_header_class' => $this->settings['collapsible_header_class'], 'title_after' => $this->settings['title_after'], 'help_text' => $this->settings['help_text'], 'view_file' => $this->view_file, 'title' => $this->title, 'paper_id' => $this->settings['paper_id'], 'paper_id_prefix' => $this->settings['paper_id_prefix'], 'yform' => Yoast_Form::get_instance(), ]; return array_merge( $this->settings['view_data'], $view_variables ); } /** * Retrieves the collapsible config based on the settings. * * @return array The config. */ protected function collapsible_config() { if ( empty( $this->settings['collapsible'] ) ) { return [ 'toggle_icon' => '', 'class' => '', 'expanded' => '', ]; } if ( ! empty( $this->settings['expanded'] ) ) { return [ 'toggle_icon' => 'dashicons-arrow-up-alt2', 'class' => 'toggleable-container', 'expanded' => 'true', ]; } return [ 'toggle_icon' => 'dashicons-arrow-down-alt2', 'class' => 'toggleable-container toggleable-container-hidden', 'expanded' => 'false', ]; } } admin/google_search_console/class-gsc.php 0000644 00000000672 15174712003 0014554 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\admin\google_search_console */ /** * Class WPSEO_GSC. */ class WPSEO_GSC { /** * The option where data will be stored. * * @var string */ public const OPTION_WPSEO_GSC = 'wpseo-gsc'; /** * Outputs the HTML for the redirect page. * * @return void */ public function display() { require_once WPSEO_PATH . 'admin/google_search_console/views/gsc-display.php'; } } admin/google_search_console/views/gsc-display.php 0000644 00000004474 15174712003 0016255 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Google_Search_Console */ // Admin header. Yoast_Form::get_instance()->admin_header( false, 'wpseo-gsc', false, 'yoast_wpseo_gsc_options' ); // GSC Error notification. $gsc_url = 'https://search.google.com/search-console/index'; $gsc_post_url = 'https://yoa.st/google-search-console-deprecated'; $gsc_style_alert = ' display: flex; align-items: baseline; position: relative; padding: 16px; border: 1px solid rgba(0, 0, 0, 0.2); font-size: 14px; font-weight: 400; line-height: 1.5; margin: 16px 0; color: #450c11; background: #f8d7da; '; $gsc_style_alert_icon = 'display: block; margin-right: 8px;'; $gsc_style_alert_content = 'max-width: 600px;'; $gsc_style_alert_link = 'color: #004973;'; $gsc_notification = sprintf( /* Translators: %1$s: expands to opening anchor tag, %2$s expands to closing anchor tag. */ __( 'Google has discontinued its Crawl Errors API. Therefore, any possible crawl errors you might have cannot be displayed here anymore. %1$sRead our statement on this for further information%2$s.', 'wordpress-seo' ), '<a style="' . $gsc_style_alert_link . '" href="' . WPSEO_Shortlinker::get( $gsc_post_url ) . '" target="_blank" rel="noopener">', WPSEO_Admin_Utils::get_new_tab_message() . '</a>', ); $gsc_notification .= '<br/><br/>'; $gsc_notification .= sprintf( /* Translators: %1$s: expands to opening anchor tag, %2$s expands to closing anchor tag. */ __( 'To view your current crawl errors, %1$splease visit Google Search Console%2$s.', 'wordpress-seo' ), '<a style="' . $gsc_style_alert_link . '" href="' . $gsc_url . '" target="_blank" rel="noopener noreferrer">', WPSEO_Admin_Utils::get_new_tab_message() . '</a>', ); ?> <div style="<?php echo $gsc_style_alert; ?>"> <span style="<?php echo $gsc_style_alert_icon; ?>"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" role="img" aria-hidden="true" focusable="false" fill="#450c11"> <path d="M6 1q1.6 0 3 .8T11.2 4t.8 3-.8 3T9 12.2 6 13t-3-.8T.8 10 0 7t.8-3T3 1.8 6 1zm1 9.7V9.3 9L6.7 9H5l-.1.3V10.9l.3.1h1.6l.1-.3zm0-2.6L7 3.2v-.1L6.8 3H5 5l-.1.2.1 4.9.3.2h1.4l.2-.1Q7 8 6.9 8z"></path> </svg> </span> <span style="<?php echo $gsc_style_alert_content; ?>"><?php echo $gsc_notification; ?></span> </div> <?php admin/google_search_console/views/gsc-redirect-nopremium.php 0000644 00000001761 15174712003 0020416 0 ustar 00 <?php /** * WPSEO plugin file. * * This is the view for the modal box that appears when premium isn't loaded. * * @package WPSEO\Admin\Google_Search_Console */ _deprecated_file( __FILE__, 'Yoast SEO 9.5' ); echo '<h1 class="wpseo-redirect-url-title">'; printf( /* Translators: %s: expands to Yoast SEO Premium */ esc_html__( 'Creating redirects is a %s feature', 'wordpress-seo' ), 'Yoast SEO Premium', ); echo '</h1>'; echo '<p>'; printf( /* Translators: %1$s: expands to 'Yoast SEO Premium', %2$s: links to Yoast SEO Premium plugin page. */ esc_html__( 'To be able to create a redirect and fix this issue, you need %1$s. You can buy the plugin, including one year of support and updates, on %2$s.', 'wordpress-seo' ), 'Yoast SEO Premium', '<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/redirects' ) ) . '" target="_blank">yoast.com</a>', ); echo '</p>'; echo '<button type="button" class="button wpseo-redirect-close">' . esc_html__( 'Close', 'wordpress-seo' ) . '</button>'; admin/exceptions/class-file-size-exception.php 0000644 00000002104 15174712003 0015511 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Exceptions */ /** * Represents named methods for exceptions. */ class WPSEO_File_Size_Exception extends Exception { /** * Gets the exception for an externally hosted file. * * @param string $file_url The file url. * * @return WPSEO_File_Size_Exception Instance of the exception. */ public static function externally_hosted( $file_url ) { $message = sprintf( /* translators: %1$s expands to the requested url */ __( 'Cannot get the size of %1$s because it is hosted externally.', 'wordpress-seo' ), $file_url, ); return new self( $message ); } /** * Gets the exception for when a unknown error occurs. * * @param string $file_url The file url. * * @return WPSEO_File_Size_Exception Instance of the exception. */ public static function unknown_error( $file_url ) { $message = sprintf( /* translators: %1$s expands to the requested url */ __( 'Cannot get the size of %1$s because of unknown reasons.', 'wordpress-seo' ), $file_url, ); return new self( $message ); } } admin/services/class-file-size.php 0000644 00000004654 15174712003 0013173 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Services */ /** * Represents the file size service. */ class WPSEO_File_Size_Service { /** * Retrieves an indexable. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response. */ public function get( WP_REST_Request $request ) { try { $file_url = $this->get_file_url( $request ); return new WP_REST_Response( [ 'type' => 'success', 'size_in_bytes' => $this->get_file_size( $file_url ), ], 200, ); } catch ( WPSEO_File_Size_Exception $exception ) { return new WP_REST_Response( [ 'type' => 'failure', 'response' => $exception->getMessage(), ], 404, ); } } /** * Retrieves the file url. * * @param WP_REST_Request $request The request to retrieve file url from. * * @return string The file url. * @throws WPSEO_File_Size_Exception The file is hosted externally. */ protected function get_file_url( WP_REST_Request $request ) { $file_url = rawurldecode( $request->get_param( 'url' ) ); if ( ! $this->is_externally_hosted( $file_url ) ) { return $file_url; } throw WPSEO_File_Size_Exception::externally_hosted( $file_url ); } /** * Checks if the file is hosted externally. * * @param string $file_url The file url. * * @return bool True if it is hosted externally. */ protected function is_externally_hosted( $file_url ) { return wp_parse_url( home_url(), PHP_URL_HOST ) !== wp_parse_url( $file_url, PHP_URL_HOST ); } /** * Returns the file size. * * @param string $file_url The file url to get the size for. * * @return int The file size. * @throws WPSEO_File_Size_Exception Retrieval of file size went wrong for unknown reasons. */ protected function get_file_size( $file_url ) { $file_config = wp_upload_dir(); $file_url = str_replace( $file_config['baseurl'], '', $file_url ); $file_size = $this->calculate_file_size( $file_url ); if ( ! $file_size ) { throw WPSEO_File_Size_Exception::unknown_error( $file_url ); } return $file_size; } /** * Calculates the file size using the Utils class. * * @param string $file_url The file to retrieve the size for. * * @return int|bool The file size or False if it could not be retrieved. */ protected function calculate_file_size( $file_url ) { return WPSEO_Image_Utils::get_file_size( [ 'path' => $file_url, ], ); } } admin/watchers/class-slug-change-watcher.php 0000644 00000016753 15174712003 0015134 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Watchers */ /** * Class WPSEO_Slug_Change_Watcher. */ class WPSEO_Slug_Change_Watcher implements WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { // If the current plugin is Yoast SEO Premium, stop registering. if ( YoastSEO()->helpers->product->is_premium() ) { return; } add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Detect a post trash. add_action( 'wp_trash_post', [ $this, 'detect_post_trash' ] ); // Detect a post delete. add_action( 'before_delete_post', [ $this, 'detect_post_delete' ] ); // Detects deletion of a term. add_action( 'delete_term_taxonomy', [ $this, 'detect_term_delete' ] ); } /** * Enqueues the quick edit handler. * * @return void */ public function enqueue_assets() { global $pagenow; if ( ! in_array( $pagenow, [ 'edit.php', 'edit-tags.php' ], true ) ) { return; } $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'quick-edit-handler' ); } /** * Shows a message when a post is about to get trashed. * * @param int $post_id The current post ID. * * @return void */ public function detect_post_trash( $post_id ) { if ( ! $this->is_post_viewable( $post_id ) ) { return; } $post_label = $this->get_post_type_label( get_post_type( $post_id ) ); /* translators: %1$s expands to the translated name of the post type. */ $first_sentence = sprintf( __( 'You just trashed a %1$s.', 'wordpress-seo' ), $post_label ); $second_sentence = __( 'Search engines and other websites can still send traffic to your trashed content.', 'wordpress-seo' ); $message = $this->get_message( $first_sentence, $second_sentence ); $this->add_notification( $message ); } /** * Shows a message when a post is about to get trashed. * * @param int $post_id The current post ID. * * @return void */ public function detect_post_delete( $post_id ) { if ( ! $this->is_post_viewable( $post_id ) ) { return; } $post_label = $this->get_post_type_label( get_post_type( $post_id ) ); /* translators: %1$s expands to the translated name of the post type. */ $first_sentence = sprintf( __( 'You just deleted a %1$s.', 'wordpress-seo' ), $post_label ); $second_sentence = __( 'Search engines and other websites can still send traffic to your deleted content.', 'wordpress-seo' ); $message = $this->get_message( $first_sentence, $second_sentence ); $this->add_notification( $message ); } /** * Shows a message when a term is about to get deleted. * * @param int $term_taxonomy_id The term taxonomy ID that will be deleted. * * @return void */ public function detect_term_delete( $term_taxonomy_id ) { if ( ! $this->is_term_viewable( $term_taxonomy_id ) ) { return; } $term = get_term_by( 'term_taxonomy_id', (int) $term_taxonomy_id ); $term_label = $this->get_taxonomy_label_for_term( $term->term_id ); /* translators: %1$s expands to the translated name of the term. */ $first_sentence = sprintf( __( 'You just deleted a %1$s.', 'wordpress-seo' ), $term_label ); $second_sentence = __( 'Search engines and other websites can still send traffic to your deleted content.', 'wordpress-seo' ); $message = $this->get_message( $first_sentence, $second_sentence ); $this->add_notification( $message ); } /** * Checks if the post is viewable. * * @param string $post_id The post id to check. * * @return bool Whether the post is viewable or not. */ protected function is_post_viewable( $post_id ) { $post_type = get_post_type( $post_id ); if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) ) { return false; } $post_status = get_post_status( $post_id ); if ( ! $this->check_visible_post_status( $post_status ) ) { return false; } return true; } /** * Checks if the term is viewable. * * @param int $term_taxonomy_id The term taxonomy ID to check. * * @return bool Whether the term is viewable or not. */ protected function is_term_viewable( $term_taxonomy_id ) { $term = get_term_by( 'term_taxonomy_id', (int) $term_taxonomy_id ); if ( ! $term || is_wp_error( $term ) ) { return false; } $taxonomy = get_taxonomy( $term->taxonomy ); if ( ! $taxonomy ) { return false; } return $taxonomy->publicly_queryable || $taxonomy->public; } /** * Gets the taxonomy label to use for a term. * * @param int $term_id The term ID. * * @return string The taxonomy's singular label. */ protected function get_taxonomy_label_for_term( $term_id ) { $term = get_term( $term_id ); $taxonomy = get_taxonomy( $term->taxonomy ); return $taxonomy->labels->singular_name; } /** * Retrieves the singular post type label. * * @param string $post_type Post type to retrieve label from. * * @return string The singular post type name. */ protected function get_post_type_label( $post_type ) { $post_type_object = get_post_type_object( $post_type ); // If the post type of this post wasn't registered default back to post. $post_type_object ??= get_post_type_object( 'post' ); return $post_type_object->labels->singular_name; } /** * Checks whether the given post status is visible or not. * * @param string $post_status The post status to check. * * @return bool Whether or not the post is visible. */ protected function check_visible_post_status( $post_status ) { $visible_post_statuses = [ 'publish', 'static', 'private', ]; return in_array( $post_status, $visible_post_statuses, true ); } /** * Returns the message around changed URLs. * * @param string $first_sentence The first sentence of the notification. * @param string $second_sentence The second sentence of the notification. * * @return string The full notification. */ protected function get_message( $first_sentence, $second_sentence ) { return '<h2>' . __( 'Make sure you don\'t miss out on traffic!', 'wordpress-seo' ) . '</h2>' . '<p>' . $first_sentence . ' ' . $second_sentence . ' ' . __( 'You should create a redirect to ensure your visitors do not get a 404 error when they click on the no longer working URL.', 'wordpress-seo' ) /* translators: %s expands to Yoast SEO Premium */ . ' ' . sprintf( __( 'With %s, you can easily create such redirects.', 'wordpress-seo' ), 'Yoast SEO Premium' ) . '</p>' . '<p><a class="yoast-button-upsell" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2" href="' . WPSEO_Shortlinker::get( 'https://yoa.st/1d0' ) . '" target="_blank">' /* translators: %s expands to Yoast SEO Premium */ . sprintf( __( 'Get %s', 'wordpress-seo' ), 'Yoast SEO Premium' ) /* translators: Hidden accessibility text. */ . '<span class="screen-reader-text">' . __( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>' . '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>' . '</a></p>'; } /** * Adds a notification to be shown on the next page request since posts are updated in an ajax request. * * @param string $message The message to add to the notification. * * @return void */ protected function add_notification( $message ) { $notification = new Yoast_Notification( $message, [ 'type' => 'notice-warning is-dismissible', 'yoast_branding' => true, ], ); $notification_center = Yoast_Notification_Center::get(); $notification_center->add_notification( $notification ); } } admin/class-database-proxy.php 0000644 00000017010 15174712003 0012372 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents the proxy for communicating with the database. */ class WPSEO_Database_Proxy { /** * Holds the table name. * * @var string */ protected $table_name; /** * Determines whether to suppress errors or not. * * @var bool */ protected $suppress_errors = true; /** * Determines if this table is multisite. * * @var bool */ protected $is_multisite_table = false; /** * Holds the last suppressed state. * * @var bool */ protected $last_suppressed_state; /** * Holds the WordPress database object. * * @var wpdb */ protected $database; /** * Holds the table prefix. * * @var string */ protected $table_prefix; /** * Sets the class attributes and registers the table. * * @param wpdb $database The database object. * @param string $table_name The table name that is represented. * @param bool $suppress_errors Should the errors be suppressed. * @param bool $is_multisite_table Should the table be global in multisite. */ public function __construct( $database, $table_name, $suppress_errors = true, $is_multisite_table = false ) { $this->table_name = $table_name; $this->suppress_errors = (bool) $suppress_errors; $this->is_multisite_table = (bool) $is_multisite_table; $this->database = $database; // If the table prefix was provided, strip it as it's handled automatically. $table_prefix = $this->get_table_prefix(); if ( ! empty( $table_prefix ) && strpos( $this->table_name, $table_prefix ) === 0 ) { $this->table_prefix = substr( $this->table_name, strlen( $table_prefix ) ); } if ( ! $this->is_table_registered() ) { $this->register_table(); } } /** * Inserts data into the database. * * @param array $data Data to insert. * @param array|string|null $format Formats for the data. * * @return int|false Total amount of inserted rows or false on error. */ public function insert( array $data, $format = null ) { $this->pre_execution(); $result = $this->database->insert( $this->get_table_name(), $data, $format ); $this->post_execution(); return $result; } /** * Updates data in the database. * * @param array $data Data to update on the table. * @param array $where Where condition as key => value array. * @param array|string|null $format Optional. Data prepare format. * @param array|string|null $where_format Optional. Where prepare format. * * @return int|false False when the update request is invalid, int on number of rows changed. */ public function update( array $data, array $where, $format = null, $where_format = null ) { $this->pre_execution(); $result = $this->database->update( $this->get_table_name(), $data, $where, $format, $where_format ); $this->post_execution(); return $result; } /** * Upserts data in the database. * * Performs an insert into and if key is duplicate it will update the existing record. * * @param array $data Data to update on the table. * @param array|null $where Unused. Where condition as key => value array. * @param array|string|null $format Optional. Data prepare format. * @param array|string|null $where_format Optional. Where prepare format. * * @return int|false False when the upsert request is invalid, int on number of rows changed. */ public function upsert( array $data, ?array $where = null, $format = null, $where_format = null ) { if ( $where_format !== null ) { _deprecated_argument( __METHOD__, '7.7.0', 'The where_format argument is deprecated' ); } $this->pre_execution(); $update = []; $keys = []; $columns = array_keys( $data ); foreach ( $columns as $column ) { $keys[] = '`' . $column . '`'; $update[] = sprintf( '`%1$s` = VALUES(`%1$s`)', $column ); } $query = sprintf( 'INSERT INTO `%1$s` (%2$s) VALUES ( %3$s ) ON DUPLICATE KEY UPDATE %4$s', $this->get_table_name(), implode( ', ', $keys ), implode( ', ', array_fill( 0, count( $data ), '%s' ) ), implode( ', ', $update ), ); $result = $this->database->query( $this->database->prepare( $query, array_values( $data ), ), ); $this->post_execution(); return $result; } /** * Deletes a record from the database. * * @param array $where Where clauses for the query. * @param array|string|null $format Formats for the data. * * @return int|false */ public function delete( array $where, $format = null ) { $this->pre_execution(); $result = $this->database->delete( $this->get_table_name(), $where, $format ); $this->post_execution(); return $result; } /** * Executes the given query and returns the results. * * @param string $query The query to execute. * * @return array|object|null The resultset */ public function get_results( $query ) { $this->pre_execution(); $results = $this->database->get_results( $query ); $this->post_execution(); return $results; } /** * Creates a table to the database. * * @param array $columns The columns to create. * @param array $indexes The indexes to use. * * @return bool True when creation is successful. */ public function create_table( array $columns, array $indexes = [] ) { $create_table = sprintf( 'CREATE TABLE IF NOT EXISTS %1$s ( %2$s ) %3$s', $this->get_table_name(), implode( ',', array_merge( $columns, $indexes ) ), $this->database->get_charset_collate(), ); $this->pre_execution(); $is_created = (bool) $this->database->query( $create_table ); $this->post_execution(); return $is_created; } /** * Checks if there is an error. * * @return bool Returns true when there is an error. */ public function has_error() { return ( $this->database->last_error !== '' ); } /** * Executed before a query will be ran. * * @return void */ protected function pre_execution() { if ( $this->suppress_errors ) { $this->last_suppressed_state = $this->database->suppress_errors(); } } /** * Executed after a query has been ran. * * @return void */ protected function post_execution() { if ( $this->suppress_errors ) { $this->database->suppress_errors( $this->last_suppressed_state ); } } /** * Returns the full table name. * * @return string Full table name including prefix. */ public function get_table_name() { return $this->get_table_prefix() . $this->table_name; } /** * Returns the prefix to use for the table. * * @return string The table prefix depending on the database context. */ protected function get_table_prefix() { if ( $this->is_multisite_table ) { return $this->database->base_prefix; } return $this->database->get_blog_prefix(); } /** * Registers the table with WordPress. * * @return void */ protected function register_table() { $table_name = $this->table_name; $full_table_name = $this->get_table_name(); $this->database->$table_name = $full_table_name; if ( $this->is_multisite_table ) { $this->database->ms_global_tables[] = $table_name; return; } $this->database->tables[] = $table_name; } /** * Checks if the table has been registered with WordPress. * * @return bool True if the table is registered, false otherwise. */ protected function is_table_registered() { if ( $this->is_multisite_table ) { return in_array( $this->table_name, $this->database->ms_global_tables, true ); } return in_array( $this->table_name, $this->database->tables, true ); } } admin/class-admin.php 0000644 00000031354 15174712003 0010546 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Integrations\Settings_Integration; /** * Class that holds most of the admin functionality for Yoast SEO. */ class WPSEO_Admin { /** * The page identifier used in WordPress to register the admin page. * * !DO NOT CHANGE THIS! * * @var string */ public const PAGE_IDENTIFIER = 'wpseo_dashboard'; /** * Array of classes that add admin functionality. * * @var array */ protected $admin_features; /** * Class constructor. */ public function __construct() { $integrations = []; global $pagenow; $wpseo_menu = new WPSEO_Menu(); $wpseo_menu->register_hooks(); if ( is_multisite() ) { WPSEO_Options::maybe_set_multisite_defaults( false ); } add_action( 'created_category', [ $this, 'schedule_rewrite_flush' ] ); add_action( 'edited_category', [ $this, 'schedule_rewrite_flush' ] ); add_action( 'delete_category', [ $this, 'schedule_rewrite_flush' ] ); add_filter( 'wpseo_accessible_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] ); add_filter( 'plugin_action_links_' . WPSEO_BASENAME, [ $this, 'add_action_link' ], 10, 2 ); add_filter( 'network_admin_plugin_action_links_' . WPSEO_BASENAME, [ $this, 'add_action_link' ], 10, 2 ); add_action( 'admin_enqueue_scripts', [ $this, 'config_page_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_global_style' ] ); add_action( 'after_switch_theme', [ $this, 'switch_theme' ] ); add_action( 'switch_theme', [ $this, 'switch_theme' ] ); add_filter( 'set-screen-option', [ $this, 'save_bulk_edit_options' ], 10, 3 ); add_action( 'admin_init', [ 'WPSEO_Plugin_Conflict', 'hook_check_for_plugin_conflicts' ], 10, 1 ); add_action( 'admin_init', [ $this, 'map_manage_options_cap' ] ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo' ); WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'home' ); $this->initialize_cornerstone_content(); if ( WPSEO_Utils::is_plugin_network_active() ) { $integrations[] = new Yoast_Network_Admin(); } $this->admin_features = [ 'dashboard_widget' => new Yoast_Dashboard_Widget(), 'wincher_dashboard_widget' => new Wincher_Dashboard_Widget(), ]; if ( WPSEO_Metabox::is_post_overview( $pagenow ) || WPSEO_Metabox::is_post_edit( $pagenow ) ) { $this->admin_features['primary_category'] = new WPSEO_Primary_Term_Admin(); } $integrations[] = new WPSEO_Yoast_Columns(); $integrations[] = new WPSEO_Statistic_Integration(); $integrations[] = new WPSEO_Capability_Manager_Integration( WPSEO_Capability_Manager_Factory::get() ); $integrations[] = new WPSEO_Admin_Gutenberg_Compatibility_Notification(); $integrations[] = new WPSEO_Expose_Shortlinks(); $integrations[] = new WPSEO_MyYoast_Proxy(); $integrations[] = new WPSEO_Schema_Person_Upgrade_Notification(); $integrations[] = new WPSEO_Tracking( 'https://tracking.yoast.com/stats', ( WEEK_IN_SECONDS * 2 ) ); $integrations[] = new WPSEO_Admin_Settings_Changed_Listener(); $integrations = array_merge( $integrations, $this->get_admin_features(), $this->initialize_cornerstone_content(), ); foreach ( $integrations as $integration ) { $integration->register_hooks(); } } /** * Schedules a rewrite flush to happen at shutdown. * * @return void */ public function schedule_rewrite_flush() { if ( WPSEO_Options::get( 'stripcategorybase' ) !== true ) { return; } // Bail if this is a multisite installation and the site has been switched. if ( is_multisite() && ms_is_switched() ) { return; } add_action( 'shutdown', 'flush_rewrite_rules' ); } /** * Returns all the classes for the admin features. * * @return array */ public function get_admin_features() { return $this->admin_features; } /** * Register assets needed on admin pages. * * @deprecated 25.5 * @codeCoverageIgnore * * @return void */ public function enqueue_assets() { _deprecated_function( __METHOD__, 'Yoast SEO 25.5' ); } /** * Returns the manage_options capability. * * @return string The capability to use. */ public function get_manage_options_cap() { /** * Filter: 'wpseo_manage_options_capability' - Allow changing the capability users need to view the settings pages. * * @param string $capability The capability. */ return apply_filters( 'wpseo_manage_options_capability', 'wpseo_manage_options' ); } /** * Maps the manage_options cap on saving an options page to wpseo_manage_options. * * @return void */ public function map_manage_options_cap() { // phpcs:ignore WordPress.Security -- The variable is only used in strpos and thus safe to not unslash or sanitize. $option_page = ! empty( $_POST['option_page'] ) ? $_POST['option_page'] : ''; if ( strpos( $option_page, 'yoast_wpseo' ) === 0 || strpos( $option_page, Settings_Integration::PAGE ) === 0 ) { add_filter( 'option_page_capability_' . $option_page, [ $this, 'get_manage_options_cap' ] ); } } /** * Adds the ability to choose how many posts are displayed per page * on the bulk edit pages. * * @return void */ public function bulk_edit_options() { $option = 'per_page'; $args = [ 'label' => __( 'Posts', 'wordpress-seo' ), 'default' => 10, 'option' => 'wpseo_posts_per_page', ]; add_screen_option( $option, $args ); } /** * Saves the posts per page limit for bulk edit pages. * * @param int $status Status value to pass through. * @param string $option Option name. * @param int $value Count value to check. * * @return int */ public function save_bulk_edit_options( $status, $option, $value ) { if ( $option && ( $value > 0 && $value < 1000 ) === 'wpseo_posts_per_page' ) { return $value; } return $status; } /** * Adds links to Premium Support and FAQ under the plugin in the plugin overview page. * * @param array $links Array of links for the plugins, adapted when the current plugin is found. * @param string $file The filename for the current plugin, which the filter loops through. * * @return array */ public function add_action_link( $links, $file ) { $first_time_configuration_notice_helper = YoastSEO()->helpers->first_time_configuration_notice; if ( $file === WPSEO_BASENAME && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { if ( is_network_admin() ) { $settings_url = network_admin_url( 'admin.php?page=' . self::PAGE_IDENTIFIER ); } else { $settings_url = admin_url( 'admin.php?page=' . self::PAGE_IDENTIFIER ); } $settings_link = '<a href="' . esc_url( $settings_url ) . '">' . __( 'Settings', 'wordpress-seo' ) . '</a>'; array_unshift( $links, $settings_link ); } // Add link to docs. $faq_link = '<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yc' ) ) . '" target="_blank">' . __( 'FAQ', 'wordpress-seo' ) . '</a>'; array_unshift( $links, $faq_link ); if ( $first_time_configuration_notice_helper->first_time_configuration_not_finished() && ! is_network_admin() ) { $configuration_title = ( ! $first_time_configuration_notice_helper->should_show_alternate_message() ) ? 'first-time configuration' : 'SEO configuration'; /* translators: CTA to finish the first time configuration. %s: Either first-time SEO configuration or SEO configuration. */ $message = sprintf( __( 'Finish your %s', 'wordpress-seo' ), $configuration_title ); $ftc_page = 'admin.php?page=wpseo_dashboard#/first-time-configuration'; $ftc_link = '<a href="' . esc_url( admin_url( $ftc_page ) ) . '" target="_blank">' . $message . '</a>'; array_unshift( $links, $ftc_link ); } $addon_manager = new WPSEO_Addon_Manager(); if ( YoastSEO()->helpers->product->is_premium() ) { // Remove Free 'deactivate' link if Premium is active as well. We don't want users to deactivate Free when Premium is active. unset( $links['deactivate'] ); $no_deactivation_explanation = '<span style="color: #32373c">' . sprintf( /* translators: %s expands to Yoast SEO Premium. */ __( 'Required by %s', 'wordpress-seo' ), 'Yoast SEO Premium', ) . '</span>'; array_unshift( $links, $no_deactivation_explanation ); if ( $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) { return $links; } // Add link to where premium can be activated. $activation_link = '<a style="font-weight: bold;" href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/activate-my-yoast' ) ) . '" target="_blank">' . __( 'Activate your subscription', 'wordpress-seo' ) . '</a>'; array_unshift( $links, $activation_link ); return $links; } // Add link to premium landing page. $premium_link = '<a style="font-weight: bold;" href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yb' ) ) . '" target="_blank" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2">' . __( 'Get Premium', 'wordpress-seo' ) . '</a>'; array_unshift( $links, $premium_link ); return $links; } /** * Enqueues the (tiny) global JS needed for the plugin. * * @return void */ public function config_page_scripts() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'admin-global' ); $asset_manager->localize_script( 'admin-global', 'wpseoAdminGlobalL10n', $this->localize_admin_global_script() ); } /** * Enqueues the (tiny) global stylesheet needed for the plugin. * * @return void */ public function enqueue_global_style() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_style( 'admin-global' ); } /** * Filter the $contactmethods array and add a set of social profiles. * * These are used with the Facebook author, rel="author" and Twitter cards implementation. * * @deprecated 22.6 * @codeCoverageIgnore * * @param array<string, string> $contactmethods Currently set contactmethods. * * @return array<string, string> Contactmethods with added contactmethods. */ public function update_contactmethods( $contactmethods ) { _deprecated_function( __METHOD__, 'Yoast SEO 22.6' ); $contactmethods['facebook'] = __( 'Facebook profile URL', 'wordpress-seo' ); $contactmethods['instagram'] = __( 'Instagram profile URL', 'wordpress-seo' ); $contactmethods['linkedin'] = __( 'LinkedIn profile URL', 'wordpress-seo' ); $contactmethods['myspace'] = __( 'MySpace profile URL', 'wordpress-seo' ); $contactmethods['pinterest'] = __( 'Pinterest profile URL', 'wordpress-seo' ); $contactmethods['soundcloud'] = __( 'SoundCloud profile URL', 'wordpress-seo' ); $contactmethods['tumblr'] = __( 'Tumblr profile URL', 'wordpress-seo' ); $contactmethods['twitter'] = __( 'X username (without @)', 'wordpress-seo' ); $contactmethods['youtube'] = __( 'YouTube profile URL', 'wordpress-seo' ); $contactmethods['wikipedia'] = __( 'Wikipedia page about you', 'wordpress-seo' ) . '<br/><small>' . __( '(if one exists)', 'wordpress-seo' ) . '</small>'; return $contactmethods; } /** * Log the updated timestamp for user profiles when theme is changed. * * @return void */ public function switch_theme() { $users = get_users( [ 'capability' => [ 'edit_posts' ] ] ); if ( is_array( $users ) && $users !== [] ) { foreach ( $users as $user ) { update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', time() ); } } } /** * Localization for the dismiss urls. * * @return array */ private function localize_admin_global_script() { return array_merge( [ 'isRtl' => is_rtl(), 'variable_warning' => sprintf( /* translators: %1$s: '%%term_title%%' variable used in titles and meta's template that's not compatible with the given template, %2$s: expands to 'HelpScout beacon' */ __( 'Warning: the variable %1$s cannot be used in this template. See the %2$s for more info.', 'wordpress-seo' ), '<code>%s</code>', 'HelpScout beacon', ), /* translators: %s: expends to Yoast SEO */ 'help_video_iframe_title' => sprintf( __( '%s video tutorial', 'wordpress-seo' ), 'Yoast SEO' ), 'scrollable_table_hint' => __( 'Scroll to see the table content.', 'wordpress-seo' ), 'wincher_is_logged_in' => WPSEO_Options::get( 'wincher_integration_active', true ) ? YoastSEO()->helpers->wincher->login_status() : false, ], YoastSEO()->helpers->wincher->get_admin_global_links(), ); } /** * Whether we are on the admin dashboard page. * * @return bool */ protected function on_dashboard_page() { return $GLOBALS['pagenow'] === 'index.php'; } /** * Loads the cornerstone filter. * * @return WPSEO_WordPress_Integration[] The integrations to initialize. */ protected function initialize_cornerstone_content() { if ( ! WPSEO_Options::get( 'enable_cornerstone_content' ) ) { return []; } return [ 'cornerstone_filter' => new WPSEO_Cornerstone_Filter(), ]; } } admin/ajax/class-shortcode-filter.php 0000644 00000003067 15174712003 0013656 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Ajax */ /** * Class WPSEO_Shortcode_Filter. * * Used for parsing WP shortcodes with AJAX. */ class WPSEO_Shortcode_Filter { /** * Initialize the AJAX hooks. */ public function __construct() { add_action( 'wp_ajax_wpseo_filter_shortcodes', [ $this, 'do_filter' ] ); } /** * Parse the shortcodes. * * @return void */ public function do_filter() { check_ajax_referer( 'wpseo-filter-shortcodes', 'nonce' ); if ( ! isset( $_POST['data'] ) || ! is_array( $_POST['data'] ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: WPSEO_Utils::format_json_encode is considered safe. wp_die( WPSEO_Utils::format_json_encode( [] ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: $shortcodes is getting sanitized later, before it's used. $shortcodes = wp_unslash( $_POST['data'] ); $parsed_shortcodes = []; foreach ( $shortcodes as $shortcode ) { if ( $shortcode !== sanitize_text_field( $shortcode ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: WPSEO_Utils::format_json_encode is considered safe. wp_die( WPSEO_Utils::format_json_encode( [] ) ); } $parsed_shortcodes[] = [ 'shortcode' => $shortcode, 'output' => do_shortcode( $shortcode ), ]; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: WPSEO_Utils::format_json_encode is considered safe. wp_die( WPSEO_Utils::format_json_encode( $parsed_shortcodes ) ); } } admin/ajax/class-yoast-dismissable-notice.php 0000644 00000003716 15174712003 0015315 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Ajax */ /** * This class will catch the request to dismiss the target notice (set by notice_name) * and will store the dismiss status as an user meta in the database. */ class Yoast_Dismissable_Notice_Ajax { /** * Notice type toggle value for user notices. * * @var string */ public const FOR_USER = 'user_meta'; /** * Notice type toggle value for network notices. * * @var string */ public const FOR_NETWORK = 'site_option'; /** * Notice type toggle value for site notices. * * @var string */ public const FOR_SITE = 'option'; /** * Name of the notice that will be dismissed. * * @var string */ private $notice_name; /** * The type of the current notice. * * @var string */ private $notice_type; /** * Initialize the hooks for the AJAX request. * * @param string $notice_name The name for the hook to catch the notice. * @param string $notice_type The notice type. */ public function __construct( $notice_name, $notice_type = self::FOR_USER ) { $this->notice_name = $notice_name; $this->notice_type = $notice_type; add_action( 'wp_ajax_wpseo_dismiss_' . $notice_name, [ $this, 'dismiss_notice' ] ); } /** * Handles the dismiss notice request. * * @return void */ public function dismiss_notice() { check_ajax_referer( 'wpseo-dismiss-' . $this->notice_name ); $this->save_dismissed(); wp_die( 'true' ); } /** * Storing the dismissed value in the database. The target location is based on the set notification type. * * @return void */ private function save_dismissed() { if ( $this->notice_type === self::FOR_SITE ) { update_option( 'wpseo_dismiss_' . $this->notice_name, 1 ); return; } if ( $this->notice_type === self::FOR_NETWORK ) { update_site_option( 'wpseo_dismiss_' . $this->notice_name, 1 ); return; } update_user_meta( get_current_user_id(), 'wpseo_dismiss_' . $this->notice_name, 1 ); } } admin/ajax/class-yoast-plugin-conflict-ajax.php 0000644 00000006662 15174712003 0015560 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Ajax */ /** * Class Yoast_Plugin_Conflict_Ajax. */ class Yoast_Plugin_Conflict_Ajax { /** * Option identifier where dismissed conflicts are stored. * * @var string */ private $option_name = 'wpseo_dismissed_conflicts'; /** * List of notification identifiers that have been dismissed. * * @var array */ private $dismissed_conflicts = []; /** * Initialize the hooks for the AJAX request. */ public function __construct() { add_action( 'wp_ajax_wpseo_dismiss_plugin_conflict', [ $this, 'dismiss_notice' ] ); } /** * Handles the dismiss notice request. * * @return void */ public function dismiss_notice() { check_ajax_referer( 'dismiss-plugin-conflict' ); if ( ! isset( $_POST['data'] ) || ! is_array( $_POST['data'] ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: WPSEO_Utils::format_json_encode is considered safe. wp_die( WPSEO_Utils::format_json_encode( [] ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: $conflict_data is getting sanitized later. $conflict_data = wp_unslash( $_POST['data'] ); $conflict_data = [ 'section' => sanitize_text_field( $conflict_data['section'] ), 'plugins' => sanitize_text_field( $conflict_data['plugins'] ), ]; $this->dismissed_conflicts = $this->get_dismissed_conflicts( $conflict_data['section'] ); $this->compare_plugins( $conflict_data['plugins'] ); $this->save_dismissed_conflicts( $conflict_data['section'] ); wp_die( 'true' ); } /** * Getting the user option from the database. * * @return bool|array */ private function get_dismissed_option() { return get_user_meta( get_current_user_id(), $this->option_name, true ); } /** * Getting the dismissed conflicts from the database * * @param string $plugin_section Type of conflict group (such as Open Graph or sitemap). * * @return array */ private function get_dismissed_conflicts( $plugin_section ) { $dismissed_conflicts = $this->get_dismissed_option(); if ( is_array( $dismissed_conflicts ) && array_key_exists( $plugin_section, $dismissed_conflicts ) ) { return $dismissed_conflicts[ $plugin_section ]; } return []; } /** * Storing the conflicting plugins as an user option in the database. * * @param string $plugin_section Plugin conflict type (such as Open Graph or sitemap). * * @return void */ private function save_dismissed_conflicts( $plugin_section ) { $dismissed_conflicts = $this->get_dismissed_option(); $dismissed_conflicts[ $plugin_section ] = $this->dismissed_conflicts; update_user_meta( get_current_user_id(), $this->option_name, $dismissed_conflicts ); } /** * Loop through the plugins to compare them with the already stored dismissed plugin conflicts. * * @param array $posted_plugins Plugin set to check. * * @return void */ public function compare_plugins( array $posted_plugins ) { foreach ( $posted_plugins as $posted_plugin ) { $this->compare_plugin( $posted_plugin ); } } /** * Check if plugin is already dismissed, if not store it in the array that will be saved later. * * @param string $posted_plugin Plugin to check against dismissed conflicts. * * @return void */ private function compare_plugin( $posted_plugin ) { if ( ! in_array( $posted_plugin, $this->dismissed_conflicts, true ) ) { $this->dismissed_conflicts[] = $posted_plugin; } } } admin/listeners/class-listener.php 0000644 00000000460 15174712003 0013305 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Listeners */ /** * Dictates the required methods for a Listener implementation. */ interface WPSEO_Listener { /** * Listens to an argument in the request URL and triggers an action. * * @return void */ public function listen(); } admin/class-plugin-conflict.php 0000644 00000010055 15174712003 0012546 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 1.7.0 */ use Yoast\WP\SEO\Config\Conflicting_Plugins; /** * Contains list of conflicting plugins. */ class WPSEO_Plugin_Conflict extends Yoast_Plugin_Conflict { /** * The plugins must be grouped per section. * * It's possible to check for each section if there are conflicting plugin. * * NOTE: when changing this array, be sure to update the array in Conflicting_Plugins_Service too. * * @var array<string, array<string>> */ protected $plugins = [ // The plugin which are writing OG metadata. 'open_graph' => Conflicting_Plugins::OPEN_GRAPH_PLUGINS, 'xml_sitemaps' => Conflicting_Plugins::XML_SITEMAPS_PLUGINS, 'cloaking' => Conflicting_Plugins::CLOAKING_PLUGINS, 'seo' => Conflicting_Plugins::SEO_PLUGINS, ]; /** * Overrides instance to set with this class as class. * * @param string $class_name Optional class name. * * @return Yoast_Plugin_Conflict */ public static function get_instance( $class_name = self::class ) { return parent::get_instance( $class_name ); } /** * After activating any plugin, this method will be executed by a hook. * * If the activated plugin is conflicting with ours a notice will be shown. * * @param string|bool $plugin Optional plugin basename to check. * * @return void */ public static function hook_check_for_plugin_conflicts( $plugin = false ) { // The instance of the plugin. $instance = self::get_instance(); // Only add the plugin as an active plugin if $plugin isn't false. if ( $plugin && is_string( $plugin ) ) { $instance->add_active_plugin( $instance->find_plugin_category( $plugin ), $plugin ); } $plugin_sections = []; // Only check for open graph problems when they are enabled. if ( WPSEO_Options::get( 'opengraph' ) ) { /* translators: %1$s expands to Yoast SEO, %2$s: 'Facebook' plugin name of possibly conflicting plugin with regard to creating OpenGraph output. */ $plugin_sections['open_graph'] = __( 'Both %1$s and %2$s create Open Graph output, which might make Facebook, X, LinkedIn and other social networks use the wrong texts and images when your pages are being shared.', 'wordpress-seo' ) . '<br/><br/>' . '<a class="button" href="' . admin_url( 'admin.php?page=wpseo_page_settings#/site-features#card-wpseo_social-opengraph' ) . '">' /* translators: %1$s expands to Yoast SEO. */ . sprintf( __( 'Configure %1$s\'s Open Graph settings', 'wordpress-seo' ), 'Yoast SEO' ) . '</a>'; } // Only check for XML conflicts if sitemaps are enabled. if ( WPSEO_Options::get( 'enable_xml_sitemap' ) ) { /* translators: %1$s expands to Yoast SEO, %2$s: 'Google XML Sitemaps' plugin name of possibly conflicting plugin with regard to the creation of sitemaps. */ $plugin_sections['xml_sitemaps'] = __( 'Both %1$s and %2$s can create XML sitemaps. Having two XML sitemaps is not beneficial for search engines and might slow down your site.', 'wordpress-seo' ) . '<br/><br/>' . '<a class="button" href="' . admin_url( 'admin.php?page=wpseo_page_settings#/site-features#card-wpseo-enable_xml_sitemap' ) . '">' /* translators: %1$s expands to Yoast SEO. */ . sprintf( __( 'Toggle %1$s\'s XML Sitemap', 'wordpress-seo' ), 'Yoast SEO' ) . '</a>'; } /* translators: %2$s expands to 'RS Head Cleaner' plugin name of possibly conflicting plugin with regard to differentiating output between search engines and normal users. */ $plugin_sections['cloaking'] = __( 'The plugin %2$s changes your site\'s output and in doing that differentiates between search engines and normal users, a process that\'s called cloaking. We highly recommend that you disable it.', 'wordpress-seo' ); /* translators: %1$s expands to Yoast SEO, %2$s: 'SEO' plugin name of possibly conflicting plugin with regard to the creation of duplicate SEO meta. */ $plugin_sections['seo'] = __( 'Both %1$s and %2$s manage the SEO of your site. Running two SEO plugins at the same time is detrimental.', 'wordpress-seo' ); $instance->check_plugin_conflicts( $plugin_sections ); } } admin/class-meta-columns.php 0000644 00000066467 15174712003 0012077 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Context\Meta_Tags_Context; use Yoast\WP\SEO\Helpers\Score_Icon_Helper; use Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration; use Yoast\WP\SEO\Surfaces\Values\Meta; /** * Class WPSEO_Meta_Columns. */ class WPSEO_Meta_Columns { /** * Holds the context objects for each indexable. * * @var Meta_Tags_Context[] */ protected $context = []; /** * Holds the SEO analysis. * * @var WPSEO_Metabox_Analysis_SEO */ private $analysis_seo; /** * Holds the readability analysis. * * @var WPSEO_Metabox_Analysis_Readability */ private $analysis_readability; /** * Admin columns cache. * * @var Admin_Columns_Cache_Integration */ private $admin_columns_cache; /** * Holds the Score_Icon_Helper. * * @var Score_Icon_Helper */ private $score_icon_helper; /** * Holds the WPSEO_Admin_Asset_Manager instance. * * @var WPSEO_Admin_Asset_Manager */ private $admin_asset_manager; /** * When page analysis is enabled, just initialize the hooks. */ public function __construct() { if ( apply_filters( 'wpseo_use_page_analysis', true ) === true ) { add_action( 'admin_init', [ $this, 'setup_hooks' ] ); } $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO(); $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability(); $this->admin_columns_cache = YoastSEO()->classes->get( Admin_Columns_Cache_Integration::class ); $this->score_icon_helper = YoastSEO()->helpers->score_icon; $this->admin_asset_manager = YoastSEO()->classes->get( WPSEO_Admin_Asset_Manager::class ); } /** * Sets up up the hooks. * * @return void */ public function setup_hooks() { $this->set_post_type_hooks(); if ( $this->analysis_seo->is_enabled() ) { add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown' ] ); } if ( $this->analysis_readability->is_enabled() ) { add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown_readability' ] ); } add_filter( 'request', [ $this, 'column_sort_orderby' ] ); add_filter( 'default_hidden_columns', [ $this, 'column_hidden' ], 10, 1 ); } /** * Adds the column headings for the SEO plugin for edit posts / pages overview. * * @param array $columns Already existing columns. * * @return array Array containing the column headings. */ public function column_heading( $columns ) { if ( $this->display_metabox() === false ) { return $columns; } $this->admin_asset_manager->enqueue_script( 'edit-page' ); $this->admin_asset_manager->enqueue_style( 'edit-page' ); $added_columns = []; if ( $this->analysis_seo->is_enabled() ) { $added_columns['wpseo-score'] = '<span class="yoast-column-seo-score yoast-column-header-has-tooltip" data-tooltip-text="' . esc_attr__( 'SEO score', 'wordpress-seo' ) . '"><span class="screen-reader-text">' . __( 'SEO score', 'wordpress-seo' ) . '</span></span>'; } if ( $this->analysis_readability->is_enabled() ) { $added_columns['wpseo-score-readability'] = '<span class="yoast-column-readability yoast-column-header-has-tooltip" data-tooltip-text="' . esc_attr__( 'Readability score', 'wordpress-seo' ) . '"><span class="screen-reader-text">' . __( 'Readability score', 'wordpress-seo' ) . '</span></span>'; } $added_columns['wpseo-title'] = __( 'SEO Title', 'wordpress-seo' ); $added_columns['wpseo-metadesc'] = __( 'Meta Desc.', 'wordpress-seo' ); if ( $this->analysis_seo->is_enabled() ) { $added_columns['wpseo-focuskw'] = __( 'Keyphrase', 'wordpress-seo' ); } return array_merge( $columns, $added_columns ); } /** * Displays the column content for the given column. * * @param string $column_name Column to display the content for. * @param int $post_id Post to display the column content for. * * @return void */ public function column_content( $column_name, $post_id ) { if ( $this->display_metabox() === false ) { return; } switch ( $column_name ) { case 'wpseo-score': // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method. echo $this->parse_column_score( $post_id ); return; case 'wpseo-score-readability': // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method. echo $this->parse_column_score_readability( $post_id ); return; case 'wpseo-title': $meta = $this->get_meta( $post_id ); if ( $meta ) { echo esc_html( $meta->title ); } return; case 'wpseo-metadesc': $metadesc_val = ''; $meta = $this->get_meta( $post_id ); if ( $meta ) { $metadesc_val = $meta->meta_description; } if ( $metadesc_val === '' ) { echo '<span aria-hidden="true">—</span><span class="screen-reader-text">', /* translators: Hidden accessibility text. */ esc_html__( 'Meta description not set.', 'wordpress-seo' ), '</span>'; return; } echo esc_html( $metadesc_val ); return; case 'wpseo-focuskw': $focuskw_val = WPSEO_Meta::get_value( 'focuskw', $post_id ); if ( $focuskw_val === '' ) { echo '<span aria-hidden="true">—</span><span class="screen-reader-text">', /* translators: Hidden accessibility text. */ esc_html__( 'Focus keyphrase not set.', 'wordpress-seo' ), '</span>'; return; } echo esc_html( $focuskw_val ); return; } } /** * Indicates which of the SEO columns are sortable. * * @param array $columns Appended with their orderby variable. * * @return array Array containing the sortable columns. */ public function column_sort( $columns ) { if ( $this->display_metabox() === false ) { return $columns; } $columns['wpseo-metadesc'] = 'wpseo-metadesc'; if ( $this->analysis_seo->is_enabled() ) { $columns['wpseo-focuskw'] = 'wpseo-focuskw'; $columns['wpseo-score'] = 'wpseo-score'; } if ( $this->analysis_readability->is_enabled() ) { $columns['wpseo-score-readability'] = 'wpseo-score-readability'; } return $columns; } /** * Hides the SEO title, meta description and focus keyword columns if the user hasn't chosen which columns to hide. * * @param array $hidden The hidden columns. * * @return array Array containing the columns to hide. */ public function column_hidden( $hidden ) { if ( ! is_array( $hidden ) ) { $hidden = []; } array_push( $hidden, 'wpseo-title', 'wpseo-metadesc' ); if ( $this->analysis_seo->is_enabled() ) { $hidden[] = 'wpseo-focuskw'; } return $hidden; } /** * Adds a dropdown that allows filtering on the posts SEO Quality. * * @return void */ public function posts_filter_dropdown() { if ( ! $this->can_display_filter() ) { return; } $ranks = WPSEO_Rank::get_all_ranks(); /* translators: Hidden accessibility text. */ echo '<label class="screen-reader-text" for="wpseo-filter">' . esc_html__( 'Filter by SEO Score', 'wordpress-seo' ) . '</label>'; echo '<select name="seo_filter" id="wpseo-filter">'; // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method. echo $this->generate_option( '', __( 'All SEO Scores', 'wordpress-seo' ) ); foreach ( $ranks as $rank ) { $selected = selected( $this->get_current_seo_filter(), $rank->get_rank(), false ); // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method. echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_label(), $selected ); } echo '</select>'; } /** * Adds a dropdown that allows filtering on the posts Readability Quality. * * @return void */ public function posts_filter_dropdown_readability() { if ( ! $this->can_display_filter() ) { return; } $ranks = WPSEO_Rank::get_all_readability_ranks(); /* translators: Hidden accessibility text. */ echo '<label class="screen-reader-text" for="wpseo-readability-filter">' . esc_html__( 'Filter by Readability Score', 'wordpress-seo' ) . '</label>'; echo '<select name="readability_filter" id="wpseo-readability-filter">'; // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method. echo $this->generate_option( '', __( 'All Readability Scores', 'wordpress-seo' ) ); foreach ( $ranks as $rank ) { $selected = selected( $this->get_current_readability_filter(), $rank->get_rank(), false ); // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method. echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_readability_labels(), $selected ); } echo '</select>'; } /** * Generates an <option> element. * * @param string $value The option's value. * @param string $label The option's label. * @param string $selected HTML selected attribute for an option. * * @return string The generated <option> element. */ protected function generate_option( $value, $label, $selected = '' ) { return '<option ' . $selected . ' value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>'; } /** * Returns the meta object for a given post ID. * * @param int $post_id The post ID. * * @return Meta The meta object. */ protected function get_meta( $post_id ) { $indexable = $this->admin_columns_cache->get_indexable( $post_id ); return YoastSEO()->meta->for_indexable( $indexable, 'Post_Type' ); } /** * Determines the SEO score filter to be later used in the meta query, based on the passed SEO filter. * * @param string $seo_filter The SEO filter to use to determine what further filter to apply. * * @return array The SEO score filter. */ protected function determine_seo_filters( $seo_filter ) { if ( $seo_filter === WPSEO_Rank::NO_FOCUS ) { return $this->create_no_focus_keyword_filter(); } if ( $seo_filter === WPSEO_Rank::NO_INDEX ) { return $this->create_no_index_filter(); } $rank = new WPSEO_Rank( $seo_filter ); return $this->create_seo_score_filter( $rank->get_starting_score(), $rank->get_end_score() ); } /** * Determines the Readability score filter to the meta query, based on the passed Readability filter. * * @param string $readability_filter The Readability filter to use to determine what further filter to apply. * * @return array The Readability score filter. */ protected function determine_readability_filters( $readability_filter ) { if ( $readability_filter === WPSEO_Rank::NO_FOCUS ) { return $this->create_no_readability_scores_filter(); } if ( $readability_filter === WPSEO_Rank::BAD ) { return $this->create_bad_readability_scores_filter(); } $rank = new WPSEO_Rank( $readability_filter ); return $this->create_readability_score_filter( $rank->get_starting_score(), $rank->get_end_score() ); } /** * Creates a keyword filter for the meta query, based on the passed Keyword filter. * * @param string $keyword_filter The keyword filter to use. * * @return array The keyword filter. */ protected function get_keyword_filter( $keyword_filter ) { return [ 'post_type' => get_query_var( 'post_type', 'post' ), 'key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'value' => sanitize_text_field( $keyword_filter ), ]; } /** * Determines whether the passed filter is considered to be valid. * * @param mixed $filter The filter to check against. * * @return bool Whether the filter is considered valid. */ protected function is_valid_filter( $filter ) { return ! empty( $filter ) && is_string( $filter ); } /** * Collects the filters and merges them into a single array. * * @return array Array containing all the applicable filters. */ protected function collect_filters() { $active_filters = []; $seo_filter = $this->get_current_seo_filter(); $readability_filter = $this->get_current_readability_filter(); $current_keyword_filter = $this->get_current_keyword_filter(); if ( $this->is_valid_filter( $seo_filter ) ) { $active_filters = array_merge( $active_filters, $this->determine_seo_filters( $seo_filter ), ); } if ( $this->is_valid_filter( $readability_filter ) ) { $active_filters = array_merge( $active_filters, $this->determine_readability_filters( $readability_filter ), ); } if ( $this->is_valid_filter( $current_keyword_filter ) ) { /** * Adapt the meta query used to filter the post overview on keyphrase. * * @internal * * @param array $keyphrase The keyphrase used in the filter. * @param array $keyword_filter The current keyword filter. */ $keyphrase_filter = apply_filters( 'wpseo_change_keyphrase_filter_in_request', $this->get_keyword_filter( $current_keyword_filter ), $current_keyword_filter, ); if ( is_array( $keyphrase_filter ) ) { $active_filters = array_merge( $active_filters, [ $keyphrase_filter ], ); } } /** * Adapt the active applicable filters on the posts overview. * * @internal * * @param array $active_filters The current applicable filters. */ return apply_filters( 'wpseo_change_applicable_filters', $active_filters ); } /** * Modify the query based on the filters that are being passed. * * @param array $vars Query variables that need to be modified based on the filters. * * @return array Array containing the meta query to use for filtering the posts overview. */ public function column_sort_orderby( $vars ) { $collected_filters = $this->collect_filters(); $order_by_column = $vars['orderby']; if ( isset( $order_by_column ) ) { // Based on the selected column, create a meta query. $order_by = $this->filter_order_by( $order_by_column ); /** * Adapt the order by part of the query on the posts overview. * * @internal * * @param array $order_by The current order by. * @param string $order_by_column The current order by column. */ $order_by = apply_filters( 'wpseo_change_order_by', $order_by, $order_by_column ); $vars = array_merge( $vars, $order_by ); } return $this->build_filter_query( $vars, $collected_filters ); } /** * Retrieves the meta robots query values to be used within the meta query. * * @return array Array containing the query parameters regarding meta robots. */ protected function get_meta_robots_query_values() { return [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'compare' => 'NOT EXISTS', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'value' => '1', 'compare' => '!=', ], ]; } /** * Determines the score filters to be used. If more than one is passed, it created an AND statement for the query. * * @param array $score_filters Array containing the score filters. * * @return array Array containing the score filters that need to be applied to the meta query. */ protected function determine_score_filters( $score_filters ) { if ( count( $score_filters ) > 1 ) { return array_merge( [ 'relation' => 'AND' ], $score_filters ); } return $score_filters; } /** * Retrieves the post type from the $_GET variable. * * @return string|null The sanitized current post type or null when the variable is not set in $_GET. */ public function get_current_post_type() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post_type'] ) && is_string( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); } return null; } /** * Retrieves the SEO filter from the $_GET variable. * * @return string|null The sanitized seo filter or null when the variable is not set in $_GET. */ public function get_current_seo_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['seo_filter'] ) && is_string( $_GET['seo_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['seo_filter'] ) ); } return null; } /** * Retrieves the Readability filter from the $_GET variable. * * @return string|null The sanitized readability filter or null when the variable is not set in $_GET. */ public function get_current_readability_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['readability_filter'] ) && is_string( $_GET['readability_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['readability_filter'] ) ); } return null; } /** * Retrieves the keyword filter from the $_GET variable. * * @return string|null The sanitized seo keyword filter or null when the variable is not set in $_GET. */ public function get_current_keyword_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['seo_kw_filter'] ) && is_string( $_GET['seo_kw_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['seo_kw_filter'] ) ); } return null; } /** * Uses the vars to create a complete filter query that can later be executed to filter out posts. * * @param array $vars Array containing the variables that will be used in the meta query. * @param array $filters Array containing the filters that we need to apply in the meta query. * * @return array Array containing the complete filter query. */ protected function build_filter_query( $vars, $filters ) { // If no filters were applied, just return everything. if ( count( $filters ) === 0 ) { return $vars; } $result = [ 'meta_query' => [] ]; $result['meta_query'] = array_merge( $result['meta_query'], [ $this->determine_score_filters( $filters ) ] ); $current_seo_filter = $this->get_current_seo_filter(); // This only applies for the SEO score filter because it can because the SEO score can be altered by the no-index option. if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, [ WPSEO_Rank::NO_INDEX ], true ) ) { $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] ); } return array_merge( $vars, $result ); } /** * Creates a Readability score filter. * * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * * @return array<array<string>> The Readability Score filter. */ protected function create_readability_score_filter( $low, $high ) { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => [ $low, $high ], 'type' => 'numeric', 'compare' => 'BETWEEN', ], ]; } /** * Creates an SEO score filter. * * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * * @return array<array<string>> The SEO score filter. */ protected function create_seo_score_filter( $low, $high ) { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => [ $low, $high ], 'type' => 'numeric', 'compare' => 'BETWEEN', ], ]; } /** * Creates a filter to retrieve posts that were set to no-index. * * @return array<array<string>> Array containin the no-index filter. */ protected function create_no_index_filter() { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'value' => '1', 'compare' => '=', ], ]; } /** * Creates a filter to retrieve posts that have no keyword set. * * @return array<array<string>> Array containing the no focus keyword filter. */ protected function create_no_focus_keyword_filter() { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], ]; } /** * Creates a filter to retrieve posts that have not been analyzed for readability yet. * * @return array<array<string>> Array containing the no readability filter. */ protected function create_no_readability_scores_filter() { // We check the existence of the Estimated Reading Time, because readability scores of posts that haven't been manually saved while Yoast SEO is active, don't exist, which is also the case for posts with not enough content. // Meanwhile, the ERT is a solid indicator of whether a post has ever been saved (aka, analyzed), so we're using that. $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => $rank->get_starting_score(), 'type' => 'numeric', 'compare' => '<', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], ], ]; } /** * Creates a filter to retrieve posts that have bad readability scores, including those that have not enough content to have one. * * @return array<array<string>> Array containing the bad readability filter. */ protected function create_bad_readability_scores_filter() { $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); return [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => [ $rank->get_starting_score(), $rank->get_end_score() ], 'type' => 'numeric', 'compare' => 'BETWEEN', ], [ [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', 'compare' => 'EXISTS', ], ], ]; } /** * Determines whether a particular post_id is of an indexable post type. * * @param string $post_id The post ID to check. * * @return bool Whether or not it is indexable. */ protected function is_indexable( $post_id ) { if ( ! empty( $post_id ) && ! $this->uses_default_indexing( $post_id ) ) { return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '2'; } $post = get_post( $post_id ); if ( is_object( $post ) ) { // If the option is false, this means we want to index it. return WPSEO_Options::get( 'noindex-' . $post->post_type, false ) === false; } return true; } /** * Determines whether the given post ID uses the default indexing settings. * * @param int $post_id The post ID to check. * * @return bool Whether or not the default indexing is being used for the post. */ protected function uses_default_indexing( $post_id ) { return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '0'; } /** * Returns filters when $order_by is matched in the if-statement. * * @param string $order_by The ID of the column by which to order the posts. * * @return array<string> Array containing the order filters. */ private function filter_order_by( $order_by ) { switch ( $order_by ) { case 'wpseo-metadesc': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'metadesc', 'orderby' => 'meta_value', ]; case 'wpseo-focuskw': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'orderby' => 'meta_value', ]; case 'wpseo-score': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'orderby' => 'meta_value_num', ]; case 'wpseo-score-readability': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'content_score', 'orderby' => 'meta_value_num', ]; } return []; } /** * Parses the score column. * * @param int $post_id The ID of the post for which to show the score. * * @return string The HTML for the SEO score indicator. */ private function parse_column_score( $post_id ) { $meta = $this->get_meta( $post_id ); if ( $meta ) { return $this->score_icon_helper->for_seo( $meta->indexable, '', __( 'Post is set to noindex.', 'wordpress-seo' ) ); } } /** * Parsing the readability score column. * * @param int $post_id The ID of the post for which to show the readability score. * * @return string The HTML for the readability score indicator. */ private function parse_column_score_readability( $post_id ) { $meta = $this->get_meta( $post_id ); if ( $meta ) { return $this->score_icon_helper->for_readability( $meta->indexable->readability_score ); } } /** * Sets up the hooks for the post_types. * * @return void */ private function set_post_type_hooks() { $post_types = WPSEO_Post_Type::get_accessible_post_types(); if ( ! is_array( $post_types ) || $post_types === [] ) { return; } foreach ( $post_types as $post_type ) { if ( $this->display_metabox( $post_type ) === false ) { continue; } add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'column_heading' ], 10, 1 ); add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 ); add_action( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ], 10, 2 ); } unset( $post_type ); } /** * Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by * choice of the admin or because the post type is not a public post type. * * @since 7.0 * * @param string|null $post_type Optional. The post type to test, defaults to the current post post_type. * * @return bool Whether or not the meta box (and associated columns etc) should be hidden. */ private function display_metabox( $post_type = null ) { $current_post_type = $this->get_current_post_type(); if ( ! isset( $post_type ) && ! empty( $current_post_type ) ) { $post_type = $current_post_type; } return WPSEO_Utils::is_metabox_active( $post_type, 'post_type' ); } /** * Determines whether or not filter dropdowns should be displayed. * * @return bool Whether or the current page can display the filter drop downs. */ public function can_display_filter() { if ( $GLOBALS['pagenow'] === 'upload.php' ) { return false; } if ( $this->display_metabox() === false ) { return false; } $screen = get_current_screen(); if ( $screen === null ) { return false; } return WPSEO_Post_Type::is_post_type_accessible( $screen->post_type ); } } admin/filters/class-cornerstone-filter.php 0000644 00000007301 15174712003 0014745 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Registers the filter for filtering posts by cornerstone content. */ class WPSEO_Cornerstone_Filter extends WPSEO_Abstract_Post_Filter { /** * Name of the meta value. * * @var string */ public const META_NAME = 'is_cornerstone'; /** * Registers the hooks. * * @return void */ public function register_hooks() { parent::register_hooks(); add_filter( 'wpseo_cornerstone_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] ); add_filter( 'wpseo_cornerstone_post_types', [ $this, 'filter_metabox_disabled' ] ); } /** * Returns the query value this filter uses. * * @return string The query value this filter uses. */ public function get_query_val() { return 'cornerstone'; } /** * Modify the query based on the seo_filter variable in $_GET. * * @param string $where Query variables. * * @return string The modified query. */ public function filter_posts( $where ) { if ( $this->is_filter_active() ) { global $wpdb; $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = '1' ) ", WPSEO_Meta::$meta_prefix . self::META_NAME, ); } return $where; } /** * Filters the post types that have the metabox disabled. * * @param array $post_types The post types to filter. * * @return array The filtered post types. */ public function filter_metabox_disabled( $post_types ) { $filtered_post_types = []; foreach ( $post_types as $post_type_key => $post_type ) { if ( ! WPSEO_Post_Type::has_metabox_enabled( $post_type_key ) ) { continue; } $filtered_post_types[ $post_type_key ] = $post_type; } return $filtered_post_types; } /** * Returns the label for this filter. * * @return string The label for this filter. */ protected function get_label() { return __( 'Cornerstone content', 'wordpress-seo' ); } /** * Returns a text explaining this filter. * * @return string|null The explanation. */ protected function get_explanation() { $post_type_object = get_post_type_object( $this->get_current_post_type() ); if ( $post_type_object === null ) { return null; } return sprintf( /* translators: %1$s expands to the posttype label, %2$s expands anchor to blog post about cornerstone content, %3$s expands to </a> */ __( 'Mark the most important %1$s as \'cornerstone content\' to improve your site structure. %2$sLearn more about cornerstone content%3$s.', 'wordpress-seo' ), strtolower( $post_type_object->labels->name ), '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/1i9' ) . '" target="_blank">', '</a>', ); } /** * Returns the total amount of articles marked as cornerstone content. * * @return int */ protected function get_post_total() { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( 1 ) FROM {$wpdb->postmeta} WHERE post_id IN( SELECT ID FROM {$wpdb->posts} WHERE post_type = %s ) AND meta_key = %s AND meta_value = '1' ", $this->get_current_post_type(), WPSEO_Meta::$meta_prefix . self::META_NAME, ), ); } /** * Returns the post types to which this filter should be added. * * @return array The post types to which this filter should be added. */ protected function get_post_types() { /** * Filter: 'wpseo_cornerstone_post_types' - Filters post types to exclude the cornerstone feature for. * * @param array $post_types The accessible post types to filter. */ $post_types = apply_filters( 'wpseo_cornerstone_post_types', parent::get_post_types() ); if ( ! is_array( $post_types ) ) { return []; } return $post_types; } } admin/filters/class-abstract-post-filter.php 0000644 00000012547 15174712003 0015202 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Filters */ /** * Class WPSEO_Abstract_Post_Filter. */ abstract class WPSEO_Abstract_Post_Filter implements WPSEO_WordPress_Integration { /** * The filter's query argument. * * @var string */ public const FILTER_QUERY_ARG = 'yoast_filter'; /** * Modify the query based on the FILTER_QUERY_ARG variable in $_GET. * * @param string $where Query variables. * * @return string The modified query. */ abstract public function filter_posts( $where ); /** * Returns the query value this filter uses. * * @return string The query value this filter uses. */ abstract public function get_query_val(); /** * Returns the total number of posts that match this filter. * * @return int The total number of posts that match this filter. */ abstract protected function get_post_total(); /** * Returns the label for this filter. * * @return string The label for this filter. */ abstract protected function get_label(); /** * Registers the hooks. * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'add_filter_links' ], 11 ); add_filter( 'posts_where', [ $this, 'filter_posts' ] ); if ( $this->is_filter_active() ) { add_action( 'restrict_manage_posts', [ $this, 'render_hidden_input' ] ); } if ( $this->is_filter_active() ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_explanation_assets' ] ); } } /** * Adds the filter links to the view_edit screens to give the user a filter link. * * @return void */ public function add_filter_links() { foreach ( $this->get_post_types() as $post_type ) { add_filter( 'views_edit-' . $post_type, [ $this, 'add_filter_link' ] ); } } /** * Enqueues the necessary assets to display a filter explanation. * * @return void */ public function enqueue_explanation_assets() { $explanation = $this->get_explanation(); if ( $explanation === null ) { return; } $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'filter-explanation' ); $asset_manager->enqueue_style( 'filter-explanation' ); $asset_manager->localize_script( 'filter-explanation', 'yoastFilterExplanation', [ 'text' => $explanation ], ); } /** * Adds a filter link to the views. * * @param array<string, string> $views Array with the views. * * @return array<string, string> Array of views including the added view. */ public function add_filter_link( $views ) { $views[ 'yoast_' . $this->get_query_val() ] = sprintf( '<a href="%1$s"%2$s>%3$s</a> (%4$s)', esc_url( $this->get_filter_url() ), ( $this->is_filter_active() ) ? ' class="current" aria-current="page"' : '', $this->get_label(), $this->get_post_total(), ); return $views; } /** * Returns a text explaining this filter. Null if no explanation is necessary. * * @return string|null The explanation or null. */ protected function get_explanation() { return null; } /** * Renders a hidden input to preserve this filter's state when using sub-filters. * * @return void */ public function render_hidden_input() { echo '<input type="hidden" name="' . esc_attr( self::FILTER_QUERY_ARG ) . '" value="' . esc_attr( $this->get_query_val() ) . '">'; } /** * Returns an url to edit.php with post_type and this filter as the query arguments. * * @return string The url to activate this filter. */ protected function get_filter_url() { $query_args = [ self::FILTER_QUERY_ARG => $this->get_query_val(), 'post_type' => $this->get_current_post_type(), ]; return add_query_arg( $query_args, 'edit.php' ); } /** * Returns true when the filter is active. * * @return bool Whether the filter is active. */ protected function is_filter_active() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET[ self::FILTER_QUERY_ARG ] ) && is_string( $_GET[ self::FILTER_QUERY_ARG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET[ self::FILTER_QUERY_ARG ] ) ) === $this->get_query_val(); } return false; } /** * Returns the current post type. * * @return string The current post type. */ protected function get_current_post_type() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post_type'] ) && is_string( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $post_type = sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); if ( ! empty( $post_type ) ) { return $post_type; } } return 'post'; } /** * Returns the post types to which this filter should be added. * * @return array The post types to which this filter should be added. */ protected function get_post_types() { return WPSEO_Post_Type::get_accessible_post_types(); } /** * Checks if the post type is supported. * * @param string $post_type Post type to check against. * * @return bool True when it is supported. */ protected function is_supported_post_type( $post_type ) { return in_array( $post_type, $this->get_post_types(), true ); } } admin/class-remote-request.php 0000644 00000006203 15174712003 0012432 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * This class handles a post request being send to a given endpoint. */ class WPSEO_Remote_Request { /** * Holds the post method. * * @var string */ public const METHOD_POST = 'post'; /** * Holds the get method. * * @var string */ public const METHOD_GET = 'get'; /** * Holds the endpoint to send the request to. * * @var string */ protected $endpoint = ''; /** * Holds the arguments to use in this request. * * @var array */ protected $args = [ 'blocking' => false, 'timeout' => 2, ]; /** * Holds the response error. * * @var WP_Error|null */ protected $response_error; /** * Holds the response body. * * @var mixed */ protected $response_body; /** * Sets the endpoint and arguments. * * @param string $endpoint The endpoint to send the request to. * @param array $args The arguments to use in this request. */ public function __construct( $endpoint, array $args = [] ) { $this->endpoint = $endpoint; $this->args = wp_parse_args( $this->args, $args ); } /** * Sets the request body. * * @param mixed $body The body to set. * * @return void */ public function set_body( $body ) { $this->args['body'] = $body; } /** * Sends the data to the given endpoint. * * @param string $method The type of request to send. * * @return bool True when sending data has been successful. */ public function send( $method = self::METHOD_POST ) { switch ( $method ) { case self::METHOD_POST: $response = $this->post(); break; case self::METHOD_GET: $response = $this->get(); break; default: /* translators: %1$s expands to the request method */ $response = new WP_Error( 1, sprintf( __( 'Request method %1$s is not valid.', 'wordpress-seo' ), $method ) ); break; } return $this->process_response( $response ); } /** * Returns the value of the response error. * * @return WP_Error|null The response error. */ public function get_response_error() { return $this->response_error; } /** * Returns the response body. * * @return mixed The response body. */ public function get_response_body() { return $this->response_body; } /** * Processes the given response. * * @param mixed $response The response to process. * * @return bool True when response is valid. */ protected function process_response( $response ) { if ( $response instanceof WP_Error ) { $this->response_error = $response; return false; } $this->response_body = wp_remote_retrieve_body( $response ); return ( wp_remote_retrieve_response_code( $response ) === 200 ); } /** * Performs a post request to the specified endpoint with set arguments. * * @return WP_Error|array The response or WP_Error on failure. */ protected function post() { return wp_remote_post( $this->endpoint, $this->args ); } /** * Performs a post request to the specified endpoint with set arguments. * * @return WP_Error|array The response or WP_Error on failure. */ protected function get() { return wp_remote_get( $this->endpoint, $this->args ); } } admin/views/class-yoast-integration-toggles.php 0000644 00000010472 15174712003 0015733 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class for managing integration toggles. */ class Yoast_Integration_Toggles { /** * Available integration toggles. * * @var array */ protected $toggles; /** * Instance holder. * * @var self|null */ protected static $instance = null; /** * Gets the main integration toggles manager instance used. * * This essentially works like a Singleton, but for its drawbacks does not restrict * instantiation otherwise. * * @return self Main instance. */ public static function instance() { self::$instance ??= new self(); return self::$instance; } /** * Gets all available integration toggles. * * @return array List of sorted Yoast_Feature_Toggle instances. */ public function get_all() { $this->toggles ??= $this->load_toggles(); return $this->toggles; } /** * Loads the available integration toggles. * * Also ensures that the toggles are all Yoast_Feature_Toggle instances and sorted by their order value. * * @return array List of sorted Yoast_Feature_Toggle instances. */ protected function load_toggles() { $integration_toggles = [ (object) [ /* translators: %s: 'Semrush' */ 'name' => sprintf( __( '%s integration', 'wordpress-seo' ), 'Semrush' ), 'setting' => 'semrush_integration_active', 'label' => sprintf( /* translators: %s: 'Semrush' */ __( 'The %s integration offers suggestions and insights for keywords related to the entered focus keyphrase.', 'wordpress-seo' ), 'Semrush', ), 'order' => 10, ], (object) [ /* translators: %s: Algolia. */ 'name' => sprintf( esc_html__( '%s integration', 'wordpress-seo' ), 'Algolia' ), 'premium' => true, 'setting' => 'algolia_integration_active', 'label' => __( 'Improve the quality of your site search! Automatically helps your users find your cornerstone and most important content in your internal search results. It also removes noindexed posts & pages from your site’s search results.', 'wordpress-seo' ), /* translators: %s: Algolia. */ 'read_more_label' => sprintf( __( 'Find out more about our %s integration.', 'wordpress-seo' ), 'Algolia' ), 'read_more_url' => 'https://yoa.st/4eu', 'premium_url' => 'https://yoa.st/4ex', 'premium_upsell_url' => 'https://yoa.st/get-algolia-integration', 'order' => 25, ], ]; /** * Filter to add integration toggles from add-ons. * * @param array $integration_toggles Array with integration toggle objects where each object * should have a `name`, `setting` and `label` property. */ $integration_toggles = apply_filters( 'wpseo_integration_toggles', $integration_toggles ); $integration_toggles = array_map( [ $this, 'ensure_toggle' ], $integration_toggles ); usort( $integration_toggles, [ $this, 'sort_toggles_callback' ] ); return $integration_toggles; } /** * Ensures that the passed value is a Yoast_Feature_Toggle. * * @param Yoast_Feature_Toggle|object|array $toggle_data Feature toggle instance, or raw object or array * containing integration toggle data. * @return Yoast_Feature_Toggle Feature toggle instance based on $toggle_data. */ protected function ensure_toggle( $toggle_data ) { if ( $toggle_data instanceof Yoast_Feature_Toggle ) { return $toggle_data; } if ( is_object( $toggle_data ) ) { $toggle_data = get_object_vars( $toggle_data ); } return new Yoast_Feature_Toggle( $toggle_data ); } /** * Callback for sorting integration toggles by their order. * * {@internal Once the minimum PHP version goes up to PHP 7.0, the logic in the function * can be replaced with the spaceship operator `<=>`.} * * @param Yoast_Feature_Toggle $feature_a Feature A. * @param Yoast_Feature_Toggle $feature_b Feature B. * * @return int An integer less than, equal to, or greater than zero indicating respectively * that feature A is considered to be less than, equal to, or greater than feature B. */ protected function sort_toggles_callback( Yoast_Feature_Toggle $feature_a, Yoast_Feature_Toggle $feature_b ) { return ( $feature_a->order - $feature_b->order ); } } admin/views/paper-collapsible.php 0000644 00000006025 15174712003 0013103 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses string $paper_id The ID of the paper. * @uses string $paper_id_prefix The ID prefix of the paper. * @uses bool $collapsible Whether the collapsible should be rendered. * @uses array $collapsible_config Configuration for the collapsible. * @uses string $collapsible_header_class Class for the collapsible header. * @uses string $title The title. * @uses string $title_after Additional content to render after the title. * @uses string $view_file Path to the view file. * @uses WPSEO_Admin_Help_Panel $help_text The help text. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } ?> <div class="<?php echo esc_attr( 'paper tab-block ' . $class ); ?>"<?php echo ( $paper_id ) ? ' id="' . esc_attr( $paper_id_prefix . $paper_id ) . '"' : ''; ?>> <?php if ( ! empty( $title ) ) { if ( ! empty( $collapsible ) ) { $button_id_attr = ''; if ( ! empty( $paper_id ) ) { $button_id_attr = sprintf( ' id="%s"', esc_attr( $paper_id_prefix . $paper_id . '-button' ) ); } printf( '<h2 class="%1$s"><button%2$s type="button" class="toggleable-container-trigger" aria-expanded="%3$s">%4$s%5$s <span class="toggleable-container-icon dashicons %6$s" aria-hidden="true"></span></button></h2>', esc_attr( 'collapsible-header ' . $collapsible_header_class ), // phpcs:ignore WordPress.Security.EscapeOutput -- $button_id_attr is escaped above. $button_id_attr, esc_attr( $collapsible_config['expanded'] ), // phpcs:ignore WordPress.Security.EscapeOutput -- $help_text is an instance of WPSEO_Admin_Help_Panel, which escapes it's own output. $help_text->get_button_html(), esc_html( $title ) . wp_kses_post( $title_after ), wp_kses_post( $collapsible_config['toggle_icon'] ), ); } else { echo '<div class="paper-title"><h2 class="help-button-inline">', esc_html( $title ), wp_kses_post( $title_after ), // phpcs:ignore WordPress.Security.EscapeOutput -- $help_text is an instance of WPSEO_Admin_Help_Panel, which escapes it's own output. $help_text->get_button_html(), '</h2></div>'; } } ?> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- $help_text is an instance of WPSEO_Admin_Help_Panel, which escapes it's own output. echo $help_text->get_panel_html(); $container_id_attr = ''; if ( ! empty( $paper_id ) ) { $container_id_attr = sprintf( ' id="%s"', esc_attr( $paper_id_prefix . $paper_id . '-container' ) ); } printf( '<div%1$s class="%2$s">%3$s</div>', // phpcs:ignore WordPress.Security.EscapeOutput -- $container_id_attr is escaped above. $container_id_attr, esc_attr( 'paper-container ' . $collapsible_config['class'] ), $content, ); ?> </div> admin/views/tool-file-editor.php 0000644 00000015403 15174712003 0012663 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $yform = Yoast_Form::get_instance(); $home_path = get_home_path(); if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { $home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; } $robots_file = $home_path . 'robots.txt'; $ht_access_file = $home_path . '.htaccess'; if ( isset( $_POST['create_robots'] ) ) { if ( ! current_user_can( 'edit_files' ) ) { $die_msg = sprintf( /* translators: %s expands to robots.txt. */ __( 'You cannot create a %s file.', 'wordpress-seo' ), 'robots.txt', ); exit( esc_html( $die_msg ) ); } check_admin_referer( 'wpseo_create_robots' ); ob_start(); error_reporting( 0 ); do_robots(); $robots_content = ob_get_clean(); $f = fopen( $robots_file, 'x' ); fwrite( $f, $robots_content ); } if ( isset( $_POST['submitrobots'] ) ) { if ( ! current_user_can( 'edit_files' ) ) { $die_msg = sprintf( /* translators: %s expands to robots.txt. */ __( 'You cannot edit the %s file.', 'wordpress-seo' ), 'robots.txt', ); exit( esc_html( $die_msg ) ); } check_admin_referer( 'wpseo-robotstxt' ); if ( isset( $_POST['robotsnew'] ) && file_exists( $robots_file ) ) { $robotsnew = sanitize_textarea_field( wp_unslash( $_POST['robotsnew'] ) ); if ( is_writable( $robots_file ) ) { $f = fopen( $robots_file, 'w+' ); fwrite( $f, $robotsnew ); fclose( $f ); $msg = sprintf( /* translators: %s expands to robots.txt. */ __( 'Updated %s', 'wordpress-seo' ), 'robots.txt', ); } } } if ( isset( $_POST['submithtaccess'] ) ) { if ( ! current_user_can( 'edit_files' ) ) { $die_msg = sprintf( /* translators: %s expands to ".htaccess". */ __( 'You cannot edit the %s file.', 'wordpress-seo' ), '.htaccess', ); exit( esc_html( $die_msg ) ); } check_admin_referer( 'wpseo-htaccess' ); if ( isset( $_POST['htaccessnew'] ) && file_exists( $ht_access_file ) ) { $ht_access_new = wp_unslash( $_POST['htaccessnew'] ); if ( is_writable( $ht_access_file ) ) { $f = fopen( $ht_access_file, 'w+' ); fwrite( $f, $ht_access_new ); fclose( $f ); } } } if ( is_multisite() ) { $action_url = network_admin_url( 'admin.php?page=wpseo_files' ); $yform->admin_header( false, 'wpseo_ms' ); } else { $action_url = admin_url( 'admin.php?page=wpseo_tools&tool=file-editor' ); } if ( isset( $msg ) && ! empty( $msg ) ) { echo '<div id="message" class="notice notice-success"><p>', esc_html( $msg ), '</p></div>'; } // N.B.: "robots.txt" is a fixed file name and should not be translatable. echo '<h2>robots.txt</h2>'; if ( ! file_exists( $robots_file ) ) { if ( is_writable( $home_path ) ) { echo '<form action="', esc_url( $action_url ), '" method="post" id="robotstxtcreateform">'; wp_nonce_field( 'wpseo_create_robots', '_wpnonce', true, true ); echo '<p>'; printf( /* translators: %s expands to robots.txt. */ esc_html__( 'You don\'t have a %s file, create one here:', 'wordpress-seo' ), 'robots.txt', ); echo '</p>'; printf( '<input type="submit" class="button" name="create_robots" value="%s">', sprintf( /* translators: %s expands to robots.txt. */ esc_attr__( 'Create %s file', 'wordpress-seo' ), 'robots.txt', ), ); echo '</form>'; } else { echo '<p>'; printf( /* translators: %s expands to robots.txt. */ esc_html__( 'If you had a %s file and it was editable, you could edit it from here.', 'wordpress-seo' ), 'robots.txt', ); echo '</p>'; } } else { $f = fopen( $robots_file, 'r' ); $content = ''; if ( filesize( $robots_file ) > 0 ) { $content = fread( $f, filesize( $robots_file ) ); } if ( ! is_writable( $robots_file ) ) { echo '<p><em>'; printf( /* translators: %s expands to robots.txt. */ esc_html__( 'If your %s were writable, you could edit it from here.', 'wordpress-seo' ), 'robots.txt', ); echo '</em></p>'; echo '<textarea class="large-text code" disabled="disabled" rows="15" name="robotsnew">', esc_textarea( $content ), '</textarea><br/>'; } else { echo '<form action="', esc_url( $action_url ), '" method="post" id="robotstxtform">'; wp_nonce_field( 'wpseo-robotstxt', '_wpnonce', true, true ); echo '<label for="robotsnew" class="yoast-inline-label">'; printf( /* translators: %s expands to robots.txt. */ esc_html__( 'Edit the content of your %s:', 'wordpress-seo' ), 'robots.txt', ); echo '</label>'; echo '<textarea class="large-text code" rows="15" name="robotsnew" id="robotsnew">', esc_textarea( $content ), '</textarea><br/>'; printf( '<div class="submit"><input class="button" type="submit" name="submitrobots" value="%s" /></div>', sprintf( /* translators: %s expands to robots.txt. */ esc_attr__( 'Save changes to %s', 'wordpress-seo' ), 'robots.txt', ), ); echo '</form>'; } } if ( ! WPSEO_Utils::is_nginx() ) { echo '<h2>'; printf( /* translators: %s expands to ".htaccess". */ esc_html__( '%s file', 'wordpress-seo' ), '.htaccess', ); echo '</h2>'; if ( file_exists( $ht_access_file ) ) { $f = fopen( $ht_access_file, 'r' ); $contentht = ''; if ( filesize( $ht_access_file ) > 0 ) { $contentht = fread( $f, filesize( $ht_access_file ) ); } if ( ! is_writable( $ht_access_file ) ) { echo '<p><em>'; printf( /* translators: %s expands to ".htaccess". */ esc_html__( 'If your %s were writable, you could edit it from here.', 'wordpress-seo' ), '.htaccess', ); echo '</em></p>'; echo '<textarea class="large-text code" disabled="disabled" rows="15" name="robotsnew">', esc_textarea( $contentht ), '</textarea><br/>'; } else { echo '<form action="', esc_url( $action_url ), '" method="post" id="htaccessform">'; wp_nonce_field( 'wpseo-htaccess', '_wpnonce', true, true ); echo '<label for="htaccessnew" class="yoast-inline-label">'; printf( /* translators: %s expands to ".htaccess". */ esc_html__( 'Edit the content of your %s:', 'wordpress-seo' ), '.htaccess', ); echo '</label>'; echo '<textarea class="large-text code" rows="15" name="htaccessnew" id="htaccessnew">', esc_textarea( $contentht ), '</textarea><br/>'; printf( '<div class="submit"><input class="button" type="submit" name="submithtaccess" value="%s" /></div>', sprintf( /* translators: %s expands to ".htaccess". */ esc_attr__( 'Save changes to %s', 'wordpress-seo' ), '.htaccess', ), ); echo '</form>'; } } else { echo '<p>'; printf( /* translators: %s expands to ".htaccess". */ esc_html__( 'If you had a %s file and it was editable, you could edit it from here.', 'wordpress-seo' ), '.htaccess', ); echo '</p>'; } } if ( is_multisite() ) { $yform->admin_footer( false ); } admin/views/class-yoast-feature-toggle.php 0000644 00000011040 15174712003 0014650 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class representing a feature toggle. */ class Yoast_Feature_Toggle { /** * Feature toggle identifier. * * @var string */ protected $name = ''; /** * Name of the setting the feature toggle is associated with. * * @var string */ protected $setting = ''; /** * Whether the feature is premium or not. * * @var bool */ protected $premium = false; /** * Whether the feature is in beta or not. * * @var bool */ protected $in_beta = false; /** * The Premium version in which this feature has been added. * * @var string */ protected $premium_version = ''; /** * The languages in which this feature is supported. * E.g. for language specific analysis support. * * If empty, the feature is considered to have support in all languages. * * @var string[] */ protected $supported_languages = []; /** * Feature toggle label. * * @var string */ protected $label = ''; /** * URL to learn more about the feature. * * @var string */ protected $read_more_url = ''; /** * URL to learn more about the premium feature. * * @var string */ protected $premium_url = ''; /** * URL to buy premium. * * @var string */ protected $premium_upsell_url = ''; /** * Label for the learn more link. * * @var string */ protected $read_more_label = ''; /** * Additional help content for the feature. * * @var string */ protected $extra = ''; /** * Additional content to be rendered after the toggle. * * @var string */ protected $after = ''; /** * Value to specify the feature toggle order. * * @var int */ protected $order = 100; /** * Disable the integration toggle. * * @var bool */ protected $disabled = false; /** * Whether the feature is new or not. * * @var bool */ protected $new = false; /** * Constructor. * * Sets the feature toggle arguments. * * @param array $args { * Feature toggle arguments. * * @type string $name Required. Feature toggle identifier. * @type string $setting Required. Name of the setting the feature toggle is associated with. * @type string $disabled Whether the feature is premium or not. * @type string $label Feature toggle label. * @type string $read_more_url URL to learn more about the feature. Default empty string. * @type string $premium_upsell_url URL to buy premium. Default empty string. * @type string $read_more_label Label for the learn more link. Default empty string. * @type string $extra Additional help content for the feature. Default empty string. * @type int $order Value to specify the feature toggle order. A lower value indicates * a higher priority. Default 100. * @type bool $disabled Disable the integration toggle. Default false. * @type string $new Whether the feature is new or not. * @type bool $in_beta Whether the feature is in beta or not. * @type array $supported_languages The languages that this feature supports. * @type string $premium_version The Premium version in which this feature was added. * } * * @throws InvalidArgumentException Thrown when a required argument is missing. */ public function __construct( array $args ) { $required_keys = [ 'name', 'setting' ]; foreach ( $required_keys as $key ) { if ( empty( $args[ $key ] ) ) { /* translators: %s: argument name */ throw new InvalidArgumentException( sprintf( __( '%s is a required feature toggle argument.', 'wordpress-seo' ), $key ) ); } } foreach ( $args as $key => $value ) { if ( property_exists( $this, $key ) ) { $this->$key = $value; } } } /** * Magic isset-er. * * @param string $key Key to check whether a value for it is set. * * @return bool True if set, false otherwise. */ public function __isset( $key ) { return isset( $this->$key ); } /** * Magic getter. * * @param string $key Key to get the value for. * * @return mixed Value for the key, or null if not set. */ public function __get( $key ) { if ( isset( $this->$key ) ) { return $this->$key; } return null; } /** * Checks whether the feature for this toggle is enabled. * * @return bool True if the feature is enabled, false otherwise. */ public function is_enabled() { return (bool) WPSEO_Options::get( $this->setting ); } } admin/views/interface-yoast-form-element.php 0000644 00000000373 15174712003 0015172 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generate the HTML for a form element. */ interface Yoast_Form_Element { /** * Return the HTML for the form element. * * @return string */ public function get_html(); } admin/views/partial-notifications-template.php 0000644 00000007635 15174712003 0015631 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * * @uses string $yoast_seo_type * @uses string $yoast_seo_dashicon * @uses string $yoast_seo_i18n_title * @uses string $yoast_seo_i18n_issues * @uses string $yoast_seo_i18n_no_issues * @uses string $yoast_seo_i18n_muted_issues_title * @uses int $yoast_seo_active_total * @uses int $yoast_seo_dismissed_total * @uses int $yoast_seo_total * @uses array $yoast_seo_active * @uses array $yoast_seo_dismissed */ if ( ! function_exists( '_yoast_display_notifications' ) ) { /** * Create the notifications HTML with restore/dismiss button. * * @param array $notifications_list List of notifications. * @param string $status Status of the notifications (active/dismissed). * * @return string The output to render. */ function _yoast_display_notifications( $notifications_list, $status ) { $notifications = ''; foreach ( $notifications_list as $notification ) { switch ( $status ) { case 'active': $button = sprintf( '<button type="button" class="button dismiss"><span class="screen-reader-text">%1$s</span><span class="dashicons dashicons-hidden"></span></button>', /* translators: Hidden accessibility text. */ esc_html__( 'Hide this item.', 'wordpress-seo' ), ); break; case 'dismissed': $button = sprintf( '<button type="button" class="button restore"><span class="screen-reader-text">%1$s</span><span class="dashicons yoast-svg-icon-eye"></span></button>', /* translators: Hidden accessibility text. */ esc_html__( 'Show this item.', 'wordpress-seo' ), ); break; } $notifications .= sprintf( '<div class="yoast-notification-holder" id="%1$s" data-nonce="%2$s" data-json="%3$s">%4$s%5$s</div>', esc_attr( $notification->get_id() ), esc_attr( $notification->get_nonce() ), esc_attr( $notification->get_json() ), // This needs to be fixed in https://github.com/Yoast/wordpress-seo-premium/issues/2548. $notification, // Note: $button is properly escaped above. $button, ); } return $notifications; } } $wpseo_i18n_summary = $yoast_seo_i18n_issues; if ( ! $yoast_seo_active ) { $yoast_seo_dashicon = 'yes'; $wpseo_i18n_summary = $yoast_seo_i18n_no_issues; } ?> <h3 class="yoast-notifications-header" id="<?php echo esc_attr( 'yoast-' . $yoast_seo_type . '-header' ); ?>"> <span class="dashicons <?php echo esc_attr( 'dashicons-' . $yoast_seo_dashicon ); ?>"></span> <?php echo esc_html( $yoast_seo_i18n_title ); ?> (<?php echo (int) $yoast_seo_active_total; ?>) </h3> <div id="<?php echo esc_attr( 'yoast-' . $yoast_seo_type ); ?>"> <?php if ( $yoast_seo_total ) : ?> <p><?php echo esc_html( $wpseo_i18n_summary ); ?></p> <div class="container yoast-notifications-active" id="<?php echo esc_attr( 'yoast-' . $yoast_seo_type . '-active' ); ?>"> <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: _yoast_display_notifications() as declared above is safe. echo _yoast_display_notifications( $yoast_seo_active, 'active' ); ?> </div> <?php if ( $yoast_seo_dismissed ) { $dismissed_paper = new WPSEO_Paper_Presenter( esc_html( $yoast_seo_i18n_muted_issues_title ), null, [ 'paper_id' => esc_attr( $yoast_seo_type . '-dismissed' ), 'paper_id_prefix' => 'yoast-', 'class' => 'yoast-notifications-dismissed', 'content' => _yoast_display_notifications( $yoast_seo_dismissed, 'dismissed' ), 'collapsible' => true, 'collapsible_header_class' => 'yoast-notification', ], ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: get_output() output is properly escaped. echo $dismissed_paper->get_output(); } ?> <?php else : ?> <p><?php echo esc_html( $yoast_seo_i18n_no_issues ); ?></p> <?php endif; ?> </div> admin/views/redirects.php 0000644 00000000364 15174712003 0011471 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 19.0 */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } ?> <div id="yoast-seo-redirects"></div> admin/views/form/select.php 0000644 00000002115 15174712003 0011723 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * * @uses string $attributes Additional attributes for the select. * @uses string $name Value for the select name attribute. * @uses string $id ID attribute for the select. * @uses array $options Array with the options to show. * @uses string $selected The current set options. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } ?> <?php /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $attributes is properly escaped in parse_attribute via get_attributes in class-yoast-input-select.php. */ ?> <select <?php echo $attributes; ?>name="<?php echo esc_attr( $name ); ?>" id="<?php echo esc_attr( $id ); ?>"> <?php foreach ( $options as $option_attribute_value => $option_html_value ) : ?> <option value="<?php echo esc_attr( $option_attribute_value ); ?>"<?php echo selected( $selected, $option_attribute_value, false ); ?>><?php echo esc_html( $option_html_value ); ?></option> <?php endforeach; ?> </select> admin/views/js-templates-primary-term.php 0000644 00000002531 15174712003 0014541 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } ?> <script type="text/html" id="tmpl-primary-term-ui"> <?php /* translators: Hidden accessibility text; %1$s expands to the term title, %2$s to the taxonomy title. */ $yoast_free_js_button_label = __( 'Make %1$s primary %2$s', 'wordpress-seo' ); $yoast_free_js_button_label = sprintf( $yoast_free_js_button_label, '{{data.term}}', '{{data.taxonomy.title}}', ); printf( '<button type="button" class="wpseo-make-primary-term" aria-label="%1$s">%2$s</button>', esc_attr( $yoast_free_js_button_label ), esc_html__( 'Make primary', 'wordpress-seo' ), ); ?> <span class="wpseo-is-primary-term" aria-hidden="true"><?php esc_html_e( 'Primary', 'wordpress-seo' ); ?></span> </script> <script type="text/html" id="tmpl-primary-term-screen-reader"> <?php /* translators: %s is the taxonomy title. This will be shown to screenreaders */ $yoast_free_js_taxonomy_title = __( 'Primary %s', 'wordpress-seo' ); $yoast_free_js_taxonomy_title = sprintf( '(' . $yoast_free_js_taxonomy_title . ')', '{{data.taxonomy.title}}', ); ?> <span class="screen-reader-text wpseo-primary-category-label"><?php echo esc_html( $yoast_free_js_taxonomy_title ); ?></span> </script> admin/views/tool-bulk-editor.php 0000644 00000006212 15174712003 0012677 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 1.5.0 */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Sanitizes the parameters that have been sent. * * @return array The sanitized fields. */ function yoast_free_bulk_sanitize_input_fields() { $possible_params = [ 'type', 'paged', 'post_type_filter', 'post_status', 'order', 'orderby', ]; $input_get = []; foreach ( $possible_params as $param_name ) { if ( isset( $_GET[ $param_name ] ) ) { $input_get[ $param_name ] = sanitize_text_field( wp_unslash( $_GET[ $param_name ] ) ); } } return $input_get; } $yoast_free_input_fields = yoast_free_bulk_sanitize_input_fields(); // Verifies the nonce. if ( ! empty( $yoast_free_input_fields ) ) { check_admin_referer( 'bulk-editor-table', 'nonce' ); } // If type is empty, fill it with value of first tab (title). if ( ! isset( $yoast_free_input_fields['type'] ) ) { $yoast_free_input_fields['type'] = 'title'; } $yoast_bulk_editor_arguments = [ 'input_fields' => $yoast_free_input_fields, 'nonce' => wp_create_nonce( 'bulk-editor-table' ), ]; $wpseo_bulk_titles_table = new WPSEO_Bulk_Title_Editor_List_Table( $yoast_bulk_editor_arguments ); $wpseo_bulk_description_table = new WPSEO_Bulk_Description_List_Table( $yoast_bulk_editor_arguments ); $yoast_free_screen_reader_content = [ 'heading_views' => __( 'Filter posts list', 'wordpress-seo' ), 'heading_pagination' => __( 'Posts list navigation', 'wordpress-seo' ), 'heading_list' => __( 'Posts list', 'wordpress-seo' ), ]; get_current_screen()->set_screen_reader_content( $yoast_free_screen_reader_content ); if ( ! empty( $_REQUEST['_wp_http_referer'] ) && isset( $_SERVER['REQUEST_URI'] ) ) { $request_uri = sanitize_file_name( wp_unslash( $_SERVER['REQUEST_URI'] ) ); wp_redirect( remove_query_arg( [ '_wp_http_referer', '_wpnonce' ], $request_uri, ), ); exit(); } /** * Renders a bulk editor tab. * * @param WPSEO_Bulk_List_Table $table The table to render. * @param string $id The id for the tab. * * @return void */ function wpseo_get_rendered_tab( $table, $id ) { ?> <div id="<?php echo esc_attr( $id ); ?>" class="wpseotab"> <?php $table->show_page(); ?> </div> <?php } ?> <script> <?php /* phpcs:ignore WordPress.Security.EscapeOutput -- WPSEO_Utils::format_json_encode is safe. */ ?> var wpseoBulkEditorNonce = <?php echo WPSEO_Utils::format_json_encode( wp_create_nonce( 'wpseo-bulk-editor' ) ); ?>; // eslint-disable-next-line var wpseo_bulk_editor_nonce = wpseoBulkEditorNonce; </script> <br/><br/> <div class="wpseo_table_page"> <h2 class="nav-tab-wrapper" id="wpseo-tabs"> <a class="nav-tab" id="title-tab" href="#top#title"><?php esc_html_e( 'Title', 'wordpress-seo' ); ?></a> <a class="nav-tab" id="description-tab" href="#top#description"><?php esc_html_e( 'Description', 'wordpress-seo' ); ?></a> </h2> <div class="tabwrapper"> <?php wpseo_get_rendered_tab( $wpseo_bulk_titles_table, 'title' ); ?> <?php wpseo_get_rendered_tab( $wpseo_bulk_description_table, 'description' ); ?> </div> </div> admin/views/class-yoast-input-select.php 0000644 00000005645 15174712003 0014370 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class for generating a html select. */ class Yoast_Input_Select { /** * The id attribute value. * * @var string */ private $select_id; /** * The name attribute value. * * @var string */ private $select_name; /** * Additional select attributes. * * @var array */ private $select_attributes = []; /** * Array with the options to parse. * * @var array */ private $select_options; /** * The current selected option. * * @var string */ private $selected_option; /** * Constructor. * * @param string $select_id ID for the select. * @param string $select_name Name for the select. * @param array $select_options Array with the options to parse. * @param string $selected_option The current selected option. */ public function __construct( $select_id, $select_name, array $select_options, $selected_option ) { $this->select_id = $select_id; $this->select_name = $select_name; $this->select_options = $select_options; $this->selected_option = $selected_option; } /** * Print the rendered view. * * @return void */ public function output_html() { // Extract it, because we want each value accessible via a variable instead of accessing it as an array. extract( $this->get_select_values() ); require WPSEO_PATH . 'admin/views/form/select.php'; } /** * Return the rendered view. * * @return string */ public function get_html() { ob_start(); $this->output_html(); $rendered_output = ob_get_contents(); ob_end_clean(); return $rendered_output; } /** * Add an attribute to the attributes property. * * @param string $attribute The name of the attribute to add. * @param string $value The value of the attribute. * * @return void */ public function add_attribute( $attribute, $value ) { $this->select_attributes[ $attribute ] = $value; } /** * Return the set fields for the select. * * @return array */ private function get_select_values() { return [ 'id' => $this->select_id, 'name' => $this->select_name, 'attributes' => $this->get_attributes(), 'options' => $this->select_options, 'selected' => $this->selected_option, ]; } /** * Return the attribute string, when there are attributes set. * * @return string */ private function get_attributes() { $attributes = $this->select_attributes; if ( ! empty( $attributes ) ) { array_walk( $attributes, [ $this, 'parse_attribute' ] ); return implode( ' ', $attributes ) . ' '; } return ''; } /** * Get an attribute from the attributes. * * @param string $value The value of the attribute. * @param string $attribute The attribute to look for. * * @return void */ private function parse_attribute( &$value, $attribute ) { $value = sprintf( '%s="%s"', sanitize_key( $attribute ), esc_attr( $value ) ); } } admin/views/tool-import-export.php 0000644 00000010314 15174712003 0013305 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $yform = Yoast_Form::get_instance(); $yoast_seo_import = false; /** * The import method is used to determine if there should be something imported. * * In case of POST the user is on the Yoast SEO import page and in case of the GET the user sees a notice from * Yoast SEO that we can import stuff for that plugin. */ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable so no need to sanitize. if ( isset( $_POST['import_external'] ) && wp_unslash( $_POST['import_external'] ) === __( 'Import', 'wordpress-seo' ) ) { check_admin_referer( 'wpseo-import-plugins' ); if ( isset( $_POST['import_external_plugin'] ) && is_string( $_POST['import_external_plugin'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a class name. $yoast_seo_class = wp_unslash( $_POST['import_external_plugin'] ); if ( class_exists( $yoast_seo_class ) ) { $yoast_seo_import = new WPSEO_Import_Plugin( new $yoast_seo_class(), 'import' ); } } } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable so no need to sanitize. elseif ( isset( $_POST['clean_external'] ) && wp_unslash( $_POST['clean_external'] ) === __( 'Clean up', 'wordpress-seo' ) ) { check_admin_referer( 'wpseo-clean-plugins' ); if ( isset( $_POST['clean_external_plugin'] ) && is_string( $_POST['clean_external_plugin'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a class name. $yoast_seo_class = wp_unslash( $_POST['clean_external_plugin'] ); if ( class_exists( $yoast_seo_class ) ) { $yoast_seo_import = new WPSEO_Import_Plugin( new $yoast_seo_class(), 'cleanup' ); } } } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing to an empty string. elseif ( isset( $_POST['settings_import'] ) && wp_unslash( $_POST['settings_import'] ) !== '' ) { $yoast_seo_import = new WPSEO_Import_Settings(); $yoast_seo_import->import(); } /** * Allow custom import actions. * * @param WPSEO_Import_Status $yoast_seo_import Contains info about the handled import. */ $yoast_seo_import = apply_filters( 'wpseo_handle_import', $yoast_seo_import ); if ( $yoast_seo_import ) { $yoast_seo_message = ''; if ( $yoast_seo_import->status instanceof WPSEO_Import_Status ) { $yoast_seo_message = $yoast_seo_import->status->get_msg(); } /** * Allow customization of import/export message. * * @param string $yoast_seo_msg The message. */ $yoast_seo_msg = apply_filters( 'wpseo_import_message', $yoast_seo_message ); if ( ! empty( $yoast_seo_msg ) ) { $yoast_seo_status = 'error'; if ( $yoast_seo_import->status->status ) { $yoast_seo_status = 'updated'; } $yoast_seo_class = 'message ' . $yoast_seo_status; echo '<div id="message" class="', esc_attr( $yoast_seo_status ), '"><p>', esc_html( $yoast_seo_msg ), '</p></div>'; } } $yoast_seo_tabs = [ 'wpseo-import' => [ 'label' => __( 'Import settings', 'wordpress-seo' ), ], 'wpseo-export' => [ 'label' => __( 'Export settings', 'wordpress-seo' ), ], 'import-seo' => [ 'label' => __( 'Import from other SEO plugins', 'wordpress-seo' ), ], ]; ?> <br/><br/> <h2 class="nav-tab-wrapper" id="wpseo-tabs"> <?php foreach ( $yoast_seo_tabs as $identifier => $tab ) : ?> <a class="nav-tab" id="<?php echo esc_attr( $identifier . '-tab' ); ?>" href="<?php echo esc_url( '#top#' . $identifier ); ?>"><?php echo esc_html( $tab['label'] ); ?></a> <?php endforeach; ?> <?php /** * Allow adding a custom import tab header. */ do_action( 'wpseo_import_tab_header' ); ?> </h2> <?php foreach ( $yoast_seo_tabs as $identifier => $tab ) { printf( '<div id="%s" class="wpseotab">', esc_attr( $identifier ) ); require_once WPSEO_PATH . 'admin/views/tabs/tool/' . $identifier . '.php'; echo '</div>'; } /** * Allow adding a custom import tab. */ do_action( 'wpseo_import_tab_content' ); admin/views/partial-notifications-warnings.php 0000644 00000002102 15174712003 0015626 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * * @uses array $notifications_data */ $yoast_seo_type = 'warnings'; $yoast_seo_dashicon = 'flag'; $yoast_seo_active = $notifications_data['warnings']['active']; $yoast_seo_dismissed = $notifications_data['warnings']['dismissed']; $yoast_seo_active_total = count( $notifications_data['warnings']['active'] ); $yoast_seo_dismissed_total = count( $notifications_data['warnings']['dismissed'] ); $yoast_seo_total = $notifications_data['metrics']['warnings']; $yoast_seo_i18n_title = __( 'Notifications', 'wordpress-seo' ); $yoast_seo_i18n_issues = ''; $yoast_seo_i18n_no_issues = __( 'No new notifications.', 'wordpress-seo' ); $yoast_seo_i18n_muted_issues_title = sprintf( /* translators: %d expands the amount of hidden notifications. */ _n( 'You have %d hidden notification:', 'You have %d hidden notifications:', $yoast_seo_dismissed_total, 'wordpress-seo' ), $yoast_seo_dismissed_total, ); require WPSEO_PATH . 'admin/views/partial-notifications-template.php'; admin/views/partial-notifications-errors.php 0000644 00000002203 15174712003 0015314 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * * @uses array $notifications_data */ $yoast_seo_type = 'errors'; $yoast_seo_dashicon = 'warning'; $yoast_seo_active = $notifications_data['errors']['active']; $yoast_seo_dismissed = $notifications_data['errors']['dismissed']; $yoast_seo_active_total = count( $yoast_seo_active ); $yoast_seo_dismissed_total = count( $yoast_seo_dismissed ); $yoast_seo_total = $notifications_data['metrics']['errors']; $yoast_seo_i18n_title = __( 'Problems', 'wordpress-seo' ); $yoast_seo_i18n_issues = __( 'We have detected the following issues that affect the SEO of your site.', 'wordpress-seo' ); $yoast_seo_i18n_no_issues = __( 'Good job! We could detect no serious SEO problems.', 'wordpress-seo' ); $yoast_seo_i18n_muted_issues_title = sprintf( /* translators: %d expands the amount of hidden notifications. */ _n( 'You have %d hidden notification:', 'You have %d hidden notifications:', $yoast_seo_dismissed_total, 'wordpress-seo' ), $yoast_seo_dismissed_total, ); require WPSEO_PATH . 'admin/views/partial-notifications-template.php'; admin/views/class-yoast-feature-toggles.php 0000644 00000030337 15174712003 0015045 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Helpers\Language_Helper; use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter; /** * Class for managing feature toggles. */ class Yoast_Feature_Toggles { /** * Available feature toggles. * * @var array */ protected $toggles; /** * Instance holder. * * @var self|null */ protected static $instance = null; /** * Gets the main feature toggles manager instance used. * * This essentially works like a Singleton, but for its drawbacks does not restrict * instantiation otherwise. * * @return self Main instance. */ public static function instance() { self::$instance ??= new self(); return self::$instance; } /** * Gets all available feature toggles. * * @return array List of sorted Yoast_Feature_Toggle instances. */ public function get_all() { $this->toggles ??= $this->load_toggles(); return $this->toggles; } /** * Loads the available feature toggles. * * Also ensures that the toggles are all Yoast_Feature_Toggle instances and sorted by their order value. * * @return array List of sorted Yoast_Feature_Toggle instances. */ protected function load_toggles() { $xml_sitemap_extra = false; if ( WPSEO_Options::get( 'enable_xml_sitemap' ) ) { $xml_sitemap_extra = '<a href="' . esc_url( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ) . '" target="_blank">' . esc_html__( 'See the XML sitemap.', 'wordpress-seo' ) . '</a>'; } $feature_toggles = [ (object) [ 'name' => __( 'SEO analysis', 'wordpress-seo' ), 'setting' => 'keyword_analysis_active', 'label' => __( 'The SEO analysis offers suggestions to improve the SEO of your text.', 'wordpress-seo' ), 'read_more_label' => __( 'Learn how the SEO analysis can help you rank.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/2ak', 'order' => 10, ], (object) [ 'name' => __( 'Readability analysis', 'wordpress-seo' ), 'setting' => 'content_analysis_active', 'label' => __( 'The readability analysis offers suggestions to improve the structure and style of your text.', 'wordpress-seo' ), 'read_more_label' => __( 'Discover why readability is important for SEO.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/2ao', 'order' => 20, ], (object) [ 'name' => __( 'Inclusive language analysis', 'wordpress-seo' ), 'supported_languages' => Language_Helper::$languages_with_inclusive_language_support, 'setting' => 'inclusive_language_analysis_active', 'label' => __( 'The inclusive language analysis offers suggestions to write more inclusive copy.', 'wordpress-seo' ), 'read_more_label' => __( 'Discover why inclusive language is important for SEO.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/inclusive-language-features-free', 'order' => 25, ], (object) [ 'name' => __( 'Cornerstone content', 'wordpress-seo' ), 'setting' => 'enable_cornerstone_content', 'label' => __( 'The cornerstone content feature lets you to mark and filter cornerstone content on your website.', 'wordpress-seo' ), 'read_more_label' => __( 'Find out how cornerstone content can help you improve your site structure.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/dashboard-help-cornerstone', 'order' => 30, ], (object) [ 'name' => __( 'Text link counter', 'wordpress-seo' ), 'setting' => 'enable_text_link_counter', 'label' => __( 'The text link counter helps you improve your site structure.', 'wordpress-seo' ), 'read_more_label' => __( 'Find out how the text link counter can enhance your SEO.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/2aj', 'order' => 40, ], (object) [ 'name' => __( 'Insights', 'wordpress-seo' ), 'setting' => 'enable_metabox_insights', 'label' => __( 'Find relevant data about your content right in the Insights section in the Yoast SEO metabox. You’ll see what words you use most often and if they’re a match with your keywords! ', 'wordpress-seo' ), 'read_more_label' => __( 'Find out how Insights can help you improve your content.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/4ew', 'premium_url' => 'https://yoa.st/2ai', 'order' => 41, ], (object) [ 'name' => __( 'Link suggestions', 'wordpress-seo' ), 'premium' => true, 'setting' => 'enable_link_suggestions', 'label' => __( 'Get relevant internal linking suggestions — while you’re writing! The link suggestions metabox shows a list of posts on your blog with similar content that might be interesting to link to. ', 'wordpress-seo' ), 'read_more_label' => __( 'Read more about how internal linking can improve your site structure.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/4ev', 'premium_url' => 'https://yoa.st/17g', 'premium_upsell_url' => 'https://yoa.st/get-link-suggestions', 'order' => 42, ], (object) [ 'name' => __( 'XML sitemaps', 'wordpress-seo' ), 'setting' => 'enable_xml_sitemap', /* translators: %s: Yoast SEO */ 'label' => sprintf( __( 'Enable the XML sitemaps that %s generates.', 'wordpress-seo' ), 'Yoast SEO' ), 'read_more_label' => __( 'Read why XML Sitemaps are important for your site.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/2a-', 'extra' => $xml_sitemap_extra, 'after' => $this->sitemaps_toggle_after(), 'order' => 60, ], (object) [ 'name' => __( 'Admin bar menu', 'wordpress-seo' ), 'setting' => 'enable_admin_bar_menu', /* translators: 1: Yoast SEO */ 'label' => sprintf( __( 'The %1$s admin bar menu contains useful links to third-party tools for analyzing pages and makes it easy to see if you have new notifications.', 'wordpress-seo' ), 'Yoast SEO' ), 'order' => 80, ], (object) [ 'name' => __( 'Security: no advanced or schema settings for authors', 'wordpress-seo' ), 'setting' => 'disableadvanced_meta', 'label' => sprintf( /* translators: 1: Yoast SEO, 2: translated version of "Off" */ __( 'The advanced section of the %1$s meta box allows a user to remove posts from the search results or change the canonical. The settings in the schema tab allows a user to change schema meta data for a post. These are things you might not want any author to do. That\'s why, by default, only editors and administrators can do this. Setting to "%2$s" allows all users to change these settings.', 'wordpress-seo' ), 'Yoast SEO', __( 'Off', 'wordpress-seo' ), ), 'order' => 90, ], (object) [ 'name' => __( 'Usage tracking', 'wordpress-seo' ), 'label' => __( 'Usage tracking', 'wordpress-seo' ), 'setting' => 'tracking', 'read_more_label' => sprintf( /* translators: 1: Yoast SEO */ __( 'Allow us to track some data about your site to improve our plugin.', 'wordpress-seo' ), 'Yoast SEO', ), 'read_more_url' => 'https://yoa.st/usage-tracking-2', 'order' => 95, ], (object) [ 'name' => __( 'REST API: Head endpoint', 'wordpress-seo' ), 'setting' => 'enable_headless_rest_endpoints', 'label' => sprintf( /* translators: 1: Yoast SEO */ __( 'This %1$s REST API endpoint gives you all the metadata you need for a specific URL. This will make it very easy for headless WordPress sites to use %1$s for all their SEO meta output.', 'wordpress-seo' ), 'Yoast SEO', ), 'order' => 100, ], (object) [ 'name' => __( 'Enhanced Slack sharing', 'wordpress-seo' ), 'setting' => 'enable_enhanced_slack_sharing', 'label' => __( 'This adds an author byline and reading time estimate to the article’s snippet when shared on Slack.', 'wordpress-seo' ), 'read_more_label' => __( 'Find out how a rich snippet can improve visibility and click-through-rate.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/help-slack-share', 'order' => 105, ], (object) [ 'name' => __( 'IndexNow', 'wordpress-seo' ), 'premium' => true, 'setting' => 'enable_index_now', 'label' => __( 'Automatically ping search engines like Bing and Yandex whenever you publish, update or delete a post.', 'wordpress-seo' ), 'read_more_label' => __( 'Find out how IndexNow can help your site.', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/index-now-read-more', 'premium_url' => 'https://yoa.st/index-now-feature', 'premium_upsell_url' => 'https://yoa.st/get-indexnow', 'order' => 110, ], (object) [ 'name' => __( 'AI title & description generator', 'wordpress-seo' ), 'premium' => true, 'setting' => 'enable_ai_generator', 'label' => __( 'Use the power of Yoast AI to automatically generate compelling titles and descriptions for your posts and pages.', 'wordpress-seo' ), 'read_more_label' => __( 'Learn more', 'wordpress-seo' ), 'read_more_url' => 'https://yoa.st/ai-generator-read-more', 'premium_url' => 'https://yoa.st/ai-generator-feature', 'premium_upsell_url' => 'https://yoa.st/get-ai-generator', 'order' => 115, ], ]; /** * Filter to add feature toggles from add-ons. * * @param array $feature_toggles Array with feature toggle objects where each object * should have a `name`, `setting` and `label` property. */ $feature_toggles = apply_filters( 'wpseo_feature_toggles', $feature_toggles ); $feature_toggles = array_map( [ $this, 'ensure_toggle' ], $feature_toggles ); usort( $feature_toggles, [ $this, 'sort_toggles_callback' ] ); return $feature_toggles; } /** * Returns html for a warning that core sitemaps are enabled when yoast seo sitemaps are disabled. * * @return string HTML string for the warning. */ protected function sitemaps_toggle_after() { $out = '<div id="yoast-seo-sitemaps-disabled-warning" style="display:none;">'; $alert = new Alert_Presenter( /* translators: %1$s: expands to an opening anchor tag, %2$s: expands to a closing anchor tag */ sprintf( esc_html__( 'Disabling Yoast SEO\'s XML sitemaps will not disable WordPress\' core sitemaps. In some cases, this %1$s may result in SEO errors on your site%2$s. These may be reported in Google Search Console and other tools.', 'wordpress-seo' ), '<a target="_blank" href="' . WPSEO_Shortlinker::get( 'https://yoa.st/44z' ) . '">', '</a>' ), 'warning', ); $out .= $alert->present(); $out .= '</div>'; return $out; } /** * Ensures that the passed value is a Yoast_Feature_Toggle. * * @param Yoast_Feature_Toggle|object|array $toggle_data Feature toggle instance, or raw object or array * containing feature toggle data. * * @return Yoast_Feature_Toggle Feature toggle instance based on $toggle_data. */ protected function ensure_toggle( $toggle_data ) { if ( $toggle_data instanceof Yoast_Feature_Toggle ) { return $toggle_data; } if ( is_object( $toggle_data ) ) { $toggle_data = get_object_vars( $toggle_data ); } return new Yoast_Feature_Toggle( $toggle_data ); } /** * Callback for sorting feature toggles by their order. * * {@internal Once the minimum PHP version goes up to PHP 7.0, the logic in the function * can be replaced with the spaceship operator `<=>`.} * * @param Yoast_Feature_Toggle $feature_a Feature A. * @param Yoast_Feature_Toggle $feature_b Feature B. * * @return int An integer less than, equal to, or greater than zero indicating respectively * that feature A is considered to be less than, equal to, or greater than feature B. */ protected function sort_toggles_callback( Yoast_Feature_Toggle $feature_a, Yoast_Feature_Toggle $feature_b ) { return ( $feature_a->order - $feature_b->order ); } } admin/views/tabs/tool/wpseo-import.php 0000644 00000002674 15174712003 0014066 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } if ( ! defined( 'WPSEO_NAMESPACES' ) || ! WPSEO_NAMESPACES ) { esc_html_e( 'Import of settings is only supported on servers that run PHP 5.3 or higher.', 'wordpress-seo' ); return; } ?> <p id="settings-import-desc"> <?php printf( /* translators: 1: expands to Yoast SEO, 2: expands to Import settings. */ esc_html__( 'Import settings from another %1$s installation by pasting them here and clicking "%2$s".', 'wordpress-seo' ), 'Yoast SEO', esc_html__( 'Import settings', 'wordpress-seo' ), ); ?> </p> <form action="<?php echo esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=import-export#top#wpseo-import' ) ); ?>" method="post" accept-charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>"> <?php wp_nonce_field( WPSEO_Import_Settings::NONCE_ACTION ); ?> <label class="yoast-inline-label" for="settings-import"> <?php printf( /* translators: %s expands to Yoast SEO */ esc_html__( '%s settings to import:', 'wordpress-seo' ), 'Yoast SEO', ); ?> </label><br /> <textarea id="settings-import" rows="10" cols="140" name="settings_import" aria-describedby="settings-import-desc"></textarea><br/> <input type="submit" class="button button-primary" value="<?php esc_attr_e( 'Import settings', 'wordpress-seo' ); ?>"/> </form> admin/views/tabs/tool/wpseo-export.php 0000644 00000002573 15174712003 0014073 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /* translators: %1$s expands to Yoast SEO */ $submit_button_value = sprintf( __( 'Export your %1$s settings', 'wordpress-seo' ), 'Yoast SEO' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: The nonce will be verified in WPSEO_Export below, We are only strictly comparing with '1'. if ( isset( $_POST['do_export'] ) && wp_unslash( $_POST['do_export'] ) === '1' ) { $export = new WPSEO_Export(); $export->export(); return; } $wpseo_export_phrase = sprintf( /* translators: %1$s expands to Yoast SEO */ __( 'Export your %1$s settings here, to copy them on another site.', 'wordpress-seo' ), 'Yoast SEO', ); ?> <p><?php echo esc_html( $wpseo_export_phrase ); ?></p> <form action="<?php echo esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=import-export#top#wpseo-export' ) ); ?>" method="post" accept-charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>"> <?php wp_nonce_field( WPSEO_Export::NONCE_ACTION ); ?> <input type="hidden" name="do_export" value="1" /> <button type="submit" class="button button-primary" id="export-button"><?php echo esc_html( $submit_button_value ); ?></button> </form> admin/views/tabs/tool/import-seo.php 0000644 00000011150 15174712003 0013504 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } // Determine if we have plugins we can import from. If so, load that tab. Otherwise, load an empty tab. $import_check = new WPSEO_Import_Plugins_Detector(); $import_check->detect(); if ( count( $import_check->needs_import ) === 0 ) { echo '<h2>', esc_html__( 'Import from other SEO plugins', 'wordpress-seo' ), '</h2>'; echo '<p>'; printf( /* translators: %s expands to Yoast SEO */ esc_html__( '%s did not detect any plugin data from plugins it can import from.', 'wordpress-seo' ), 'Yoast SEO', ); echo '</p>'; return; } /** * Creates a select box given a name and plugins array. * * @param string $name Name field for the select field. * @param array $plugins An array of plugins and classes. * * @return void */ function wpseo_import_external_select( $name, $plugins ) { esc_html_e( 'Plugin: ', 'wordpress-seo' ); echo '<select name="', esc_attr( $name ), '">'; foreach ( $plugins as $class => $plugin ) { /* translators: %s is replaced with the name of the plugin we're importing from. */ echo '<option value="' . esc_attr( $class ) . '">' . esc_html( $plugin ) . '</option>'; } echo '</select>'; } ?> <h2><?php esc_html_e( 'Import from other SEO plugins', 'wordpress-seo' ); ?></h2> <p> <?php esc_html_e( 'We\'ve detected data from one or more SEO plugins on your site. Please follow the following steps to import that data:', 'wordpress-seo' ); ?> </p> <div class="tab-block"> <h3><?php esc_html_e( 'Step 1: Create a backup', 'wordpress-seo' ); ?></h3> <p> <?php esc_html_e( 'Please make a backup of your database before starting this process.', 'wordpress-seo' ); ?> </p> </div> <div class="tab-block"> <h3><?php esc_html_e( 'Step 2: Import', 'wordpress-seo' ); ?></h3> <p class="yoast-import-explanation"> <?php printf( /* translators: 1: expands to Yoast SEO */ esc_html__( 'This will import the post metadata like SEO titles and descriptions into your %1$s metadata. It will only do this when there is no existing %1$s metadata yet. The original data will remain in place.', 'wordpress-seo' ), 'Yoast SEO', ); ?> </p> <form action="<?php echo esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=import-export#top#import-seo' ) ); ?>" method="post" accept-charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>"> <?php wp_nonce_field( 'wpseo-import-plugins', '_wpnonce', true, true ); wpseo_import_external_select( 'import_external_plugin', $import_check->needs_import ); ?> <?php /** * WARNING: This hook is intended for internal use only. * Don't use it in your code as it will be removed shortly. */ do_action( 'wpseo_import_other_plugins_internal' ); ?> <input type="submit" class="button button-primary" name="import_external" value="<?php esc_attr_e( 'Import', 'wordpress-seo' ); ?>" /> </form> </div> <div class="tab-block"> <h3><?php esc_html_e( 'Step 3: Check your data', 'wordpress-seo' ); ?></h3> <p> <?php esc_html_e( 'Please check your posts and pages and see if the metadata was successfully imported.', 'wordpress-seo' ); ?> </p> </div> <div class="tab-block"> <h3><?php esc_html_e( 'Step 4: Go through the first time configuration', 'wordpress-seo' ); ?></h3> <p> <?php $ftc_page = 'admin.php?page=wpseo_dashboard#/first-time-configuration'; printf( /* translators: 1: Link start tag to the First time configuration tab in the General page, 2: Link closing tag. */ esc_html__( 'You should finish the %1$sfirst time configuration%2$s to make sure your SEO data has been optimized and you’ve set the essential Yoast SEO settings for your site.', 'wordpress-seo' ), '<a href="' . esc_url( admin_url( $ftc_page ) ) . '">', '</a>', ); ?> </p> </div> <div class="tab-block"> <h3><?php esc_html_e( 'Step 5: Clean up', 'wordpress-seo' ); ?></h3> <p class="yoast-cleanup-explanation"> <?php esc_html_e( 'Once you\'re certain your site is OK, you can clean up. This will remove all the original data.', 'wordpress-seo' ); ?> </p> <form action="<?php echo esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=import-export#top#import-seo' ) ); ?>" method="post" accept-charset="<?php echo esc_attr( get_bloginfo( 'charset' ) ); ?>"> <?php wp_nonce_field( 'wpseo-clean-plugins', '_wpnonce', true, true ); wpseo_import_external_select( 'clean_external_plugin', $import_check->needs_import ); ?> <input type="submit" class="button button-primary" name="clean_external" value="<?php esc_attr_e( 'Clean', 'wordpress-seo' ); ?>"/> </form> </div> admin/views/tabs/dashboard/site-analysis.php 0000644 00000000653 15174712003 0015153 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * WARNING: This hook is intended for internal use only. * Don't use it in your code as it will be removed shortly. */ do_action( 'wpseo_settings_tab_site_analysis_internal', $yform ); admin/views/tabs/dashboard/first-time-configuration.php 0000644 00000000371 15174712003 0017313 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } echo '<div id="wpseo-first-time-configuration"></div>'; admin/views/tabs/dashboard/dashboard.php 0000644 00000002050 15174712003 0014306 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Notifications template variables. * * @noinspection PhpUnusedLocalVariableInspection * * @var array $notifications_data */ $notifications_data = Yoast_Notifications::get_template_variables(); $wpseo_contributors_phrase = sprintf( /* translators: %1$s expands to Yoast SEO */ __( 'See who contributed to %1$s.', 'wordpress-seo' ), 'Yoast SEO', ); ?> <div class="tab-block"> <div class="yoast-notifications"> <div class="yoast-container yoast-container__error"> <?php require WPSEO_PATH . 'admin/views/partial-notifications-errors.php'; ?> </div> <div class="yoast-container yoast-container__warning"> <?php require WPSEO_PATH . 'admin/views/partial-notifications-warnings.php'; ?> </div> </div> </div> <div class="tab-block"> <h2><?php esc_html_e( 'Credits', 'wordpress-seo' ); ?></h2> <p> <a href="<?php WPSEO_Shortlinker::show( 'https://yoa.st/yoast-seo-credits' ); ?>" target="_blank"><?php echo esc_html( $wpseo_contributors_phrase ); ?></a> </p> </div> admin/views/tabs/network/general.php 0000644 00000004031 15174712003 0013537 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } echo '<div class="tab-block">'; /* * {@internal Important: Make sure the options added to the array here are in line with the * options set in the WPSEO_Option_MS::$allowed_access_options property.}} */ $yform->select( 'access', /* translators: %1$s expands to Yoast SEO */ sprintf( __( 'Who should have access to the %1$s settings', 'wordpress-seo' ), 'Yoast SEO' ), [ 'admin' => __( 'Site Admins (default)', 'wordpress-seo' ), 'superadmin' => __( 'Super Admins only', 'wordpress-seo' ), ], ); if ( get_blog_count() <= 100 ) { $network_admin = new Yoast_Network_Admin(); $yform->select( 'defaultblog', __( 'New sites in the network inherit their SEO settings from this site', 'wordpress-seo' ), $network_admin->get_site_choices( true, true ), ); echo '<p>' . esc_html__( 'Choose the site whose settings you want to use as default for all sites that are added to your network. If you choose \'None\', the normal plugin defaults will be used.', 'wordpress-seo' ) . '</p>'; } else { $yform->textinput( 'defaultblog', __( 'New sites in the network inherit their SEO settings from this site', 'wordpress-seo' ) ); echo '<p>'; printf( /* translators: 1: link open tag; 2: link close tag. */ esc_html__( 'Enter the %1$sSite ID%2$s for the site whose settings you want to use as default for all sites that are added to your network. Leave empty for none (i.e. the normal plugin defaults will be used).', 'wordpress-seo' ), '<a href="' . esc_url( network_admin_url( 'sites.php' ) ) . '">', '</a>', ); echo '</p>'; } echo '<p><strong>' . esc_html__( 'Take note:', 'wordpress-seo' ) . '</strong> ' . esc_html__( 'Privacy sensitive (FB admins and such), theme specific (title rewrite) and a few very site specific settings will not be imported to new sites.', 'wordpress-seo' ) . '</p>'; echo '</div>'; admin/views/tabs/network/crawl-settings.php 0000644 00000003211 15174712003 0015067 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $feature_toggles = Yoast_Feature_Toggles::instance()->get_all(); ?> <h2><?php esc_html_e( 'Crawl settings', 'wordpress-seo' ); ?></h2> <div class="yoast-measure"> <?php printf( /* translators: %s expands to Yoast SEO */ esc_html__( 'This tab allows you to selectively disable %s features for all sites in the network. By default all features are enabled, which allows site admins to choose for themselves if they want to toggle a feature on or off for their site. When you disable a feature here, site admins will not be able to use that feature at all.', 'wordpress-seo' ), 'Yoast SEO', ); echo '<p style="margin: 0.5em 0 1em;">'; printf( /* translators: %1$s opens the link to the Yoast.com article about Crawl settings, %2$s closes the link, */ esc_html__( '%1$sLearn more about crawl settings.%2$s', 'wordpress-seo' ), '<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/crawl-settings' ) ) . '" target="_blank" rel="noopener noreferrer">', '</a>', ); echo '</p>'; /** * Fires when displaying the crawl cleanup network tab. * * @param Yoast_Form $yform The yoast form object. */ do_action( 'wpseo_settings_tab_crawl_cleanup_network', $yform ); ?> </div> <?php /* * Required to prevent our settings framework from saving the default because the field * isn't explicitly set when saving the Dashboard page. */ $yform->hidden( 'show_onboarding_notice', 'wpseo_show_onboarding_notice' ); admin/views/tabs/network/features.php 0000644 00000007304 15174712003 0013746 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ use Yoast\WP\SEO\Presenters\Admin\Beta_Badge_Presenter; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $feature_toggles = Yoast_Feature_Toggles::instance()->get_all(); ?> <h2><?php esc_html_e( 'Features', 'wordpress-seo' ); ?></h2> <div class="yoast-measure"> <?php printf( /* translators: %s expands to Yoast SEO */ esc_html__( 'This tab allows you to selectively disable %s features for all sites in the network. By default all features are enabled, which allows site admins to choose for themselves if they want to toggle a feature on or off for their site. When you disable a feature here, site admins will not be able to use that feature at all.', 'wordpress-seo' ), 'Yoast SEO', ); foreach ( $feature_toggles as $feature ) { $is_premium = YoastSEO()->helpers->product->is_premium(); $premium_version = YoastSEO()->helpers->product->get_premium_version(); if ( $feature->premium && $feature->premium_version ) { $not_supported_in_current_premium_version = $is_premium && version_compare( $premium_version, $feature->premium_version, '<' ); if ( $not_supported_in_current_premium_version ) { continue; } } $help_text = esc_html( $feature->label ); if ( ! empty( $feature->extra ) ) { $help_text .= ' ' . $feature->extra; } if ( ! empty( $feature->read_more_label ) ) { $url = $feature->read_more_url; if ( ! empty( $feature->premium ) && $feature->premium === true ) { $url = $feature->premium_url; } $help_text .= sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( WPSEO_Shortlinker::get( $url ) ), esc_html( $feature->read_more_label ), ); } $feature_help = new WPSEO_Admin_Help_Panel( WPSEO_Option::ALLOW_KEY_PREFIX . $feature->setting, /* translators: Hidden accessibility text; %s expands to a feature's name. */ sprintf( esc_html__( 'Help on: %s', 'wordpress-seo' ), esc_html( $feature->name ) ), $help_text, ); $name = $feature->name; if ( ! empty( $feature->premium ) && $feature->premium === true ) { $name .= ' ' . new Premium_Badge_Presenter( $feature->name ); } if ( ! empty( $feature->in_beta ) && $feature->in_beta === true ) { $name .= ' ' . new Beta_Badge_Presenter( $feature->name ); } $disabled = false; $show_premium_upsell = false; $premium_upsell_url = ''; $note_when_disabled = ''; if ( $feature->premium === true && YoastSEO()->helpers->product->is_premium() === false ) { $disabled = true; $show_premium_upsell = true; $premium_upsell_url = WPSEO_Shortlinker::get( $feature->premium_upsell_url ); } $preserve_disabled_value = false; if ( $disabled ) { $preserve_disabled_value = true; } $yform->toggle_switch( WPSEO_Option::ALLOW_KEY_PREFIX . $feature->setting, [ 'on' => __( 'Allow Control', 'wordpress-seo' ), 'off' => __( 'Disable', 'wordpress-seo' ), ], $name, $feature_help->get_button_html() . $feature_help->get_panel_html(), [ 'disabled' => $disabled, 'preserve_disabled_value' => $preserve_disabled_value, 'show_premium_upsell' => $show_premium_upsell, 'premium_upsell_url' => $premium_upsell_url, 'note_when_disabled' => $note_when_disabled, ], ); } ?> </div> <?php /* * Required to prevent our settings framework from saving the default because the field * isn't explicitly set when saving the Dashboard page. */ $yform->hidden( 'show_onboarding_notice', 'wpseo_show_onboarding_notice' ); admin/views/tabs/network/restore-site.php 0000644 00000001640 15174712003 0014552 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } echo '<p>' . esc_html__( 'Using this form you can reset a site to the default SEO settings.', 'wordpress-seo' ) . '</p>'; if ( get_blog_count() <= 100 ) { $network_admin = new Yoast_Network_Admin(); $yform->select( 'site_id', __( 'Site ID', 'wordpress-seo' ), $network_admin->get_site_choices( false, true ), ); } else { $yform->textinput( 'site_id', __( 'Site ID', 'wordpress-seo' ) ); } wp_nonce_field( 'wpseo-network-restore', 'restore_site_nonce', false ); echo '<button type="submit" name="action" value="' . esc_attr( Yoast_Network_Admin::RESTORE_SITE_ACTION ) . '" class="button button-primary">' . esc_html__( 'Restore site to defaults', 'wordpress-seo' ) . '</button>'; admin/views/tabs/network/integrations.php 0000644 00000006625 15174712003 0014643 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Views * * @uses Yoast_Form $yform Form object. */ use Yoast\WP\SEO\Presenters\Admin\Badge_Presenter; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $integration_toggles = Yoast_Integration_Toggles::instance()->get_all(); ?> <h2><?php esc_html_e( 'Integrations', 'wordpress-seo' ); ?></h2> <div class="yoast-measure"> <?php printf( /* translators: %1$s expands to Yoast SEO */ esc_html__( 'This tab allows you to selectively disable %1$s integrations with third-party products for all sites in the network. By default all integrations are enabled, which allows site admins to choose for themselves if they want to toggle an integration on or off for their site. When you disable an integration here, site admins will not be able to use that integration at all.', 'wordpress-seo' ), 'Yoast SEO', ); foreach ( $integration_toggles as $integration ) { $help_text = esc_html( $integration->label ); if ( ! empty( $integration->extra ) ) { $help_text .= ' ' . $integration->extra; } if ( ! empty( $integration->read_more_label ) ) { $help_text .= ' '; $help_text .= sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( WPSEO_Shortlinker::get( $integration->read_more_url ) ), esc_html( $integration->read_more_label ), ); } $feature_help = new WPSEO_Admin_Help_Panel( WPSEO_Option::ALLOW_KEY_PREFIX . $integration->setting, /* translators: Hidden accessibility text; %s expands to an integration's name. */ sprintf( esc_html__( 'Help on: %s', 'wordpress-seo' ), esc_html( $integration->name ) ), $help_text, ); $name = $integration->name; if ( ! empty( $integration->premium ) && $integration->premium === true ) { $name .= ' ' . new Premium_Badge_Presenter( $integration->name ); } if ( ! empty( $integration->new ) && $integration->new === true ) { $name .= ' ' . new Badge_Presenter( $integration->name ); } $disabled = $integration->disabled; $show_premium_upsell = false; $premium_upsell_url = ''; if ( $integration->premium === true && YoastSEO()->helpers->product->is_premium() === false ) { $disabled = true; $show_premium_upsell = true; $premium_upsell_url = WPSEO_Shortlinker::get( $integration->premium_upsell_url ); } $preserve_disabled_value = false; if ( $disabled ) { $preserve_disabled_value = true; } $yform->toggle_switch( WPSEO_Option::ALLOW_KEY_PREFIX . $integration->setting, [ 'on' => __( 'Allow Control', 'wordpress-seo' ), 'off' => __( 'Disable', 'wordpress-seo' ), ], $name, $feature_help->get_button_html() . $feature_help->get_panel_html(), [ 'disabled' => $disabled, 'preserve_disabled_value' => $preserve_disabled_value, 'show_premium_upsell' => $show_premium_upsell, 'premium_upsell_url' => $premium_upsell_url, ], ); do_action( 'Yoast\WP\SEO\admin_network_integration_after', $integration ); } ?> </div> <?php /* * Required to prevent our settings framework from saving the default because the field isn't * explicitly set when saving the Dashboard page. */ $yform->hidden( 'show_onboarding_notice', 'wpseo_show_onboarding_notice' ); admin/interface-installable.php 0000644 00000000376 15174712003 0012603 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents the interface for an installable object. */ interface WPSEO_Installable { /** * Runs the installation routine. * * @return void */ public function install(); } admin/class-admin-utils.php 0000644 00000004231 15174712003 0011676 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents the utils for the admin. */ class WPSEO_Admin_Utils { /** * Gets the install URL for the passed plugin slug. * * @param string $slug The slug to create an install link for. * * @return string The install URL. Empty string if the current user doesn't have the proper capabilities. */ public static function get_install_url( $slug ) { if ( ! current_user_can( 'install_plugins' ) ) { return ''; } return wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . dirname( $slug ) ), 'install-plugin_' . dirname( $slug ), ); } /** * Gets the activation URL for the passed plugin slug. * * @param string $slug The slug to create an activation link for. * * @return string The activation URL. Empty string if the current user doesn't have the proper capabilities. */ public static function get_activation_url( $slug ) { if ( ! current_user_can( 'activate_plugins' ) ) { return ''; } return wp_nonce_url( self_admin_url( 'plugins.php?action=activate&plugin_status=all&paged=1&s&plugin=' . $slug ), 'activate-plugin_' . $slug, ); } /** * Creates a link if the passed plugin is deemend a directly-installable plugin. * * @param array $plugin The plugin to create the link for. * * @return string The link to the plugin install. Returns the title if the plugin is deemed a Premium product. */ public static function get_install_link( $plugin ) { $install_url = self::get_install_url( $plugin['slug'] ); if ( $install_url === '' || ( isset( $plugin['premium'] ) && $plugin['premium'] === true ) ) { return $plugin['title']; } return sprintf( '<a href="%s">%s</a>', $install_url, $plugin['title'], ); } /** * Gets a visually hidden accessible message for links that open in a new browser tab. * * @return string The visually hidden accessible message. */ public static function get_new_tab_message() { return sprintf( '<span class="screen-reader-text">%s</span>', /* translators: Hidden accessibility text. */ esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ), ); } } admin/class-gutenberg-compatibility.php 0000644 00000004741 15174712003 0014307 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Gutenberg_Compatibility */ /** * Class WPSEO_Gutenberg_Compatibility */ class WPSEO_Gutenberg_Compatibility { /** * The currently released version of Gutenberg. * * @var string */ public const CURRENT_RELEASE = '22.6.0'; /** * The minimally supported version of Gutenberg by the plugin. * * @var string */ public const MINIMUM_SUPPORTED = '22.6.0'; /** * Holds the current version. * * @var string */ protected $current_version = ''; /** * WPSEO_Gutenberg_Compatibility constructor. */ public function __construct() { $this->current_version = $this->detect_installed_gutenberg_version(); } /** * Determines whether or not Gutenberg is installed. * * @return bool Whether or not Gutenberg is installed. */ public function is_installed() { return $this->current_version !== ''; } /** * Determines whether or not the currently installed version of Gutenberg is below the minimum supported version. * * @return bool True if the currently installed version is below the minimum supported version. False otherwise. */ public function is_below_minimum() { return version_compare( $this->current_version, $this->get_minimum_supported_version(), '<' ); } /** * Gets the currently installed version. * * @return string The currently installed version. */ public function get_installed_version() { return $this->current_version; } /** * Determines whether or not the currently installed version of Gutenberg is the latest, fully compatible version. * * @return bool Whether or not the currently installed version is fully compatible. */ public function is_fully_compatible() { return version_compare( $this->current_version, $this->get_latest_release(), '>=' ); } /** * Gets the latest released version of Gutenberg. * * @return string The latest release. */ protected function get_latest_release() { return self::CURRENT_RELEASE; } /** * Gets the minimum supported version of Gutenberg. * * @return string The minumum supported release. */ protected function get_minimum_supported_version() { return self::MINIMUM_SUPPORTED; } /** * Detects the currently installed Gutenberg version. * * @return string The currently installed Gutenberg version. Empty if the version couldn't be detected. */ protected function detect_installed_gutenberg_version() { if ( defined( 'GUTENBERG_VERSION' ) ) { return GUTENBERG_VERSION; } return ''; } } admin/class-collector.php 0000644 00000001755 15174712003 0011446 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Collects the data from the added collection objects. */ class WPSEO_Collector { /** * Holds the collections. * * @var WPSEO_Collection[] */ protected $collections = []; /** * Adds a collection object to the collections. * * @param WPSEO_Collection $collection The collection object to add. * * @return void */ public function add_collection( WPSEO_Collection $collection ) { $this->collections[] = $collection; } /** * Collects the data from the collection objects. * * @return array The collected data. */ public function collect() { $data = []; foreach ( $this->collections as $collection ) { $data = array_merge( $data, $collection->get() ); } return $data; } /** * Returns the collected data as a JSON encoded string. * * @return string|false The encode string. */ public function get_as_json() { return WPSEO_Utils::format_json_encode( $this->collect() ); } } admin/capabilities/class-capability-manager-wp.php 0000644 00000002331 15174712003 0016255 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Default WordPress capability manager implementation. */ final class WPSEO_Capability_Manager_WP extends WPSEO_Abstract_Capability_Manager { /** * Adds the capabilities to the roles. * * @return void */ public function add() { foreach ( $this->capabilities as $capability => $roles ) { $filtered_roles = $this->filter_roles( $capability, $roles ); $wp_roles = $this->get_wp_roles( $filtered_roles ); foreach ( $wp_roles as $wp_role ) { $wp_role->add_cap( $capability ); } } } /** * Unregisters the capabilities from the system. * * @return void */ public function remove() { // Remove from any roles it has been added to. $roles = wp_roles()->get_names(); $roles = array_keys( $roles ); foreach ( $this->capabilities as $capability => $_roles ) { $registered_roles = array_unique( array_merge( $roles, $this->capabilities[ $capability ] ) ); // Allow filtering of roles. $filtered_roles = $this->filter_roles( $capability, $registered_roles ); $wp_roles = $this->get_wp_roles( $filtered_roles ); foreach ( $wp_roles as $wp_role ) { $wp_role->remove_cap( $capability ); } } } } admin/capabilities/class-capability-utils.php 0000644 00000004536 15174712003 0015370 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Capability Utils collection. */ class WPSEO_Capability_Utils { /** * Checks if the user has the proper capabilities. * * @param string $capability Capability to check. * * @return bool True if the user has the proper rights. */ public static function current_user_can( $capability ) { if ( $capability === 'wpseo_manage_options' ) { return self::has( $capability ); } return self::has_any( [ 'wpseo_manage_options', $capability ] ); } /** * Retrieves the users that have the specified capability. * * @param string $capability The name of the capability. * * @return array The users that have the capability. */ public static function get_applicable_users( $capability ) { $applicable_roles = self::get_applicable_roles( $capability ); if ( $applicable_roles === [] ) { return []; } return get_users( [ 'role__in' => $applicable_roles ] ); } /** * Retrieves the roles that have the specified capability. * * @param string $capability The name of the capability. * * @return array The names of the roles that have the capability. */ public static function get_applicable_roles( $capability ) { $roles = wp_roles(); $role_names = $roles->get_names(); $applicable_roles = []; foreach ( array_keys( $role_names ) as $role_name ) { $role = $roles->get_role( $role_name ); if ( ! $role ) { continue; } // Add role if it has the capability. if ( array_key_exists( $capability, $role->capabilities ) && $role->capabilities[ $capability ] === true ) { $applicable_roles[] = $role_name; } } return $applicable_roles; } /** * Checks if the current user has at least one of the supplied capabilities. * * @param array $capabilities Capabilities to check against. * * @return bool True if the user has at least one capability. */ protected static function has_any( array $capabilities ) { foreach ( $capabilities as $capability ) { if ( self::has( $capability ) ) { return true; } } return false; } /** * Checks if the user has a certain capability. * * @param string $capability Capability to check against. * * @return bool True if the user has the capability. */ protected static function has( $capability ) { return current_user_can( $capability ); } } admin/capabilities/class-capability-manager-integration.php 0000644 00000006053 15174712003 0020157 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Integrates Yoast SEO capabilities with third party role manager plugins. * * Integrates with: Members * Integrates with: User Role Editor */ class WPSEO_Capability_Manager_Integration implements WPSEO_WordPress_Integration { /** * Capability manager to use. * * @var WPSEO_Capability_Manager */ public $manager; /** * WPSEO_Capability_Manager_Integration constructor. * * @param WPSEO_Capability_Manager $manager The capability manager to use. */ public function __construct( WPSEO_Capability_Manager $manager ) { $this->manager = $manager; } /** * Registers the hooks. * * @return void */ public function register_hooks() { add_filter( 'members_get_capabilities', [ $this, 'get_capabilities' ] ); add_action( 'members_register_cap_groups', [ $this, 'action_members_register_cap_group' ] ); add_filter( 'ure_capabilities_groups_tree', [ $this, 'filter_ure_capabilities_groups_tree' ] ); add_filter( 'ure_custom_capability_groups', [ $this, 'filter_ure_custom_capability_groups' ], 10, 2 ); } /** * Get the Yoast SEO capabilities. * Optionally append them to an existing array. * * @param array $caps Optional existing capability list. * @return array */ public function get_capabilities( array $caps = [] ) { if ( ! did_action( 'wpseo_register_capabilities' ) ) { do_action( 'wpseo_register_capabilities' ); } return array_merge( $caps, $this->manager->get_capabilities() ); } /** * Add capabilities to its own group in the Members plugin. * * @see members_register_cap_group() * * @return void */ public function action_members_register_cap_group() { if ( ! function_exists( 'members_register_cap_group' ) ) { return; } // Register the yoast group. $args = [ 'label' => esc_html__( 'Yoast SEO', 'wordpress-seo' ), 'caps' => $this->get_capabilities(), 'icon' => 'dashicons-admin-plugins', 'diff_added' => true, ]; members_register_cap_group( 'wordpress-seo', $args ); } /** * Adds Yoast SEO capability group in the User Role Editor plugin. * * @see URE_Capabilities_Groups_Manager::get_groups_tree() * * @param array $groups Current groups. * * @return array Filtered list of capabilty groups. */ public function filter_ure_capabilities_groups_tree( $groups = [] ) { $groups = (array) $groups; $groups['wordpress-seo'] = [ 'caption' => 'Yoast SEO', 'parent' => 'custom', 'level' => 3, ]; return $groups; } /** * Adds capabilities to the Yoast SEO group in the User Role Editor plugin. * * @see URE_Capabilities_Groups_Manager::get_cap_groups() * * @param array $groups Current capability groups. * @param string $cap_id Capability identifier. * * @return array List of filtered groups. */ public function filter_ure_custom_capability_groups( $groups = [], $cap_id = '' ) { if ( in_array( $cap_id, $this->get_capabilities(), true ) ) { $groups = (array) $groups; $groups[] = 'wordpress-seo'; } return $groups; } } admin/capabilities/class-capability-manager-factory.php 0000644 00000001373 15174712003 0017303 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Capability Manager Factory. */ class WPSEO_Capability_Manager_Factory { /** * Returns the Manager to use. * * @param string $plugin_type Whether it's Free or Premium. * * @return WPSEO_Capability_Manager Manager to use. */ public static function get( $plugin_type = 'free' ) { static $manager = []; if ( ! array_key_exists( $plugin_type, $manager ) ) { if ( function_exists( 'wpcom_vip_add_role_caps' ) ) { $manager[ $plugin_type ] = new WPSEO_Capability_Manager_VIP(); } if ( ! function_exists( 'wpcom_vip_add_role_caps' ) ) { $manager[ $plugin_type ] = new WPSEO_Capability_Manager_WP(); } } return $manager[ $plugin_type ]; } } admin/capabilities/class-capability-manager.php 0000644 00000001554 15174712003 0015637 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Capability Manager interface. */ interface WPSEO_Capability_Manager { /** * Registers a capability. * * @param string $capability Capability to register. * @param array $roles Roles to add the capability to. * @param bool $overwrite Optional. Use add or overwrite as registration method. * * @return void */ public function register( $capability, array $roles, $overwrite = false ); /** * Adds the registerd capabilities to the system. * * @return void */ public function add(); /** * Removes the registered capabilities from the system. * * @return void */ public function remove(); /** * Returns the list of registered capabilities. * * @return string[] List of registered capabilities. */ public function get_capabilities(); } admin/capabilities/class-register-capabilities.php 0000644 00000006344 15174712003 0016363 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Capabilities registration class. */ class WPSEO_Register_Capabilities implements WPSEO_WordPress_Integration { /** * Registers the hooks. * * @return void */ public function register_hooks() { add_action( 'wpseo_register_capabilities', [ $this, 'register' ] ); if ( is_multisite() ) { add_action( 'user_has_cap', [ $this, 'filter_user_has_wpseo_manage_options_cap' ], 10, 4 ); } /** * Maybe add manage_privacy_options capability for wpseo_manager user role. */ add_filter( 'map_meta_cap', [ $this, 'map_meta_cap_for_seo_manager' ], 10, 2 ); } /** * Registers the capabilities. * * @return void */ public function register() { $manager = WPSEO_Capability_Manager_Factory::get(); $manager->register( 'wpseo_bulk_edit', [ 'editor', 'wpseo_editor', 'wpseo_manager' ] ); $manager->register( 'wpseo_edit_advanced_metadata', [ 'editor', 'wpseo_editor', 'wpseo_manager' ] ); $manager->register( 'wpseo_manage_options', [ 'administrator', 'wpseo_manager' ] ); $manager->register( 'view_site_health_checks', [ 'wpseo_manager' ] ); } /** * Revokes the 'wpseo_manage_options' capability from administrator users if it should * only be granted to network administrators. * * @param array $allcaps An array of all the user's capabilities. * @param array $caps Actual capabilities being checked. * @param array $args Optional parameters passed to has_cap(), typically object ID. * @param WP_User $user The user object. * * @return array Possibly modified array of the user's capabilities. */ public function filter_user_has_wpseo_manage_options_cap( $allcaps, $caps, $args, $user ) { // We only need to do something if 'wpseo_manage_options' is being checked. if ( ! in_array( 'wpseo_manage_options', $caps, true ) ) { return $allcaps; } // If the user does not have 'wpseo_manage_options' anyway, we don't need to revoke access. if ( empty( $allcaps['wpseo_manage_options'] ) ) { return $allcaps; } // If the user does not have 'delete_users', they are not an administrator. if ( empty( $allcaps['delete_users'] ) ) { return $allcaps; } $options = WPSEO_Options::get_instance(); if ( $options->get( 'access' ) === 'superadmin' && ! is_super_admin( $user->ID ) ) { unset( $allcaps['wpseo_manage_options'] ); } return $allcaps; } /** * Maybe add manage_privacy_options capability for wpseo_manager user role. * * @param string[] $caps Primitive capabilities required of the user. * @param string[] $cap Capability being checked. * * @return string[] Filtered primitive capabilities required of the user. */ public function map_meta_cap_for_seo_manager( $caps, $cap ) { $user = wp_get_current_user(); // No multisite support. if ( is_multisite() ) { return $caps; } if ( ! is_array( $user->roles ) ) { return $caps; } // User must be of role wpseo_manager. if ( ! in_array( 'wpseo_manager', $user->roles, true ) ) { return $caps; } // Remove manage_options cap requirement if requested cap is manage_privacy_options. if ( $cap === 'manage_privacy_options' ) { return array_diff( $caps, [ 'manage_options' ] ); } return $caps; } } admin/capabilities/class-capability-manager-vip.php 0000644 00000003640 15174712003 0016431 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * VIP implementation of the Capability Manager. */ final class WPSEO_Capability_Manager_VIP extends WPSEO_Abstract_Capability_Manager { /** * Adds the registered capabilities to the system. * * @return void */ public function add() { $role_capabilities = []; foreach ( $this->capabilities as $capability => $roles ) { $role_capabilities = $this->get_role_capabilities( $role_capabilities, $capability, $roles ); } foreach ( $role_capabilities as $role => $capabilities ) { wpcom_vip_add_role_caps( $role, $capabilities ); } } /** * Removes the registered capabilities from the system * * @return void */ public function remove() { // Remove from any role it has been added to. $roles = wp_roles()->get_names(); $roles = array_keys( $roles ); $role_capabilities = []; foreach ( array_keys( $this->capabilities ) as $capability ) { // Allow filtering of roles. $role_capabilities = $this->get_role_capabilities( $role_capabilities, $capability, $roles ); } foreach ( $role_capabilities as $role => $capabilities ) { wpcom_vip_remove_role_caps( $role, $capabilities ); } } /** * Returns the roles which the capability is registered on. * * @param array $role_capabilities List of all roles with their capabilities. * @param string $capability Capability to filter roles for. * @param array $roles List of default roles. * * @return array List of capabilities. */ protected function get_role_capabilities( $role_capabilities, $capability, $roles ) { // Allow filtering of roles. $filtered_roles = $this->filter_roles( $capability, $roles ); foreach ( $filtered_roles as $role ) { if ( ! isset( $add_role_caps[ $role ] ) ) { $role_capabilities[ $role ] = []; } $role_capabilities[ $role ][] = $capability; } return $role_capabilities; } } admin/capabilities/class-abstract-capability-manager.php 0000644 00000004360 15174712003 0017436 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Capabilities */ /** * Abstract Capability Manager shared code. */ abstract class WPSEO_Abstract_Capability_Manager implements WPSEO_Capability_Manager { /** * Registered capabilities. * * @var array */ protected $capabilities = []; /** * Registers a capability. * * @param string $capability Capability to register. * @param array $roles Roles to add the capability to. * @param bool $overwrite Optional. Use add or overwrite as registration method. * * @return void */ public function register( $capability, array $roles, $overwrite = false ) { if ( $overwrite || ! isset( $this->capabilities[ $capability ] ) ) { $this->capabilities[ $capability ] = $roles; return; } // Combine configurations. $this->capabilities[ $capability ] = array_merge( $roles, $this->capabilities[ $capability ] ); // Remove doubles. $this->capabilities[ $capability ] = array_unique( $this->capabilities[ $capability ] ); } /** * Returns the list of registered capabilitities. * * @return string[] Registered capabilities. */ public function get_capabilities() { return array_keys( $this->capabilities ); } /** * Returns a list of WP_Role roles. * * The string array of role names are converted to actual WP_Role objects. * These are needed to be able to use the API on them. * * @param array $roles Roles to retrieve the objects for. * * @return WP_Role[] List of WP_Role objects. */ protected function get_wp_roles( array $roles ) { $wp_roles = array_map( 'get_role', $roles ); return array_filter( $wp_roles ); } /** * Filter capability roles. * * @param string $capability Capability to filter roles for. * @param array $roles List of roles which can be filtered. * * @return array Filtered list of roles for the capability. */ protected function filter_roles( $capability, array $roles ) { /** * Filter: Allow changing roles that a capability is added to. * * @param array $roles The default roles to be filtered. */ $filtered = apply_filters( $capability . '_roles', $roles ); // Make sure we have the expected type. if ( ! is_array( $filtered ) ) { return []; } return $filtered; } } admin/roles/class-role-manager.php 0000644 00000001401 15174712003 0013141 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Roles */ /** * Role Manager interface. */ interface WPSEO_Role_Manager { /** * Registers a role. * * @param string $role Role to register. * @param string $display_name Display name to use. * @param string|null $template Optional. Role to base the new role on. * * @return void */ public function register( $role, $display_name, $template = null ); /** * Adds the registered roles. * * @return void */ public function add(); /** * Removes the registered roles. * * @return void */ public function remove(); /** * Returns the list of registered roles. * * @return string[] List or registered roles. */ public function get_roles(); } admin/roles/class-role-manager-wp.php 0000644 00000002657 15174712003 0013603 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Roles */ /** * WordPress' default implementation of the Role Manager. */ final class WPSEO_Role_Manager_WP extends WPSEO_Abstract_Role_Manager { /** * Adds a role to the system. * * @param string $role Role to add. * @param string $display_name Name to display for the role. * @param array $capabilities Capabilities to add to the role. * * @return void */ protected function add_role( $role, $display_name, array $capabilities = [] ) { $wp_role = get_role( $role ); if ( $wp_role ) { foreach ( $capabilities as $capability => $grant ) { $wp_role->add_cap( $capability, $grant ); } return; } add_role( $role, $display_name, $capabilities ); } /** * Removes a role from the system. * * @param string $role Role to remove. * * @return void */ protected function remove_role( $role ) { remove_role( $role ); } /** * Formats the capabilities to the required format. * * @param array $capabilities Capabilities to format. * @param bool $enabled Whether these capabilities should be enabled or not. * * @return array Formatted capabilities. */ protected function format_capabilities( array $capabilities, $enabled = true ) { // Flip keys and values. $capabilities = array_flip( $capabilities ); // Set all values to $enabled. return array_fill_keys( array_keys( $capabilities ), $enabled ); } } admin/roles/class-role-manager-factory.php 0000644 00000000537 15174712003 0014617 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Roles */ /** * Role Manager Factory. */ class WPSEO_Role_Manager_Factory { /** * Retrieves the Role manager to use. * * @return WPSEO_Role_Manager */ public static function get() { static $manager = null; $manager ??= new WPSEO_Role_Manager_WP(); return $manager; } } admin/roles/class-abstract-role-manager.php 0000644 00000006607 15174712003 0014757 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Roles */ /** * Abstract Role Manager template. */ abstract class WPSEO_Abstract_Role_Manager implements WPSEO_Role_Manager { /** * Registered roles. * * @var array */ protected $roles = []; /** * Registers a role. * * @param string $role Role to register. * @param string $display_name Display name to use. * @param string|null $template Optional. Role to base the new role on. * * @return void */ public function register( $role, $display_name, $template = null ) { $this->roles[ $role ] = (object) [ 'display_name' => $display_name, 'template' => $template, ]; } /** * Returns the list of registered roles. * * @return string[] List or registered roles. */ public function get_roles() { return array_keys( $this->roles ); } /** * Adds the registered roles. * * @return void */ public function add() { foreach ( $this->roles as $role => $data ) { $capabilities = $this->get_capabilities( $data->template ); $capabilities = $this->filter_existing_capabilties( $role, $capabilities ); $this->add_role( $role, $data->display_name, $capabilities ); } } /** * Removes the registered roles. * * @return void */ public function remove() { $roles = array_keys( $this->roles ); array_map( [ $this, 'remove_role' ], $roles ); } /** * Returns the capabilities for the specified role. * * @param string $role Role to fetch capabilities from. * * @return array List of capabilities. */ protected function get_capabilities( $role ) { if ( ! is_string( $role ) || empty( $role ) ) { return []; } $wp_role = get_role( $role ); if ( ! $wp_role ) { return []; } return $wp_role->capabilities; } /** * Returns true if the capability exists on the role. * * @param WP_Role $role Role to check capability against. * @param string $capability Capability to check. * * @return bool True if the capability is defined for the role. */ protected function capability_exists( WP_Role $role, $capability ) { return ! array_key_exists( $capability, $role->capabilities ); } /** * Filters out capabilities that are already set for the role. * * This makes sure we don't override configurations that have been previously set. * * @param string $role The role to check against. * @param array $capabilities The capabilities that should be set. * * @return array Capabilties that can be safely set. */ protected function filter_existing_capabilties( $role, array $capabilities ) { if ( $capabilities === [] ) { return $capabilities; } $wp_role = get_role( $role ); if ( ! $wp_role ) { return $capabilities; } foreach ( $capabilities as $capability => $grant ) { if ( $this->capability_exists( $wp_role, $capability ) ) { unset( $capabilities[ $capability ] ); } } return $capabilities; } /** * Adds a role to the system. * * @param string $role Role to add. * @param string $display_name Name to display for the role. * @param array $capabilities Capabilities to add to the role. * * @return void */ abstract protected function add_role( $role, $display_name, array $capabilities = [] ); /** * Removes a role from the system. * * @param string $role Role to remove. * * @return void */ abstract protected function remove_role( $role ); } admin/roles/class-register-roles.php 0000644 00000001142 15174712003 0013540 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Roles */ /** * Role registration class. */ class WPSEO_Register_Roles implements WPSEO_WordPress_Integration { /** * Adds hooks. * * @return void */ public function register_hooks() { add_action( 'wpseo_register_roles', [ $this, 'register' ] ); } /** * Registers the roles. * * @return void */ public function register() { $role_manager = WPSEO_Role_Manager_Factory::get(); $role_manager->register( 'wpseo_manager', 'SEO Manager', 'editor' ); $role_manager->register( 'wpseo_editor', 'SEO Editor', 'editor' ); } } admin/class-yoast-plugin-conflict.php 0000644 00000024447 15174712003 0013715 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 1.7.0 */ /** * Base class for handling plugin conflicts. */ class Yoast_Plugin_Conflict { /** * The plugins must be grouped per section. * * It's possible to check for each section if there are conflicting plugins. * * @var array */ protected $plugins = []; /** * All the current active plugins will be stored in this private var. * * @var array */ protected $all_active_plugins = []; /** * After searching for active plugins that are in $this->plugins the active plugins will be stored in this * property. * * @var array */ protected $active_conflicting_plugins = []; /** * Property for holding instance of itself. * * @var Yoast_Plugin_Conflict */ protected static $instance; /** * For the use of singleton pattern. Create instance of itself and return this instance. * * @param string $class_name Give the classname to initialize. If classname is * false (empty) it will use it's own __CLASS__. * * @return Yoast_Plugin_Conflict */ public static function get_instance( $class_name = '' ) { if ( self::$instance === null ) { if ( ! is_string( $class_name ) || $class_name === '' ) { $class_name = self::class; } self::$instance = new $class_name(); } return self::$instance; } /** * Setting instance, all active plugins and search for active plugins. * * Protected constructor to prevent creating a new instance of the * *Singleton* via the `new` operator from outside this class. */ protected function __construct() { // Set active plugins. $this->all_active_plugins = get_option( 'active_plugins' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['action'] ) && is_string( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information and only comparing the variable in a condition. $action = wp_unslash( $_GET['action'] ); if ( $action === 'deactivate' ) { $this->remove_deactivated_plugin(); } } // Search for active plugins. $this->search_active_plugins(); } /** * Check if there are conflicting plugins for given $plugin_section. * * @param string $plugin_section Type of plugin conflict (such as Open Graph or sitemap). * * @return bool */ public function check_for_conflicts( $plugin_section ) { static $sections_checked; // Return early if there are no active conflicting plugins at all. if ( empty( $this->active_conflicting_plugins ) ) { return false; } $sections_checked ??= []; if ( ! in_array( $plugin_section, $sections_checked, true ) ) { $sections_checked[] = $plugin_section; return ( ! empty( $this->active_conflicting_plugins[ $plugin_section ] ) ); } return false; } /** * Checks for given $plugin_sections for conflicts. * * @param array $plugin_sections Set of sections. * * @return void */ public function check_plugin_conflicts( $plugin_sections ) { foreach ( $plugin_sections as $plugin_section => $readable_plugin_section ) { // Check for conflicting plugins and show error if there are conflicts. if ( $this->check_for_conflicts( $plugin_section ) ) { $this->set_error( $plugin_section, $readable_plugin_section ); } } // List of all active sections. $sections = array_keys( $plugin_sections ); // List of all sections. $all_plugin_sections = array_keys( $this->plugins ); /* * Get all sections that are inactive. * These plugins need to be cleared. * * This happens when Sitemaps or OpenGraph implementations toggle active/disabled. */ $inactive_sections = array_diff( $all_plugin_sections, $sections ); if ( ! empty( $inactive_sections ) ) { foreach ( $inactive_sections as $section ) { array_walk( $this->plugins[ $section ], [ $this, 'clear_error' ] ); } } // For active sections clear errors for inactive plugins. foreach ( $sections as $section ) { // By default, clear errors for all plugins of the section. $inactive_plugins = $this->plugins[ $section ]; // If there are active plugins, filter them from being cleared. if ( isset( $this->active_conflicting_plugins[ $section ] ) ) { $inactive_plugins = array_diff( $this->plugins[ $section ], $this->active_conflicting_plugins[ $section ] ); } array_walk( $inactive_plugins, [ $this, 'clear_error' ] ); } } /** * Setting an error on the screen. * * @param string $plugin_section Type of conflict group (such as Open Graph or sitemap). * @param string $readable_plugin_section This is the value for the translation. * * @return void */ protected function set_error( $plugin_section, $readable_plugin_section ) { $notification_center = Yoast_Notification_Center::get(); foreach ( $this->active_conflicting_plugins[ $plugin_section ] as $plugin_file ) { $plugin_name = $this->get_plugin_name( $plugin_file ); $error_message = ''; /* translators: %1$s: 'Facebook & Open Graph' plugin name(s) of possibly conflicting plugin(s), %2$s to Yoast SEO */ $error_message .= '<p>' . sprintf( __( 'The %1$s plugin might cause issues when used in conjunction with %2$s.', 'wordpress-seo' ), '<em>' . $plugin_name . '</em>', 'Yoast SEO' ) . '</p>'; $error_message .= '<p>' . sprintf( $readable_plugin_section, 'Yoast SEO', $plugin_name ) . '</p>'; /* translators: %s: 'Facebook' plugin name of possibly conflicting plugin */ $error_message .= '<a class="button button-primary" href="' . wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $plugin_file . '&plugin_status=all', 'deactivate-plugin_' . $plugin_file ) . '">' . sprintf( __( 'Deactivate %s', 'wordpress-seo' ), $this->get_plugin_name( $plugin_file ) ) . '</a> '; $identifier = $this->get_notification_identifier( $plugin_file ); // Add the message to the notifications center. $notification_center->add_notification( new Yoast_Notification( $error_message, [ 'type' => Yoast_Notification::ERROR, 'id' => 'wpseo-conflict-' . $identifier, ], ), ); } } /** * Clear the notification for a plugin. * * @param string $plugin_file Clear the optional notification for this plugin. * * @return void */ public function clear_error( $plugin_file ) { $identifier = $this->get_notification_identifier( $plugin_file ); $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification_by_id( 'wpseo-conflict-' . $identifier ); } /** * Loop through the $this->plugins to check if one of the plugins is active. * * This method will store the active plugins in $this->active_plugins. * * @return void */ protected function search_active_plugins() { foreach ( $this->plugins as $plugin_section => $plugins ) { $this->check_plugins_active( $plugins, $plugin_section ); } } /** * Loop through plugins and check if each plugin is active. * * @param array $plugins Set of plugins. * @param string $plugin_section Type of conflict group (such as Open Graph or sitemap). * * @return void */ protected function check_plugins_active( $plugins, $plugin_section ) { foreach ( $plugins as $plugin ) { if ( $this->check_plugin_is_active( $plugin ) ) { $this->add_active_plugin( $plugin_section, $plugin ); } } } /** * Check if given plugin exists in array with all_active_plugins. * * @param string $plugin Plugin basename string. * * @return bool */ protected function check_plugin_is_active( $plugin ) { return in_array( $plugin, $this->all_active_plugins, true ); } /** * Add plugin to the list of active plugins. * * This method will check first if key $plugin_section exists, if not it will create an empty array * If $plugin itself doesn't exist it will be added. * * @param string $plugin_section Type of conflict group (such as Open Graph or sitemap). * @param string $plugin Plugin basename string. * * @return void */ protected function add_active_plugin( $plugin_section, $plugin ) { if ( ! array_key_exists( $plugin_section, $this->active_conflicting_plugins ) ) { $this->active_conflicting_plugins[ $plugin_section ] = []; } if ( ! in_array( $plugin, $this->active_conflicting_plugins[ $plugin_section ], true ) ) { $this->active_conflicting_plugins[ $plugin_section ][] = $plugin; } } /** * Search in $this->plugins for the given $plugin. * * If there is a result it will return the plugin category. * * @param string $plugin Plugin basename string. * * @return int|string */ protected function find_plugin_category( $plugin ) { foreach ( $this->plugins as $plugin_section => $plugins ) { if ( in_array( $plugin, $plugins, true ) ) { return $plugin_section; } } } /** * Get plugin name from file. * * @param string $plugin Plugin path relative to plugins directory. * * @return string|bool Plugin name or false when no name is set. */ protected function get_plugin_name( $plugin ) { $plugin_details = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); if ( $plugin_details['Name'] !== '' ) { return $plugin_details['Name']; } return false; } /** * When being in the deactivation process the currently deactivated plugin has to be removed. * * @return void */ private function remove_deactivated_plugin() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: On the deactivation screen the nonce is already checked by WordPress itself. if ( ! isset( $_GET['plugin'] ) || ! is_string( $_GET['plugin'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: On the deactivation screen the nonce is already checked by WordPress itself. $deactivated_plugin = sanitize_text_field( wp_unslash( $_GET['plugin'] ) ); $key_to_remove = array_search( $deactivated_plugin, $this->all_active_plugins, true ); if ( $key_to_remove !== false ) { unset( $this->all_active_plugins[ $key_to_remove ] ); } } /** * Get the identifier from the plugin file. * * @param string $plugin_file Plugin file to get Identifier from. * * @return string */ private function get_notification_identifier( $plugin_file ) { return md5( $plugin_file ); } } admin/class-yoast-network-settings-api.php 0000644 00000010222 15174712003 0014700 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Network */ /** * Implements a network settings API for the plugin's multisite settings. */ class Yoast_Network_Settings_API { /** * Registered network settings. * * @var array */ private $registered_settings = []; /** * Options whitelist, keyed by option group. * * @var array */ private $whitelist_options = []; /** * The singleton instance of this class. * * @var Yoast_Network_Settings_API|null */ private static $instance = null; /** * Registers a network setting and its data. * * @param string $option_group The group the network option is part of. * @param string $option_name The name of the network option to sanitize and save. * @param array $args { * Optional. Data used to describe the network setting when registered. * * @type callable $sanitize_callback A callback function that sanitizes the network option's value. * @type mixed $default Default value when calling `get_network_option()`. * } * * @return void */ public function register_setting( $option_group, $option_name, $args = [] ) { $defaults = [ 'group' => $option_group, 'sanitize_callback' => null, ]; $args = wp_parse_args( $args, $defaults ); if ( ! isset( $this->whitelist_options[ $option_group ] ) ) { $this->whitelist_options[ $option_group ] = []; } $this->whitelist_options[ $option_group ][] = $option_name; if ( ! empty( $args['sanitize_callback'] ) ) { add_filter( "sanitize_option_{$option_name}", [ $this, 'filter_sanitize_option' ], 10, 2 ); } if ( array_key_exists( 'default', $args ) ) { add_filter( "default_site_option_{$option_name}", [ $this, 'filter_default_option' ], 10, 2 ); } $this->registered_settings[ $option_name ] = $args; } /** * Gets the registered settings and their data. * * @return array Array of $option_name => $data pairs. */ public function get_registered_settings() { return $this->registered_settings; } /** * Gets the whitelisted options for a given option group. * * @param string $option_group Option group. * * @return array List of option names, or empty array if unknown option group. */ public function get_whitelist_options( $option_group ) { if ( ! isset( $this->whitelist_options[ $option_group ] ) ) { return []; } return $this->whitelist_options[ $option_group ]; } /** * Filters sanitization for a network option value. * * This method is added as a filter to `sanitize_option_{$option}` for network options that are * registered with a sanitize callback. * * @param string $value The sanitized option value. * @param string $option The option name. * * @return string The filtered sanitized option value. */ public function filter_sanitize_option( $value, $option ) { if ( empty( $this->registered_settings[ $option ] ) ) { return $value; } return call_user_func( $this->registered_settings[ $option ]['sanitize_callback'], $value ); } /** * Filters the default value for a network option. * * This function is added as a filter to `default_site_option_{$option}` for network options that * are registered with a default. * * @param mixed $default_value Existing default value to return. * @param string $option The option name. * * @return mixed The filtered default value. */ public function filter_default_option( $default_value, $option ) { // If a default value was manually passed to the function, allow it to override. if ( $default_value !== false ) { return $default_value; } if ( empty( $this->registered_settings[ $option ] ) ) { return $default_value; } return $this->registered_settings[ $option ]['default']; } /** * Checks whether the requirements to use this class are met. * * @return bool True if requirements are met, false otherwise. */ public function meets_requirements() { return is_multisite(); } /** * Gets the singleton instance of this class. * * @return Yoast_Network_Settings_API The singleton instance. */ public static function get() { self::$instance ??= new self(); return self::$instance; } } admin/class-yoast-network-admin.php 0000644 00000023743 15174712003 0013375 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Internals */ /** * Multisite utility class for network admin functionality. */ class Yoast_Network_Admin implements WPSEO_WordPress_AJAX_Integration, WPSEO_WordPress_Integration { /** * Action identifier for updating plugin network options. * * @var string */ public const UPDATE_OPTIONS_ACTION = 'yoast_handle_network_options'; /** * Action identifier for restoring a site. * * @var string */ public const RESTORE_SITE_ACTION = 'yoast_restore_site'; /** * Gets the available sites as choices, e.g. for a dropdown. * * @param bool $include_empty Optional. Whether to include an initial placeholder choice. * Default false. * @param bool $show_title Optional. Whether to show the title for each site. This requires * switching through the sites, so has performance implications for * sites that do not use a persistent cache. * Default false. * * @return array Choices as $site_id => $site_label pairs. */ public function get_site_choices( $include_empty = false, $show_title = false ) { $choices = []; if ( $include_empty ) { $choices['-'] = __( 'None', 'wordpress-seo' ); } $criteria = [ 'deleted' => 0, 'network_id' => get_current_network_id(), ]; $sites = get_sites( $criteria ); foreach ( $sites as $site ) { $site_name = $site->domain . $site->path; if ( $show_title ) { $site_name = $site->blogname . ' (' . $site->domain . $site->path . ')'; } $choices[ $site->blog_id ] = $site->blog_id . ': ' . $site_name; $site_states = $this->get_site_states( $site ); if ( ! empty( $site_states ) ) { $choices[ $site->blog_id ] .= ' [' . implode( ', ', $site_states ) . ']'; } } return $choices; } /** * Gets the states of a site. * * @param WP_Site $site Site object. * * @return array Array of $state_slug => $state_label pairs. */ public function get_site_states( $site ) { $available_states = [ 'public' => __( 'public', 'wordpress-seo' ), 'archived' => __( 'archived', 'wordpress-seo' ), 'mature' => __( 'mature', 'wordpress-seo' ), 'spam' => __( 'spam', 'wordpress-seo' ), 'deleted' => __( 'deleted', 'wordpress-seo' ), ]; $site_states = []; foreach ( $available_states as $state_slug => $state_label ) { if ( $site->$state_slug === '1' ) { $site_states[ $state_slug ] = $state_label; } } return $site_states; } /** * Handles a request to update plugin network options. * * This method works similar to how option updates are handled in `wp-admin/options.php` and * `wp-admin/network/settings.php`. * * @return void */ public function handle_update_options_request() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification will happen in verify_request below. if ( ! isset( $_POST['network_option_group'] ) || ! is_string( $_POST['network_option_group'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification will happen in verify_request below. $option_group = sanitize_text_field( wp_unslash( $_POST['network_option_group'] ) ); if ( empty( $option_group ) ) { return; } $this->verify_request( "{$option_group}-network-options" ); $whitelist_options = Yoast_Network_Settings_API::get()->get_whitelist_options( $option_group ); if ( empty( $whitelist_options ) ) { add_settings_error( $option_group, 'settings_updated', __( 'You are not allowed to modify unregistered network settings.', 'wordpress-seo' ), 'error' ); $this->terminate_request(); return; } // phpcs:disable WordPress.Security.NonceVerification -- Nonce verified via `verify_request()` above. foreach ( $whitelist_options as $option_name ) { $value = null; if ( isset( $_POST[ $option_name ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Adding sanitize_text_field around this will break the saving of settings because it expects a string: https://github.com/Yoast/wordpress-seo/issues/12440. $value = wp_unslash( $_POST[ $option_name ] ); } WPSEO_Options::update_site_option( $option_name, $value ); } // phpcs:enable WordPress.Security.NonceVerification $settings_errors = get_settings_errors(); if ( empty( $settings_errors ) ) { add_settings_error( $option_group, 'settings_updated', __( 'Settings Updated.', 'wordpress-seo' ), 'updated' ); } $this->terminate_request(); } /** * Handles a request to restore a site's default settings. * * @return void */ public function handle_restore_site_request() { $this->verify_request( 'wpseo-network-restore', 'restore_site_nonce' ); $option_group = 'wpseo_ms'; // phpcs:ignore WordPress.Security.NonceVerification -- Nonce verified via `verify_request()` above. $site_id = ! empty( $_POST[ $option_group ]['site_id'] ) ? (int) $_POST[ $option_group ]['site_id'] : 0; if ( ! $site_id ) { add_settings_error( $option_group, 'settings_updated', __( 'No site has been selected to restore.', 'wordpress-seo' ), 'error' ); $this->terminate_request(); return; } $site = get_site( $site_id ); if ( ! $site ) { /* translators: %s expands to the ID of a site within a multisite network. */ add_settings_error( $option_group, 'settings_updated', sprintf( __( 'Site with ID %d not found.', 'wordpress-seo' ), $site_id ), 'error' ); } else { WPSEO_Options::reset_ms_blog( $site_id ); /* translators: %s expands to the name of a site within a multisite network. */ add_settings_error( $option_group, 'settings_updated', sprintf( __( '%s restored to default SEO settings.', 'wordpress-seo' ), esc_html( $site->blogname ) ), 'updated' ); } $this->terminate_request(); } /** * Outputs nonce, action and option group fields for a network settings page in the plugin. * * @param string $option_group Option group name for the current page. * * @return void */ public function settings_fields( $option_group ) { ?> <input type="hidden" name="network_option_group" value="<?php echo esc_attr( $option_group ); ?>" /> <input type="hidden" name="action" value="<?php echo esc_attr( self::UPDATE_OPTIONS_ACTION ); ?>" /> <?php wp_nonce_field( "$option_group-network-options" ); } /** * Enqueues network admin assets. * * @return void */ public function enqueue_assets() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'network-admin' ); $translations = [ /* translators: %s: success message */ 'success_prefix' => __( 'Success: %s', 'wordpress-seo' ), /* translators: %s: error message */ 'error_prefix' => __( 'Error: %s', 'wordpress-seo' ), ]; $asset_manager->localize_script( 'network-admin', 'wpseoNetworkAdminGlobalL10n', $translations, ); } /** * Hooks in the necessary actions and filters. * * @return void */ public function register_hooks() { if ( ! $this->meets_requirements() ) { return; } add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'admin_action_' . self::UPDATE_OPTIONS_ACTION, [ $this, 'handle_update_options_request' ] ); add_action( 'admin_action_' . self::RESTORE_SITE_ACTION, [ $this, 'handle_restore_site_request' ] ); } /** * Hooks in the necessary AJAX actions. * * @return void */ public function register_ajax_hooks() { add_action( 'wp_ajax_' . self::UPDATE_OPTIONS_ACTION, [ $this, 'handle_update_options_request' ] ); add_action( 'wp_ajax_' . self::RESTORE_SITE_ACTION, [ $this, 'handle_restore_site_request' ] ); } /** * Checks whether the requirements to use this class are met. * * @return bool True if requirements are met, false otherwise. */ public function meets_requirements() { return is_multisite() && is_network_admin(); } /** * Verifies that the current request is valid. * * @param string $action Nonce action. * @param string $query_arg Optional. Nonce query argument. Default '_wpnonce'. * * @return void */ public function verify_request( $action, $query_arg = '_wpnonce' ) { $has_access = current_user_can( 'wpseo_manage_network_options' ); if ( wp_doing_ajax() ) { check_ajax_referer( $action, $query_arg ); if ( ! $has_access ) { wp_die( -1, 403 ); } return; } check_admin_referer( $action, $query_arg ); if ( ! $has_access ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'wordpress-seo' ) ); } } /** * Terminates the current request by either redirecting back or sending an AJAX response. * * @return void */ public function terminate_request() { if ( wp_doing_ajax() ) { $settings_errors = get_settings_errors(); if ( ! empty( $settings_errors ) && $settings_errors[0]['type'] === 'updated' ) { wp_send_json_success( $settings_errors, 200 ); } wp_send_json_error( $settings_errors, 400 ); } $this->persist_settings_errors(); $this->redirect_back( [ 'settings-updated' => 'true' ] ); } /** * Persists settings errors. * * Settings errors are stored in a transient for 30 seconds so that this transient * can be retrieved on the next page load. * * @return void */ protected function persist_settings_errors() { /* * A regular transient is used here, since it is automatically cleared right after the redirect. * A network transient would be cleaner, but would require a lot of copied code from core for * just a minor adjustment when displaying settings errors. */ set_transient( 'settings_errors', get_settings_errors(), 30 ); } /** * Redirects back to the referer URL, with optional query arguments. * * @param array $query_args Optional. Query arguments to add to the redirect URL. Default none. * * @return void */ protected function redirect_back( $query_args = [] ) { $sendback = wp_get_referer(); if ( ! empty( $query_args ) ) { $sendback = add_query_arg( $query_args, $sendback ); } wp_safe_redirect( $sendback ); exit(); } } admin/class-admin-asset-dev-server-location.php 0000644 00000003154 15174712003 0015546 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Changes the asset paths to dev server paths. */ final class WPSEO_Admin_Asset_Dev_Server_Location implements WPSEO_Admin_Asset_Location { /** * Holds the dev server's default URL. * * @var string */ public const DEFAULT_URL = 'http://localhost:8080'; /** * Holds the url where the server is located. * * @var string */ private $url; /** * Class constructor. * * @param string|null $url Where the dev server is located. */ public function __construct( $url = null ) { $url ??= self::DEFAULT_URL; $this->url = $url; } /** * Determines the URL of the asset on the dev server. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. Usually JS or CSS. * * @return string The URL of the asset. */ public function get_url( WPSEO_Admin_Asset $asset, $type ) { if ( $type === WPSEO_Admin_Asset::TYPE_CSS ) { return $this->get_default_url( $asset, $type ); } $path = sprintf( 'js/dist/%s%s.js', $asset->get_src(), $asset->get_suffix() ); return trailingslashit( $this->url ) . $path; } /** * Determines the URL of the asset not using the dev server. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. * * @return string The URL of the asset file. */ public function get_default_url( WPSEO_Admin_Asset $asset, $type ) { $default_location = new WPSEO_Admin_Asset_SEO_Location( WPSEO_FILE ); return $default_location->get_url( $asset, $type ); } } admin/class-admin-gutenberg-compatibility-notification.php 0000644 00000005064 15174712003 0020060 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Handles the Gutenberg Compatibility notification showing and hiding. */ class WPSEO_Admin_Gutenberg_Compatibility_Notification implements WPSEO_WordPress_Integration { /** * Notification ID to use. * * @var string */ private $notification_id = 'wpseo-outdated-gutenberg-plugin'; /** * Instance of gutenberg compatibility checker. * * @var WPSEO_Gutenberg_Compatibility */ protected $compatibility_checker; /** * Instance of Yoast Notification Center. * * @var Yoast_Notification_Center */ protected $notification_center; /** * WPSEO_Admin_Gutenberg_Compatibility_Notification constructor. */ public function __construct() { $this->compatibility_checker = new WPSEO_Gutenberg_Compatibility(); $this->notification_center = Yoast_Notification_Center::get(); } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'manage_notification' ] ); } /** * Manages if the notification should be shown or removed. * * @return void */ public function manage_notification() { /** * Filter: 'yoast_display_gutenberg_compat_notification' - Allows developer to disable the Gutenberg compatibility * notification. * * @param bool $display_notification */ $display_notification = apply_filters( 'yoast_display_gutenberg_compat_notification', true ); if ( ! $this->compatibility_checker->is_installed() || $this->compatibility_checker->is_fully_compatible() || ! $display_notification ) { $this->notification_center->remove_notification_by_id( $this->notification_id ); return; } $this->add_notification(); } /** * Adds the notification to the notificaton center. * * @return void */ protected function add_notification() { $level = $this->compatibility_checker->is_below_minimum() ? Yoast_Notification::ERROR : Yoast_Notification::WARNING; $message = sprintf( /* translators: %1$s expands to Yoast SEO, %2$s expands to the installed version, %3$s expands to Gutenberg */ __( '%1$s detected you are using version %2$s of %3$s, please update to the latest version to prevent compatibility issues.', 'wordpress-seo' ), 'Yoast SEO', $this->compatibility_checker->get_installed_version(), 'Gutenberg', ); $notification = new Yoast_Notification( $message, [ 'id' => $this->notification_id, 'type' => $level, 'priority' => 1, ], ); $this->notification_center->add_notification( $notification ); } } admin/class-expose-shortlinks.php 0000644 00000016127 15174712003 0013160 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Exposes shortlinks in a global, so that we can pass them to our Javascript components. */ class WPSEO_Expose_Shortlinks implements WPSEO_WordPress_Integration { /** * Array containing the keys and shortlinks. * * @var array */ private $shortlinks = [ 'shortlinks.advanced.allow_search_engines' => 'https://yoa.st/allow-search-engines', 'shortlinks.advanced.follow_links' => 'https://yoa.st/follow-links', 'shortlinks.advanced.meta_robots' => 'https://yoa.st/meta-robots-advanced', 'shortlinks.advanced.breadcrumbs_title' => 'https://yoa.st/breadcrumbs-title', 'shortlinks.metabox.schema.explanation' => 'https://yoa.st/400', 'shortlinks.metabox.schema.page_type' => 'https://yoa.st/402', 'shortlinks.sidebar.schema.explanation' => 'https://yoa.st/401', 'shortlinks.sidebar.schema.page_type' => 'https://yoa.st/403', 'shortlinks.focus_keyword_info' => 'https://yoa.st/focus-keyword', 'shortlinks.nofollow_sponsored' => 'https://yoa.st/nofollow-sponsored', 'shortlinks.snippet_preview_info' => 'https://yoa.st/snippet-preview', 'shortlinks.cornerstone_content_info' => 'https://yoa.st/1i9', 'shortlinks.upsell.social_preview.social' => 'https://yoa.st/social-preview-facebook', 'shortlinks.upsell.social_preview.x' => 'https://yoa.st/social-preview-twitter', 'shortlinks.upsell.sidebar.news' => 'https://yoa.st/get-news-sidebar', 'shortlinks.upsell.sidebar.premium_seo_analysis_button' => 'https://yoa.st/premium-seo-analysis-sidebar', 'shortlinks.upsell.sidebar.additional_link' => 'https://yoa.st/textlink-keywords-sidebar', 'shortlinks.upsell.sidebar.additional_button' => 'https://yoa.st/add-keywords-sidebar', 'shortlinks.upsell.sidebar.word_complexity' => 'https://yoa.st/word-complexity-sidebar', 'shortlinks.upsell.sidebar.internal_linking_suggestions' => 'https://yoa.st/internal-linking-suggestions-sidebar', 'shortlinks.upsell.sidebar.highlighting_seo_analysis' => 'https://yoa.st/highlighting-seo-analysis', 'shortlinks.upsell.sidebar.highlighting_readability_analysis' => 'https://yoa.st/highlighting-readability-analysis', 'shortlinks.upsell.sidebar.highlighting_inclusive_analysis' => 'https://yoa.st/highlighting-inclusive-analysis', 'shortlinks.upsell.sidebar.content_blocks' => 'https://yoa.st/content-blocks-sidebar', 'shortlinks.upsell.metabox.news' => 'https://yoa.st/get-news-metabox', 'shortlinks.upsell.metabox.go_premium' => 'https://yoa.st/pe-premium-page', 'shortlinks.upsell.metabox.premium_seo_analysis_button' => 'https://yoa.st/premium-seo-analysis-metabox', 'shortlinks.upsell.metabox.additional_link' => 'https://yoa.st/textlink-keywords-metabox', 'shortlinks.upsell.metabox.additional_button' => 'https://yoa.st/add-keywords-metabox', 'shortlinks.upsell.metabox.word_complexity' => 'https://yoa.st/word-complexity-metabox', 'shortlinks.upsell.metabox.internal_linking_suggestions' => 'https://yoa.st/internal-linking-suggestions-metabox', 'shortlinks.upsell.metabox.content_blocks' => 'https://yoa.st/content-blocks-metabox', 'shortlinks.upsell.gsc.create_redirect_button' => 'https://yoa.st/redirects', 'shortlinks.readability_analysis_info' => 'https://yoa.st/readability-analysis', 'shortlinks.inclusive_language_analysis_info' => 'https://yoa.st/inclusive-language-analysis', 'shortlinks.activate_premium_info' => 'https://yoa.st/activate-subscription', 'shortlinks.wincher.seo_performance' => 'https://yoa.st/wincher-integration', 'shortlinks-insights-estimated_reading_time' => 'https://yoa.st/4fd', 'shortlinks-insights-flesch_reading_ease' => 'https://yoa.st/34r', 'shortlinks-insights-flesch_reading_ease_sidebar' => 'https://yoa.st/4mf', 'shortlinks-insights-flesch_reading_ease_metabox' => 'https://yoa.st/4mg', 'shortlinks-insights-flesch_reading_ease_article' => 'https://yoa.st/34s', 'shortlinks-insights-keyword_research_link' => 'https://yoa.st/keyword-research-metabox', 'shortlinks-insights-upsell-sidebar-prominent_words' => 'https://yoa.st/prominent-words-upsell-sidebar', 'shortlinks-insights-upsell-metabox-prominent_words' => 'https://yoa.st/prominent-words-upsell-metabox', 'shortlinks-insights-upsell-elementor-prominent_words' => 'https://yoa.st/prominent-words-upsell-elementor', 'shortlinks-insights-word_count' => 'https://yoa.st/word-count', 'shortlinks-insights-upsell-sidebar-text_formality' => 'https://yoa.st/formality-upsell-sidebar', 'shortlinks-insights-upsell-metabox-text_formality' => 'https://yoa.st/formality-upsell-metabox', 'shortlinks-insights-upsell-elementor-text_formality' => 'https://yoa.st/formality-upsell-elementor', 'shortlinks-insights-text_formality_info_free' => 'https://yoa.st/formality-free', 'shortlinks-insights-text_formality_info_premium' => 'https://yoa.st/formality', ]; /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_filter( 'wpseo_admin_l10n', [ $this, 'expose_shortlinks' ] ); } /** * Adds shortlinks to the passed array. * * @param array $input The array to add shortlinks to. * * @return array The passed array with the additional shortlinks. */ public function expose_shortlinks( $input ) { foreach ( $this->get_shortlinks() as $key => $shortlink ) { $input[ $key ] = WPSEO_Shortlinker::get( $shortlink ); } $input['default_query_params'] = WPSEO_Shortlinker::get_query_params(); return $input; } /** * Retrieves the shortlinks. * * @return array The shortlinks. */ private function get_shortlinks() { if ( ! $this->is_term_edit() ) { return $this->shortlinks; } $shortlinks = $this->shortlinks; $shortlinks['shortlinks.upsell.metabox.additional_link'] = 'https://yoa.st/textlink-keywords-metabox-term'; $shortlinks['shortlinks.upsell.metabox.additional_button'] = 'https://yoa.st/add-keywords-metabox-term'; $shortlinks['shortlinks.upsell.metabox.word_complexity'] = 'https://yoa.st/word-complexity-metabox-term'; $shortlinks['shortlinks.upsell.metabox.internal_linking_suggestions'] = 'https://yoa.st/internal-linking-suggestions-metabox-term'; return $shortlinks; } /** * Checks if the current page is a term edit page. * * @return bool True when page is term edit. */ private function is_term_edit() { global $pagenow; return WPSEO_Taxonomy::is_term_edit( $pagenow ); } } admin/class-asset.php 0000644 00000010465 15174712003 0010575 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents a WPSEO asset */ class WPSEO_Admin_Asset { /** * Constant used to identify file type as a JS file. * * @var string */ public const TYPE_JS = 'js'; /** * Constant used to identify file type as a CSS file. * * @var string */ public const TYPE_CSS = 'css'; /** * The name option identifier. * * @var string */ public const NAME = 'name'; /** * The source option identifier. * * @var string */ public const SRC = 'src'; /** * The dependencies option identifier. * * @var string */ public const DEPS = 'deps'; /** * The version option identifier. * * @var string */ public const VERSION = 'version'; /* Style specific. */ /** * The media option identifier. * * @var string */ public const MEDIA = 'media'; /** * The rtl option identifier. * * @var string */ public const RTL = 'rtl'; /* Script specific. */ /** * The "in footer" option identifier. * * @var string */ public const IN_FOOTER = 'in_footer'; /** * Asset identifier. * * @var string */ protected $name; /** * Path to the asset. * * @var string */ protected $src; /** * Asset dependencies. * * @var string|array */ protected $deps; /** * Asset version. * * @var string */ protected $version; /** * For CSS Assets. The type of media for which this stylesheet has been defined. * * See https://www.w3.org/TR/CSS2/media.html#media-types. * * @var string */ protected $media; /** * For JS Assets. Whether or not the script should be loaded in the footer. * * @var bool */ protected $in_footer; /** * For JS Assets. The script's async/defer strategy. * * @var string */ protected $strategy; /** * For CSS Assets. Whether this stylesheet is a right-to-left stylesheet. * * @var bool */ protected $rtl; /** * File suffix. * * @var string */ protected $suffix; /** * Default asset arguments. * * @var array */ private $defaults = [ 'deps' => [], 'in_footer' => true, 'rtl' => true, 'media' => 'all', 'version' => '', 'suffix' => '', 'strategy' => '', ]; /** * Constructs an instance of the WPSEO_Admin_Asset class. * * @param array $args The arguments for this asset. * * @throws InvalidArgumentException Throws when no name or src has been provided. */ public function __construct( array $args ) { if ( ! isset( $args['name'] ) ) { throw new InvalidArgumentException( 'name is a required argument' ); } if ( ! isset( $args['src'] ) ) { throw new InvalidArgumentException( 'src is a required argument' ); } $args = array_merge( $this->defaults, $args ); $this->name = $args['name']; $this->src = $args['src']; $this->deps = $args['deps']; $this->version = $args['version']; $this->media = $args['media']; $this->in_footer = $args['in_footer']; $this->strategy = $args['strategy']; $this->rtl = $args['rtl']; $this->suffix = $args['suffix']; } /** * Returns the asset identifier. * * @return string */ public function get_name() { return $this->name; } /** * Returns the path to the asset. * * @return string */ public function get_src() { return $this->src; } /** * Returns the asset dependencies. * * @return array|string */ public function get_deps() { return $this->deps; } /** * Returns the asset version. * * @return string|null */ public function get_version() { if ( ! empty( $this->version ) ) { return $this->version; } return null; } /** * Returns the media type for CSS assets. * * @return string */ public function get_media() { return $this->media; } /** * Returns whether a script asset should be loaded in the footer of the page. * * @return bool */ public function is_in_footer() { return $this->in_footer; } /** * Returns the script asset's async/defer loading strategy. * * @return string */ public function get_strategy() { return $this->strategy; } /** * Returns whether this CSS has a RTL counterpart. * * @return bool */ public function has_rtl() { return $this->rtl; } /** * Returns the file suffix. * * @return string */ public function get_suffix() { return $this->suffix; } } admin/ajax.php 0000644 00000026245 15174712003 0007301 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } /** * Convenience function to JSON encode and echo results and then die. * * @param array $results Results array for encoding. * * @return void */ function wpseo_ajax_json_echo_die( $results ) { // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. echo WPSEO_Utils::format_json_encode( $results ); exit(); } /** * Function used from AJAX calls, takes it variables from $_POST, dies on exit. * * @return void */ function wpseo_set_option() { if ( ! current_user_can( 'manage_options' ) ) { exit( '-1' ); } check_ajax_referer( 'wpseo-setoption' ); if ( ! isset( $_POST['option'] ) || ! is_string( $_POST['option'] ) ) { exit( '-1' ); } $option = sanitize_text_field( wp_unslash( $_POST['option'] ) ); if ( $option !== 'page_comments' ) { exit( '-1' ); } update_option( $option, 0 ); exit( '1' ); } add_action( 'wp_ajax_wpseo_set_option', 'wpseo_set_option' ); /** * Since 3.2 Notifications are dismissed in the Notification Center. */ add_action( 'wp_ajax_yoast_dismiss_notification', [ 'Yoast_Notification_Center', 'ajax_dismiss_notification' ] ); /** * Function used to remove the admin notices for several purposes, dies on exit. * * @return void */ function wpseo_set_ignore() { if ( ! current_user_can( 'manage_options' ) ) { exit( '-1' ); } check_ajax_referer( 'wpseo-ignore' ); if ( ! isset( $_POST['option'] ) || ! is_string( $_POST['option'] ) ) { exit( '-1' ); } $ignore_key = sanitize_text_field( wp_unslash( $_POST['option'] ) ); WPSEO_Options::set( 'ignore_' . $ignore_key, true ); exit( '1' ); } add_action( 'wp_ajax_wpseo_set_ignore', 'wpseo_set_ignore' ); /** * Save an individual SEO title from the Bulk Editor. * * @return void */ function wpseo_save_title() { wpseo_save_what( 'title' ); } add_action( 'wp_ajax_wpseo_save_title', 'wpseo_save_title' ); /** * Save an individual meta description from the Bulk Editor. * * @return void */ function wpseo_save_description() { wpseo_save_what( 'metadesc' ); } add_action( 'wp_ajax_wpseo_save_metadesc', 'wpseo_save_description' ); /** * Save titles & descriptions. * * @param string $what Type of item to save (title, description). * * @return void */ function wpseo_save_what( $what ) { check_ajax_referer( 'wpseo-bulk-editor' ); if ( ! isset( $_POST['new_value'], $_POST['wpseo_post_id'], $_POST['existing_value'] ) || ! is_string( $_POST['new_value'] ) || ! is_string( $_POST['existing_value'] ) ) { exit( '-1' ); } $new = sanitize_text_field( wp_unslash( $_POST['new_value'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting the unsafe value to an integer. $post_id = (int) wp_unslash( $_POST['wpseo_post_id'] ); $original = sanitize_text_field( wp_unslash( $_POST['existing_value'] ) ); if ( $post_id === 0 ) { exit( '-1' ); } $results = wpseo_upsert_new( $what, $post_id, $new, $original ); wpseo_ajax_json_echo_die( $results ); } /** * Helper function to update a post's meta data, returning relevant information * about the information updated and the results or the meta update. * * @param int $post_id Post ID. * @param string $new_meta_value New meta value to record. * @param string $orig_meta_value Original meta value. * @param string $meta_key Meta key string. * @param string $return_key Return key string to use in results. * * @return array */ function wpseo_upsert_meta( $post_id, $new_meta_value, $orig_meta_value, $meta_key, $return_key ) { $post_id = (int) $post_id; $sanitized_new_meta_value = wp_strip_all_tags( $new_meta_value ); $orig_meta_value = wp_strip_all_tags( $orig_meta_value ); $upsert_results = [ 'status' => 'success', 'post_id' => $post_id, "new_{$return_key}" => $sanitized_new_meta_value, "original_{$return_key}" => $orig_meta_value, ]; $the_post = get_post( $post_id ); if ( empty( $the_post ) ) { $upsert_results['status'] = 'failure'; $upsert_results['results'] = __( 'Post doesn\'t exist.', 'wordpress-seo' ); return $upsert_results; } $post_type_object = get_post_type_object( $the_post->post_type ); if ( ! $post_type_object ) { $upsert_results['status'] = 'failure'; $upsert_results['results'] = sprintf( /* translators: %s expands to post type. */ __( 'Post has an invalid Content Type: %s.', 'wordpress-seo' ), $the_post->post_type, ); return $upsert_results; } if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { $upsert_results['status'] = 'failure'; $upsert_results['results'] = sprintf( /* translators: %s expands to post type name. */ __( 'You can\'t edit %s.', 'wordpress-seo' ), $post_type_object->label, ); return $upsert_results; } if ( ! current_user_can( $post_type_object->cap->edit_others_posts ) && (int) $the_post->post_author !== get_current_user_id() ) { $upsert_results['status'] = 'failure'; $upsert_results['results'] = sprintf( /* translators: %s expands to the name of a post type (plural). */ __( 'You can\'t edit %s that aren\'t yours.', 'wordpress-seo' ), $post_type_object->label, ); return $upsert_results; } if ( $sanitized_new_meta_value === $orig_meta_value && $sanitized_new_meta_value !== $new_meta_value ) { $upsert_results['status'] = 'failure'; $upsert_results['results'] = __( 'You have used HTML in your value which is not allowed.', 'wordpress-seo' ); return $upsert_results; } $res = update_post_meta( $post_id, $meta_key, $sanitized_new_meta_value ); $upsert_results['status'] = ( $res !== false ) ? 'success' : 'failure'; $upsert_results['results'] = $res; return $upsert_results; } /** * Save all titles sent from the Bulk Editor. * * @return void */ function wpseo_save_all_titles() { wpseo_save_all( 'title' ); } add_action( 'wp_ajax_wpseo_save_all_titles', 'wpseo_save_all_titles' ); /** * Save all description sent from the Bulk Editor. * * @return void */ function wpseo_save_all_descriptions() { wpseo_save_all( 'metadesc' ); } add_action( 'wp_ajax_wpseo_save_all_descriptions', 'wpseo_save_all_descriptions' ); /** * Utility function to save values. * * @param string $what Type of item so save. * * @return void */ function wpseo_save_all( $what ) { check_ajax_referer( 'wpseo-bulk-editor' ); $results = []; if ( ! isset( $_POST['items'], $_POST['existingItems'] ) ) { wpseo_ajax_json_echo_die( $results ); } $new_values = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], wp_unslash( (array) $_POST['items'] ) ); $original_values = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], wp_unslash( (array) $_POST['existingItems'] ) ); foreach ( $new_values as $post_id => $new_value ) { $original_value = $original_values[ $post_id ]; $results[] = wpseo_upsert_new( $what, $post_id, $new_value, $original_value ); } wpseo_ajax_json_echo_die( $results ); } /** * Insert a new value. * * @param string $what Item type (such as title). * @param int $post_id Post ID. * @param string $new_value New value to record. * @param string $original Original value. * * @return string */ function wpseo_upsert_new( $what, $post_id, $new_value, $original ) { $meta_key = WPSEO_Meta::$meta_prefix . $what; return wpseo_upsert_meta( $post_id, $new_value, $original, $meta_key, $what ); } /** * Retrieves the post ids where the keyword is used before as well as the types of those posts. * * @return void */ function ajax_get_keyword_usage_and_post_types() { check_ajax_referer( 'wpseo-keyword-usage-and-post-types', 'nonce' ); if ( ! isset( $_POST['post_id'], $_POST['keyword'] ) || ! is_string( $_POST['keyword'] ) ) { exit( '-1' ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We are casting to an integer. $post_id = (int) wp_unslash( $_POST['post_id'] ); if ( $post_id === 0 || ! current_user_can( 'edit_post', $post_id ) ) { exit( '-1' ); } $keyword = sanitize_text_field( wp_unslash( $_POST['keyword'] ) ); $post_ids = WPSEO_Meta::keyword_usage( $keyword, $post_id ); $return_object = [ 'keyword_usage' => $post_ids, 'post_types' => WPSEO_Meta::post_types_for_ids( $post_ids ), ]; wp_die( // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. WPSEO_Utils::format_json_encode( $return_object ), ); } add_action( 'wp_ajax_get_focus_keyword_usage_and_post_types', 'ajax_get_keyword_usage_and_post_types' ); /** * Retrieves the keyword for the keyword doubles of the termpages. * * @return void */ function ajax_get_term_keyword_usage() { check_ajax_referer( 'wpseo-keyword-usage', 'nonce' ); if ( ! isset( $_POST['post_id'], $_POST['keyword'], $_POST['taxonomy'] ) || ! is_string( $_POST['keyword'] ) || ! is_string( $_POST['taxonomy'] ) ) { wp_die( -1 ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting the unsafe input to an integer. $post_id = (int) wp_unslash( $_POST['post_id'] ); if ( $post_id === 0 ) { wp_die( -1 ); } $keyword = sanitize_text_field( wp_unslash( $_POST['keyword'] ) ); $taxonomy_name = sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ); $taxonomy = get_taxonomy( $taxonomy_name ); if ( ! $taxonomy ) { wp_die( 0 ); } if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) { wp_die( -1 ); } $usage = WPSEO_Taxonomy_Meta::get_keyword_usage( $keyword, $post_id, $taxonomy_name ); // Normalize the result so it is the same as the post keyword usage AJAX request. $usage = $usage[ $keyword ]; wp_die( // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. WPSEO_Utils::format_json_encode( $usage ), ); } add_action( 'wp_ajax_get_term_keyword_usage', 'ajax_get_term_keyword_usage' ); /** * Registers hooks for all AJAX integrations. * * @return void */ function wpseo_register_ajax_integrations() { $integrations = [ new Yoast_Network_Admin() ]; foreach ( $integrations as $integration ) { $integration->register_ajax_hooks(); } } wpseo_register_ajax_integrations(); new WPSEO_Shortcode_Filter(); new WPSEO_Taxonomy_Columns(); /* ********************* DEPRECATED FUNCTIONS ********************* */ /** * Retrieves the keyword for the keyword doubles. * * @return void */ function ajax_get_keyword_usage() { _deprecated_function( __METHOD__, 'WPSEO 20.4' ); check_ajax_referer( 'wpseo-keyword-usage', 'nonce' ); if ( ! isset( $_POST['post_id'], $_POST['keyword'] ) || ! is_string( $_POST['keyword'] ) ) { exit( '-1' ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We are casting to an integer. $post_id = (int) wp_unslash( $_POST['post_id'] ); if ( $post_id === 0 || ! current_user_can( 'edit_post', $post_id ) ) { exit( '-1' ); } $keyword = sanitize_text_field( wp_unslash( $_POST['keyword'] ) ); wp_die( // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. WPSEO_Utils::format_json_encode( WPSEO_Meta::keyword_usage( $keyword, $post_id ) ), ); } admin/import/plugins/class-import-seopressor.php 0000644 00000011445 15174712003 0016164 0 ustar 00 <?php /** * File with the class to handle data from SEOPressor. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_SEOPressor. * * Class with functionality to import & clean SEOPressor post metadata. */ class WPSEO_Import_SEOPressor extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'SEOpressor'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_seop_settings'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_seop_settings', ], ]; /** * Imports the post meta values to Yoast SEO. * * @return bool Import success status. */ protected function import() { // Query for all the posts that have an _seop_settings meta set. $query_posts = new WP_Query( 'post_type=any&meta_key=_seop_settings&order=ASC&fields=ids&nopaging=true' ); foreach ( $query_posts->posts as $post_id ) { $this->import_post_focus_keywords( $post_id ); $this->import_seopressor_post_settings( $post_id ); } return true; } /** * Removes all the post meta fields SEOpressor creates. * * @return bool Cleanup status. */ protected function cleanup() { global $wpdb; // If we get to replace the data, let's do some proper cleanup. return $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_seop_%'" ); } /** * Imports the data. SEOpressor stores most of the data in one post array, this loops over it. * * @param int $post_id Post ID. * * @return void */ private function import_seopressor_post_settings( $post_id ) { $settings = get_post_meta( $post_id, '_seop_settings', true ); foreach ( [ 'fb_description' => 'opengraph-description', 'fb_title' => 'opengraph-title', 'fb_type' => 'og_type', 'fb_img' => 'opengraph-image', 'meta_title' => 'title', 'meta_description' => 'metadesc', 'meta_canonical' => 'canonical', 'tw_description' => 'twitter-description', 'tw_title' => 'twitter-title', 'tw_image' => 'twitter-image', ] as $seopressor_key => $yoast_key ) { $this->import_meta_helper( $seopressor_key, $yoast_key, $settings, $post_id ); } if ( isset( $settings['meta_rules'] ) ) { $this->import_post_robots( $settings['meta_rules'], $post_id ); } } /** * Imports the focus keywords, and stores them for later use. * * @param int $post_id Post ID. * * @return void */ private function import_post_focus_keywords( $post_id ) { // Import the focus keyword. $focuskw = trim( get_post_meta( $post_id, '_seop_kw_1', true ) ); $this->maybe_save_post_meta( 'focuskw', $focuskw, $post_id ); // Import additional focus keywords for use in premium. $focuskw2 = trim( get_post_meta( $post_id, '_seop_kw_2', true ) ); $focuskw3 = trim( get_post_meta( $post_id, '_seop_kw_3', true ) ); $focus_keywords = []; if ( ! empty( $focuskw2 ) ) { $focus_keywords[] = $focuskw2; } if ( ! empty( $focuskw3 ) ) { $focus_keywords[] = $focuskw3; } if ( $focus_keywords !== [] ) { $this->maybe_save_post_meta( 'focuskeywords', WPSEO_Utils::format_json_encode( $focus_keywords ), $post_id ); } } /** * Retrieves the SEOpressor robot value and map this to Yoast SEO values. * * @param string $meta_rules The meta rules taken from the SEOpressor settings array. * @param int $post_id The post id of the current post. * * @return void */ private function import_post_robots( $meta_rules, $post_id ) { $seopressor_robots = explode( '#|#|#', $meta_rules ); $robot_value = $this->get_robot_value( $seopressor_robots ); // Saving the new meta values for Yoast SEO. $this->maybe_save_post_meta( 'meta-robots-noindex', $robot_value['index'], $post_id ); $this->maybe_save_post_meta( 'meta-robots-nofollow', $robot_value['follow'], $post_id ); $this->maybe_save_post_meta( 'meta-robots-adv', $robot_value['advanced'], $post_id ); } /** * Gets the robot config by given SEOpressor robots value. * * @param array $seopressor_robots The value in SEOpressor that needs to be converted to the Yoast format. * * @return array The robots values in Yoast format. */ private function get_robot_value( $seopressor_robots ) { $return = [ 'index' => 2, 'follow' => 0, 'advanced' => '', ]; if ( in_array( 'noindex', $seopressor_robots, true ) ) { $return['index'] = 1; } if ( in_array( 'nofollow', $seopressor_robots, true ) ) { $return['follow'] = 1; } foreach ( [ 'noarchive', 'nosnippet', 'noimageindex' ] as $needle ) { if ( in_array( $needle, $seopressor_robots, true ) ) { $return['advanced'] .= $needle . ','; } } $return['advanced'] = rtrim( $return['advanced'], ',' ); return $return; } } admin/import/plugins/class-import-aioseo-v4.php 0000644 00000022165 15174712003 0015567 0 ustar 00 <?php /** * File with the class to handle data from All in One SEO Pack, versions 4 and up. * * @package WPSEO\Admin\Import\Plugins */ use Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Cleanup_Action; use Yoast\WP\SEO\Actions\Importing\Aioseo\Aioseo_Posts_Importing_Action; /** * Class with functionality to import & clean All in One SEO Pack post metadata, versions 4 and up. */ class WPSEO_Import_AIOSEO_V4 extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'All In One SEO Pack'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_aioseo_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_aioseo_title', 'new_key' => 'title', ], [ 'old_key' => '_aioseo_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_aioseo_og_title', 'new_key' => 'opengraph-title', ], [ 'old_key' => '_aioseo_og_description', 'new_key' => 'opengraph-description', ], [ 'old_key' => '_aioseo_twitter_title', 'new_key' => 'twitter-title', ], [ 'old_key' => '_aioseo_twitter_description', 'new_key' => 'twitter-description', ], ]; /** * Mapping between the AiOSEO replace vars and the Yoast replace vars. * * @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/ * * @var array */ protected $replace_vars = [ // They key is the AiOSEO replace var, the value is the Yoast replace var (see class-wpseo-replace-vars). '#author_first_name' => '%%author_first_name%%', '#author_last_name' => '%%author_last_name%%', '#author_name' => '%%name%%', '#categories' => '%%category%%', '#current_date' => '%%currentdate%%', '#current_day' => '%%currentday%%', '#current_month' => '%%currentmonth%%', '#current_year' => '%%currentyear%%', '#permalink' => '%%permalink%%', '#post_content' => '%%post_content%%', '#post_date' => '%%date%%', '#post_day' => '%%post_day%%', '#post_month' => '%%post_month%%', '#post_title' => '%%title%%', '#post_year' => '%%post_year%%', '#post_excerpt_only' => '%%excerpt_only%%', '#post_excerpt' => '%%excerpt%%', '#separator_sa' => '%%sep%%', '#site_title' => '%%sitename%%', '#tagline' => '%%sitedesc%%', '#taxonomy_title' => '%%category_title%%', ]; /** * Replaces the AiOSEO variables in our temporary table with Yoast variables (replace vars). * * @param array $replace_values Key value pair of values to replace with other values. This is only used in the base class but not here. * That is because this class doesn't have any `convert` keys in `$clone_keys`. * For that reason, we're overwriting the base class' `meta_key_clone_replace()` function without executing that base functionality. * * @return void */ protected function meta_key_clone_replace( $replace_values ) { global $wpdb; // At this point we're already looping through all the $clone_keys (this happens in meta_keys_clone() in the abstract class). // Now, we'll also loop through the replace_vars array, which holds the mappings between the AiOSEO variables and the Yoast variables. // We'll replace all the AiOSEO variables in the temporary table with their Yoast equivalents. foreach ( $this->replace_vars as $aioseo_variable => $yoast_variable ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: We need this query and this is done at many other places as well, for example class-import-rankmath. $wpdb->query( $wpdb->prepare( 'UPDATE tmp_meta_table SET meta_value = REPLACE( meta_value, %s, %s )', $aioseo_variable, $yoast_variable, ), ); } // The AiOSEO custom fields take the form of `#custom_field-myfield`. // These should be mapped to %%cf_myfield%%. $meta_values_with_custom_fields = $this->get_meta_values_with_custom_field_or_taxonomy( $wpdb, 'custom_field' ); $unique_custom_fields = $this->get_unique_custom_fields_or_taxonomies( $meta_values_with_custom_fields, 'custom_field' ); $this->replace_custom_field_or_taxonomy_replace_vars( $unique_custom_fields, $wpdb, 'custom_field', 'cf' ); // Map `#tax_name-{tax-slug}` to `%%ct_{tax-slug}%%``. $meta_values_with_custom_taxonomies = $this->get_meta_values_with_custom_field_or_taxonomy( $wpdb, 'tax_name' ); $unique_custom_taxonomies = $this->get_unique_custom_fields_or_taxonomies( $meta_values_with_custom_taxonomies, 'tax_name' ); $this->replace_custom_field_or_taxonomy_replace_vars( $unique_custom_taxonomies, $wpdb, 'tax_name', 'ct' ); } /** * Filters out all unique custom fields/taxonomies/etc. used in an AiOSEO replace var. * * @param string[] $meta_values An array of all the meta values that * contain one or more AIOSEO custom field replace vars * (in the form `#custom_field-xyz`). * @param string $aioseo_prefix The AiOSEO prefix to use * (e.g. `custom-field` for custom fields or `tax_name` for custom taxonomies). * * @return string[] An array of all the unique custom fields/taxonomies/etc. used in the replace vars. * E.g. `xyz` in the above example. */ protected function get_unique_custom_fields_or_taxonomies( $meta_values, $aioseo_prefix ) { $unique_custom_fields_or_taxonomies = []; foreach ( $meta_values as $meta_value ) { // Find all custom field replace vars, store them in `$matches`. preg_match_all( "/#$aioseo_prefix-([\w-]+)/", $meta_value, $matches, ); /* * `$matches[1]` contain the captured matches of the * first capturing group (the `([\w-]+)` in the regex above). */ $custom_fields_or_taxonomies = $matches[1]; foreach ( $custom_fields_or_taxonomies as $custom_field_or_taxonomy ) { $unique_custom_fields_or_taxonomies[ trim( $custom_field_or_taxonomy ) ] = 1; } } return array_keys( $unique_custom_fields_or_taxonomies ); } /** * Replaces every AIOSEO custom field/taxonomy/etc. replace var with the Yoast version. * * E.g. `#custom_field-xyz` becomes `%%cf_xyz%%`. * * @param string[] $unique_custom_fields_or_taxonomies An array of unique custom fields to replace the replace vars of. * @param wpdb $wpdb The WordPress database object. * @param string $aioseo_prefix The AiOSEO prefix to use * (e.g. `custom-field` for custom fields or `tax_name` for custom taxonomies). * @param string $yoast_prefix The Yoast prefix to use (e.g. `cf` for custom fields). * * @return void */ protected function replace_custom_field_or_taxonomy_replace_vars( $unique_custom_fields_or_taxonomies, $wpdb, $aioseo_prefix, $yoast_prefix ) { foreach ( $unique_custom_fields_or_taxonomies as $unique_custom_field_or_taxonomy ) { $aioseo_variable = "#{$aioseo_prefix}-{$unique_custom_field_or_taxonomy}"; $yoast_variable = "%%{$yoast_prefix}_{$unique_custom_field_or_taxonomy}%%"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( 'UPDATE tmp_meta_table SET meta_value = REPLACE( meta_value, %s, %s )', $aioseo_variable, $yoast_variable, ), ); } } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching /** * Retrieve all the meta values from the temporary meta table that contain * at least one AiOSEO custom field replace var. * * @param wpdb $wpdb The WordPress database object. * @param string $aioseo_prefix The AiOSEO prefix to use * (e.g. `custom-field` for custom fields or `tax_name` for custom taxonomies). * * @return string[] All meta values that contain at least one AioSEO custom field replace var. */ protected function get_meta_values_with_custom_field_or_taxonomy( $wpdb, $aioseo_prefix ) { return $wpdb->get_col( $wpdb->prepare( 'SELECT meta_value FROM tmp_meta_table WHERE meta_value LIKE %s', "%#$aioseo_prefix-%", ), ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching /** * Detects whether there is AIOSEO data to import by looking whether the AIOSEO data have been cleaned up. * * @return bool Boolean indicating whether there is something to import. */ protected function detect() { $aioseo_cleanup_action = YoastSEO()->classes->get( Aioseo_Cleanup_Action::class ); return ( $aioseo_cleanup_action->get_total_unindexed() > 0 ); } /** * Import AIOSEO post data from their custom indexable table. Not currently used. * * @return void */ protected function import() { // This is overriden from the import.js and never run. $aioseo_posts_import_action = YoastSEO()->classes->get( Aioseo_Posts_Importing_Action::class ); $aioseo_posts_import_action->index(); } } admin/import/plugins/class-abstract-plugin-importer.php 0000644 00000020454 15174712003 0017406 0 ustar 00 <?php /** * This file holds the abstract class for dealing with imports from other plugins. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Plugin_Importer. * * Class with functionality to import meta data from other plugins. */ abstract class WPSEO_Plugin_Importer { /** * Holds the import status object. * * @var WPSEO_Import_Status */ protected $status; /** * The plugin name. * * @var string */ protected $plugin_name; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys; /** * Class constructor. */ public function __construct() {} /** * Returns the string for the plugin we're importing from. * * @return string Plugin name. */ public function get_plugin_name() { return $this->plugin_name; } /** * Imports the settings and post meta data from another SEO plugin. * * @return WPSEO_Import_Status Import status object. */ public function run_import() { $this->status = new WPSEO_Import_Status( 'import', false ); if ( ! $this->detect() ) { return $this->status; } $this->status->set_status( $this->import() ); // Flush the entire cache, as we no longer know what's valid and what's not. wp_cache_flush(); return $this->status; } /** * Handles post meta data to import. * * @return bool Import success status. */ protected function import() { return $this->meta_keys_clone( $this->clone_keys ); } /** * Removes the plugin data from the database. * * @return WPSEO_Import_Status Import status object. */ public function run_cleanup() { $this->status = new WPSEO_Import_Status( 'cleanup', false ); if ( ! $this->detect() ) { return $this->status; } return $this->status->set_status( $this->cleanup() ); } /** * Removes the plugin data from the database. * * @return bool Cleanup status. */ protected function cleanup() { global $wpdb; if ( empty( $this->meta_key ) ) { return true; } $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $this->meta_key, ), ); $result = $wpdb->__get( 'result' ); if ( ! $result ) { $this->cleanup_error_msg(); } return $result; } /** * Sets the status message for when a cleanup has gone bad. * * @return void */ protected function cleanup_error_msg() { /* translators: %s is replaced with the plugin's name. */ $this->status->set_msg( sprintf( __( 'Cleanup of %s data failed.', 'wordpress-seo' ), $this->plugin_name ) ); } /** * Detects whether an import for this plugin is needed. * * @return WPSEO_Import_Status Import status object. */ public function run_detect() { $this->status = new WPSEO_Import_Status( 'detect', false ); if ( ! $this->detect() ) { return $this->status; } return $this->status->set_status( true ); } /** * Detects whether there is post meta data to import. * * @return bool Boolean indicating whether there is something to import. */ protected function detect() { global $wpdb; $meta_keys = wp_list_pluck( $this->clone_keys, 'old_key' ); $result = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) AS `count` FROM {$wpdb->postmeta} WHERE meta_key IN ( " . implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ) . ' )', $meta_keys, ), ); if ( $result === '0' ) { return false; } return true; } /** * Helper function to clone meta keys and (optionally) change their values in bulk. * * @param string $old_key The existing meta key. * @param string $new_key The new meta key. * @param array $replace_values An array, keys old value, values new values. * * @return bool Clone status. */ protected function meta_key_clone( $old_key, $new_key, $replace_values = [] ) { global $wpdb; // First we create a temp table with all the values for meta_key. $result = $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange -- This is intentional + temporary. "CREATE TEMPORARY TABLE tmp_meta_table SELECT * FROM {$wpdb->postmeta} WHERE meta_key = %s", $old_key, ), ); if ( $result === false ) { $this->set_missing_db_rights_status(); return false; } // Delete all the values in our temp table for posts that already have data for $new_key. $wpdb->query( $wpdb->prepare( "DELETE FROM tmp_meta_table WHERE post_id IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s )", WPSEO_Meta::$meta_prefix . $new_key, ), ); /* * We set meta_id to NULL so on re-insert into the postmeta table, MYSQL can set * new meta_id's and we don't get duplicates. */ $wpdb->query( 'UPDATE tmp_meta_table SET meta_id = NULL' ); // Now we rename the meta_key. $wpdb->query( $wpdb->prepare( 'UPDATE tmp_meta_table SET meta_key = %s', WPSEO_Meta::$meta_prefix . $new_key, ), ); $this->meta_key_clone_replace( $replace_values ); // With everything done, we insert all our newly cloned lines into the postmeta table. $wpdb->query( "INSERT INTO {$wpdb->postmeta} SELECT * FROM tmp_meta_table" ); // Now we drop our temporary table. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange -- This is intentional + a temporary table. $wpdb->query( 'DROP TEMPORARY TABLE IF EXISTS tmp_meta_table' ); return true; } /** * Clones multiple meta keys. * * @param array $clone_keys The keys to clone. * * @return bool Success status. */ protected function meta_keys_clone( $clone_keys ) { foreach ( $clone_keys as $clone_key ) { $result = $this->meta_key_clone( $clone_key['old_key'], $clone_key['new_key'], ( $clone_key['convert'] ?? [] ) ); if ( ! $result ) { return false; } } return true; } /** * Sets the import status to false and returns a message about why it failed. * * @return void */ protected function set_missing_db_rights_status() { $this->status->set_status( false ); /* translators: %s is replaced with Yoast SEO. */ $this->status->set_msg( sprintf( __( 'The %s importer functionality uses temporary database tables. It seems your WordPress install does not have the capability to do this, please consult your hosting provider.', 'wordpress-seo' ), 'Yoast SEO' ) ); } /** * Helper function to search for a key in an array and maybe save it as a meta field. * * @param string $plugin_key The key in the $data array to check. * @param string $yoast_key The identifier we use in our meta settings. * @param array $data The array of data for this post to sift through. * @param int $post_id The post ID. * * @return void */ protected function import_meta_helper( $plugin_key, $yoast_key, $data, $post_id ) { if ( ! empty( $data[ $plugin_key ] ) ) { $this->maybe_save_post_meta( $yoast_key, $data[ $plugin_key ], $post_id ); } } /** * Saves a post meta value if it doesn't already exist. * * @param string $new_key The key to save. * @param mixed $value The value to set the key to. * @param int $post_id The Post to save the meta for. * * @return void */ protected function maybe_save_post_meta( $new_key, $value, $post_id ) { // Big. Fat. Sigh. Mostly used for _yst_is_cornerstone, but might be useful for other hidden meta's. $key = WPSEO_Meta::$meta_prefix . $new_key; $wpseo_meta = true; if ( substr( $new_key, 0, 1 ) === '_' ) { $key = $new_key; $wpseo_meta = false; } $existing_value = get_post_meta( $post_id, $key, true ); if ( empty( $existing_value ) ) { if ( $wpseo_meta ) { WPSEO_Meta::set_value( $new_key, $value, $post_id ); return; } update_post_meta( $post_id, $new_key, $value ); } } /** * Replaces values in our temporary table according to our settings. * * @param array $replace_values Key value pair of values to replace with other values. * * @return void */ protected function meta_key_clone_replace( $replace_values ) { global $wpdb; // Now we replace values if needed. if ( is_array( $replace_values ) && $replace_values !== [] ) { foreach ( $replace_values as $old_value => $new_value ) { $wpdb->query( $wpdb->prepare( 'UPDATE tmp_meta_table SET meta_value = %s WHERE meta_value = %s', $new_value, $old_value, ), ); } } } } admin/import/plugins/class-import-squirrly.php 0000644 00000012056 15174712003 0015651 0 ustar 00 <?php /** * File with the class to handle data from Squirrly. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Squirrly post metadata. */ class WPSEO_Import_Squirrly extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'Squirrly SEO'; /** * Holds the name of the table Squirrly uses to store data. * * @var string */ protected $table_name; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_sq_post_keyword'; /** * Data to import from (and the target to field) the serialized array stored in the SEO field in the Squirrly table. * * @var array */ protected $seo_field_keys = [ 'noindex' => 'meta-robots-noindex', 'nofollow' => 'meta-robots-nofollow', 'title' => 'title', 'description' => 'metadesc', 'canonical' => 'canonical', 'cornerstone' => '_yst_is_cornerstone', 'tw_media' => 'twitter-image', 'tw_title' => 'twitter-title', 'tw_description' => 'twitter-description', 'og_title' => 'opengraph-title', 'og_description' => 'opengraph-description', 'og_media' => 'opengraph-image', 'focuskw' => 'focuskw', ]; /** * WPSEO_Import_Squirrly constructor. */ public function __construct() { parent::__construct(); global $wpdb; $this->table_name = $wpdb->prefix . 'qss'; } /** * Imports the post meta values to Yoast SEO. * * @return bool Import success status. */ protected function import() { $results = $this->retrieve_posts(); foreach ( $results as $post ) { $return = $this->import_post_values( $post->identifier ); if ( ! $return ) { return false; } } return true; } /** * Retrieve the posts from the Squirrly Database. * * @return array Array of post IDs from the DB. */ protected function retrieve_posts() { global $wpdb; return $wpdb->get_results( $wpdb->prepare( $this->retrieve_posts_query(), get_current_blog_id(), ), ); } /** * Returns the query to return an identifier for the posts to import. * * @return string Query to get post ID's from the DB. */ protected function retrieve_posts_query() { return "SELECT post_id AS identifier FROM {$this->table_name} WHERE blog_id = %d"; } /** * Removes the DB table and the post meta field Squirrly creates. * * @return bool Cleanup status. */ protected function cleanup() { global $wpdb; // If we can clean, let's clean. $wpdb->query( "DROP TABLE {$this->table_name}" ); // This removes the post meta field for the focus keyword from the DB. parent::cleanup(); // If we can still see the table, something went wrong. if ( $this->detect() ) { $this->cleanup_error_msg(); return false; } return true; } /** * Detects whether there is post meta data to import. * * @return bool Boolean indicating whether there is something to import. */ protected function detect() { global $wpdb; $result = $wpdb->get_var( "SHOW TABLES LIKE '{$this->table_name}'" ); if ( is_wp_error( $result ) || $result === null ) { return false; } return true; } /** * Imports the data of a post out of Squirrly's DB table. * * @param mixed $post_identifier Post identifier, can be ID or string. * * @return bool Import status. */ private function import_post_values( $post_identifier ) { $data = $this->retrieve_post_data( $post_identifier ); if ( ! $data ) { return false; } if ( ! is_numeric( $post_identifier ) ) { $post_id = url_to_postid( $post_identifier ); } if ( is_numeric( $post_identifier ) ) { $post_id = (int) $post_identifier; $data['focuskw'] = $this->maybe_add_focus_kw( $post_identifier ); } foreach ( $this->seo_field_keys as $squirrly_key => $yoast_key ) { $this->import_meta_helper( $squirrly_key, $yoast_key, $data, $post_id ); } return true; } /** * Retrieves the Squirrly SEO data for a post from the DB. * * @param int $post_identifier Post ID. * * @return array|bool Array of data or false. */ private function retrieve_post_data( $post_identifier ) { global $wpdb; if ( is_numeric( $post_identifier ) ) { $post_identifier = (int) $post_identifier; $query_where = 'post_id = %d'; } if ( ! is_numeric( $post_identifier ) ) { $query_where = 'URL = %s'; } $replacements = [ get_current_blog_id(), $post_identifier, ]; $data = $wpdb->get_var( $wpdb->prepare( "SELECT seo FROM {$this->table_name} WHERE blog_id = %d AND " . $query_where, $replacements, ), ); if ( ! $data || is_wp_error( $data ) ) { return false; } $data = maybe_unserialize( $data ); return $data; } /** * Squirrly stores the focus keyword in post meta. * * @param int $post_id Post ID. * * @return string The focus keyword. */ private function maybe_add_focus_kw( $post_id ) { $focuskw = get_post_meta( $post_id, '_sq_post_keyword', true ); if ( $focuskw ) { $focuskw = json_decode( $focuskw ); return $focuskw->keyword; } return ''; } } admin/import/plugins/class-import-platinum-seo-pack.php 0000644 00000005511 15174712003 0017306 0 ustar 00 <?php /** * File with the class to handle data from Platinum SEO Pack. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Ultimate SEO post metadata. */ class WPSEO_Import_Platinum_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'Platinum SEO Pack'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = 'title'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => 'description', 'new_key' => 'metadesc', ], [ 'old_key' => 'title', 'new_key' => 'title', ], ]; /** * Runs the import of post meta keys stored by Platinum SEO Pack. * * @return bool */ protected function import() { $return = parent::import(); if ( $return ) { $this->import_robots_meta(); } return $return; } /** * Cleans up all the meta values Platinum SEO pack creates. * * @return bool */ protected function cleanup() { $this->meta_key = 'title'; parent::cleanup(); $this->meta_key = 'description'; parent::cleanup(); $this->meta_key = 'metarobots'; parent::cleanup(); return true; } /** * Finds all the robotsmeta fields to import and deals with them. * * There are four potential values that Platinum SEO stores: * - index,folllow * - index,nofollow * - noindex,follow * - noindex,nofollow * * We only have to deal with the latter 3, the first is our default. * * @return void */ protected function import_robots_meta() { $this->import_by_meta_robots( 'index,nofollow', [ 'nofollow' ] ); $this->import_by_meta_robots( 'noindex,follow', [ 'noindex' ] ); $this->import_by_meta_robots( 'noindex,nofollow', [ 'noindex', 'nofollow' ] ); } /** * Imports the values for all index, nofollow posts. * * @param string $value The meta robots value to find posts for. * @param array $metas The meta field(s) to save. * * @return void */ protected function import_by_meta_robots( $value, $metas ) { $posts = $this->find_posts_by_robots_meta( $value ); if ( ! $posts ) { return; } foreach ( $posts as $post_id ) { foreach ( $metas as $meta ) { $this->maybe_save_post_meta( 'meta-robots-' . $meta, 1, $post_id ); } } } /** * Finds posts by a given meta robots value. * * @param string $meta_value Robots meta value. * * @return array|bool Array of Post IDs on success, false on failure. */ protected function find_posts_by_robots_meta( $meta_value ) { $posts = get_posts( [ 'post_type' => 'any', 'meta_key' => 'robotsmeta', 'meta_value' => $meta_value, 'order' => 'ASC', 'fields' => 'ids', 'nopaging' => true, ], ); if ( empty( $posts ) ) { return false; } return $posts; } } admin/import/plugins/class-import-wpseo.php 0000644 00000016370 15174712003 0015117 0 ustar 00 <?php /** * File with the class to handle data from wpSEO.de. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_WPSEO. * * Class with functionality to import & clean wpSEO.de post metadata. */ class WPSEO_Import_WPSEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'wpSEO.de'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_wpseo_edit_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_wpseo_edit_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_wpseo_edit_title', 'new_key' => 'title', ], [ 'old_key' => '_wpseo_edit_canonical', 'new_key' => 'canonical', ], [ 'old_key' => '_wpseo_edit_og_title', 'new_key' => 'opengraph-title', ], [ 'old_key' => '_wpseo_edit_og_description', 'new_key' => 'opengraph-description', ], [ 'old_key' => '_wpseo_edit_og_image', 'new_key' => 'opengraph-image', ], [ 'old_key' => '_wpseo_edit_twittercard_title', 'new_key' => 'twitter-title', ], [ 'old_key' => '_wpseo_edit_twittercard_description', 'new_key' => 'twitter-description', ], [ 'old_key' => '_wpseo_edit_twittercard_image', 'new_key' => 'twitter-image', ], ]; /** * The values 1 - 6 are the configured values from wpSEO. This array will map the values of wpSEO to our values. * * There are some double array like 1-6 and 3-4. The reason is they only set the index value. The follow value is * the default we use in the cases there isn't a follow value present. * * @var array */ private $robot_values = [ // In wpSEO: index, follow. 1 => [ 'index' => 2, 'follow' => 0, ], // In wpSEO: index, nofollow. 2 => [ 'index' => 2, 'follow' => 1, ], // In wpSEO: noindex. 3 => [ 'index' => 1, 'follow' => 0, ], // In wpSEO: noindex, follow. 4 => [ 'index' => 1, 'follow' => 0, ], // In wpSEO: noindex, nofollow. 5 => [ 'index' => 1, 'follow' => 1, ], // In wpSEO: index. 6 => [ 'index' => 2, 'follow' => 0, ], ]; /** * Imports wpSEO settings. * * @return bool Import success status. */ protected function import() { $status = parent::import(); if ( $status ) { $this->import_post_robots(); $this->import_taxonomy_metas(); } return $status; } /** * Removes wpseo.de post meta's. * * @return bool Cleanup status. */ protected function cleanup() { $this->cleanup_term_meta(); $result = $this->cleanup_post_meta(); return $result; } /** * Detects whether there is post meta data to import. * * @return bool Boolean indicating whether there is something to import. */ protected function detect() { if ( parent::detect() ) { return true; } global $wpdb; $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'wpseo_category_%'" ); if ( $count !== '0' ) { return true; } return false; } /** * Imports the robot values from WPSEO plugin. These have to be converted to the Yoast format. * * @return void */ private function import_post_robots() { $query_posts = new WP_Query( 'post_type=any&meta_key=_wpseo_edit_robots&order=ASC&fields=ids&nopaging=true' ); if ( ! empty( $query_posts->posts ) ) { foreach ( array_values( $query_posts->posts ) as $post_id ) { $this->import_post_robot( $post_id ); } } } /** * Gets the wpSEO robot value and map this to Yoast SEO values. * * @param int $post_id The post id of the current post. * * @return void */ private function import_post_robot( $post_id ) { $wpseo_robots = get_post_meta( $post_id, '_wpseo_edit_robots', true ); $robot_value = $this->get_robot_value( $wpseo_robots ); // Saving the new meta values for Yoast SEO. $this->maybe_save_post_meta( 'meta-robots-noindex', $robot_value['index'], $post_id ); $this->maybe_save_post_meta( 'meta-robots-nofollow', $robot_value['follow'], $post_id ); } /** * Imports the taxonomy metas from wpSEO. * * @return void */ private function import_taxonomy_metas() { $terms = get_terms( [ 'taxonomy' => get_taxonomies(), 'hide_empty' => false, ], ); $tax_meta = get_option( 'wpseo_taxonomy_meta' ); foreach ( $terms as $term ) { $this->import_taxonomy_description( $tax_meta, $term->taxonomy, $term->term_id ); $this->import_taxonomy_robots( $tax_meta, $term->taxonomy, $term->term_id ); } update_option( 'wpseo_taxonomy_meta', $tax_meta ); } /** * Imports the meta description to Yoast SEO. * * @param array $tax_meta The array with the current metadata. * @param string $taxonomy String with the name of the taxonomy. * @param string $term_id The ID of the current term. * * @return void */ private function import_taxonomy_description( &$tax_meta, $taxonomy, $term_id ) { $description = get_option( 'wpseo_' . $taxonomy . '_' . $term_id, false ); if ( $description !== false ) { // Import description. $tax_meta[ $taxonomy ][ $term_id ]['wpseo_desc'] = $description; } } /** * Imports the robot value to Yoast SEO. * * @param array $tax_meta The array with the current metadata. * @param string $taxonomy String with the name of the taxonomy. * @param string $term_id The ID of the current term. * * @return void */ private function import_taxonomy_robots( &$tax_meta, $taxonomy, $term_id ) { $wpseo_robots = get_option( 'wpseo_' . $taxonomy . '_' . $term_id . '_robots', false ); if ( $wpseo_robots === false ) { return; } // The value 1, 2 and 6 are the index values in wpSEO. $new_robot_value = 'noindex'; if ( in_array( (int) $wpseo_robots, [ 1, 2, 6 ], true ) ) { $new_robot_value = 'index'; } $tax_meta[ $taxonomy ][ $term_id ]['wpseo_noindex'] = $new_robot_value; } /** * Deletes the wpSEO taxonomy meta data. * * @param string $taxonomy String with the name of the taxonomy. * @param string $term_id The ID of the current term. * * @return void */ private function delete_taxonomy_metas( $taxonomy, $term_id ) { delete_option( 'wpseo_' . $taxonomy . '_' . $term_id ); delete_option( 'wpseo_' . $taxonomy . '_' . $term_id . '_robots' ); } /** * Gets the robot config by given wpSEO robots value. * * @param string $wpseo_robots The value in wpSEO that needs to be converted to the Yoast format. * * @return string The correct robot value. */ private function get_robot_value( $wpseo_robots ) { if ( array_key_exists( $wpseo_robots, $this->robot_values ) ) { return $this->robot_values[ $wpseo_robots ]; } return $this->robot_values[1]; } /** * Deletes wpSEO postmeta from the database. * * @return bool Cleanup status. */ private function cleanup_post_meta() { global $wpdb; // If we get to replace the data, let's do some proper cleanup. return $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_wpseo_edit_%'" ); } /** * Cleans up the wpSEO term meta. * * @return void */ private function cleanup_term_meta() { $terms = get_terms( [ 'taxonomy' => get_taxonomies(), 'hide_empty' => false, ], ); foreach ( $terms as $term ) { $this->delete_taxonomy_metas( $term->taxonomy, $term->term_id ); } } } admin/import/plugins/class-import-aioseo.php 0000644 00000004717 15174712003 0015243 0 ustar 00 <?php /** * File with the class to handle data from All in One SEO Pack, versions 3 and under. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean All in One SEO Pack post metadata, versions 3 and under. */ class WPSEO_Import_AIOSEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'All In One SEO Pack'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_aioseop_%'; /** * OpenGraph keys to import. * * @var array */ protected $import_keys = [ 'aioseop_opengraph_settings_title' => 'opengraph-title', 'aioseop_opengraph_settings_desc' => 'opengraph-description', 'aioseop_opengraph_settings_customimg' => 'opengraph-image', 'aioseop_opengraph_settings_customimg_twitter' => 'twitter-image', ]; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_aioseop_title', 'new_key' => 'title', ], [ 'old_key' => '_aioseop_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_aioseop_noindex', 'new_key' => 'meta-robots-noindex', 'convert' => [ 'on' => 1 ], ], [ 'old_key' => '_aioseop_nofollow', 'new_key' => 'meta-robots-nofollow', 'convert' => [ 'on' => 1 ], ], ]; /** * Import All In One SEO meta values. * * @return bool Import success status. */ protected function import() { $status = parent::import(); if ( $status ) { $this->import_opengraph(); } return $status; } /** * Imports the OpenGraph and Twitter settings for all posts. * * @return bool */ protected function import_opengraph() { $query_posts = new WP_Query( 'post_type=any&meta_key=_aioseop_opengraph_settings&order=ASC&fields=ids&nopaging=true' ); if ( ! empty( $query_posts->posts ) ) { foreach ( array_values( $query_posts->posts ) as $post_id ) { $this->import_post_opengraph( $post_id ); } } return true; } /** * Imports the OpenGraph and Twitter settings for a single post. * * @param int $post_id Post ID. * * @return void */ private function import_post_opengraph( $post_id ) { $meta = get_post_meta( $post_id, '_aioseop_opengraph_settings', true ); $meta = maybe_unserialize( $meta ); foreach ( $this->import_keys as $old_key => $new_key ) { $this->maybe_save_post_meta( $new_key, $meta[ $old_key ], $post_id ); } } } admin/import/plugins/class-import-greg-high-performance-seo.php 0000644 00000001371 15174712003 0020701 0 ustar 00 <?php /** * File with the class to handle data from Ultimate SEO. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Ultimate SEO post metadata. */ class WPSEO_Import_Greg_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = "Greg's High Performance SEO"; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_ghpseo_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_ghpseo_alternative_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_ghpseo_secondary_title', 'new_key' => 'title', ], ]; } admin/import/plugins/class-import-ultimate-seo.php 0000644 00000002262 15174712003 0016365 0 ustar 00 <?php /** * File with the class to handle data from Ultimate SEO. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Ultimate SEO post metadata. */ class WPSEO_Import_Ultimate_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'Ultimate SEO'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_su_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_su_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_su_title', 'new_key' => 'title', ], [ 'old_key' => '_su_og_title', 'new_key' => 'opengraph-title', ], [ 'old_key' => '_su_og_description', 'new_key' => 'opengraph-description', ], [ 'old_key' => '_su_og_image', 'new_key' => 'opengraph-image', ], [ 'old_key' => '_su_meta_robots_noindex', 'new_key' => 'meta-robots-noindex', 'convert' => [ 'on' => 1 ], ], [ 'old_key' => '_su_meta_robots_nofollow', 'new_key' => 'meta-robots-nofollow', 'convert' => [ 'on' => 1 ], ], ]; } admin/import/plugins/class-import-smartcrawl.php 0000644 00000006221 15174712003 0016133 0 ustar 00 <?php /** * File with the class to handle data from Smartcrawl SEO. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Smartcrawl SEO post metadata. */ class WPSEO_Import_Smartcrawl_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'Smartcrawl SEO'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_wds_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_wds_metadesc', 'new_key' => 'metadesc', ], [ 'old_key' => '_wds_title', 'new_key' => 'title', ], [ 'old_key' => '_wds_canonical', 'new_key' => 'canonical', ], [ 'old_key' => '_wds_focus-keywords', 'new_key' => 'focuskw', ], [ 'old_key' => '_wds_meta-robots-noindex', 'new_key' => 'meta-robots-noindex', ], [ 'old_key' => '_wds_meta-robots-nofollow', 'new_key' => 'meta-robots-nofollow', ], ]; /** * Used for importing Twitter and Facebook meta's. * * @var array */ protected $social_keys = []; /** * Handles post meta data to import. * * @return bool Import success status. */ protected function import() { $return = parent::import(); if ( $return ) { $this->import_opengraph(); $this->import_twitter(); } return $return; } /** * Imports the OpenGraph meta keys saved by Smartcrawl. * * @return bool Import status. */ protected function import_opengraph() { $this->social_keys = [ 'title' => 'opengraph-title', 'description' => 'opengraph-description', 'images' => 'opengraph-image', ]; return $this->post_find_import( '_wds_opengraph' ); } /** * Imports the Twitter meta keys saved by Smartcrawl. * * @return bool Import status. */ protected function import_twitter() { $this->social_keys = [ 'title' => 'twitter-title', 'description' => 'twitter-description', ]; return $this->post_find_import( '_wds_twitter' ); } /** * Imports a post's serialized post meta values. * * @param int $post_id Post ID. * @param string $key The meta key to import. * * @return void */ protected function import_serialized_post_meta( $post_id, $key ) { $data = get_post_meta( $post_id, $key, true ); $data = maybe_unserialize( $data ); foreach ( $this->social_keys as $key => $meta_key ) { if ( ! isset( $data[ $key ] ) ) { return; } $value = $data[ $key ]; if ( is_array( $value ) ) { $value = $value[0]; } $this->maybe_save_post_meta( $meta_key, $value, $post_id ); } } /** * Finds all the posts with a certain meta key and imports its values. * * @param string $key The meta key to search for. * * @return bool Import status. */ protected function post_find_import( $key ) { $query_posts = new WP_Query( 'post_type=any&meta_key=' . $key . '&order=ASC&fields=ids&nopaging=true' ); if ( empty( $query_posts->posts ) ) { return false; } foreach ( array_values( $query_posts->posts ) as $post_id ) { $this->import_serialized_post_meta( $post_id, $key ); } return true; } } admin/import/plugins/class-import-headspace.php 0000644 00000001762 15174712003 0015676 0 ustar 00 <?php /** * File with the class to handle data from HeadSpace. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_HeadSpace. * * Class with functionality to import & clean HeadSpace SEO post metadata. */ class WPSEO_Import_HeadSpace extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'HeadSpace SEO'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_headspace_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_headspace_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_headspace_page_title', 'new_key' => 'title', ], [ 'old_key' => '_headspace_noindex', 'new_key' => 'meta-robots-noindex', 'convert' => [ 'on' => 1 ], ], [ 'old_key' => '_headspace_nofollow', 'new_key' => 'meta-robots-nofollow', 'convert' => [ 'on' => 1 ], ], ]; } admin/import/plugins/class-importers.php 0000644 00000001634 15174712003 0014473 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Plugin_Importers. * * Object which contains all importers. */ class WPSEO_Plugin_Importers { /** * List of supported importers. * * @var array */ private static $importers = [ 'WPSEO_Import_AIOSEO', 'WPSEO_Import_AIOSEO_V4', 'WPSEO_Import_Greg_SEO', 'WPSEO_Import_HeadSpace', 'WPSEO_Import_Jetpack_SEO', 'WPSEO_Import_WP_Meta_SEO', 'WPSEO_Import_Platinum_SEO', 'WPSEO_Import_Premium_SEO_Pack', 'WPSEO_Import_RankMath', 'WPSEO_Import_SEOPressor', 'WPSEO_Import_SEO_Framework', 'WPSEO_Import_Smartcrawl_SEO', 'WPSEO_Import_Squirrly', 'WPSEO_Import_Ultimate_SEO', 'WPSEO_Import_WooThemes_SEO', 'WPSEO_Import_WPSEO', ]; /** * Returns an array of importers available. * * @return array Available importers. */ public static function get() { return self::$importers; } } admin/import/plugins/class-import-woothemes-seo.php 0000644 00000004753 15174712003 0016562 0 ustar 00 <?php /** * File with the class to handle data from WooThemes SEO. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_WooThemes_SEO * * Class with functionality to import & clean WooThemes SEO post metadata. */ class WPSEO_Import_WooThemes_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'WooThemes SEO'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = 'seo_title'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => 'seo_description', 'new_key' => 'metadesc', ], [ 'old_key' => 'seo_title', 'new_key' => 'title', ], [ 'old_key' => 'seo_noindex', 'new_key' => 'meta-robots-noindex', ], [ 'old_key' => 'seo_follow', 'new_key' => 'meta-robots-nofollow', ], ]; /** * Holds the meta fields we can delete after import. * * @var array */ protected $cleanup_metas = [ 'seo_follow', 'seo_noindex', 'seo_title', 'seo_description', 'seo_keywords', ]; /** * Holds the options we can delete after import. * * @var array */ protected $cleanup_options = [ 'seo_woo_archive_layout', 'seo_woo_single_layout', 'seo_woo_page_layout', 'seo_woo_wp_title', 'seo_woo_meta_single_desc', 'seo_woo_meta_single_key', 'seo_woo_home_layout', ]; /** * Cleans up the WooThemes SEO settings. * * @return bool Cleanup status. */ protected function cleanup() { $result = $this->cleanup_meta(); if ( $result ) { $this->cleanup_options(); } return $result; } /** * Removes the Woo Options from the database. * * @return void */ private function cleanup_options() { foreach ( $this->cleanup_options as $option ) { delete_option( $option ); } } /** * Removes the post meta fields from the database. * * @return bool Cleanup status. */ private function cleanup_meta() { foreach ( $this->cleanup_metas as $key ) { $result = $this->cleanup_meta_key( $key ); if ( ! $result ) { return false; } } return true; } /** * Removes a single meta field from the postmeta table in the database. * * @param string $key The meta_key to delete. * * @return bool Cleanup status. */ private function cleanup_meta_key( $key ) { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s", $key, ), ); return $wpdb->__get( 'result' ); } } admin/import/plugins/class-import-wp-meta-seo.php 0000644 00000003056 15174712003 0016115 0 ustar 00 <?php /** * File with the class to handle data from WP Meta SEO. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean WP Meta SEO post metadata. */ class WPSEO_Import_WP_Meta_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'WP Meta SEO'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_metaseo_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_metaseo_metadesc', 'new_key' => 'metadesc', ], [ 'old_key' => '_metaseo_metatitle', 'new_key' => 'title', ], [ 'old_key' => '_metaseo_metaopengraph-title', 'new_key' => 'opengraph-title', ], [ 'old_key' => '_metaseo_metaopengraph-desc', 'new_key' => 'opengraph-description', ], [ 'old_key' => '_metaseo_metaopengraph-image', 'new_key' => 'opengraph-image', ], [ 'old_key' => '_metaseo_metatwitter-title', 'new_key' => 'twitter-title', ], [ 'old_key' => '_metaseo_metatwitter-desc', 'new_key' => 'twitter-description', ], [ 'old_key' => '_metaseo_metatwitter-image', 'new_key' => 'twitter-image', ], [ 'old_key' => '_metaseo_metaindex', 'new_key' => 'meta-robots-noindex', 'convert' => [ 'index' => 0, 'noindex' => 1, ], ], [ 'old_key' => '_metaseo_metafollow', 'new_key' => 'meta-robots-nofollow', 'convert' => [ 'follow' => 0, 'nofollow' => 1, ], ], ]; } admin/import/plugins/class-import-seo-framework.php 0000644 00000003464 15174712003 0016543 0 ustar 00 <?php /** * File with the class to handle data from SEO Framework. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean SEO Framework post metadata. */ class WPSEO_Import_SEO_Framework extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'The SEO Framework'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = '_genesis_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => '_genesis_description', 'new_key' => 'metadesc', ], [ 'old_key' => '_genesis_title', 'new_key' => 'title', ], [ 'old_key' => '_genesis_noindex', 'new_key' => 'meta-robots-noindex', ], [ 'old_key' => '_genesis_nofollow', 'new_key' => 'meta-robots-nofollow', ], [ 'old_key' => '_genesis_canonical_uri', 'new_key' => 'canonical', ], [ 'old_key' => '_open_graph_title', 'new_key' => 'opengraph-title', ], [ 'old_key' => '_open_graph_description', 'new_key' => 'opengraph-description', ], [ 'old_key' => '_social_image_url', 'new_key' => 'opengraph-image', ], [ 'old_key' => '_twitter_title', 'new_key' => 'twitter-title', ], [ 'old_key' => '_twitter_description', 'new_key' => 'twitter-description', ], ]; /** * Removes all the metadata set by the SEO Framework plugin. * * @return bool */ protected function cleanup() { $set1 = parent::cleanup(); $this->meta_key = '_social_image_%'; $set2 = parent::cleanup(); $this->meta_key = '_twitter_%'; $set3 = parent::cleanup(); $this->meta_key = '_open_graph_%'; $set4 = parent::cleanup(); return ( $set1 || $set2 || $set3 || $set4 ); } } admin/import/plugins/class-import-jetpack.php 0000644 00000001333 15174712003 0015374 0 ustar 00 <?php /** * File with the class to handle data from Jetpack's Advanced SEO settings. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_Jetpack_SEO. * * Class with functionality to import & clean Jetpack SEO post metadata. */ class WPSEO_Import_Jetpack_SEO extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'Jetpack'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = 'advanced_seo_description'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => 'advanced_seo_description', 'new_key' => 'metadesc', ], ]; } admin/import/plugins/class-import-premium-seo-pack.php 0000644 00000001455 15174712003 0017136 0 ustar 00 <?php /** * File with the class to handle data from Premium SEO Pack. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import & clean Premium SEO Pack post metadata. */ class WPSEO_Import_Premium_SEO_Pack extends WPSEO_Import_Squirrly { /** * The plugin name. * * @var string */ protected $plugin_name = 'Premium SEO Pack'; /** * WPSEO_Import_Premium_SEO_Pack constructor. */ public function __construct() { parent::__construct(); global $wpdb; $this->table_name = $wpdb->prefix . 'psp'; $this->meta_key = ''; } /** * Returns the query to return an identifier for the posts to import. * * @return string */ protected function retrieve_posts_query() { return "SELECT URL AS identifier FROM {$this->table_name} WHERE blog_id = %d"; } } admin/import/plugins/class-import-rankmath.php 0000644 00000011255 15174712003 0015564 0 ustar 00 <?php /** * File with the class to handle data from RankMath. * * @package WPSEO\Admin\Import\Plugins */ /** * Class with functionality to import RankMath post metadata. */ class WPSEO_Import_RankMath extends WPSEO_Plugin_Importer { /** * The plugin name. * * @var string */ protected $plugin_name = 'RankMath'; /** * Meta key, used in SQL LIKE clause for delete query. * * @var string */ protected $meta_key = 'rank_math_%'; /** * Array of meta keys to detect and import. * * @var array */ protected $clone_keys = [ [ 'old_key' => 'rank_math_description', 'new_key' => 'metadesc', ], [ 'old_key' => 'rank_math_title', 'new_key' => 'title', ], [ 'old_key' => 'rank_math_canonical_url', 'new_key' => 'canonical', ], [ 'old_key' => 'rank_math_primary_category', 'new_key' => 'primary_category', ], [ 'old_key' => 'rank_math_facebook_title', 'new_key' => 'opengraph-title', ], [ 'old_key' => 'rank_math_facebook_description', 'new_key' => 'opengraph-description', ], [ 'old_key' => 'rank_math_facebook_image', 'new_key' => 'opengraph-image', ], [ 'old_key' => 'rank_math_facebook_image_id', 'new_key' => 'opengraph-image-id', ], [ 'old_key' => 'rank_math_twitter_title', 'new_key' => 'twitter-title', ], [ 'old_key' => 'rank_math_twitter_description', 'new_key' => 'twitter-description', ], [ 'old_key' => 'rank_math_twitter_image', 'new_key' => 'twitter-image', ], [ 'old_key' => 'rank_math_twitter_image_id', 'new_key' => 'twitter-image-id', ], [ 'old_key' => 'rank_math_focus_keyword', 'new_key' => 'focuskw', ], ]; /** * Handles post meta data to import. * * @return bool Import success status. */ protected function import() { global $wpdb; // Replace % with %% as their variables are the same except for that. $wpdb->query( "UPDATE $wpdb->postmeta SET meta_value = REPLACE( meta_value, '%', '%%' ) WHERE meta_key IN ( 'rank_math_description', 'rank_math_title' )" ); $this->import_meta_robots(); $return = $this->meta_keys_clone( $this->clone_keys ); // Return %% to % so our import is non-destructive. $wpdb->query( "UPDATE $wpdb->postmeta SET meta_value = REPLACE( meta_value, '%%', '%' ) WHERE meta_key IN ( 'rank_math_description', 'rank_math_title' )" ); if ( $return ) { $this->import_settings(); } return $return; } /** * RankMath stores robots meta quite differently, so we have to parse it out. * * @return void */ private function import_meta_robots() { global $wpdb; $post_metas = $wpdb->get_results( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = 'rank_math_robots'" ); foreach ( $post_metas as $post_meta ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- Reason: We can't control the form in which Rankmath sends the data. $robots_values = unserialize( $post_meta->meta_value ); foreach ( [ 'noindex', 'nofollow' ] as $directive ) { $directive_key = array_search( $directive, $robots_values, true ); if ( $directive_key !== false ) { update_post_meta( $post_meta->post_id, '_yoast_wpseo_meta-robots-' . $directive, 1 ); unset( $robots_values[ $directive_key ] ); } } if ( count( $robots_values ) > 0 ) { $value = implode( ',', $robots_values ); update_post_meta( $post_meta->post_id, '_yoast_wpseo_meta-robots-adv', $value ); } } } /** * Imports some of the RankMath settings. * * @return void */ private function import_settings() { $settings = [ 'title_separator' => 'separator', 'homepage_title' => 'title-home-wpseo', 'homepage_description' => 'metadesc-home-wpseo', 'author_archive_title' => 'title-author-wpseo', 'date_archive_title' => 'title-archive-wpseo', 'search_title' => 'title-search-wpseo', '404_title' => 'title-404-wpseo', 'pt_post_title' => 'title-post', 'pt_page_title' => 'title-page', ]; $options = get_option( 'rank-math-options-titles' ); foreach ( $settings as $import_setting_key => $setting_key ) { if ( ! empty( $options[ $import_setting_key ] ) ) { $value = $options[ $import_setting_key ]; // Make sure replace vars work. $value = str_replace( '%', '%%', $value ); WPSEO_Options::set( $setting_key, $value ); } } } /** * Removes the plugin data from the database. * * @return bool Cleanup status. */ protected function cleanup() { $return = parent::cleanup(); if ( $return ) { global $wpdb; $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'rank-math-%'" ); $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE '%rank_math%'" ); } return $return; } } admin/import/class-import-detector.php 0000644 00000001355 15174712003 0014107 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_Plugins_Detector. * * Class with functionality to detect whether we should import from another SEO plugin. */ class WPSEO_Import_Plugins_Detector { /** * Plugins we need to import from. * * @var array */ public $needs_import = []; /** * Detects whether we need to import anything. * * @return void */ public function detect() { foreach ( WPSEO_Plugin_Importers::get() as $importer_class ) { $importer = new $importer_class(); $detect = new WPSEO_Import_Plugin( $importer, 'detect' ); if ( $detect->status->status ) { $this->needs_import[ $importer_class ] = $importer->get_plugin_name(); } } } } admin/import/class-import-status.php 0000644 00000005312 15174712003 0013616 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Import */ /** * Class WPSEO_ImportStatus. * * Holds the status of and message about imports. */ class WPSEO_Import_Status { /** * The import status. * * @var bool */ public $status = false; /** * The import message. * * @var string */ private $msg = ''; /** * The type of action performed. * * @var string */ private $action; /** * WPSEO_Import_Status constructor. * * @param string $action The type of import action. * @param bool $status The status of the import. * @param string $msg Extra messages about the status. */ public function __construct( $action, $status, $msg = '' ) { $this->action = $action; $this->status = $status; $this->msg = $msg; } /** * Get the import message. * * @return string Message about current status. */ public function get_msg() { if ( $this->msg !== '' ) { return $this->msg; } if ( $this->status === false ) { /* translators: %s is replaced with the name of the plugin we're trying to find data from. */ return __( '%s data not found.', 'wordpress-seo' ); } return $this->get_default_success_message(); } /** * Get the import action. * * @return string Import action type. */ public function get_action() { return $this->action; } /** * Set the import action, set status to false. * * @param string $action The type of action to set as import action. * * @return void */ public function set_action( $action ) { $this->action = $action; $this->status = false; } /** * Sets the importer status message. * * @param string $msg The message to set. * * @return void */ public function set_msg( $msg ) { $this->msg = $msg; } /** * Sets the importer status. * * @param bool $status The status to set. * * @return WPSEO_Import_Status The current object. */ public function set_status( $status ) { $this->status = (bool) $status; return $this; } /** * Returns a success message depending on the action. * * @return string Returns a success message for the current action. */ private function get_default_success_message() { switch ( $this->action ) { case 'import': /* translators: %s is replaced with the name of the plugin we're importing data from. */ return __( '%s data successfully imported.', 'wordpress-seo' ); case 'cleanup': /* translators: %s is replaced with the name of the plugin we're removing data from. */ return __( '%s data successfully removed.', 'wordpress-seo' ); case 'detect': default: /* translators: %s is replaced with the name of the plugin we've found data from. */ return __( '%s data found.', 'wordpress-seo' ); } } } admin/import/class-import-plugin.php 0000644 00000002667 15174712003 0013603 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Import\Plugins */ /** * Class WPSEO_Import_Plugin. * * Class with functionality to import Yoast SEO settings from other plugins. */ class WPSEO_Import_Plugin { /** * Holds the status of and message about imports. * * @var WPSEO_Import_Status */ public $status; /** * Class with functionality to import meta data from other plugins. * * @var WPSEO_Plugin_Importer */ protected $importer; /** * Import class constructor. * * @param WPSEO_Plugin_Importer $importer The importer that needs to perform this action. * @param string $action The action to perform. */ public function __construct( WPSEO_Plugin_Importer $importer, $action ) { $this->importer = $importer; switch ( $action ) { case 'cleanup': $this->status = $this->importer->run_cleanup(); break; case 'import': $this->status = $this->importer->run_import(); break; case 'detect': default: $this->status = $this->importer->run_detect(); } $this->status->set_msg( $this->complete_msg( $this->status->get_msg() ) ); } /** * Convenience function to replace %s with plugin name in import message. * * @param string $msg Message string. * * @return string Returns message with plugin name instead of replacement variables. */ protected function complete_msg( $msg ) { return sprintf( $msg, $this->importer->get_plugin_name() ); } } admin/import/class-import-settings.php 0000644 00000005714 15174712003 0014141 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Import */ /** * Class WPSEO_Import_Settings. * * Class with functionality to import the Yoast SEO settings. */ class WPSEO_Import_Settings { /** * Nonce action key. * * @var string */ public const NONCE_ACTION = 'wpseo-import-settings'; /** * Holds the import status instance. * * @var WPSEO_Import_Status */ public $status; /** * Holds the old WPSEO version. * * @var string */ private $old_wpseo_version; /** * Class constructor. */ public function __construct() { $this->status = new WPSEO_Import_Status( 'import', false ); } /** * Imports the data submitted by the user. * * @return void */ public function import() { check_admin_referer( self::NONCE_ACTION ); if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { return; } if ( ! isset( $_POST['settings_import'] ) || ! is_string( $_POST['settings_import'] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: The raw content will be parsed afterwards. $content = wp_unslash( $_POST['settings_import'] ); if ( empty( $content ) ) { return; } $this->parse_options( $content ); } /** * Parse the options. * * @param string $raw_options The content to parse. * * @return void */ protected function parse_options( $raw_options ) { $options = parse_ini_string( $raw_options, true, INI_SCANNER_RAW ); if ( is_array( $options ) && $options !== [] ) { $this->import_options( $options ); return; } $this->status->set_msg( __( 'Settings could not be imported:', 'wordpress-seo' ) . ' ' . __( 'No settings found.', 'wordpress-seo' ) ); } /** * Parse the option group and import it. * * @param string $name Name string. * @param array $option_group Option group data. * @param array $options Options data. * * @return void */ protected function parse_option_group( $name, $option_group, $options ) { // Make sure that the imported options are cleaned/converted on import. $option_instance = WPSEO_Options::get_option_instance( $name ); if ( is_object( $option_instance ) && method_exists( $option_instance, 'import' ) ) { $option_instance->import( $option_group, $this->old_wpseo_version, $options ); } } /** * Imports the options if found. * * @param array $options The options parsed from the provided settings. * * @return void */ protected function import_options( $options ) { if ( isset( $options['wpseo']['version'] ) && $options['wpseo']['version'] !== '' ) { $this->old_wpseo_version = $options['wpseo']['version']; } foreach ( $options as $name => $option_group ) { $this->parse_option_group( $name, $option_group, $options ); } $this->status->set_msg( __( 'Settings successfully imported.', 'wordpress-seo' ) ); $this->status->set_status( true ); // Reset the cached option values. WPSEO_Options::clear_cache(); } } admin/class-premium-popup.php 0000644 00000005473 15174712003 0012300 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class WPSEO_Premium_popup. */ class WPSEO_Premium_Popup { /** * An unique identifier for the popup * * @var string */ private $identifier = ''; /** * The heading level of the title of the popup. * * @var string */ private $heading_level = ''; /** * The title of the popup. * * @var string */ private $title = ''; /** * The content of the popup. * * @var string */ private $content = ''; /** * The URL for where the button should link to. * * @var string */ private $url = ''; /** * Wpseo_Premium_Popup constructor. * * @param string $identifier An unique identifier for the popup. * @param string $heading_level The heading level for the title of the popup. * @param string $title The title of the popup. * @param string $content The content of the popup. * @param string $url The URL for where the button should link to. */ public function __construct( $identifier, $heading_level, $title, $content, $url ) { $this->identifier = $identifier; $this->heading_level = $heading_level; $this->title = $title; $this->content = $content; $this->url = $url; } /** * Returns the premium popup as an HTML string. * * @param bool $popup Show this message as a popup show it straight away. * * @return string */ public function get_premium_message( $popup = true ) { // Don't show in Premium. if ( defined( 'WPSEO_PREMIUM_FILE' ) ) { return ''; } $assets_uri = trailingslashit( plugin_dir_url( WPSEO_FILE ) ); /* translators: %s expands to Yoast SEO Premium */ $cta_text = esc_html( sprintf( __( 'Get %s', 'wordpress-seo' ), 'Yoast SEO Premium' ) ); /* translators: Hidden accessibility text. */ $new_tab_message = '<span class="screen-reader-text">' . esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'; $caret_icon = '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'; $classes = ''; if ( $popup ) { $classes = ' hidden'; } $micro_copy = __( '1 year free support and updates included!', 'wordpress-seo' ); $popup = <<<EO_POPUP <div id="wpseo-{$this->identifier}-popup" class="wpseo-premium-popup wp-clearfix$classes"> <img class="alignright wpseo-premium-popup-icon" src="{$assets_uri}packages/js/images/Yoast_SEO_Icon.svg" width="150" height="150" alt="Yoast SEO" /> <{$this->heading_level} id="wpseo-contact-support-popup-title" class="wpseo-premium-popup-title">{$this->title}</{$this->heading_level}> {$this->content} <a id="wpseo-{$this->identifier}-popup-button" class="yoast-button-upsell" href="{$this->url}" target="_blank"> {$cta_text} {$new_tab_message} {$caret_icon} </a><br/> <small>{$micro_copy}</small> </div> EO_POPUP; return $popup; } } admin/class-config.php 0000644 00000011665 15174712003 0010726 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; use Yoast\WP\SEO\Integrations\Academy_Integration; use Yoast\WP\SEO\Integrations\Admin\Redirects_Page_Integration; use Yoast\WP\SEO\Integrations\Settings_Integration; use Yoast\WP\SEO\Integrations\Support_Integration; use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration; use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; /** * Class WPSEO_Admin_Pages. * * Class with functionality for the Yoast SEO admin pages. */ class WPSEO_Admin_Pages { /** * The option in use for the current admin page. * * @var string */ public $currentoption = 'wpseo'; /** * Holds the asset manager. * * @var WPSEO_Admin_Asset_Manager */ private $asset_manager; /** * Class constructor, which basically only hooks the init function on the init hook. */ public function __construct() { add_action( 'init', [ $this, 'init' ], 20 ); $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } /** * Make sure the needed scripts are loaded for admin pages. * * @return void */ public function init() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // Don't load the scripts for the following pages. $page_exceptions = in_array( $page, [ Settings_Integration::PAGE, Academy_Integration::PAGE, Support_Integration::PAGE, Plans_Page_Integration::PAGE, Redirects_Page_Integration::PAGE, ], true, ); $new_dashboard_page = ( $page === General_Page_Integration::PAGE && ! is_network_admin() ); if ( $page_exceptions || $new_dashboard_page ) { // Bail, this is managed in the applicable integration. return; } add_action( 'admin_enqueue_scripts', [ $this, 'config_page_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'config_page_styles' ] ); } /** * Loads the required styles for the config page. * * @return void */ public function config_page_styles() { wp_enqueue_style( 'dashboard' ); wp_enqueue_style( 'thickbox' ); wp_enqueue_style( 'global' ); wp_enqueue_style( 'wp-admin' ); $this->asset_manager->enqueue_style( 'admin-css' ); $this->asset_manager->enqueue_style( 'monorepo' ); } /** * Loads the required scripts for the config page. * * @return void */ public function config_page_scripts() { $this->asset_manager->enqueue_script( 'settings' ); wp_enqueue_script( 'dashboard' ); wp_enqueue_script( 'thickbox' ); $alert_dismissal_action = YoastSEO()->classes->get( Alert_Dismissal_Action::class ); $dismissed_alerts = $alert_dismissal_action->all_dismissed(); $script_data = [ 'dismissedAlerts' => $dismissed_alerts, 'isRtl' => is_rtl(), 'isPremium' => YoastSEO()->helpers->product->is_premium(), 'currentPromotions' => YoastSEO()->classes->get( Promotion_Manager::class ) ->get_current_promotions(), 'webinarIntroFirstTimeConfigUrl' => $this->get_webinar_shortlink(), 'linkParams' => WPSEO_Shortlinker::get_query_params(), 'pluginUrl' => plugins_url( '', WPSEO_FILE ), ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; if ( in_array( $page, [ WPSEO_Admin::PAGE_IDENTIFIER, 'wpseo_workouts' ], true ) ) { wp_enqueue_media(); $script_data['userEditUrl'] = add_query_arg( 'user_id', '{user_id}', admin_url( 'user-edit.php' ) ); } if ( $page === 'wpseo_tools' ) { $this->enqueue_tools_scripts(); } $this->asset_manager->localize_script( 'settings', 'wpseoScriptData', $script_data ); } /** * Enqueues and handles all the tool dependencies. * * @return void */ private function enqueue_tools_scripts() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $tool = isset( $_GET['tool'] ) && is_string( $_GET['tool'] ) ? sanitize_text_field( wp_unslash( $_GET['tool'] ) ) : ''; if ( empty( $tool ) ) { $this->asset_manager->enqueue_script( 'yoast-seo' ); } if ( $tool === 'bulk-editor' ) { $this->asset_manager->enqueue_script( 'bulk-editor' ); } } /** * Returns the appropriate shortlink for the Webinar. * * @return string The shortlink for the Webinar. */ private function get_webinar_shortlink() { if ( YoastSEO()->helpers->product->is_premium() ) { return WPSEO_Shortlinker::get( 'https://yoa.st/webinar-intro-first-time-config-premium' ); } return WPSEO_Shortlinker::get( 'https://yoa.st/webinar-intro-first-time-config' ); } } admin/class-product-upsell-notice.php 0000644 00000013404 15174712003 0013713 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents the upsell notice. */ class WPSEO_Product_Upsell_Notice { /** * Holds the name of the user meta key. * * The value of this database field holds whether the user has dismissed this notice or not. * * @var string */ public const USER_META_DISMISSED = 'wpseo-remove-upsell-notice'; /** * Holds the option name. * * @var string */ public const OPTION_NAME = 'wpseo'; /** * Holds the options. * * @var array */ protected $options; /** * Sets the options, because they always have to be there on instance. */ public function __construct() { $this->options = $this->get_options(); } /** * Checks if the notice should be added or removed. * * @return void */ public function initialize() { $this->remove_notification(); } /** * Sets the upgrade notice. * * @return void */ public function set_upgrade_notice() { if ( $this->has_first_activated_on() ) { return; } $this->set_first_activated_on(); $this->add_notification(); } /** * Listener for the upsell notice. * * @return void */ public function dismiss_notice_listener() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are validating a nonce here. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'dismiss-5star-upsell' ) ) { return; } $dismiss_upsell = isset( $_GET['yoast_dismiss'] ) && is_string( $_GET['yoast_dismiss'] ) ? sanitize_text_field( wp_unslash( $_GET['yoast_dismiss'] ) ) : ''; if ( $dismiss_upsell !== 'upsell' ) { return; } $this->dismiss_notice(); if ( wp_safe_redirect( admin_url( 'admin.php?page=wpseo_dashboard' ) ) ) { exit(); } } /** * When the notice should be shown. * * @return bool */ protected function should_add_notification() { return ( $this->options['first_activated_on'] < strtotime( '-2weeks' ) ); } /** * Checks if the options has a first activated on date value. * * @return bool */ protected function has_first_activated_on() { return $this->options['first_activated_on'] !== false; } /** * Sets the first activated on. * * @return void */ protected function set_first_activated_on() { $this->options['first_activated_on'] = strtotime( '-2weeks' ); $this->save_options(); } /** * Adds a notification to the notification center. * * @return void */ protected function add_notification() { $notification_center = Yoast_Notification_Center::get(); $notification_center->add_notification( $this->get_notification() ); } /** * Removes a notification to the notification center. * * @return void */ protected function remove_notification() { $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification( $this->get_notification() ); } /** * Returns a premium upsell section if using the free plugin. * * @return string */ protected function get_premium_upsell_section() { if ( ! YoastSEO()->helpers->product->is_premium() ) { return sprintf( /* translators: %1$s expands anchor to premium plugin page, %2$s expands to </a> */ __( 'By the way, did you know we also have a %1$sPremium plugin%2$s? It offers advanced features, like a redirect manager and support for multiple keyphrases. It also comes with 24/7 personal support.', 'wordpress-seo' ), "<a href='" . WPSEO_Shortlinker::get( 'https://yoa.st/premium-notification' ) . "'>", '</a>', ); } return ''; } /** * Gets the notification value. * * @return Yoast_Notification */ protected function get_notification() { $message = sprintf( /* translators: %1$s expands to Yoast SEO, %2$s is a link start tag to the plugin page on WordPress.org, %3$s is the link closing tag. */ __( 'We\'ve noticed you\'ve been using %1$s for some time now; we hope you love it! We\'d be thrilled if you could %2$sgive us a 5 stars rating on WordPress.org%3$s!', 'wordpress-seo' ), 'Yoast SEO', '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/rate-yoast-seo' ) . '">', '</a>', ) . "\n\n"; $message .= sprintf( /* translators: %1$s is a link start tag to the bugreport guidelines on the Yoast help center, %2$s is the link closing tag. */ __( 'If you are experiencing issues, %1$splease file a bug report%2$s and we\'ll do our best to help you out.', 'wordpress-seo' ), '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/bugreport' ) . '">', '</a>', ) . "\n\n"; $message .= $this->get_premium_upsell_section() . "\n\n"; $message .= '<a class="button" href="' . wp_nonce_url( admin_url( '?page=' . WPSEO_Admin::PAGE_IDENTIFIER . '&yoast_dismiss=upsell' ), 'dismiss-5star-upsell' ) . '">' . __( 'Please don\'t show me this notification anymore', 'wordpress-seo' ) . '</a>'; $notification = new Yoast_Notification( $message, [ 'type' => Yoast_Notification::WARNING, 'id' => 'wpseo-upsell-notice', 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ], ); return $notification; } /** * Dismisses the notice. * * @return bool */ protected function is_notice_dismissed() { return get_user_meta( get_current_user_id(), self::USER_META_DISMISSED, true ) === '1'; } /** * Dismisses the notice. * * @return void */ protected function dismiss_notice() { update_user_meta( get_current_user_id(), self::USER_META_DISMISSED, true ); } /** * Returns the set options. * * @return mixed */ protected function get_options() { return get_option( self::OPTION_NAME ); } /** * Saves the options to the database. * * @return void */ protected function save_options() { update_option( self::OPTION_NAME, $this->options ); } } admin/class-admin-asset-seo-location.php 0000644 00000004116 15174712003 0014251 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Determines the location of an asset within the SEO plugin. */ final class WPSEO_Admin_Asset_SEO_Location implements WPSEO_Admin_Asset_Location { /** * Path to the plugin file. * * @var string */ protected $plugin_file; /** * Whether or not to add the file suffix to the asset. * * @var bool */ protected $add_suffix = true; /** * The plugin file to base the asset location upon. * * @param string $plugin_file The plugin file string. * @param bool $add_suffix Optional. Whether or not a file suffix should be added. */ public function __construct( $plugin_file, $add_suffix = true ) { $this->plugin_file = $plugin_file; $this->add_suffix = $add_suffix; } /** * Determines the URL of the asset on the dev server. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. Usually JS or CSS. * * @return string The URL of the asset. */ public function get_url( WPSEO_Admin_Asset $asset, $type ) { $path = $this->get_path( $asset, $type ); if ( empty( $path ) ) { return ''; } return plugins_url( $path, $this->plugin_file ); } /** * Determines the path relative to the plugin folder of an asset. * * @param WPSEO_Admin_Asset $asset The asset to determine the path for. * @param string $type The type of asset. * * @return string The path to the asset file. */ protected function get_path( WPSEO_Admin_Asset $asset, $type ) { $relative_path = ''; $rtl_suffix = ''; switch ( $type ) { case WPSEO_Admin_Asset::TYPE_JS: $relative_path = 'js/dist/' . $asset->get_src(); if ( $this->add_suffix ) { $relative_path .= $asset->get_suffix() . '.js'; } break; case WPSEO_Admin_Asset::TYPE_CSS: // Path and suffix for RTL stylesheets. if ( is_rtl() && $asset->has_rtl() ) { $rtl_suffix = '-rtl'; } $relative_path = 'css/dist/' . $asset->get_src() . $rtl_suffix . $asset->get_suffix() . '.css'; break; } return $relative_path; } } admin/class-admin-asset-analysis-worker-location.php 0000644 00000003472 15174712003 0016621 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Represents a way to determine the analysis worker asset location. */ final class WPSEO_Admin_Asset_Analysis_Worker_Location implements WPSEO_Admin_Asset_Location { /** * Holds the asset's location. * * @var WPSEO_Admin_Asset_Location */ private $asset_location; /** * Holds the asset itself. * * @var WPSEO_Admin_Asset */ private $asset; /** * Constructs the location of the analysis worker asset. * * @param string $flat_version The flat version of the asset. * @param string $name The name of the analysis worker asset. */ public function __construct( $flat_version = '', $name = 'analysis-worker' ) { if ( $flat_version === '' ) { $asset_manager = new WPSEO_Admin_Asset_Manager(); $flat_version = $asset_manager->flatten_version( WPSEO_VERSION ); } $analysis_worker = $name . '-' . $flat_version . '.js'; $this->asset_location = WPSEO_Admin_Asset_Manager::create_default_location(); $this->asset = new WPSEO_Admin_Asset( [ 'name' => $name, 'src' => $analysis_worker, ], ); } /** * Retrieves the analysis worker asset. * * @return WPSEO_Admin_Asset The analysis worker asset. */ public function get_asset() { return $this->asset; } /** * Determines the URL of the asset on the dev server. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. Usually JS or CSS. * * @return string The URL of the asset. */ public function get_url( WPSEO_Admin_Asset $asset, $type ) { $scheme = wp_parse_url( $asset->get_src(), PHP_URL_SCHEME ); if ( in_array( $scheme, [ 'http', 'https' ], true ) ) { return $asset->get_src(); } return $this->asset_location->get_url( $asset, $type ); } } admin/formatter/class-metabox-formatter.php 0000644 00000005211 15174712003 0015112 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Formatter */ use Yoast\WP\SEO\Config\Schema_Types; use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository; /** * This class forces needed methods for the metabox localization. */ class WPSEO_Metabox_Formatter { /** * Object that provides formatted values. * * @var WPSEO_Metabox_Formatter_Interface */ private $formatter; /** * Setting the formatter property. * * @param WPSEO_Metabox_Formatter_Interface $formatter Object that provides the formatted values. */ public function __construct( WPSEO_Metabox_Formatter_Interface $formatter ) { $this->formatter = $formatter; } /** * Returns the values. * * @return array<string, string|array<string|int|bool>|bool|int> */ public function get_values() { $defaults = $this->get_defaults(); $values = $this->formatter->get_values(); return ( $values + $defaults ); } /** * Returns array with all the values always needed by a scraper object. * * @return array<string, string|array<string|int|bool>|bool|int> Default settings for the metabox. */ private function get_defaults() { $schema_types = new Schema_Types(); $defaults = [ 'author_name' => get_the_author_meta( 'display_name' ), 'keyword_usage' => [], 'title_template' => '', 'metadesc_template' => '', 'schema' => [ 'displayFooter' => WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ), 'pageTypeOptions' => $schema_types->get_page_type_options(), 'articleTypeOptions' => $schema_types->get_article_type_options(), ], 'twitterCardType' => 'summary_large_image', /** * Filter to determine if the markers should be enabled or not. * * @param bool $showMarkers Should the markers being enabled. Default = true. */ 'show_markers' => apply_filters( 'wpseo_enable_assessment_markers', true ), ]; $integration_information_repo = YoastSEO()->classes->get( Integration_Information_Repository::class ); $enabled_integrations = $integration_information_repo->get_integration_information(); $defaults = array_merge( $defaults, $enabled_integrations ); $enabled_features_repo = YoastSEO()->classes->get( Enabled_Analysis_Features_Repository::class ); $enabled_features = $enabled_features_repo->get_enabled_features()->parse_to_legacy_array(); return array_merge( $defaults, $enabled_features ); } } admin/formatter/class-term-metabox-formatter.php 0000644 00000004325 15174712003 0016064 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Formatter */ use Yoast\WP\SEO\Editors\Application\Seo\Term_Seo_Information_Repository; /** * This class provides data for the term metabox by return its values for localization. */ class WPSEO_Term_Metabox_Formatter implements WPSEO_Metabox_Formatter_Interface { /** * The term the metabox formatter is for. * * @var WP_Term|stdClass */ private $term; /** * The term's taxonomy. * * @var stdClass */ private $taxonomy; /** * Whether we must return social templates values. * * @var bool */ private $use_social_templates = false; /** * Array with the WPSEO_Titles options. * * @var array */ protected $options; /** * WPSEO_Taxonomy_Scraper constructor. * * @param stdClass $taxonomy Taxonomy. * @param WP_Term|stdClass $term Term. */ public function __construct( $taxonomy, $term ) { $this->taxonomy = $taxonomy; $this->term = $term; $this->use_social_templates = $this->use_social_templates(); } /** * Determines whether the social templates should be used. * * @return bool Whether the social templates should be used. */ public function use_social_templates() { return WPSEO_Options::get( 'opengraph', false ) === true; } /** * Returns the translated values. * * @return array */ public function get_values() { $values = []; // Todo: a column needs to be added on the termpages to add a filter for the keyword, so this can be used in the focus keyphrase doubles. if ( is_object( $this->term ) && property_exists( $this->term, 'taxonomy' ) ) { $values = [ 'taxonomy' => $this->term->taxonomy, 'semrushIntegrationActive' => 0, 'wincherIntegrationActive' => 0, 'isInsightsEnabled' => $this->is_insights_enabled(), ]; $repo = YoastSEO()->classes->get( Term_Seo_Information_Repository::class ); $repo->set_term( $this->term ); $values = ( $repo->get_seo_data() + $values ); } return $values; } /** * Determines whether the insights feature is enabled for this taxonomy. * * @return bool */ protected function is_insights_enabled() { return WPSEO_Options::get( 'enable_metabox_insights', false ); } } admin/formatter/interface-metabox-formatter.php 0000644 00000000402 15174712003 0015742 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Formatter */ /** * Interface to force get_values. */ interface WPSEO_Metabox_Formatter_Interface { /** * Returns formatter values. * * @return array */ public function get_values(); } admin/formatter/class-post-metabox-formatter.php 0000644 00000004170 15174712003 0016100 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Formatter */ use Yoast\WP\SEO\Editors\Application\Seo\Post_Seo_Information_Repository; /** * This class provides data for the post metabox by return its values for localization. */ class WPSEO_Post_Metabox_Formatter implements WPSEO_Metabox_Formatter_Interface { /** * Holds the WordPress Post. * * @var WP_Post */ private $post; /** * The permalink to follow. * * @var string */ private $permalink; /** * Constructor. * * @param WP_Post|array $post Post object. * @param array $options Title options to use. * @param string $structure The permalink to follow. */ public function __construct( $post, array $options, $structure ) { $this->post = $post; $this->permalink = $structure; } /** * Determines whether the social templates should be used. * * @deprecated 23.1 * @codeCoverageIgnore * * @return void */ public function use_social_templates() { _deprecated_function( __METHOD__, 'Yoast SEO 23.1' ); } /** * Returns the translated values. * * @return array */ public function get_values() { $values = [ 'metaDescriptionDate' => '', ]; if ( $this->post instanceof WP_Post ) { /** @var Post_Seo_Information_Repository $repo */ $repo = YoastSEO()->classes->get( Post_Seo_Information_Repository::class ); $repo->set_post( $this->post ); $values_to_set = [ 'isInsightsEnabled' => $this->is_insights_enabled(), ]; $values = ( $values_to_set + $values ); $values = ( $repo->get_seo_data() + $values ); } /** * Filter: 'wpseo_post_edit_values' - Allows changing the values Yoast SEO uses inside the post editor. * * @param array $values The key-value map Yoast SEO uses inside the post editor. * @param WP_Post $post The post opened in the editor. */ return apply_filters( 'wpseo_post_edit_values', $values, $this->post ); } /** * Determines whether the insights feature is enabled for this post. * * @return bool */ protected function is_insights_enabled() { return WPSEO_Options::get( 'enable_metabox_insights', false ); } } admin/statistics/class-statistics-service.php 0000644 00000016101 15174712003 0015471 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Statistics */ /** * Class WPSEO_Statistics_Service. */ class WPSEO_Statistics_Service { /** * Cache transient id. * * @var string */ public const CACHE_TRANSIENT_KEY = 'wpseo-statistics-totals'; /** * Class that generates interesting statistics about things. * * @var WPSEO_Statistics */ protected $statistics; /** * Statistics labels. * * @var string[] */ protected $labels; /** * WPSEO_Statistics_Service contructor. * * @param WPSEO_Statistics $statistics The statistics class to retrieve statistics from. */ public function __construct( WPSEO_Statistics $statistics ) { $this->statistics = $statistics; } /** * Fetches statistics by REST request. * * @return WP_REST_Response The response object. */ public function get_statistics() { // Switch to the user locale with fallback to the site locale. switch_to_locale( get_user_locale() ); $this->labels = $this->labels(); $statistics = $this->statistic_items(); $data = [ 'header' => $this->get_header_from_statistics( $statistics ), 'seo_scores' => $statistics['scores'], ]; return new WP_REST_Response( $data ); } /** * Gets a header summarizing the given statistics results. * * @param array $statistics The statistics results. * * @return string The header summing up the statistics results. */ private function get_header_from_statistics( array $statistics ) { // Personal interpretation to allow release, should be looked at later. if ( $statistics['division'] === false ) { return __( 'You don\'t have any published posts, your SEO scores will appear here once you make your first post!', 'wordpress-seo' ); } if ( $statistics['division']['good'] > 0.66 ) { return __( 'Hey, your SEO is doing pretty well! Check out the stats:', 'wordpress-seo' ); } return __( 'Below are your published posts\' SEO scores. Now is as good a time as any to start improving some of your posts!', 'wordpress-seo' ); } /** * An array representing items to be added to the At a Glance dashboard widget. * * @return array The statistics for the current user. */ private function statistic_items() { $transient = $this->get_transient(); $user_id = get_current_user_id(); if ( isset( $transient[ $user_id ] ) ) { return $transient[ $user_id ]; } return $this->set_statistic_items_for_user( $transient, $user_id ); } /** * Gets the statistics transient value. Returns array if transient wasn't set. * * @return array|mixed Returns the transient or an empty array if the transient doesn't exist. */ private function get_transient() { $transient = get_transient( self::CACHE_TRANSIENT_KEY ); if ( $transient === false ) { return []; } return $transient; } /** * Set the statistics transient cache for a specific user. * * @param array $transient The current stored transient with the cached data. * @param int $user The user's ID to assign the retrieved values to. * * @return array The statistics transient for the user. */ private function set_statistic_items_for_user( $transient, $user ) { $scores = $this->get_seo_scores_with_post_count(); $division = $this->get_seo_score_division( $scores ); $transient[ $user ] = [ // Use array_values because array_filter may return non-zero indexed arrays. 'scores' => array_values( array_filter( $scores, [ $this, 'filter_items' ] ) ), 'division' => $division, ]; set_transient( self::CACHE_TRANSIENT_KEY, $transient, DAY_IN_SECONDS ); return $transient[ $user ]; } /** * Gets the division of SEO scores. * * @param array $scores The SEO scores. * * @return array|bool The division of SEO scores, false if there are no posts. */ private function get_seo_score_division( array $scores ) { $total = 0; $division = []; foreach ( $scores as $score ) { $total += $score['count']; } if ( $total === 0 ) { return false; } foreach ( $scores as $score ) { $division[ $score['seo_rank'] ] = ( $score['count'] / $total ); } return $division; } /** * Get all SEO ranks and data associated with them. * * @return array An array of SEO scores and associated data. */ private function get_seo_scores_with_post_count() { $ranks = WPSEO_Rank::get_all_ranks(); return array_map( [ $this, 'map_rank_to_widget' ], $ranks ); } /** * Converts a rank to data usable in the dashboard widget. * * @param WPSEO_Rank $rank The rank to map. * * @return array The mapped rank. */ private function map_rank_to_widget( WPSEO_Rank $rank ) { return [ 'seo_rank' => $rank->get_rank(), 'label' => $this->get_label_for_rank( $rank ), 'count' => $this->statistics->get_post_count( $rank ), 'link' => $this->get_link_for_rank( $rank ), ]; } /** * Returns a dashboard widget label to use for a certain rank. * * @param WPSEO_Rank $rank The rank to return a label for. * * @return string The label for the rank. */ private function get_label_for_rank( WPSEO_Rank $rank ) { return $this->labels[ $rank->get_rank() ]; } /** * Determines the labels for the various scoring ranks that are known within Yoast SEO. * * @return array Array containing the translatable labels. */ private function labels() { return [ WPSEO_Rank::NO_FOCUS => sprintf( /* translators: %1$s expands to an opening strong tag, %2$s expands to a closing strong tag */ __( 'Posts %1$swithout%2$s a focus keyphrase', 'wordpress-seo' ), '<strong>', '</strong>', ), WPSEO_Rank::BAD => sprintf( /* translators: %s expands to the score */ __( 'Posts with the SEO score: %s', 'wordpress-seo' ), '<strong>' . __( 'Needs improvement', 'wordpress-seo' ) . '</strong>', ), WPSEO_Rank::OK => sprintf( /* translators: %s expands to the score */ __( 'Posts with the SEO score: %s', 'wordpress-seo' ), '<strong>' . __( 'OK', 'wordpress-seo' ) . '</strong>', ), WPSEO_Rank::GOOD => sprintf( /* translators: %s expands to the score */ __( 'Posts with the SEO score: %s', 'wordpress-seo' ), '<strong>' . __( 'Good', 'wordpress-seo' ) . '</strong>', ), WPSEO_Rank::NO_INDEX => __( 'Posts that should not show up in search results', 'wordpress-seo' ), ]; } /** * Filter items if they have a count of zero. * * @param array $item The item to potentially filter out. * * @return bool Whether or not the count is zero. */ private function filter_items( $item ) { return $item['count'] !== 0; } /** * Returns a link for the overview of posts of a certain rank. * * @param WPSEO_Rank $rank The rank to return a link for. * * @return string The link that shows an overview of posts with that rank. */ private function get_link_for_rank( WPSEO_Rank $rank ) { if ( current_user_can( 'edit_others_posts' ) === false ) { return esc_url( admin_url( 'edit.php?post_status=publish&post_type=post&seo_filter=' . $rank->get_rank() . '&author=' . get_current_user_id() ) ); } return esc_url( admin_url( 'edit.php?post_status=publish&post_type=post&seo_filter=' . $rank->get_rank() ) ); } } admin/statistics/class-statistics-integration.php 0000644 00000001347 15174712003 0016362 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Statistics */ /** * Class WPSEO_Statistic_Integration. */ class WPSEO_Statistic_Integration implements WPSEO_WordPress_Integration { /** * Adds hooks to clear the cache. * * @return void */ public function register_hooks() { add_action( 'wp_insert_post', [ $this, 'clear_cache' ] ); add_action( 'delete_post', [ $this, 'clear_cache' ] ); } /** * Clears the dashboard widget items cache. * * @return void */ public function clear_cache() { // Bail if this is a multisite installation and the site has been switched. if ( is_multisite() && ms_is_switched() ) { return; } delete_transient( WPSEO_Statistics_Service::CACHE_TRANSIENT_KEY ); } } admin/class-admin-asset-manager.php 0000644 00000051113 15174712003 0013266 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * This class registers all the necessary styles and scripts. * * Also has methods for the enqueing of scripts and styles. * It automatically adds a prefix to the handle. */ class WPSEO_Admin_Asset_Manager { /** * Prefix for naming the assets. * * @var string */ public const PREFIX = 'yoast-seo-'; /** * Class that manages the assets' location. * * @var WPSEO_Admin_Asset_Location */ protected $asset_location; /** * Prefix for naming the assets. * * @var string */ private $prefix; /** * Constructs a manager of assets. Needs a location to know where to register assets at. * * @param WPSEO_Admin_Asset_Location|null $asset_location The provider of the asset location. * @param string $prefix The prefix for naming assets. */ public function __construct( ?WPSEO_Admin_Asset_Location $asset_location = null, $prefix = self::PREFIX ) { $asset_location ??= self::create_default_location(); $this->asset_location = $asset_location; $this->prefix = $prefix; } /** * Enqueues scripts. * * @param string $script The name of the script to enqueue. * * @return void */ public function enqueue_script( $script ) { wp_enqueue_script( $this->prefix . $script ); } /** * Enqueues styles. * * @param string $style The name of the style to enqueue. * * @return void */ public function enqueue_style( $style ) { wp_enqueue_style( $this->prefix . $style ); } /** * Enqueues the appropriate language for the user. * * @return void */ public function enqueue_user_language_script() { $this->enqueue_script( 'language-' . YoastSEO()->helpers->language->get_researcher_language() ); } /** * Registers scripts based on it's parameters. * * @param WPSEO_Admin_Asset $script The script to register. * * @return void */ public function register_script( WPSEO_Admin_Asset $script ) { $url = $script->get_src() ? $this->get_url( $script, WPSEO_Admin_Asset::TYPE_JS ) : false; $args = [ 'in_footer' => $script->is_in_footer(), ]; if ( $script->get_strategy() !== '' ) { $args['strategy'] = $script->get_strategy(); } wp_register_script( $this->prefix . $script->get_name(), $url, $script->get_deps(), $script->get_version(), $args, ); if ( in_array( 'wp-i18n', $script->get_deps(), true ) ) { wp_set_script_translations( $this->prefix . $script->get_name(), 'wordpress-seo' ); } } /** * Registers styles based on it's parameters. * * @param WPSEO_Admin_Asset $style The style to register. * * @return void */ public function register_style( WPSEO_Admin_Asset $style ) { wp_register_style( $this->prefix . $style->get_name(), $this->get_url( $style, WPSEO_Admin_Asset::TYPE_CSS ), $style->get_deps(), $style->get_version(), $style->get_media(), ); } /** * Calls the functions that register scripts and styles with the scripts and styles to be registered as arguments. * * @return void */ public function register_assets() { $this->register_scripts( $this->scripts_to_be_registered() ); $this->register_styles( $this->styles_to_be_registered() ); } /** * Registers all the scripts passed to it. * * @param array $scripts The scripts passed to it. * * @return void */ public function register_scripts( $scripts ) { foreach ( $scripts as $script ) { $script = new WPSEO_Admin_Asset( $script ); $this->register_script( $script ); } } /** * Registers all the styles it receives. * * @param array $styles Styles that need to be registered. * * @return void */ public function register_styles( $styles ) { foreach ( $styles as $style ) { $style = new WPSEO_Admin_Asset( $style ); $this->register_style( $style ); } } /** * Localizes the script. * * @param string $handle The script handle. * @param string $object_name The object name. * @param array $data The l10n data. * * @return void */ public function localize_script( $handle, $object_name, $data ) { wp_localize_script( $this->prefix . $handle, $object_name, $data ); } /** * Adds an inline script. * * @param string $handle The script handle. * @param string $data The l10n data. * @param string $position Optional. Whether to add the inline script before the handle or after. * * @return void */ public function add_inline_script( $handle, $data, $position = 'after' ) { wp_add_inline_script( $this->prefix . $handle, $data, $position ); } /** * A list of styles that shouldn't be registered but are needed in other locations in the plugin. * * @return array */ public function special_styles() { $flat_version = $this->flatten_version( WPSEO_VERSION ); $asset_args = [ 'name' => 'inside-editor', 'src' => 'inside-editor-' . $flat_version, ]; return [ 'inside-editor' => new WPSEO_Admin_Asset( $asset_args ) ]; } /** * Flattens a version number for use in a filename. * * @param string $version The original version number. * * @return string The flattened version number. */ public function flatten_version( $version ) { $parts = explode( '.', $version ); if ( count( $parts ) === 2 && preg_match( '/^\d+$/', $parts[1] ) === 1 ) { $parts[] = '0'; } return implode( '', $parts ); } /** * Creates a default location object for use in the admin asset manager. * * @return WPSEO_Admin_Asset_Location The location to use in the asset manager. */ public static function create_default_location() { if ( defined( 'YOAST_SEO_DEV_SERVER' ) && YOAST_SEO_DEV_SERVER ) { $url = defined( 'YOAST_SEO_DEV_SERVER_URL' ) ? YOAST_SEO_DEV_SERVER_URL : WPSEO_Admin_Asset_Dev_Server_Location::DEFAULT_URL; return new WPSEO_Admin_Asset_Dev_Server_Location( $url ); } return new WPSEO_Admin_Asset_SEO_Location( WPSEO_FILE, false ); } /** * Checks if the given script is enqueued. * * @param string $script The script to check. * * @return bool True when the script is enqueued. */ public function is_script_enqueued( $script ) { return wp_script_is( $this->prefix . $script ); } /** * Gets the list of Elementor dependencies. * * @return array<string> The array of elementor dependencies. */ protected function get_elementor_dependencies() { $dependencies = [ 'backbone-marionette', 'elementor-common-modules', self::PREFIX . 'api-client', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ]; // Conditionally add Elementor v2 dependency if available. if ( wp_script_is( 'elementor-v2-editor-app-bar', 'registered' ) ) { $dependencies[] = 'elementor-v2-editor-app-bar'; } return $dependencies; } /** * Returns the scripts that need to be registered. * * @todo Data format is not self-documenting. Needs explanation inline. R. * * @return array The scripts that need to be registered. */ protected function scripts_to_be_registered() { $header_scripts = [ 'admin-global', 'block-editor', 'classic-editor', 'post-edit', 'help-scout-beacon', 'redirect-old-features-tab', ]; $elementor_dependencies = $this->get_elementor_dependencies(); $additional_dependencies = [ 'analysis-worker' => [ self::PREFIX . 'analysis-package' ], 'api-client' => [ 'wp-api' ], 'crawl-settings' => [ 'jquery' ], 'dashboard-widget' => [ self::PREFIX . 'api-client' ], 'wincher-dashboard-widget' => [ self::PREFIX . 'api-client' ], 'editor-modules' => [ 'jquery' ], 'elementor' => $elementor_dependencies, 'indexation' => [ 'jquery-ui-core', 'jquery-ui-progressbar', ], 'first-time-configuration' => [ self::PREFIX . 'api-client', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], 'integrations-page' => [ self::PREFIX . 'api-client', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], 'post-edit' => [ self::PREFIX . 'api-client', self::PREFIX . 'block-editor', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], 'reindex-links' => [ 'jquery-ui-core', 'jquery-ui-progressbar', ], 'settings' => [ 'jquery-ui-core', 'jquery-ui-progressbar', self::PREFIX . 'api-client', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], 'term-edit' => [ self::PREFIX . 'api-client', self::PREFIX . 'classic-editor', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', ], 'general-page' => [ self::PREFIX . 'api-client', ], ]; $plugin_scripts = $this->load_generated_asset_file( [ 'asset_file' => __DIR__ . '/../src/generated/assets/plugin.php', 'ext_length' => 3, 'additional_deps' => $additional_dependencies, 'header_scripts' => $header_scripts, ], ); $external_scripts = $this->load_generated_asset_file( [ 'asset_file' => __DIR__ . '/../src/generated/assets/externals.php', 'ext_length' => 3, 'suffix' => '-package', 'base_dir' => 'externals/', 'additional_deps' => $additional_dependencies, 'header_scripts' => $header_scripts, ], ); $language_scripts = $this->load_generated_asset_file( [ 'asset_file' => __DIR__ . '/../src/generated/assets/languages.php', 'ext_length' => 3, 'suffix' => '-language', 'base_dir' => 'languages/', 'additional_deps' => $additional_dependencies, 'header_scripts' => $header_scripts, ], ); $renamed_scripts = $this->load_renamed_scripts(); $scripts = array_merge( $plugin_scripts, $external_scripts, $language_scripts, $renamed_scripts, ); $scripts['installation-success'] = [ 'name' => 'installation-success', 'src' => 'installation-success.js', 'deps' => [ 'wp-a11y', 'wp-dom-ready', 'wp-components', 'wp-element', 'wp-i18n', self::PREFIX . 'components-new-package', self::PREFIX . 'externals-components', ], 'version' => $scripts['installation-success']['version'], ]; $scripts['post-edit-classic'] = [ 'name' => 'post-edit-classic', 'src' => $scripts['post-edit']['src'], 'deps' => array_map( static function ( $dep ) { if ( $dep === self::PREFIX . 'block-editor' ) { return self::PREFIX . 'classic-editor'; } return $dep; }, $scripts['post-edit']['deps'], ), 'in_footer' => ! in_array( 'post-edit-classic', $header_scripts, true ), 'version' => $scripts['post-edit']['version'], ]; $scripts['workouts'] = [ 'name' => 'workouts', 'src' => 'workouts.js', 'deps' => [ 'clipboard', 'lodash', 'wp-api-fetch', 'wp-a11y', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n', self::PREFIX . 'externals-components', self::PREFIX . 'externals-contexts', self::PREFIX . 'externals-redux', self::PREFIX . 'analysis', self::PREFIX . 'components-new-package', ], 'version' => $scripts['workouts']['version'], ]; // Add the current language to every script that requires the analysis package. foreach ( $scripts as $name => $script ) { if ( substr( $name, -8 ) === 'language' ) { continue; } if ( in_array( self::PREFIX . 'analysis-package', $script['deps'], true ) ) { $scripts[ $name ]['deps'][] = self::PREFIX . YoastSEO()->helpers->language->get_researcher_language() . '-language'; } } return $scripts; } /** * Loads a generated asset file. * * @param array $args { * The arguments. * * @type string $asset_file The asset file to load. * @type int $ext_length The length of the extension, including suffix, of the filename. * @type string $suffix Optional. The suffix of the asset name. * @type array<string, string[]> $additional_deps Optional. The additional dependencies assets may have. * @type string $base_dir Optional. The base directory of the asset. * @type string[] $header_scripts Optional. The script names that should be in the header. * } * * @return array { * The scripts to be registered. * * @type string $name The name of the asset. * @type string $src The src of the asset. * @type string[] $deps The dependenies of the asset. * @type bool $in_footer Whether or not the asset should be in the footer. * } */ protected function load_generated_asset_file( $args ) { $args = wp_parse_args( $args, [ 'suffix' => '', 'additional_deps' => [], 'base_dir' => '', 'header_scripts' => [], ], ); $scripts = []; $assets = require $args['asset_file']; foreach ( $assets as $file => $data ) { $name = substr( $file, 0, -$args['ext_length'] ); $name = strtolower( preg_replace( '/([A-Z])/', '-$1', $name ) ); $name .= $args['suffix']; $deps = $data['dependencies']; if ( isset( $args['additional_deps'][ $name ] ) ) { $deps = array_merge( $deps, $args['additional_deps'][ $name ] ); } $scripts[ $name ] = [ 'name' => $name, 'src' => $args['base_dir'] . $file, 'deps' => $deps, 'in_footer' => ! in_array( $name, $args['header_scripts'], true ), 'version' => $data['version'], ]; } return $scripts; } /** * Loads the scripts that should be renamed for BC. * * @return array { * The scripts to be registered. * * @type string $name The name of the asset. * @type string $src The src of the asset. * @type string[] $deps The dependenies of the asset. * @type bool $in_footer Whether or not the asset should be in the footer. * } */ protected function load_renamed_scripts() { $scripts = []; $renamed_scripts = [ 'admin-global-script' => 'admin-global', 'analysis' => 'analysis-package', 'analysis-report' => 'analysis-report-package', 'api' => 'api-client', 'commons' => 'commons-package', 'edit-page' => 'edit-page-script', 'draft-js' => 'draft-js-package', 'feature-flag' => 'feature-flag-package', 'helpers' => 'helpers-package', 'jed' => 'jed-package', 'chart.js' => 'chart.js-package', 'network-admin-script' => 'network-admin', 'redux' => 'redux-package', 'replacement-variable-editor' => 'replacement-variable-editor-package', 'search-metadata-previews' => 'search-metadata-previews-package', 'social-metadata-forms' => 'social-metadata-forms-package', 'styled-components' => 'styled-components-package', 'style-guide' => 'style-guide-package', 'yoast-components' => 'components-new-package', ]; foreach ( $renamed_scripts as $original => $replacement ) { $scripts[] = [ 'name' => $original, 'src' => false, 'deps' => [ self::PREFIX . $replacement ], ]; } return $scripts; } /** * Returns the styles that need to be registered. * * @todo Data format is not self-documenting. Needs explanation inline. R. * * @return array Styles that need to be registered. */ protected function styles_to_be_registered() { $flat_version = $this->flatten_version( WPSEO_VERSION ); return [ [ 'name' => 'admin-css', 'src' => 'yst_plugin_tools-' . $flat_version, 'deps' => [ self::PREFIX . 'toggle-switch' ], ], [ 'name' => 'toggle-switch', 'src' => 'toggle-switch-' . $flat_version, ], [ 'name' => 'dismissible', 'src' => 'wpseo-dismissible-' . $flat_version, ], [ 'name' => 'notifications', 'src' => 'notifications-' . $flat_version, ], [ 'name' => 'alert', 'src' => 'alerts-' . $flat_version, ], [ 'name' => 'edit-page', 'src' => 'edit-page-' . $flat_version, ], [ 'name' => 'featured-image', 'src' => 'featured-image-' . $flat_version, ], [ 'name' => 'metabox-css', 'src' => 'metabox-' . $flat_version, 'deps' => [ self::PREFIX . 'admin-css', self::PREFIX . 'tailwind', 'wp-components', ], ], [ 'name' => 'block-editor', 'src' => 'block-editor-' . $flat_version, ], [ 'name' => 'ai-generator', 'src' => 'ai-generator-' . $flat_version, 'deps' => [ self::PREFIX . 'ai-frontend', self::PREFIX . 'tailwind', self::PREFIX . 'introductions', ], ], [ 'name' => 'ai-fix-assessments', 'src' => 'ai-fix-assessments-' . $flat_version, ], [ 'name' => 'ai-frontend', 'src' => 'ai-frontend-' . $flat_version, ], [ 'name' => 'introductions', 'src' => 'introductions-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'wp-dashboard', 'src' => 'dashboard-' . $flat_version, ], [ 'name' => 'scoring', 'src' => 'yst_seo_score-' . $flat_version, ], [ 'name' => 'adminbar', 'src' => 'adminbar-' . $flat_version, 'deps' => [ 'admin-bar', ], ], [ 'name' => 'primary-category', 'src' => 'metabox-primary-category-' . $flat_version, ], [ 'name' => 'admin-global', 'src' => 'admin-global-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'filter-explanation', 'src' => 'filter-explanation-' . $flat_version, ], [ 'name' => 'monorepo', 'src' => 'monorepo-' . $flat_version, ], [ 'name' => 'structured-data-blocks', 'src' => 'structured-data-blocks-' . $flat_version, 'deps' => [ 'dashicons', 'forms', 'wp-edit-blocks', ], ], [ 'name' => 'elementor', 'src' => 'elementor-' . $flat_version, ], [ 'name' => 'tailwind', 'src' => 'tailwind-' . $flat_version, // Note: The RTL suffix is not added here. // Tailwind and our UI library provide styling that should be standalone compatible with RTL. // To make it easier we should use the logical properties and values when possible. // If there are exceptions, we can use the Tailwind modifier, e.g. `rtl:yst-space-x-reverse`. 'rtl' => false, ], [ 'name' => 'new-settings', 'src' => 'new-settings-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'redirects', 'src' => 'redirects-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'black-friday-banner', 'src' => 'black-friday-banner-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'academy', 'src' => 'academy-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'general-page', 'src' => 'general-page-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'installation-success', 'src' => 'installation-success-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'support', 'src' => 'support-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'workouts', 'src' => 'workouts-' . $flat_version, 'deps' => [ self::PREFIX . 'monorepo', ], ], [ 'name' => 'first-time-configuration', 'src' => 'first-time-configuration-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], [ 'name' => 'inside-editor', 'src' => 'inside-editor-' . $flat_version, ], [ 'name' => 'plans', 'src' => 'plans-' . $flat_version, 'deps' => [ self::PREFIX . 'tailwind' ], ], ]; } /** * Determines the URL of the asset. * * @param WPSEO_Admin_Asset $asset The asset to determine the URL for. * @param string $type The type of asset. Usually JS or CSS. * * @return string The URL of the asset. */ protected function get_url( WPSEO_Admin_Asset $asset, $type ) { $scheme = wp_parse_url( $asset->get_src(), PHP_URL_SCHEME ); if ( in_array( $scheme, [ 'http', 'https' ], true ) ) { return $asset->get_src(); } return $this->asset_location->get_url( $asset, $type ); } } admin/class-plugin-availability.php 0000644 00000024102 15174712003 0013415 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Plugin_Availability */ use Yoast\WP\SEO\Conditionals\Conditional; use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; /** * Class WPSEO_Plugin_Availability */ class WPSEO_Plugin_Availability { /** * Holds the plugins. * * @var array */ protected $plugins = []; /** * Registers the plugins so we can access them. * * @return void */ public function register() { $this->register_yoast_plugins(); $this->register_yoast_plugins_status(); } /** * Registers all the available Yoast SEO plugins. * * @return void */ protected function register_yoast_plugins() { $this->plugins = [ 'yoast-seo-premium' => [ 'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y7' ), 'title' => 'Yoast SEO Premium', 'description' => sprintf( /* translators: %1$s expands to Yoast SEO */ __( 'The premium version of %1$s with more features & support.', 'wordpress-seo' ), 'Yoast SEO', ), 'installed' => false, 'slug' => 'wordpress-seo-premium/wp-seo-premium.php', 'version_sync' => true, 'premium' => true, ], 'video-seo-for-wordpress-seo-by-yoast' => [ 'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y8' ), 'title' => 'Video SEO', 'description' => __( 'Optimize your videos to show them off in search results and get more clicks!', 'wordpress-seo' ), 'installed' => false, 'slug' => 'wpseo-video/video-seo.php', 'version_sync' => true, 'premium' => true, ], 'yoast-news-seo' => [ 'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1y9' ), 'title' => 'News SEO', 'description' => __( 'Are you in Google News? Increase your traffic from Google News by optimizing for it!', 'wordpress-seo' ), 'installed' => false, 'slug' => 'wpseo-news/wpseo-news.php', 'version_sync' => true, 'premium' => true, ], 'local-seo-for-yoast-seo' => [ 'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1ya' ), 'title' => 'Local SEO', 'description' => __( 'Rank better locally and in Google Maps, without breaking a sweat!', 'wordpress-seo' ), 'installed' => false, 'slug' => 'wordpress-seo-local/local-seo.php', 'version_sync' => true, 'premium' => true, ], 'yoast-woocommerce-seo' => [ 'url' => WPSEO_Shortlinker::get( 'https://yoa.st/1o0' ), 'title' => 'Yoast WooCommerce SEO', 'description' => sprintf( /* translators: %1$s expands to Yoast SEO */ __( 'Seamlessly integrate WooCommerce with %1$s and get extra features!', 'wordpress-seo' ), 'Yoast SEO', ), '_dependencies' => [ 'WooCommerce' => [ 'slug' => 'woocommerce/woocommerce.php', // Kept for backwards compatibility, in case external code uses get_dependencies(). Deprecated in 22.4. 'conditional' => new WooCommerce_Conditional(), ], ], 'installed' => false, 'slug' => 'wpseo-woocommerce/wpseo-woocommerce.php', 'version_sync' => true, 'premium' => true, ], ]; } /** * Sets certain plugin properties based on WordPress' status. * * @return void */ protected function register_yoast_plugins_status() { foreach ( $this->plugins as $name => $plugin ) { $plugin_slug = $plugin['slug']; $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_slug; if ( file_exists( $plugin_path ) ) { $plugin_data = get_plugin_data( $plugin_path, false, false ); $this->plugins[ $name ]['installed'] = true; $this->plugins[ $name ]['version'] = $plugin_data['Version']; $this->plugins[ $name ]['active'] = is_plugin_active( $plugin_slug ); } } } /** * Checks if there are dependencies available for the plugin. * * @param array $plugin The information available about the plugin. * * @return bool Whether there is a dependency present. */ public function has_dependencies( $plugin ) { return ( isset( $plugin['_dependencies'] ) && ! empty( $plugin['_dependencies'] ) ); } /** * Gets the dependencies for the plugin. * * @param array $plugin The information available about the plugin. * * @return array Array containing all the dependencies associated with the plugin. */ public function get_dependencies( $plugin ) { if ( ! $this->has_dependencies( $plugin ) ) { return []; } return $plugin['_dependencies']; } /** * Checks if all dependencies are satisfied. * * @param array $plugin The information available about the plugin. * * @return bool Whether or not the dependencies are satisfied. */ public function dependencies_are_satisfied( $plugin ) { if ( ! $this->has_dependencies( $plugin ) ) { return true; } $dependencies = $this->get_dependencies( $plugin ); $active_dependencies = array_filter( $dependencies, [ $this, 'is_dependency_active' ] ); return count( $active_dependencies ) === count( $dependencies ); } /** * Checks whether or not one of the plugins is properly installed and usable. * * @param array $plugin The information available about the plugin. * * @return bool Whether or not the plugin is properly installed. */ public function is_installed( $plugin ) { if ( empty( $plugin ) ) { return false; } return $this->is_available( $plugin ); } /** * Checks for the availability of the plugin. * * @param array $plugin The information available about the plugin. * * @return bool Whether or not the plugin is available. */ public function is_available( $plugin ) { return isset( $plugin['installed'] ) && $plugin['installed'] === true; } /** * Checks whether a dependency is active. * * @param array<string, Conditional> $dependency The information about the dependency to look for. * * @return bool Whether or not the dependency is active. */ public function is_dependency_active( $dependency ) { return $dependency['conditional']->is_met(); } /** * Gets an array of plugins that have defined dependencies. * * @return array Array of the plugins that have dependencies. */ public function get_plugins_with_dependencies() { return array_filter( $this->plugins, [ $this, 'has_dependencies' ] ); } /** * Determines whether or not a plugin is active. * * @deprecated 23.4 * @codeCoverageIgnore * * @param string $plugin The plugin slug to check. * * @return bool Whether or not the plugin is active. */ public function is_active( $plugin ) { _deprecated_function( __METHOD__, 'Yoast SEO 23.4', 'is_plugin_active' ); return is_plugin_active( $plugin ); } /** * Gets all the possibly available plugins. * * @deprecated 23.4 * @codeCoverageIgnore * * @return array Array containing the information about the plugins. */ public function get_plugins() { _deprecated_function( __METHOD__, 'Yoast SEO 23.4', 'WPSEO_Addon_Manager::get_addon_filenames' ); return $this->plugins; } /** * Gets a specific plugin. Returns an empty array if it cannot be found. * * @deprecated 23.4 * @codeCoverageIgnore * * @param string $plugin The plugin to search for. * * @return array The plugin properties. */ public function get_plugin( $plugin ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- needed for BC reasons _deprecated_function( __METHOD__, 'Yoast SEO 23.4', 'WPSEO_Addon_Manager::get_plugin_file' ); if ( ! isset( $this->plugins[ $plugin ] ) ) { return []; } return $this->plugins[ $plugin ]; } /** * Gets the version of the plugin. * * @deprecated 23.4 * @codeCoverageIgnore * * @param array $plugin The information available about the plugin. * * @return string The version associated with the plugin. */ public function get_version( $plugin ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- needed for BC reasons _deprecated_function( __METHOD__, 'Yoast SEO 23.4', 'WPSEO_Addon_Manager::get_installed_addons_versions' ); if ( ! isset( $plugin['version'] ) ) { return ''; } return $plugin['version']; } /** * Checks whether a dependency is available. * * @deprecated 22.4 * @codeCoverageIgnore * * @param array $dependency The information about the dependency to look for. * * @return bool Whether or not the dependency is available. */ public function is_dependency_available( $dependency ) { _deprecated_function( __METHOD__, 'Yoast SEO 22.4' ); return isset( get_plugins()[ $dependency['slug'] ] ); } /** * Gets the names of the dependencies. * * @deprecated 23.4 * @codeCoverageIgnore * * @param array $plugin The plugin to get the dependency names from. * * @return array Array containing the names of the associated dependencies. */ public function get_dependency_names( $plugin ) { // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- needed for BC reasons _deprecated_function( __METHOD__, 'Yoast SEO 23.4' ); if ( ! $this->has_dependencies( $plugin ) ) { return []; } return array_keys( $plugin['_dependencies'] ); } /** * Determines whether or not a plugin is a Premium product. * * @deprecated 23.4 * @codeCoverageIgnore * * @param array $plugin The plugin to check. * * @return bool Whether or not the plugin is a Premium product. */ public function is_premium( $plugin ) { _deprecated_function( __METHOD__, 'Yoast SEO 23.4' ); return isset( $plugin['premium'] ) && $plugin['premium'] === true; } /** * Gets all installed plugins. * * @deprecated 23.4 * @codeCoverageIgnore * * @return array The installed plugins. */ public function get_installed_plugins() { _deprecated_function( __METHOD__, 'Yoast SEO 23.4', 'WPSEO_Addon_Manager::get_installed_addons_versions' ); $installed = []; foreach ( $this->plugins as $plugin_key => $plugin ) { if ( $this->is_installed( $plugin ) ) { $installed[ $plugin_key ] = $plugin; } } return $installed; } } admin/tracking/class-tracking-settings-data.php 0000644 00000016762 15174712003 0015635 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ /** * Collects anonymized settings data. */ class WPSEO_Tracking_Settings_Data implements WPSEO_Collection { /** * The options that need to be anonymized before they can be sent elsewhere. * * @var array All of the option_names which need to be * anonymized before they can be sent elsewhere. */ private $anonymous_settings = [ 'baiduverify', 'googleverify', 'msverify', 'yandexverify', 'ahrefsverify', 'myyoast-oauth', 'website_name', 'alternate_website_name', 'company_logo', 'company_name', 'company_alternate_name', 'person_name', 'person_logo', 'person_logo_id', 'company_logo_id', 'facebook_site', 'instagram_url', 'linkedin_url', 'myspace_url', 'og_default_image', 'og_default_image_id', 'og_frontpage_title', 'og_frontpage_desc', 'og_frontpage_image', 'og_frontpage_image_id', 'open_graph_frontpage_title', 'open_graph_frontpage_desc', 'open_graph_frontpage_image', 'open_graph_frontpage_image_id', 'other_social_urls', 'mastodon_url', 'pinterest_url', 'pinterestverify', 'twitter_site', 'youtube_url', 'wikipedia_url', 'semrush_tokens', 'wincher_tokens', 'wincher_website_id', 'least_readability_ignore_list', 'least_seo_score_ignore_list', 'most_linked_ignore_list', 'least_linked_ignore_list', 'indexables_page_reading_list', 'publishing_principles_id', 'ownership_funding_info_id', 'actionable_feedback_policy_id', 'corrections_policy_id', 'ethics_policy_id', 'diversity_policy_id', 'diversity_staffing_report_id', ]; /** * The options we want to track. * * @var array The option_names for the options we want to track. */ private $include_list = [ 'ms_defaults_set', 'version', 'disableadvanced_meta', 'ryte_indexability', 'baiduverify', 'googleverify', 'ahrefsverify', 'msverify', 'yandexverify', 'site_type', 'has_multiple_authors', 'environment_type', 'content_analysis_active', 'keyword_analysis_active', 'inclusive_language_analysis_active', 'enable_admin_bar_menu', 'enable_cornerstone_content', 'enable_xml_sitemap', 'enable_text_link_counter', 'show_onboarding_notice', 'first_activated_on', 'myyoast-oauth', 'dynamic_permalinks', 'website_name', 'alternate_website_name', 'company_logo', 'company_name', 'company_or_person', 'person_name', 'forcerewritetitle', 'separator', 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo', 'title-search-wpseo', 'title-404-wpseo', 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo', 'rssbefore', 'rssafter', 'noindex-author-wpseo', 'noindex-author-noposts-wpseo', 'noindex-archive-wpseo', 'disable-author', 'disable-date', 'disable-post_format', 'disable-attachment', 'breadcrumbs-404crumb', 'breadcrumbs-display-blog-page', 'breadcrumbs-boldlast', 'breadcrumbs-archiveprefix', 'breadcrumbs-enable', 'breadcrumbs-home', 'breadcrumbs-prefix', 'breadcrumbs-searchprefix', 'breadcrumbs-sep', 'person_logo', 'person_logo_id', 'company_logo_id', 'company_or_person_user_id', 'stripcategorybase', 'noindex-post', 'display-metabox-pt-post', 'noindex-page', 'display-metabox-pt-page', 'noindex-attachment', 'display-metabox-pt-attachment', 'display-metabox-tax-category', 'noindex-tax-category', 'display-metabox-tax-post_tag', 'noindex-tax-post_tag', 'display-metabox-tax-post_format', 'noindex-tax-post_format', 'taxonomy-category-ptparent', 'taxonomy-post_tag-ptparent', 'taxonomy-post_format-ptparent', 'breadcrumbs-blog-remove', 'hideeditbox-post', 'hideeditbox-page', 'hideeditbox-attachment', 'hideeditbox-tax-category', 'hideeditbox-tax-post_tag', 'hideeditbox-tax-post_format', 'facebook_site', 'instagram_url', 'linkedin_url', 'myspace_url', 'og_default_image', 'og_default_image_id', 'og_frontpage_title', 'og_frontpage_desc', 'og_frontpage_image', 'og_frontpage_image_id', 'open_graph_frontpage_title', 'open_graph_frontpage_desc', 'open_graph_frontpage_image', 'open_graph_frontpage_image_id', 'opengraph', 'pinterest_url', 'pinterestverify', 'twitter', 'twitter_site', 'twitter_card_type', 'youtube_url', 'wikipedia_url', 'mastodon_url', 'indexables_indexing_completed', 'semrush_integration_active', 'semrush_tokens', 'semrush_country_code', 'enable_enhanced_slack_sharing', 'enable_metabox_insights', 'enable_link_suggestions', 'enable_index_now', 'enable_ai_generator', 'workouts', 'wincher_integration_active', 'wincher_tokens', 'wincher_website_id', 'wincher_automatically_add_keyphrases', 'first_time_install', 'other_social_urls', 'remove_feed_global', 'remove_feed_global_comments', 'remove_feed_post_comments', 'remove_feed_authors', 'remove_feed_categories', 'remove_feed_tags', 'remove_feed_custom_taxonomies', 'remove_feed_post_types', 'remove_feed_search', 'remove_atom_rdf_feeds', 'remove_shortlinks', 'remove_rest_api_links', 'remove_rsd_wlw_links', 'remove_oembed_links', 'remove_generator', 'remove_emoji_scripts', 'remove_powered_by_header', 'remove_pingback_header', 'clean_campaign_tracking_urls', 'clean_permalinks', 'clean_permalinks_extra_variables', 'search_cleanup', 'search_cleanup_emoji', 'search_cleanup_patterns', 'search_character_limit', 'redirect_search_pretty_urls', 'indexables_overview_state', 'deny_search_crawling', 'deny_wp_json_crawling', 'deny_adsbot_crawling', 'deny_ccbot_crawling', 'deny_google_extended_crawling', 'deny_gptbot_crawling', 'last_known_no_unindexed', 'site_kit_connected', 'site_kit_tracking_setup_widget_loaded', 'site_kit_tracking_first_interaction_stage', 'site_kit_tracking_last_interaction_stage', 'site_kit_tracking_setup_widget_temporarily_dismissed', 'site_kit_tracking_setup_widget_permanently_dismissed', 'google_site_kit_feature_enabled', 'ai_free_sparks_started_on', 'enable_llms_txt', 'llms_txt_selection_mode', 'configuration_finished_steps', 'enable_schema_aggregation_endpoint', 'schema_aggregation_endpoint_enabled_on', 'enable_task_list', 'enable_schema', // No need to add anything from WPSEO_Option_Tracking_Only as they are added automatically below. ]; /** * Returns the collection data. * * @return array The collection data. */ public function get() { /** * Filter: 'wpseo_tracking_settings_include_list' - Allow filtering the settings included in tracking. * * @param string $include_list The list with included setting names. */ $this->include_list = apply_filters( 'wpseo_tracking_settings_include_list', $this->include_list ); // Always include the tracking only option keys. $this->include_list = array_merge( $this->include_list, YoastSEO()->helpers->options->get_tracking_only_options() ); $options = WPSEO_Options::get_all(); // Returns the settings of which the keys intersect with the values of the include list. $options = array_intersect_key( $options, array_flip( $this->include_list ) ); return [ 'settings' => $this->anonymize_settings( $options ), ]; } /** * Anonimizes the WPSEO_Options array by replacing all $anonymous_settings values to 'used'. * * @param array $settings The settings. * * @return array The anonymized settings. */ private function anonymize_settings( $settings ) { foreach ( $this->anonymous_settings as $setting ) { if ( ! empty( $settings[ $setting ] ) ) { $settings[ $setting ] = 'used'; } } return $settings; } } admin/tracking/class-tracking-default-data.php 0000644 00000002644 15174712003 0015413 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ /** * Represents the default data. */ class WPSEO_Tracking_Default_Data implements WPSEO_Collection { /** * Returns the collection data. * * @return array The collection data. */ public function get() { return [ 'siteTitle' => get_option( 'blogname' ), '@timestamp' => (int) gmdate( 'Uv' ), 'wpVersion' => $this->get_wordpress_version(), 'homeURL' => home_url(), 'adminURL' => admin_url(), 'isMultisite' => is_multisite(), 'siteLanguage' => get_bloginfo( 'language' ), 'gmt_offset' => get_option( 'gmt_offset' ), 'timezoneString' => get_option( 'timezone_string' ), 'migrationStatus' => get_option( 'yoast_migrations_free' ), 'countPosts' => $this->get_post_count( 'post' ), 'countPages' => $this->get_post_count( 'page' ), ]; } /** * Returns the number of posts of a certain type. * * @param string $post_type The post type return the count for. * * @return int The count for this post type. */ protected function get_post_count( $post_type ) { $count = wp_count_posts( $post_type ); if ( isset( $count->publish ) ) { return $count->publish; } return 0; } /** * Returns the WordPress version. * * @return string The version. */ protected function get_wordpress_version() { global $wp_version; return $wp_version; } } admin/tracking/class-tracking-server-data.php 0000644 00000003757 15174712003 0015303 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ /** * Represents the server data. */ class WPSEO_Tracking_Server_Data implements WPSEO_Collection { /** * Returns the collection data. * * @return array The collection data. */ public function get() { return [ 'server' => $this->get_server_data(), ]; } /** * Returns the values with server details. * * @return array Array with the value. */ protected function get_server_data() { $server_data = []; // Validate if the server address is a valid IP-address. $ipaddress = isset( $_SERVER['SERVER_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['SERVER_ADDR'] ), FILTER_VALIDATE_IP ) : ''; if ( $ipaddress ) { $server_data['ip'] = $ipaddress; $server_data['Hostname'] = gethostbyaddr( $ipaddress ); } $server_data['os'] = function_exists( 'php_uname' ) ? php_uname() : PHP_OS; $server_data['PhpVersion'] = PHP_VERSION; $server_data['CurlVersion'] = $this->get_curl_info(); $server_data['PhpExtensions'] = $this->get_php_extensions(); return $server_data; } /** * Returns details about the curl version. * * @return array|null The curl info. Or null when curl isn't available.. */ protected function get_curl_info() { if ( ! function_exists( 'curl_version' ) ) { return null; } $curl = curl_version(); $ssl_support = true; if ( ! $curl['features'] && CURL_VERSION_SSL ) { $ssl_support = false; } return [ 'version' => $curl['version'], 'sslSupport' => $ssl_support, ]; } /** * Returns a list with php extensions. * * @return array Returns the state of the php extensions. */ protected function get_php_extensions() { return [ 'imagick' => extension_loaded( 'imagick' ), 'filter' => extension_loaded( 'filter' ), 'bcmath' => extension_loaded( 'bcmath' ), 'pcre' => extension_loaded( 'pcre' ), 'xml' => extension_loaded( 'xml' ), 'pdo_mysql' => extension_loaded( 'pdo_mysql' ), ]; } } admin/tracking/class-tracking.php 0000644 00000015555 15174712003 0013067 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ use Yoast\WP\SEO\Analytics\Application\Missing_Indexables_Collector; use Yoast\WP\SEO\Analytics\Application\To_Be_Cleaned_Indexables_Collector; /** * This class handles the tracking routine. */ class WPSEO_Tracking implements WPSEO_WordPress_Integration { /** * The tracking option name. * * @var string */ protected $option_name = 'wpseo_tracking_last_request'; /** * The limit for the option. * * @var int */ protected $threshold = 0; /** * The endpoint to send the data to. * * @var string */ protected $endpoint = ''; /** * The current time. * * @var int */ private $current_time; /** * WPSEO_Tracking constructor. * * @param string $endpoint The endpoint to send the data to. * @param int $threshold The limit for the option. */ public function __construct( $endpoint, $threshold ) { if ( ! $this->tracking_enabled() ) { return; } $this->endpoint = $endpoint; $this->threshold = $threshold; $this->current_time = time(); } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { if ( ! $this->tracking_enabled() ) { return; } // Send tracking data on `admin_init`. add_action( 'admin_init', [ $this, 'send' ], 1 ); // Add an action hook that will be triggered at the specified time by `wp_schedule_single_event()`. add_action( 'wpseo_send_tracking_data_after_core_update', [ $this, 'send' ] ); // Call `wp_schedule_single_event()` after a WordPress core update. add_action( 'upgrader_process_complete', [ $this, 'schedule_tracking_data_sending' ], 10, 2 ); } /** * Schedules a new sending of the tracking data after a WordPress core update. * * @param bool|WP_Upgrader $upgrader Optional. WP_Upgrader instance or false. * Depending on context, it might be a Theme_Upgrader, * Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader. * instance. Default false. * @param array $data Array of update data. * * @return void */ public function schedule_tracking_data_sending( $upgrader = false, $data = [] ) { // Return if it's not a WordPress core update. if ( ! $upgrader || ! isset( $data['type'] ) || $data['type'] !== 'core' ) { return; } /* * To uniquely identify the scheduled cron event, `wp_next_scheduled()` * needs to receive the same arguments as those used when originally * scheduling the event otherwise it will always return false. */ if ( ! wp_next_scheduled( 'wpseo_send_tracking_data_after_core_update', [ true ] ) ) { /* * Schedule sending of data tracking 6 hours after a WordPress core * update. Pass a `true` parameter for the callback `$force` argument. */ wp_schedule_single_event( ( time() + ( HOUR_IN_SECONDS * 6 ) ), 'wpseo_send_tracking_data_after_core_update', [ true ] ); } } /** * Sends the tracking data. * * @param bool $force Whether to send the tracking data ignoring the two * weeks time threshold. Default false. * * @return void */ public function send( $force = false ) { if ( ! $this->should_send_tracking( $force ) ) { return; } // Set a 'content-type' header of 'application/json'. $tracking_request_args = [ 'headers' => [ 'content-type:' => 'application/json', ], ]; $collector = $this->get_collector(); $request = new WPSEO_Remote_Request( $this->endpoint, $tracking_request_args ); $request->set_body( $collector->get_as_json() ); $request->send(); update_option( $this->option_name, $this->current_time, 'yes' ); } /** * Determines whether to send the tracking data. * * Returns false if tracking is disabled or the current page is one of the * admin plugins pages. Returns true when there's no tracking data stored or * the data was sent more than two weeks ago. The two weeks interval is set * when instantiating the class. * * @param bool $ignore_time_treshhold Whether to send the tracking data ignoring * the two weeks time treshhold. Default false. * * @return bool True when tracking data should be sent. */ protected function should_send_tracking( $ignore_time_treshhold = false ) { global $pagenow; // Only send tracking on the main site of a multi-site instance. This returns true on non-multisite installs. if ( is_network_admin() || ! is_main_site() ) { return false; } // Because we don't want to possibly block plugin actions with our routines. if ( in_array( $pagenow, [ 'plugins.php', 'plugin-install.php', 'plugin-editor.php' ], true ) ) { return false; } $last_time = get_option( $this->option_name ); // When tracking data haven't been sent yet or when sending data is forced. if ( ! $last_time || $ignore_time_treshhold ) { return true; } return $this->exceeds_treshhold( $this->current_time - $last_time ); } /** * Checks if the given amount of seconds exceeds the set threshold. * * @param int $seconds The amount of seconds to check. * * @return bool True when seconds is bigger than threshold. */ protected function exceeds_treshhold( $seconds ) { return ( $seconds > $this->threshold ); } /** * Returns the collector for collecting the data. * * @return WPSEO_Collector The instance of the collector. */ public function get_collector() { $collector = new WPSEO_Collector(); $collector->add_collection( new WPSEO_Tracking_Default_Data() ); $collector->add_collection( new WPSEO_Tracking_Server_Data() ); $collector->add_collection( new WPSEO_Tracking_Theme_Data() ); $collector->add_collection( new WPSEO_Tracking_Plugin_Data() ); $collector->add_collection( new WPSEO_Tracking_Settings_Data() ); $collector->add_collection( new WPSEO_Tracking_Addon_Data() ); $collector->add_collection( YoastSEO()->classes->get( Missing_Indexables_Collector::class ) ); $collector->add_collection( YoastSEO()->classes->get( To_Be_Cleaned_Indexables_Collector::class ) ); return $collector; } /** * See if we should run tracking at all. * * @return bool True when we can track, false when we can't. */ private function tracking_enabled() { // Check if we're allowing tracking. $tracking = WPSEO_Options::get( 'tracking' ); if ( $tracking === false ) { return false; } // Save this state. if ( $tracking === null ) { /** * Filter: 'wpseo_enable_tracking' - Enables the data tracking of Yoast SEO Premium and add-ons. * * @param string|false $is_enabled The enabled state. Default is false. */ $tracking = apply_filters( 'wpseo_enable_tracking', false ); WPSEO_Options::set( 'tracking', $tracking ); } if ( $tracking === false ) { return false; } if ( ! YoastSEO()->helpers->environment->is_production_mode() ) { return false; } return true; } } admin/tracking/class-tracking-plugin-data.php 0000644 00000004042 15174712003 0015257 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ /** * Represents the plugin data. */ class WPSEO_Tracking_Plugin_Data implements WPSEO_Collection { /** * Plugins with auto updating enabled. * * @var array */ private $auto_update_plugin_list; /** * Returns the collection data. * * @return array The collection data. */ public function get() { return [ 'plugins' => $this->get_plugin_data(), ]; } /** * Returns all plugins. * * @return array The formatted plugins. */ protected function get_plugin_data() { if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = wp_get_active_and_valid_plugins(); $plugins = array_map( 'get_plugin_data', $plugins ); $this->set_auto_update_plugin_list(); $plugins = array_map( [ $this, 'format_plugin' ], $plugins ); $plugin_data = []; foreach ( $plugins as $plugin ) { $plugin_key = sanitize_title( $plugin['name'] ); $plugin_data[ $plugin_key ] = $plugin; } return $plugin_data; } /** * Sets all auto updating plugin data so it can be used in the tracking list. * * @return void */ public function set_auto_update_plugin_list() { $auto_update_plugins = []; $auto_update_plugin_files = get_option( 'auto_update_plugins' ); if ( $auto_update_plugin_files ) { foreach ( $auto_update_plugin_files as $auto_update_plugin ) { $data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $auto_update_plugin ); $auto_update_plugins[ $data['Name'] ] = $data; } } $this->auto_update_plugin_list = $auto_update_plugins; } /** * Formats the plugin array. * * @param array $plugin The plugin details. * * @return array The formatted array. */ protected function format_plugin( array $plugin ) { return [ 'name' => $plugin['Name'], 'version' => $plugin['Version'], 'auto_updating' => array_key_exists( $plugin['Name'], $this->auto_update_plugin_list ), ]; } } admin/tracking/class-tracking-theme-data.php 0000644 00000002245 15174712003 0015066 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ /** * Represents the theme data. */ class WPSEO_Tracking_Theme_Data implements WPSEO_Collection { /** * Returns the collection data. * * @return array The collection data. */ public function get() { $theme = wp_get_theme(); return [ 'theme' => [ 'name' => $theme->get( 'Name' ), 'url' => $theme->get( 'ThemeURI' ), 'version' => $theme->get( 'Version' ), 'author' => [ 'name' => $theme->get( 'Author' ), 'url' => $theme->get( 'AuthorURI' ), ], 'parentTheme' => $this->get_parent_theme( $theme ), 'blockTemplateSupport' => current_theme_supports( 'block-templates' ), 'isBlockTheme' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(), ], ]; } /** * Returns the name of the parent theme. * * @param WP_Theme $theme The theme object. * * @return string|null The name of the parent theme or null. */ private function get_parent_theme( WP_Theme $theme ) { if ( is_child_theme() ) { return $theme->get( 'Template' ); } return null; } } admin/tracking/class-tracking-addon-data.php 0000644 00000010170 15174712003 0015045 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Tracking */ use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; /** * Represents the addon option data. */ class WPSEO_Tracking_Addon_Data implements WPSEO_Collection { /** * The local options we want to track. * * @var string[] The option_names for the options we want to track. */ private $local_include_list = [ 'use_multiple_locations', 'multiple_locations_same_organization', 'business_type', 'woocommerce_local_pickup_setting', ]; /** * The woo options we want to track. * * @var string[] The option_names for the options we want to track. */ private $woo_include_list = []; /** * The news options we want to track. * * @var string[] The option_names for the options we want to track. */ private $news_include_list = []; /** * The video options we want to track. * * @var string[] The option_names for the options we want to track. */ private $video_include_list = []; /** * Returns the collection data. * * @return array The collection data. */ public function get() { $addon_settings = []; $addon_manager = new WPSEO_Addon_Manager(); if ( $addon_manager->is_installed( WPSEO_Addon_Manager::LOCAL_SLUG ) ) { $addon_settings = $this->get_local_addon_settings( $addon_settings, 'wpseo_local', WPSEO_Addon_Manager::LOCAL_SLUG, $this->local_include_list ); } if ( $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { $addon_settings = $this->get_addon_settings( $addon_settings, 'wpseo_woo', WPSEO_Addon_Manager::WOOCOMMERCE_SLUG, $this->woo_include_list ); } if ( $addon_manager->is_installed( WPSEO_Addon_Manager::NEWS_SLUG ) ) { $addon_settings = $this->get_addon_settings( $addon_settings, 'wpseo_news', WPSEO_Addon_Manager::NEWS_SLUG, $this->news_include_list ); } if ( $addon_manager->is_installed( WPSEO_Addon_Manager::VIDEO_SLUG ) ) { $addon_settings = $this->get_addon_settings( $addon_settings, 'wpseo_video', WPSEO_Addon_Manager::VIDEO_SLUG, $this->video_include_list ); } return $addon_settings; } /** * Gets the tracked options from the addon * * @param array $addon_settings The current list of addon settings. * @param string $source_name The option key of the addon. * @param string $slug The addon slug. * @param array $option_include_list All the options to be included in tracking. * * @return array */ public function get_addon_settings( array $addon_settings, $source_name, $slug, $option_include_list ) { $source_options = get_option( $source_name, [] ); if ( ! is_array( $source_options ) || empty( $source_options ) ) { return $addon_settings; } $addon_settings[ $slug ] = array_intersect_key( $source_options, array_flip( $option_include_list ) ); return $addon_settings; } /** * Filter business_type in local addon settings. * * Remove the business_type setting when 'multiple_locations_shared_business_info' setting is turned off. * * @param array $addon_settings The current list of addon settings. * @param string $source_name The option key of the addon. * @param string $slug The addon slug. * @param array $option_include_list All the options to be included in tracking. * * @return array */ public function get_local_addon_settings( array $addon_settings, $source_name, $slug, $option_include_list ) { $source_options = get_option( $source_name, [] ); if ( ! is_array( $source_options ) || empty( $source_options ) ) { return $addon_settings; } $addon_settings[ $slug ] = array_intersect_key( $source_options, array_flip( $option_include_list ) ); if ( array_key_exists( 'use_multiple_locations', $source_options ) && array_key_exists( 'business_type', $addon_settings[ $slug ] ) && $source_options['use_multiple_locations'] === 'on' && $source_options['multiple_locations_shared_business_info'] === 'off' ) { $addon_settings[ $slug ]['business_type'] = 'multiple_locations'; } if ( ! ( new WooCommerce_Conditional() )->is_met() ) { unset( $addon_settings[ $slug ]['woocommerce_local_pickup_setting'] ); } return $addon_settings; } } admin/class-wincher-dashboard-widget.php 0000644 00000007040 15174712003 0014316 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Wincher dashboard widget. */ class Wincher_Dashboard_Widget implements WPSEO_WordPress_Integration { /** * Holds an instance of the admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Wincher_Dashboard_Widget constructor. */ public function __construct() { $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } /** * Register WordPress hooks. * * @return void */ public function register_hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_wincher_dashboard_assets' ] ); add_action( 'admin_init', [ $this, 'queue_wincher_dashboard_widget' ] ); } /** * Adds the Wincher dashboard widget if it should be shown. * * @return void */ public function queue_wincher_dashboard_widget() { if ( $this->show_widget() ) { add_action( 'wp_dashboard_setup', [ $this, 'add_wincher_dashboard_widget' ] ); } } /** * Adds the Wincher dashboard widget to WordPress. * * @return void */ public function add_wincher_dashboard_widget() { add_filter( 'postbox_classes_dashboard_wpseo-wincher-dashboard-overview', [ $this, 'wpseo_wincher_dashboard_overview_class' ] ); wp_add_dashboard_widget( 'wpseo-wincher-dashboard-overview', /* translators: %1$s expands to Yoast SEO, %2$s to Wincher */ sprintf( __( '%1$s / %2$s: Top Keyphrases', 'wordpress-seo' ), 'Yoast SEO', 'Wincher' ), [ $this, 'display_wincher_dashboard_widget' ], ); } /** * Adds CSS classes to the dashboard widget. * * @param array $classes An array of postbox CSS classes. * * @return array */ public function wpseo_wincher_dashboard_overview_class( $classes ) { $classes[] = 'yoast wpseo-wincherdashboard-overview'; return $classes; } /** * Displays the Wincher dashboard widget. * * @return void */ public function display_wincher_dashboard_widget() { echo '<div id="yoast-seo-wincher-dashboard-widget"></div>'; } /** * Enqueues assets for the dashboard if the current page is the dashboard. * * @return void */ public function enqueue_wincher_dashboard_assets() { if ( ! $this->is_dashboard_screen() ) { return; } $this->asset_manager->localize_script( 'wincher-dashboard-widget', 'wpseoWincherDashboardWidgetL10n', $this->localize_wincher_dashboard_script() ); $this->asset_manager->enqueue_script( 'wincher-dashboard-widget' ); $this->asset_manager->enqueue_style( 'wp-dashboard' ); $this->asset_manager->enqueue_style( 'monorepo' ); } /** * Translates strings used in the Wincher dashboard widget. * * @return array The translated strings. */ public function localize_wincher_dashboard_script() { return [ 'wincher_is_logged_in' => YoastSEO()->helpers->wincher->login_status(), 'wincher_website_id' => WPSEO_Options::get( 'wincher_website_id', '' ), ]; } /** * Checks if the current screen is the dashboard screen. * * @return bool Whether or not this is the dashboard screen. */ private function is_dashboard_screen() { $current_screen = get_current_screen(); return ( $current_screen instanceof WP_Screen && $current_screen->id === 'dashboard' ); } /** * Returns true when the Wincher dashboard widget should be shown. * * @return bool */ private function show_widget() { $analysis_seo = new WPSEO_Metabox_Analysis_SEO(); $user_can_edit = $analysis_seo->is_enabled() && current_user_can( 'edit_posts' ); $is_wincher_active = YoastSEO()->helpers->wincher->is_active(); return $user_can_edit && $is_wincher_active; } } admin/class-yoast-form.php 0000644 00000107350 15174712003 0011556 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Presenters\Admin\Light_Switch_Presenter; use Yoast\WP\SEO\Presenters\Admin\Sidebar_Presenter; /** * Admin form class. * * @since 2.0 */ class Yoast_Form { /** * Instance of this class * * @since 2.0 * * @var Yoast_Form */ public static $instance; /** * The short name of the option to use for the current page. * * @since 2.0 * * @var string */ public $option_name; /** * Option instance. * * @since 8.4 * @var WPSEO_Option|null */ protected $option_instance = null; /** * Get the singleton instance of this class. * * @since 2.0 * * @return Yoast_Form */ public static function get_instance() { if ( ! ( self::$instance instanceof self ) ) { self::$instance = new self(); } return self::$instance; } /** * Generates the header for admin pages. * * @since 2.0 * * @param bool $form Whether or not the form start tag should be included. * @param string $option The short name of the option to use for the current page. * @param bool $contains_files Whether the form should allow for file uploads. * @param bool $option_long_name Group name of the option. * * @return void */ public function admin_header( $form = true, $option = 'wpseo', $contains_files = false, $option_long_name = false ) { if ( ! $option_long_name ) { $option_long_name = WPSEO_Options::get_group_name( $option ); } ?> <div class="wrap yoast wpseo-admin-page <?php echo esc_attr( 'page-' . $option ); ?>"> <?php /** * Display the updated/error messages. * Only needed as our settings page is not under options, otherwise it will automatically be included. * * @see settings_errors() */ require_once ABSPATH . 'wp-admin/options-head.php'; ?> <h1 id="wpseo-title"><?php echo esc_html( get_admin_page_title() ); ?></h1> <div id="yst-settings-header-root"></div> <div class="wpseo_content_wrapper"> <div class="wpseo_content_cell" id="wpseo_content_top"> <?php if ( $form === true ) { $enctype = ( $contains_files ) ? ' enctype="multipart/form-data"' : ''; $network_admin = new Yoast_Network_Admin(); if ( $network_admin->meets_requirements() ) { $action_url = network_admin_url( 'settings.php' ); $hidden_fields_cb = [ $network_admin, 'settings_fields' ]; } else { $action_url = admin_url( 'options.php' ); $hidden_fields_cb = 'settings_fields'; } echo '<form action="' . esc_url( $action_url ) . '" method="post" id="wpseo-conf"' . $enctype . ' accept-charset="' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nothing to escape. . esc_attr( get_bloginfo( 'charset' ) ) . '" novalidate="novalidate">'; call_user_func( $hidden_fields_cb, $option_long_name ); } $this->set_option( $option ); } /** * Set the option used in output for form elements. * * @since 2.0 * * @param string $option_name Option key. * * @return void */ public function set_option( $option_name ) { $this->option_name = $option_name; $this->option_instance = WPSEO_Options::get_option_instance( $option_name ); if ( ! $this->option_instance ) { $this->option_instance = null; } } /** * Generates the footer for admin pages. * * @since 2.0 * * @param bool $submit Whether or not a submit button and form end tag should be shown. * @param bool $show_sidebar Whether or not to show the banner sidebar - used by premium plugins to disable it. * * @return void */ public function admin_footer( $submit = true, $show_sidebar = true ) { if ( $submit ) { $settings_changed_listener = new WPSEO_Admin_Settings_Changed_Listener(); echo '<div id="wpseo-submit-container">'; echo '<div id="wpseo-submit-container-float" class="wpseo-admin-submit">'; submit_button( __( 'Save changes', 'wordpress-seo' ) ); $settings_changed_listener->show_success_message(); echo '</div>'; echo '<div id="wpseo-submit-container-fixed" class="wpseo-admin-submit wpseo-admin-submit-fixed" style="display: none;">'; submit_button( __( 'Save changes', 'wordpress-seo' ) ); $settings_changed_listener->show_success_message(); echo '</div>'; echo '</div>'; echo ' </form>'; } /** * Apply general admin_footer hooks. */ do_action( 'wpseo_admin_footer', $this ); /** * Run possibly set actions to add for example an i18n box. */ do_action( 'wpseo_admin_promo_footer' ); echo ' </div><!-- end of div wpseo_content_top -->'; if ( $show_sidebar ) { $this->admin_sidebar(); } echo '</div><!-- end of div wpseo_content_wrapper -->'; do_action( 'wpseo_admin_below_content', $this ); echo ' </div><!-- end of wrap -->'; } /** * Generates the sidebar for admin pages. * * @since 2.0 * * @return void */ public function admin_sidebar() { // No banners in Premium. $addon_manager = new WPSEO_Addon_Manager(); if ( YoastSEO()->helpers->product->is_premium() && $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) { return; } $sidebar_presenter = new Sidebar_Presenter(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in presenter. echo $sidebar_presenter->present(); } /** * Output a label element. * * @since 2.0 * * @param string $text Label text string, which can contain escaped html. * @param array $attr HTML attributes set. * * @return void */ public function label( $text, $attr ) { $defaults = [ 'class' => 'checkbox', 'close' => true, 'for' => '', 'aria_label' => '', ]; $attr = wp_parse_args( $attr, $defaults ); $aria_label = ''; if ( $attr['aria_label'] !== '' ) { $aria_label = ' aria-label="' . esc_attr( $attr['aria_label'] ) . '"'; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. Specifically, the $text variable can contain escaped html. echo "<label class='" . esc_attr( $attr['class'] ) . "' for='" . esc_attr( $attr['for'] ) . "'$aria_label>$text"; if ( $attr['close'] ) { echo '</label>'; } } /** * Output a legend element. * * @since 3.4 * * @param string $text Legend text string. * @param array $attr HTML attributes set. * * @return void */ public function legend( $text, $attr ) { $defaults = [ 'id' => '', 'class' => '', ]; $attr = wp_parse_args( $attr, $defaults ); $id = ( $attr['id'] === '' ) ? '' : ' id="' . esc_attr( $attr['id'] ) . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo '<legend class="' . esc_attr( 'yoast-form-legend ' . $attr['class'] ) . '"' . $id . '>' . $text . '</legend>'; } /** * Create a Checkbox input field. * * @since 2.0 * * @param string $variable The variable within the option to create the checkbox for. * @param string $label The label to show for the variable. * @param bool $label_left Whether the label should be left (true) or right (false). * @param array $attr Extra attributes to add to the checkbox. * * @return void */ public function checkbox( $variable, $label, $label_left = false, $attr = [] ) { $val = $this->get_field_value( $variable, false ); $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); if ( $val === true ) { $val = 'on'; } $class = ''; if ( $label_left !== false ) { $this->label( $label_left, [ 'for' => $variable ] ); } else { $class = 'double'; } $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input class="', esc_attr( 'checkbox ' . $class ), '" type="checkbox" id="', esc_attr( $variable ), '" name="', esc_attr( $this->option_name . '[' . $variable . ']' ), '" value="on"', checked( $val, 'on', false ), $disabled_attribute, '/>'; if ( ! empty( $label ) ) { $this->label( $label, [ 'for' => $variable ] ); } echo '<br class="clear" />'; } /** * Creates a Checkbox input field list. * * @since 12.8 * * @param string $variable The variables within the option to create the checkbox list for. * @param string $labels The labels to show for the variable. * @param array $attr Extra attributes to add to the checkbox list. * * @return void */ public function checkbox_list( $variable, $labels, $attr = [] ) { $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $values = $this->get_field_value( $variable, [] ); foreach ( $labels as $name => $label ) { printf( '<input class="checkbox double" id="%1$s" type="checkbox" name="%2$s" %3$s %5$s value="%4$s"/>', esc_attr( $variable . '-' . $name ), esc_attr( $this->option_name . '[' . $variable . '][' . $name . ']' ), checked( ! empty( $values[ $name ] ), true, false ), esc_attr( $name ), disabled( ( isset( $attr['disabled'] ) && $attr['disabled'] ), true, false ), ); printf( '<label class="checkbox" for="%1$s">%2$s</label>', esc_attr( $variable . '-' . $name ), // #1 esc_html( $label ), ); echo '<br class="clear">'; } } /** * Create a light switch input field using a single checkbox. * * @since 3.1 * * @param string $variable The variable within the option to create the checkbox for. * @param string $label The visual label text for the toggle. * @param array $buttons Array of two visual labels for the buttons (defaults Disabled/Enabled). * @param bool $reverse Reverse order of buttons (default true). * @param string $help Inline Help that will be printed out before the toggle. * @param bool $strong Whether the visual label is displayed in strong text. Default is false. * Starting from Yoast SEO 16.5, the visual label is forced to bold via CSS. * @param array $attr Extra attributes to add to the light switch. * * @return void */ public function light_switch( $variable, $label, $buttons = [], $reverse = true, $help = '', $strong = false, $attr = [] ) { $val = $this->get_field_value( $variable, false ); $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); if ( $val === true ) { $val = 'on'; } $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); $output = new Light_Switch_Presenter( $variable, $label, $buttons, $this->option_name . '[' . $variable . ']', $val, $reverse, $help, $strong, $disabled_attribute, ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: All output is properly escaped or hardcoded in the presenter. echo $output; } /** * Create a Text input field. * * @since 2.0 * @since 2.1 Introduced the `$attr` parameter. * * @param string $variable The variable within the option to create the text input field for. * @param string $label The label to show for the variable. * @param array|string $attr Extra attributes to add to the input field. Can be class, disabled, autocomplete. * * @return void */ public function textinput( $variable, $label, $attr = [] ) { $type = 'text'; if ( ! is_array( $attr ) ) { $attr = [ 'class' => $attr, 'disabled' => false, ]; } $defaults = [ 'placeholder' => '', 'class' => '', ]; $attr = wp_parse_args( $attr, $defaults ); $val = $this->get_field_value( $variable, '' ); if ( isset( $attr['type'] ) && $attr['type'] === 'url' ) { $val = urldecode( $val ); $type = 'url'; } $attributes = isset( $attr['autocomplete'] ) ? ' autocomplete="' . esc_attr( $attr['autocomplete'] ) . '"' : ''; $this->label( $label, [ 'for' => $variable, 'class' => 'textinput', ], ); $aria_attributes = Yoast_Input_Validation::get_the_aria_invalid_attribute( $variable ); $aria_attributes .= Yoast_Input_Validation::get_the_aria_describedby_attribute( $variable ); $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input', $attributes, $aria_attributes, ' class="', esc_attr( 'textinput ' . $attr['class'] ), '" placeholder="', esc_attr( $attr['placeholder'] ), '" type="', $type, '" id="', esc_attr( $variable ), '" name="', esc_attr( $this->option_name . '[' . $variable . ']' ), '" value="', esc_attr( $val ), '"', $disabled_attribute, '/>', '<br class="clear" />'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in getter. echo Yoast_Input_Validation::get_the_error_description( $variable ); } /** * Create a Number input field. * * @param string $variable The variable within the option to create the text input field for. * @param string $label The label to show for the variable. * @param array|string $attr Extra attributes to add to the input field. Can be class, disabled, autocomplete. * * @return void */ public function number( $variable, $label, $attr = [] ) { $type = 'number'; $defaults = [ 'placeholder' => '', 'class' => 'number', 'disabled' => false, 'min' => 0, 'max' => 100, ]; $attr = wp_parse_args( $attr, $defaults ); $val = $this->get_field_value( $variable, 0 ); $this->label( $label, [ 'for' => $variable, 'class' => 'textinput ' . $attr['class'], ], ); $aria_attributes = Yoast_Input_Validation::get_the_aria_invalid_attribute( $variable ); $aria_attributes .= Yoast_Input_Validation::get_the_aria_describedby_attribute( $variable ); $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input' . $aria_attributes . ' class="' . esc_attr( $attr['class'] ) . '" type="' . $type . '" id="', esc_attr( $variable ), '" min="', esc_attr( $attr['min'] ), '" max="', esc_attr( $attr['max'] ), '" name="', esc_attr( $this->option_name . '[' . $variable . ']' ), '" value="', esc_attr( $val ), '"', $disabled_attribute, '/>', '<br class="clear" />'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in getter. echo Yoast_Input_Validation::get_the_error_description( $variable ); } /** * Creates a text input field with with the ability to add content after the label. * * @param string $variable The variable within the option to create the text input field for. * @param string $label The label to show for the variable. * @param array $attr Extra attributes to add to the input field. * * @return void */ public function textinput_extra_content( $variable, $label, $attr = [] ) { $type = 'text'; $defaults = [ 'class' => 'yoast-field-group__inputfield', 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $val = $this->get_field_value( $variable, '' ); if ( isset( $attr['type'] ) && $attr['type'] === 'url' ) { $val = urldecode( $val ); $type = 'url'; } echo '<div class="yoast-field-group__title">'; $this->label( $label, [ 'for' => $variable, 'class' => $attr['class'] . '--label', ], ); if ( isset( $attr['extra_content'] ) ) { // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: may contain HTML that should not be escaped. echo $attr['extra_content']; } echo '</div>'; $aria_attributes = Yoast_Input_Validation::get_the_aria_invalid_attribute( $variable ); $aria_attributes .= Yoast_Input_Validation::get_the_aria_describedby_attribute( $variable ); // phpcs:disable WordPress.Security.EscapeOutput -- Reason: output is properly escaped or hardcoded. printf( '<input type="%1$s" name="%2$s" id="%3$s" class="%4$s"%5$s%6$s%7$s value="%8$s"%9$s>', $type, esc_attr( $this->option_name . '[' . $variable . ']' ), esc_attr( $variable ), esc_attr( $attr['class'] ), isset( $attr['placeholder'] ) ? ' placeholder="' . esc_attr( $attr['placeholder'] ) . '"' : '', isset( $attr['autocomplete'] ) ? ' autocomplete="' . esc_attr( $attr['autocomplete'] ) . '"' : '', $aria_attributes, esc_attr( $val ), $this->get_disabled_attribute( $variable, $attr ), ); // phpcs:enable // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: output is properly escaped. echo Yoast_Input_Validation::get_the_error_description( $variable ); } /** * Create a textarea. * * @since 2.0 * * @param string $variable The variable within the option to create the textarea for. * @param string $label The label to show for the variable. * @param string|array $attr The CSS class or an array of attributes to assign to the textarea. * * @return void */ public function textarea( $variable, $label, $attr = [] ) { if ( ! is_array( $attr ) ) { $attr = [ 'class' => $attr, ]; } $defaults = [ 'cols' => '', 'rows' => '', 'class' => '', 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $val = $this->get_field_value( $variable, '' ); $this->label( $label, [ 'for' => $variable, 'class' => 'textinput', ], ); $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<textarea cols="' . esc_attr( $attr['cols'] ) . '" rows="' . esc_attr( $attr['rows'] ) . '" class="' . esc_attr( 'textinput ' . $attr['class'] ) . '" id="' . esc_attr( $variable ) . '" name="' . esc_attr( $this->option_name . '[' . $variable . ']' ), '"', $disabled_attribute, '>' . esc_textarea( $val ) . '</textarea><br class="clear" />'; } /** * Create a hidden input field. * * @since 2.0 * * @param string $variable The variable within the option to create the hidden input for. * @param string $id The ID of the element. * @param mixed $val Optional. The value to set in the input field. Otherwise the value from the options will be used. * * @return void */ public function hidden( $variable, $id = '', $val = null ) { $val ??= $this->get_field_value( $variable, '' ); if ( is_bool( $val ) ) { $val = ( $val === true ) ? 'true' : 'false'; } if ( $id === '' ) { $id = 'hidden_' . $variable; } echo '<input type="hidden" id="' . esc_attr( $id ) . '" name="' . esc_attr( $this->option_name . '[' . $variable . ']' ), '" value="' . esc_attr( $val ) . '"/>'; } /** * Create a Select Box. * * @since 2.0 * * @param string $variable The variable within the option to create the select for. * @param string $label The label to show for the variable. * @param array $select_options The select options to choose from. * @param string $styled The select style. Use 'styled' to get a styled select. Default 'unstyled'. * @param bool $show_label Whether or not to show the label, if not, it will be applied as an aria-label. * @param array $attr Extra attributes to add to the select. * @param string $help Optional. Inline Help HTML that will be printed after the label. Default is empty. * * @return void */ public function select( $variable, $label, array $select_options, $styled = 'unstyled', $show_label = true, $attr = [], $help = '' ) { if ( empty( $select_options ) ) { return; } $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); if ( $show_label ) { $this->label( $label, [ 'for' => $variable, 'class' => 'select', ], ); echo $help; // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: The help contains HTML. } $select_name = esc_attr( $this->option_name ) . '[' . esc_attr( $variable ) . ']'; $active_option = $this->get_field_value( $variable, '' ); $wrapper_start_tag = ''; $wrapper_end_tag = ''; $select = new Yoast_Input_Select( $variable, $select_name, $select_options, $active_option ); $select->add_attribute( 'class', 'select' ); if ( $this->is_control_disabled( $variable ) || ( isset( $attr['disabled'] ) && $attr['disabled'] ) ) { $select->add_attribute( 'disabled', 'disabled' ); } if ( ! $show_label ) { $select->add_attribute( 'aria-label', $label ); } if ( $styled === 'styled' ) { $wrapper_start_tag = '<span class="yoast-styled-select">'; $wrapper_end_tag = '</span>'; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo $wrapper_start_tag; $select->output_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo $wrapper_end_tag; echo '<br class="clear"/>'; } /** * Create a File upload field. * * @since 2.0 * * @param string $variable The variable within the option to create the file upload field for. * @param string $label The label to show for the variable. * @param array $attr Extra attributes to add to the file upload input. * * @return void */ public function file_upload( $variable, $label, $attr = [] ) { $val = $this->get_field_value( $variable, '' ); if ( is_array( $val ) ) { $val = $val['url']; } $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $var_esc = esc_attr( $variable ); $this->label( $label, [ 'for' => $variable, 'class' => 'select', ], ); $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input type="file" value="' . esc_attr( $val ) . '" class="textinput" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" id="' . $var_esc . '"', $disabled_attribute, '/>'; // Need to save separate array items in hidden inputs, because empty file inputs type will be deleted by settings API. if ( ! empty( $val ) ) { $this->hidden( 'file', $this->option_name . '_file' ); $this->hidden( 'url', $this->option_name . '_url' ); $this->hidden( 'type', $this->option_name . '_type' ); } echo '<br class="clear"/>'; } /** * Media input. * * @since 2.0 * @deprecated 23.5 * @codeCoverageIgnore * * @param string $variable Option name. * @param string $label Label message. * @param array $attr Extra attributes to add to the media input and buttons. * * @return void */ public function media_input( $variable, $label, $attr = [] ) { _deprecated_function( __METHOD__, 'Yoast SEO 23.5' ); $val = $this->get_field_value( $variable, '' ); $id_value = $this->get_field_value( $variable . '_id', '' ); $var_esc = esc_attr( $variable ); $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $this->label( $label, [ 'for' => 'wpseo_' . $variable, 'class' => 'select', ], ); $id_field_id = 'wpseo_' . $var_esc . '_id'; echo '<span>'; echo '<input', ' class="textinput"', ' id="wpseo_', $var_esc, '"', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. ' type="text" size="36"', ' name="', esc_attr( $this->option_name ), '[', $var_esc, ']"', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. ' value="', esc_attr( $val ), '"', ' readonly="readonly"', ' /> '; echo '<input', ' type="hidden"', ' id="', esc_attr( $id_field_id ), '"', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. ' name="', esc_attr( $this->option_name ), '[', $var_esc, '_id]"', ' value="', esc_attr( $id_value ), '"', ' />'; echo '</span>'; echo '<br class="clear"/>'; } /** * Create a Radio input field. * * @since 2.0 * * @param string $variable The variable within the option to create the radio button for. * @param array $values The radio options to choose from. * @param string $legend Optional. The legend to show for the field set, if any. * @param array $legend_attr Optional. The attributes for the legend, if any. * @param array $attr Extra attributes to add to the radio button. * * @return void */ public function radio( $variable, $values, $legend = '', $legend_attr = [], $attr = [] ) { if ( ! is_array( $values ) || $values === [] ) { return; } $val = $this->get_field_value( $variable, false ); $var_esc = esc_attr( $variable ); $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo '<fieldset class="yoast-form-fieldset wpseo_radio_block" id="' . $var_esc . '">'; if ( is_string( $legend ) && $legend !== '' ) { $legend_defaults = [ 'id' => '', 'class' => 'radiogroup', ]; $legend_attr = wp_parse_args( $legend_attr, $legend_defaults ); $this->legend( $legend, $legend_attr ); } foreach ( $values as $key => $value ) { $label = $value; $aria_label = ''; if ( is_array( $value ) ) { $label = ( $value['label'] ?? '' ); $aria_label = ( $value['aria_label'] ?? '' ); } $key_esc = esc_attr( $key ); $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input type="radio" class="radio" id="' . $var_esc . '-' . $key_esc . '" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" value="' . $key_esc . '" ' . checked( $val, $key_esc, false ) . $disabled_attribute . ' />'; $this->label( $label, [ 'for' => $var_esc . '-' . $key_esc, 'class' => 'radio', 'aria_label' => $aria_label, ], ); } echo '</fieldset>'; } /** * Create a toggle switch input field using two radio buttons. * * @since 3.1 * * @param string $variable The variable within the option to create the radio buttons for. * @param array $values Associative array of on/off keys and their values to be used as * the label elements text for the radio buttons. Optionally, each * value can be an array of visible label text and screen reader text. * @param string $label The visual label for the radio buttons group, used as the fieldset legend. * @param string $help Inline Help that will be printed out before the visible toggles text. * @param array $attr Extra attributes to add to the toggle switch. * * @return void */ public function toggle_switch( $variable, $values, $label, $help = '', $attr = [] ) { if ( ! is_array( $values ) || $values === [] ) { return; } $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); if ( isset( $attr['preserve_disabled_value'] ) && $attr['preserve_disabled_value'] ) { $this->hidden( $variable ); $variable .= '_disabled'; } $val = $this->get_field_value( $variable, false ); if ( $val === true ) { $val = 'on'; } if ( $val === false ) { $val = 'off'; } $help_class = ! empty( $help ) ? ' switch-container__has-help' : ''; $has_premium_upsell = ( isset( $attr['show_premium_upsell'] ) && $attr['show_premium_upsell'] && isset( $attr['premium_upsell_url'] ) && ! empty( $attr['premium_upsell_url'] ) ); $upsell_class = ( $has_premium_upsell ) ? ' premium-upsell' : ''; $var_esc = esc_attr( $variable ); printf( '<div class="%s">', esc_attr( 'switch-container' . $help_class . $upsell_class ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo '<fieldset id="', $var_esc, '" class="fieldset-switch-toggle"><legend>', $label, '</legend>', $help; // Show disabled note if attribute does not exists or does exist and is set to true. if ( ! isset( $attr['show_disabled_note'] ) || ( $attr['show_disabled_note'] === true ) ) { if ( isset( $attr['note_when_disabled'] ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo $this->get_disabled_note( $variable, $attr['note_when_disabled'] ); } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. echo $this->get_disabled_note( $variable ); } } echo '<div class="switch-toggle switch-candy switch-yoast-seo">'; foreach ( $values as $key => $value ) { $screen_reader_text_html = ''; if ( is_array( $value ) ) { $screen_reader_text = $value['screen_reader_text']; $screen_reader_text_html = '<span class="screen-reader-text"> ' . esc_html( $screen_reader_text ) . '</span>'; $value = $value['text']; } $key_esc = esc_attr( $key ); $for = $var_esc . '-' . $key_esc; $disabled_attribute = $this->get_disabled_attribute( $variable, $attr ); // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $disabled_attribute output is hardcoded and all other output is properly escaped. echo '<input type="radio" id="' . $for . '" name="' . esc_attr( $this->option_name ) . '[' . $var_esc . ']" value="' . $key_esc . '" ' . checked( $val, $key_esc, false ) . $disabled_attribute . ' />', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped before. '<label for="', $for, '">', esc_html( $value ), $screen_reader_text_html, '</label>'; } $upsell_button = ''; if ( $has_premium_upsell ) { $upsell_button = '<a class="yoast-button yoast-button--buy yoast-button--small" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2" href=' . esc_url( $attr['premium_upsell_url'] ) . ' target="_blank">' . esc_html__( 'Unlock with Premium!', 'wordpress-seo' ) /* translators: Hidden accessibility text. */ . '<span class="screen-reader-text">' . esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>' . '<span aria-hidden="true" class="yoast-button--buy__caret"></span></a>'; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All variable output is escaped above. echo '<a></a></div></fieldset><div class="clear"></div>' . $upsell_button . '</div>' . PHP_EOL . PHP_EOL; } /** * Creates a toggle switch to define whether an indexable should be indexed or not. * * @param string $variable The variable within the option to create the radio buttons for. * @param string $label The visual label for the radio buttons group, used as the fieldset legend. * @param string $help Inline Help that will be printed out before the visible toggles text. * @param array $attr Extra attributes to add to the index switch. * * @return void */ public function index_switch( $variable, $label, $help = '', $attr = [] ) { $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $index_switch_values = [ 'off' => __( 'On', 'wordpress-seo' ), 'on' => __( 'Off', 'wordpress-seo' ), ]; $is_disabled = ( isset( $attr['disabled'] ) && $attr['disabled'] ); $this->toggle_switch( $variable, $index_switch_values, sprintf( /* translators: %s expands to an indexable object's name, like a post type or taxonomy */ esc_html__( 'Show %s in search results?', 'wordpress-seo' ), $label, ), $help, [ 'disabled' => $is_disabled ], ); } /** * Creates a toggle switch to show hide certain options. * * @param string $variable The variable within the option to create the radio buttons for. * @param string $label The visual label for the radio buttons group, used as the fieldset legend. * @param bool $inverse_keys Whether or not the option keys need to be inverted to support older functions. * @param string $help Inline Help that will be printed out before the visible toggles text. * @param array $attr Extra attributes to add to the show-hide switch. * * @return void */ public function show_hide_switch( $variable, $label, $inverse_keys = false, $help = '', $attr = [] ) { $defaults = [ 'disabled' => false, ]; $attr = wp_parse_args( $attr, $defaults ); $on_key = ( $inverse_keys ) ? 'off' : 'on'; $off_key = ( $inverse_keys ) ? 'on' : 'off'; $show_hide_switch = [ $on_key => __( 'On', 'wordpress-seo' ), $off_key => __( 'Off', 'wordpress-seo' ), ]; $is_disabled = ( isset( $attr['disabled'] ) && $attr['disabled'] ); $this->toggle_switch( $variable, $show_hide_switch, $label, $help, [ 'disabled' => $is_disabled ], ); } /** * Retrieves the value for the form field. * * @param string $field_name The field name to retrieve the value for. * @param string|null $default_value The default value, when field has no value. * * @return mixed|null The retrieved value. */ protected function get_field_value( $field_name, $default_value = null ) { // On multisite subsites, the Usage tracking feature should always be set to Off. if ( $this->is_tracking_on_subsite( $field_name ) ) { return false; } return WPSEO_Options::get( $field_name, $default_value ); } /** * Checks whether a given control should be disabled. * * @param string $variable The variable within the option to check whether its control should be disabled. * * @return bool True if control should be disabled, false otherwise. */ protected function is_control_disabled( $variable ) { if ( $this->option_instance === null ) { return false; } // Disable the Usage tracking feature for multisite subsites. if ( $this->is_tracking_on_subsite( $variable ) ) { return true; } return $this->option_instance->is_disabled( $variable ); } /** * Gets the explanation note to print if a given control is disabled. * * @param string $variable The variable within the option to print a disabled note for. * @param string $custom_note An optional custom note to print instead. * * @return string Explanation note HTML string, or empty string if no note necessary. */ protected function get_disabled_note( $variable, $custom_note = '' ) { if ( $custom_note === '' && ! $this->is_control_disabled( $variable ) ) { return ''; } $disabled_message = esc_html__( 'This feature has been disabled by the network admin.', 'wordpress-seo' ); // The explanation to show when disabling the Usage tracking feature for multisite subsites. if ( $this->is_tracking_on_subsite( $variable ) ) { $disabled_message = esc_html__( 'This feature has been disabled since subsites never send tracking data.', 'wordpress-seo' ); } if ( $custom_note ) { $disabled_message = esc_html( $custom_note ); } return '<p class="disabled-note">' . $disabled_message . '</p>'; } /** * Determines whether we are dealing with the Usage tracking feature on a multisite subsite. * This feature requires specific behavior for the toggle switch. * * @param string $feature_setting The feature setting. * * @return bool True if we are dealing with the Usage tracking feature on a multisite subsite. */ protected function is_tracking_on_subsite( $feature_setting ) { return ( $feature_setting === 'tracking' && ! is_network_admin() && ! is_main_site() ); } /** * Returns the disabled attribute HTML. * * @param string $variable The variable within the option of the related form element. * @param array $attr Extra attributes added to the form element. * * @return string The disabled attribute HTML. */ protected function get_disabled_attribute( $variable, $attr ) { if ( $this->is_control_disabled( $variable ) || ( isset( $attr['disabled'] ) && $attr['disabled'] ) ) { return ' disabled'; } return ''; } } admin/class-bulk-title-editor-list-table.php 0000644 00000004357 15174712003 0015057 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Bulk Editor * @since 1.5.0 */ /** * Implements table for bulk title editing. */ class WPSEO_Bulk_Title_Editor_List_Table extends WPSEO_Bulk_List_Table { /** * Current type for this class will be title. * * @var string */ protected $page_type = 'title'; /** * Settings with are used in __construct. * * @var array */ protected $settings = [ 'singular' => 'wpseo_bulk_title', 'plural' => 'wpseo_bulk_titles', 'ajax' => true, ]; /** * The field in the database where meta field is saved. * * @var string */ protected $target_db_field = 'title'; /** * The columns shown on the table. * * @return array */ public function get_columns() { $columns = [ /* translators: %1$s expands to Yoast SEO */ 'col_existing_yoast_seo_title' => sprintf( __( 'Existing %1$s Title', 'wordpress-seo' ), 'Yoast SEO' ), /* translators: %1$s expands to Yoast SEO */ 'col_new_yoast_seo_title' => sprintf( __( 'New %1$s Title', 'wordpress-seo' ), 'Yoast SEO' ), ]; return $this->merge_columns( $columns ); } /** * Parse the title columns. * * @param string $column_name Column name. * @param object $record Data object. * @param string $attributes HTML attributes. * * @return string */ protected function parse_page_specific_column( $column_name, $record, $attributes ) { // Fill meta data if exists in $this->meta_data. $meta_data = ( ! empty( $this->meta_data[ $record->ID ] ) ) ? $this->meta_data[ $record->ID ] : []; switch ( $column_name ) { case 'col_existing_yoast_seo_title': // @todo Inconsistent return/echo behavior R. // I traced the escaping of the attributes to WPSEO_Bulk_List_Table::column_attributes. // The output of WPSEO_Bulk_List_Table::parse_meta_data_field is properly escaped. // phpcs:ignore WordPress.Security.EscapeOutput echo $this->parse_meta_data_field( $record->ID, $attributes ); break; case 'col_new_yoast_seo_title': return sprintf( '<input type="text" id="%1$s" name="%1$s" class="wpseo-new-title" data-id="%2$s" aria-labelledby="col_new_yoast_seo_title" />', 'wpseo-new-title-' . $record->ID, $record->ID, ); } unset( $meta_data ); } } admin/class-option-tabs-formatter.php 0000644 00000005532 15174712003 0013715 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Options\Tabs */ use Yoast\WP\SEO\Presenters\Admin\Beta_Badge_Presenter; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; /** * Class WPSEO_Option_Tabs_Formatter. */ class WPSEO_Option_Tabs_Formatter { /** * Retrieves the path to the view of the tab. * * @param WPSEO_Option_Tabs $option_tabs Option Tabs to get base from. * @param WPSEO_Option_Tab $tab Tab to get name from. * * @return string */ public function get_tab_view( WPSEO_Option_Tabs $option_tabs, WPSEO_Option_Tab $tab ) { return WPSEO_PATH . 'admin/views/tabs/' . $option_tabs->get_base() . '/' . $tab->get_name() . '.php'; } /** * Outputs the option tabs. * * @param WPSEO_Option_Tabs $option_tabs Option Tabs to get tabs from. * * @return void */ public function run( WPSEO_Option_Tabs $option_tabs ) { echo '<h2 class="nav-tab-wrapper" id="wpseo-tabs">'; foreach ( $option_tabs->get_tabs() as $tab ) { $label = esc_html( $tab->get_label() ); if ( $tab->is_beta() ) { $label = '<span style="margin-right:4px;">' . $label . '</span>' . new Beta_Badge_Presenter( $tab->get_name() ); } elseif ( $tab->is_premium() ) { $label = '<span style="margin-right:4px;">' . $label . '</span>' . new Premium_Badge_Presenter( $tab->get_name() ); } printf( '<a class="nav-tab" id="%1$s" href="%2$s">%3$s</a>', esc_attr( $tab->get_name() . '-tab' ), esc_url( '#top#' . $tab->get_name() ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: we do this on purpose $label, ); } echo '</h2>'; foreach ( $option_tabs->get_tabs() as $tab ) { $identifier = $tab->get_name(); $class = 'wpseotab ' . ( $tab->has_save_button() ? 'save' : 'nosave' ); printf( '<div id="%1$s" class="%2$s">', esc_attr( $identifier ), esc_attr( $class ) ); $tab_filter_name = sprintf( '%s_%s', $option_tabs->get_base(), $tab->get_name() ); /** * Allows to override the content that is display on the specific option tab. * * @internal For internal Yoast SEO use only. * * @param string|null $tab_contents The content that should be displayed for this tab. Leave empty for default behaviour. * @param WPSEO_Option_Tabs $option_tabs The registered option tabs. * @param WPSEO_Option_Tab $tab The tab that is being displayed. */ $option_tab_content = apply_filters( 'wpseo_option_tab-' . $tab_filter_name, null, $option_tabs, $tab ); if ( ! empty( $option_tab_content ) ) { echo wp_kses_post( $option_tab_content ); } if ( empty( $option_tab_content ) ) { // Output the settings view for all tabs. $tab_view = $this->get_tab_view( $option_tabs, $tab ); if ( is_file( $tab_view ) ) { $yform = Yoast_Form::get_instance(); require $tab_view; } } echo '</div>'; } } } admin/class-primary-term-admin.php 0000644 00000016506 15174712003 0013176 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Adds the UI to change the primary term for a post. */ class WPSEO_Primary_Term_Admin implements WPSEO_WordPress_Integration { /** * Constructor. * * @return void */ public function register_hooks() { add_filter( 'wpseo_content_meta_section_content', [ $this, 'add_input_fields' ] ); add_action( 'admin_footer', [ $this, 'wp_footer' ], 10 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Gets the current post ID. * * @return int The post ID. */ protected function get_current_id() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer. $post_id = isset( $_GET['post'] ) && is_string( $_GET['post'] ) ? (int) wp_unslash( $_GET['post'] ) : 0; if ( $post_id === 0 && isset( $GLOBALS['post_ID'] ) ) { $post_id = (int) $GLOBALS['post_ID']; } return $post_id; } /** * Adds hidden fields for primary taxonomies. * * @param string $content The metabox content. * * @return string The HTML content. */ public function add_input_fields( $content ) { $taxonomies = $this->get_primary_term_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $content .= $this->primary_term_field( $taxonomy->name ); $content .= wp_nonce_field( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce', false, false ); } return $content; } /** * Generates the HTML for a hidden field for a primary taxonomy. * * @param string $taxonomy_name The taxonomy's slug. * * @return string The HTML for a hidden primary taxonomy field. */ protected function primary_term_field( $taxonomy_name ) { return sprintf( '<input class="yoast-wpseo-primary-term" type="hidden" id="%1$s" name="%2$s" value="%3$s" />', esc_attr( $this->generate_field_id( $taxonomy_name ) ), esc_attr( $this->generate_field_name( $taxonomy_name ) ), esc_attr( $this->get_primary_term( $taxonomy_name ) ), ); } /** * Generates an id for a primary taxonomy's hidden field. * * @param string $taxonomy_name The taxonomy's slug. * * @return string The field id. */ protected function generate_field_id( $taxonomy_name ) { return 'yoast-wpseo-primary-' . $taxonomy_name; } /** * Generates a name for a primary taxonomy's hidden field. * * @param string $taxonomy_name The taxonomy's slug. * * @return string The field id. */ protected function generate_field_name( $taxonomy_name ) { return WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy_name . '_term'; } /** * Adds primary term templates. * * @return void */ public function wp_footer() { $taxonomies = $this->get_primary_term_taxonomies(); if ( ! empty( $taxonomies ) ) { $this->include_js_templates(); } } /** * Enqueues all the assets needed for the primary term interface. * * @return void */ public function enqueue_assets() { global $pagenow; if ( ! WPSEO_Metabox::is_post_edit( $pagenow ) ) { return; } $taxonomies = $this->get_primary_term_taxonomies(); // Only enqueue if there are taxonomies that need a primary term. if ( empty( $taxonomies ) ) { return; } $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_style( 'primary-category' ); $mapped_taxonomies = $this->get_mapped_taxonomies_for_js( $taxonomies ); $data = [ 'taxonomies' => $mapped_taxonomies, ]; $asset_manager->localize_script( 'post-edit', 'wpseoPrimaryCategoryL10n', $data ); $asset_manager->localize_script( 'post-edit-classic', 'wpseoPrimaryCategoryL10n', $data ); } /** * Gets the id of the primary term. * * @param string $taxonomy_name Taxonomy name for the term. * * @return int primary term id */ protected function get_primary_term( $taxonomy_name ) { $primary_term = new WPSEO_Primary_Term( $taxonomy_name, $this->get_current_id() ); return $primary_term->get_primary_term(); } /** * Returns all the taxonomies for which the primary term selection is enabled. * * @param int|null $post_id Default current post ID. * @return array */ protected function get_primary_term_taxonomies( $post_id = null ) { $post_id ??= $this->get_current_id(); $taxonomies = wp_cache_get( 'primary_term_taxonomies_' . $post_id, 'wpseo' ); if ( $taxonomies !== false ) { return $taxonomies; } $taxonomies = $this->generate_primary_term_taxonomies( $post_id ); wp_cache_set( 'primary_term_taxonomies_' . $post_id, $taxonomies, 'wpseo' ); return $taxonomies; } /** * Includes templates file. * * @return void */ protected function include_js_templates() { include_once WPSEO_PATH . 'admin/views/js-templates-primary-term.php'; } /** * Generates the primary term taxonomies. * * @param int $post_id ID of the post. * * @return array */ protected function generate_primary_term_taxonomies( $post_id ) { $post_type = get_post_type( $post_id ); $all_taxonomies = get_object_taxonomies( $post_type, 'objects' ); $all_taxonomies = array_filter( $all_taxonomies, [ $this, 'filter_hierarchical_taxonomies' ] ); /** * Filters which taxonomies for which the user can choose the primary term. * * @param array $taxonomies An array of taxonomy objects that are primary_term enabled. * @param string $post_type The post type for which to filter the taxonomies. * @param array $all_taxonomies All taxonomies for this post types, even ones that don't have primary term * enabled. */ $taxonomies = (array) apply_filters( 'wpseo_primary_term_taxonomies', $all_taxonomies, $post_type, $all_taxonomies ); return $taxonomies; } /** * Creates a map of taxonomies for localization. * * @param array $taxonomies The taxononmies that should be mapped. * * @return array The mapped taxonomies. */ protected function get_mapped_taxonomies_for_js( $taxonomies ) { return array_map( [ $this, 'map_taxonomies_for_js' ], $taxonomies ); } /** * Returns an array suitable for use in the javascript. * * @param stdClass $taxonomy The taxonomy to map. * * @return array The mapped taxonomy. */ private function map_taxonomies_for_js( $taxonomy ) { $primary_term = $this->get_primary_term( $taxonomy->name ); if ( empty( $primary_term ) ) { $primary_term = ''; } $terms = get_terms( [ 'taxonomy' => $taxonomy->name, 'update_term_meta_cache' => false, 'fields' => 'id=>name', ], ); $mapped_terms_for_js = []; foreach ( $terms as $id => $name ) { $mapped_terms_for_js[] = [ 'id' => $id, 'name' => $name, ]; } return [ 'title' => $taxonomy->labels->singular_name, 'name' => $taxonomy->name, 'primary' => $primary_term, 'singularLabel' => $taxonomy->labels->singular_name, 'fieldId' => $this->generate_field_id( $taxonomy->name ), 'restBase' => ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name, 'terms' => $mapped_terms_for_js, ]; } /** * Returns whether or not a taxonomy is hierarchical. * * @param stdClass $taxonomy Taxonomy object. * * @return bool */ private function filter_hierarchical_taxonomies( $taxonomy ) { return (bool) $taxonomy->hierarchical; } } admin/class-suggested-plugins.php 0000644 00000010520 15174712003 0013117 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Suggested_Plugins */ use Yoast\WP\SEO\Conditionals\Conditional; /** * Class WPSEO_Suggested_Plugins */ class WPSEO_Suggested_Plugins implements WPSEO_WordPress_Integration { /** * Holds the availability checker. * * @var WPSEO_Plugin_Availability */ protected $availability_checker; /** * Holds the notification center. * * @var Yoast_Notification_Center */ protected $notification_center; /** * WPSEO_Suggested_Plugins constructor. * * @param WPSEO_Plugin_Availability $availability_checker The availability checker to use. * @param Yoast_Notification_Center $notification_center The notification center to add notifications to. */ public function __construct( WPSEO_Plugin_Availability $availability_checker, Yoast_Notification_Center $notification_center ) { $this->availability_checker = $availability_checker; $this->notification_center = $notification_center; } /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this->availability_checker, 'register' ] ); add_action( 'admin_init', [ $this, 'add_notifications' ] ); } /** * Adds notifications (when necessary). * * @return void */ public function add_notifications() { $checker = $this->availability_checker; // Get all Yoast plugins that have dependencies. $plugins = $checker->get_plugins_with_dependencies(); foreach ( $plugins as $plugin_name => $plugin ) { $notification_id = 'wpseo-suggested-plugin-' . $plugin_name; if ( ! $checker->dependencies_are_satisfied( $plugin ) ) { $this->notification_center->remove_notification_by_id( $notification_id ); continue; } if ( ! $checker->is_installed( $plugin ) ) { $notification = $this->get_yoast_seo_suggested_plugins_notification( $notification_id, $plugin ); $this->notification_center->add_notification( $notification ); continue; } $this->notification_center->remove_notification_by_id( $notification_id ); } } /** * Build Yoast SEO suggested plugins notification. * * @param string $notification_id The id of the notification to be created. * @param array<string, string|bool|array<string, Conditional>> $plugin The plugin to retrieve the data from. * * @return Yoast_Notification The notification containing the suggested plugin. */ protected function get_yoast_seo_suggested_plugins_notification( $notification_id, $plugin ) { $message = $this->create_install_suggested_plugin_message( $plugin ); return new Yoast_Notification( $message, [ 'id' => $notification_id, 'type' => Yoast_Notification::WARNING, 'capabilities' => [ 'install_plugins' ], ], ); } /** * Creates a message to suggest the installation of a particular plugin. * * @param array $suggested_plugin The suggested plugin. * * @return string The install suggested plugin message. */ protected function create_install_suggested_plugin_message( $suggested_plugin ) { /* translators: %1$s expands to an opening strong tag, %2$s expands to the dependency name, %3$s expands to a closing strong tag, %4$s expands to an opening anchor tag, %5$s expands to a closing anchor tag. */ $message = __( 'It looks like you aren\'t using our %1$s%2$s addon%3$s. %4$sUpgrade today%5$s to unlock more tools and SEO features to make your products stand out in search results.', 'wordpress-seo' ); $install_link = WPSEO_Admin_Utils::get_install_link( $suggested_plugin ); return sprintf( $message, '<strong>', $install_link, '</strong>', $this->create_more_information_link( $suggested_plugin['url'], $suggested_plugin['title'] ), '</a>', ); } /** * Creates a more information link that directs the user to WordPress.org Plugin repository. * * @param string $url The URL to the plugin's page. * @param string $name The name of the plugin. * * @return string The more information link. */ protected function create_more_information_link( $url, $name ) { return sprintf( '<a href="%s" aria-label="%s" target="_blank" rel="noopener noreferrer">', $url, /* translators: Hidden accessibility text; %1$s expands to the dependency name */ sprintf( __( 'More information about %1$s', 'wordpress-seo' ), $name ), ); } } admin/menu/class-replacevar-field.php 0000644 00000004170 15174712003 0013623 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Renders a single replacement variable field. */ class WPSEO_Replacevar_Field { /** * Forms instance. * * @var Yoast_Form Yoast */ private $yform; /** * The id for the hidden field. * * @var string */ private $field_id; /** * The label for the field. * * @var string */ private $label; /** * The page type for the context of the recommended replace vars. * * @var string */ private $page_type_recommended; /** * The page type for the context of the editor specific replace vars. * * @var string */ private $page_type_specific; /** * Constructs the object. * * @param Yoast_Form $yform Yoast forms. * @param string $field_id The field id. * @param string $label The field label. * @param string $page_type_recommended The page type for the context of the recommended replace vars. * @param string $page_type_specific The page type for the context of the editor specific replace vars. */ public function __construct( Yoast_Form $yform, $field_id, $label, $page_type_recommended, $page_type_specific ) { $this->yform = $yform; $this->field_id = $field_id; $this->label = $label; $this->page_type_recommended = $page_type_recommended; $this->page_type_specific = $page_type_specific; } /** * Renders a div for the react application to mount to, and hidden inputs where * the app should store it's value so they will be properly saved when the form * is submitted. * * @return void */ public function render() { $this->yform->hidden( $this->field_id, $this->field_id ); printf( '<div data-react-replacevar-field data-react-replacevar-field-id="%1$s" data-react-replacevar-field-label="%2$s" data-react-replacevar-page-type-recommended="%3$s" data-react-replacevar-page-type-specific="%4$s"></div>', esc_attr( $this->field_id ), esc_attr( $this->label ), esc_attr( $this->page_type_recommended ), esc_attr( $this->page_type_specific ), ); } } admin/menu/class-replacevar-editor.php 0000644 00000013706 15174712003 0014033 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Renders a replacement variable editor. */ class WPSEO_Replacevar_Editor { /** * Yoast Forms instance. * * @var Yoast_Form */ private $yform; /** * The arguments required for the div to render. * * @var array { * @type string $title The title field id. * @type string $description The description field id. * @type string $page_type_recommended The page type for the context of the recommended replace vars. * @type string $page_type_specific The page type for the context of the editor specific replace vars. * @type bool $paper_style Optional. Whether the editor has paper style. * @type string $label_title Optional. The label to use for the title field. * @type string $label_description Optional. The label to use for the description field. * @type string $description_placeholder Optional. The placeholder text to use for the description field. * @type bool $has_new_badge Optional. Whether to show the "New" badge. * @type bool $has_premium_badge Optional. Whether to show the "Premium" badge. * } */ private $arguments; /** * Constructs the object. * * @param Yoast_Form $yform Yoast forms. * @param array $arguments { * The arguments that can be given. * * @type string $title The title field id. * @type string $description The description field id. * @type string $page_type_recommended The page type for the context of the recommended replace vars. * @type string $page_type_specific The page type for the context of the editor specific replace vars. * @type bool $paper_style Optional. Whether the editor has paper style. * @type string $label_title Optional. The label to use for the title field. * @type string $label_description Optional. The label to use for the description field. * @type string $description_placeholder Optional. The placeholder text to use for the description field. * @type bool $has_new_badge Optional. Whether to show the "New" badge. * @type bool $has_premium_badge Optional. Whether to show the "Premium" badge. * } */ public function __construct( Yoast_Form $yform, $arguments ) { $arguments = wp_parse_args( $arguments, [ 'paper_style' => true, 'label_title' => '', 'label_description' => '', 'description_placeholder' => '', 'has_new_badge' => false, 'is_disabled' => false, 'has_premium_badge' => false, ], ); $this->validate_arguments( $arguments ); $this->yform = $yform; $this->arguments = [ 'title' => (string) $arguments['title'], 'description' => (string) $arguments['description'], 'page_type_recommended' => (string) $arguments['page_type_recommended'], 'page_type_specific' => (string) $arguments['page_type_specific'], 'paper_style' => (bool) $arguments['paper_style'], 'label_title' => (string) $arguments['label_title'], 'label_description' => (string) $arguments['label_description'], 'description_placeholder' => (string) $arguments['description_placeholder'], 'has_new_badge' => (bool) $arguments['has_new_badge'], 'is_disabled' => (bool) $arguments['is_disabled'], 'has_premium_badge' => (bool) $arguments['has_premium_badge'], ]; } /** * Renders a div for the react application to mount to, and hidden inputs where * the app should store it's value so they will be properly saved when the form * is submitted. * * @return void */ public function render() { $this->yform->hidden( $this->arguments['title'], $this->arguments['title'] ); $this->yform->hidden( $this->arguments['description'], $this->arguments['description'] ); printf( '<div data-react-replacevar-editor data-react-replacevar-title-field-id="%1$s" data-react-replacevar-metadesc-field-id="%2$s" data-react-replacevar-page-type-recommended="%3$s" data-react-replacevar-page-type-specific="%4$s" data-react-replacevar-paper-style="%5$s" data-react-replacevar-label-title="%6$s" data-react-replacevar-label-description="%7$s" data-react-replacevar-description-placeholder="%8$s" data-react-replacevar-has-new-badge="%9$s" data-react-replacevar-is-disabled="%10$s" data-react-replacevar-has-premium-badge="%11$s" ></div>', esc_attr( $this->arguments['title'] ), esc_attr( $this->arguments['description'] ), esc_attr( $this->arguments['page_type_recommended'] ), esc_attr( $this->arguments['page_type_specific'] ), esc_attr( $this->arguments['paper_style'] ), esc_attr( $this->arguments['label_title'] ), esc_attr( $this->arguments['label_description'] ), esc_attr( $this->arguments['description_placeholder'] ), esc_attr( $this->arguments['has_new_badge'] ), esc_attr( $this->arguments['is_disabled'] ), esc_attr( $this->arguments['has_premium_badge'] ), ); } /** * Validates the replacement variable editor arguments. * * @param array $arguments The arguments to validate. * * @return void * * @throws InvalidArgumentException Thrown when not all required arguments are present. */ protected function validate_arguments( array $arguments ) { $required_arguments = [ 'title', 'description', 'page_type_recommended', 'page_type_specific', 'paper_style', ]; foreach ( $required_arguments as $field_name ) { if ( ! array_key_exists( $field_name, $arguments ) ) { throw new InvalidArgumentException( sprintf( /* translators: %1$s expands to the missing field name. */ __( 'Not all required fields are given. Missing field %1$s', 'wordpress-seo' ), $field_name, ), ); } } } } admin/menu/class-menu.php 0000644 00000003777 15174712003 0011376 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Registers the regular admin menu and network admin menu implementations. */ class WPSEO_Menu implements WPSEO_WordPress_Integration { /** * The page identifier used in WordPress to register the admin page. * * !DO NOT CHANGE THIS! * * @var string */ public const PAGE_IDENTIFIER = 'wpseo_dashboard'; /** * List of classes that add admin functionality. * * @var array */ protected $admin_features; /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { $admin_menu = new WPSEO_Admin_Menu( $this ); $admin_menu->register_hooks(); if ( WPSEO_Utils::is_plugin_network_active() ) { $network_admin_menu = new WPSEO_Network_Admin_Menu( $this ); $network_admin_menu->register_hooks(); } $capability_normalizer = new WPSEO_Submenu_Capability_Normalize(); $capability_normalizer->register_hooks(); } /** * Returns the main menu page identifier. * * @return string Page identifier to use. */ public function get_page_identifier() { return self::PAGE_IDENTIFIER; } /** * Loads the requested admin settings page. * * @return void */ public function load_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); $this->show_page( $page ); } } /** * Shows an admin settings page. * * @param string $page Page to display. * * @return void */ protected function show_page( $page ) { switch ( $page ) { case 'wpseo_tools': require_once WPSEO_PATH . 'admin/pages/tools.php'; break; case 'wpseo_files': require_once WPSEO_PATH . 'admin/views/tool-file-editor.php'; break; default: break; } } } admin/menu/class-submenu-capability-normalize.php 0000644 00000001770 15174712003 0016214 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Normalize submenu capabilities to `wpseo_manage_options`. */ class WPSEO_Submenu_Capability_Normalize implements WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_filter( 'wpseo_submenu_pages', [ $this, 'normalize_submenus_capability' ] ); } /** * Normalizes any `manage_options` to `wpseo_manage_options`. * * This is needed as the module plugins are not updated with the new capabilities directly, * but they should not be shown as main menu items. * * @param array $submenu_pages List of subpages to convert. * * @return array Converted subpages. */ public function normalize_submenus_capability( $submenu_pages ) { foreach ( $submenu_pages as $index => $submenu_page ) { if ( $submenu_page[3] === 'manage_options' ) { $submenu_pages[ $index ][3] = 'wpseo_manage_options'; } } return $submenu_pages; } } admin/menu/class-base-menu.php 0000644 00000020656 15174712003 0012301 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; /** * Admin menu base class. */ abstract class WPSEO_Base_Menu implements WPSEO_WordPress_Integration { /** * A menu. * * @var WPSEO_Menu */ protected $menu; /** * Constructs the Admin Menu. * * @param WPSEO_Menu $menu Menu to use. */ public function __construct( WPSEO_Menu $menu ) { $this->menu = $menu; } /** * Returns the list of registered submenu pages. * * @return array List of registered submenu pages. */ abstract public function get_submenu_pages(); /** * Creates a submenu formatted array. * * @param string $page_title Page title to use. * @param string $page_slug Page slug to use. * @param callable|null $callback Optional. Callback which handles the page request. * @param callable[]|null $hook Optional. Hook to trigger when the page is registered. * * @return array Formatted submenu. */ protected function get_submenu_page( $page_title, $page_slug, $callback = null, $hook = null ) { $callback ??= $this->get_admin_page_callback(); return [ $this->get_page_identifier(), '', $page_title, $this->get_manage_capability(), $page_slug, $callback, $hook, ]; } /** * Registers submenu pages as menu pages. * * This method should only be used if the user does not have the required capabilities * to access the parent menu page. * * @param array $submenu_pages List of submenu pages to register. * * @return void */ protected function register_menu_pages( $submenu_pages ) { if ( ! is_array( $submenu_pages ) || empty( $submenu_pages ) ) { return; } // Loop through submenu pages and add them. array_walk( $submenu_pages, [ $this, 'register_menu_page' ] ); } /** * Registers submenu pages. * * @param array $submenu_pages List of submenu pages to register. * * @return void */ protected function register_submenu_pages( $submenu_pages ) { if ( ! is_array( $submenu_pages ) || empty( $submenu_pages ) ) { return; } // Loop through submenu pages and add them. array_walk( $submenu_pages, [ $this, 'register_submenu_page' ] ); } /** * Registers a submenu page as a menu page. * * This method should only be used if the user does not have the required capabilities * to access the parent menu page. * * @param array $submenu_page { * Submenu page definition. * * @type string $0 Parent menu page slug. * @type string $1 Page title, currently unused. * @type string $2 Title to display in the menu. * @type string $3 Required capability to access the page. * @type string $4 Page slug. * @type callable $5 Callback to run when the page is rendered. * @type array $6 Optional. List of callbacks to run when the page is loaded. * } * * @return void */ protected function register_menu_page( $submenu_page ) { // If the submenu page requires the general manage capability, it must be added as an actual submenu page. if ( $submenu_page[3] === $this->get_manage_capability() ) { return; } $page_title = 'Yoast SEO: ' . $submenu_page[2]; // Register submenu page as menu page. $hook_suffix = add_menu_page( $page_title, $submenu_page[2], $submenu_page[3], $submenu_page[4], $submenu_page[5], $this->get_icon_svg(), 99, ); // If necessary, add hooks for the submenu page. if ( isset( $submenu_page[6] ) && ( is_array( $submenu_page[6] ) ) ) { $this->add_page_hooks( $hook_suffix, $submenu_page[6] ); } } /** * Registers a submenu page. * * This method will override the capability of the page to automatically use the * general manage capability. Use the `register_menu_page()` method if the submenu * page should actually use a different capability. * * @param array $submenu_page { * Submenu page definition. * * @type string $0 Parent menu page slug. * @type string $1 Page title, currently unused. * @type string $2 Title to display in the menu. * @type string $3 Required capability to access the page. * @type string $4 Page slug. * @type callable $5 Callback to run when the page is rendered. * @type array $6 Optional. List of callbacks to run when the page is loaded. * } * * @return void */ protected function register_submenu_page( $submenu_page ) { $page_title = $submenu_page[2]; /* * Handle the Google Search Console special case by passing a fake parent * page slug. This way, the sub-page is stil registered and can be accessed * directly. Its menu item won't be displayed. */ if ( $submenu_page[4] === 'wpseo_search_console' ) { // Set the parent page slug to a non-existing one. $submenu_page[0] = 'wpseo_fake_menu_parent_page_slug'; } $page_title .= ' - Yoast SEO'; // Register submenu page. $hook_suffix = add_submenu_page( $submenu_page[0], $page_title, $submenu_page[2], $submenu_page[3], $submenu_page[4], $submenu_page[5], ); // If necessary, add hooks for the submenu page. if ( isset( $submenu_page[6] ) && ( is_array( $submenu_page[6] ) ) ) { $this->add_page_hooks( $hook_suffix, $submenu_page[6] ); } } /** * Adds hook callbacks for a given admin page hook suffix. * * @param string $hook_suffix Admin page hook suffix, as returned by `add_menu_page()` * or `add_submenu_page()`. * @param array $callbacks Callbacks to add. * * @return void */ protected function add_page_hooks( $hook_suffix, array $callbacks ) { foreach ( $callbacks as $callback ) { add_action( 'load-' . $hook_suffix, $callback ); } } /** * Gets the main admin page identifier. * * @return string Admin page identifier. */ protected function get_page_identifier() { return $this->menu->get_page_identifier(); } /** * Checks whether the current user has capabilities to manage all options. * * @return bool True if capabilities are sufficient, false otherwise. */ protected function check_manage_capability() { return WPSEO_Capability_Utils::current_user_can( $this->get_manage_capability() ); } /** * Returns the capability that is required to manage all options. * * @return string Capability to check against. */ abstract protected function get_manage_capability(); /** * Returns the page handler callback. * * @return array Callback page handler. */ protected function get_admin_page_callback() { return [ $this->menu, 'load_page' ]; } /** * Returns the page title to use for the licenses page. * * @deprecated 25.5 * @codeCoverageIgnore * * @return string The title for the license page. */ protected function get_license_page_title() { static $title = null; _deprecated_function( __METHOD__, 'Yoast SEO 25.5' ); $title ??= __( 'Upgrades', 'wordpress-seo' ); if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) && ! YoastSEO()->helpers->product->is_premium() ) { $title = __( 'Upgrades', 'wordpress-seo' ) . '<span class="yoast-menu-bf-sale-badge">' . __( '30% OFF', 'wordpress-seo' ) . '</span>'; } return $title; } /** * Returns a base64 URL for the svg for use in the menu. * * @param bool $base64 Whether or not to return base64'd output. * * @return string SVG icon. */ public function get_icon_svg( $base64 = true ) { $svg = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" style="fill:#82878c" viewBox="0 0 512 512" role="img" aria-hidden="true" focusable="false"><g><g><g><g><path d="M203.6,395c6.8-17.4,6.8-36.6,0-54l-79.4-204h70.9l47.7,149.4l74.8-207.6H116.4c-41.8,0-76,34.2-76,76V357c0,41.8,34.2,76,76,76H173C189,424.1,197.6,410.3,203.6,395z"/></g><g><path d="M471.6,154.8c0-41.8-34.2-76-76-76h-3L285.7,365c-9.6,26.7-19.4,49.3-30.3,68h216.2V154.8z"/></g></g><path stroke-width="2.974" stroke-miterlimit="10" d="M338,1.3l-93.3,259.1l-42.1-131.9h-89.1l83.8,215.2c6,15.5,6,32.5,0,48c-7.4,19-19,37.3-53,41.9l-7.2,1v76h8.3c81.7,0,118.9-57.2,149.6-142.9L431.6,1.3H338z M279.4,362c-32.9,92-67.6,128.7-125.7,131.8v-45c37.5-7.5,51.3-31,59.1-51.1c7.5-19.3,7.5-40.7,0-60l-75-192.7h52.8l53.3,166.8l105.9-294h58.1L279.4,362z"/></g></g></svg>'; if ( $base64 ) { //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- This encoding is intended. return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } return $svg; } } admin/menu/class-admin-menu.php 0000644 00000007376 15174712003 0012463 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Registers the admin menu on the left of the admin area. */ class WPSEO_Admin_Menu extends WPSEO_Base_Menu { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { // Needs the lower than default priority so other plugins can hook underneath it without issue. add_action( 'admin_menu', [ $this, 'register_settings_page' ], 5 ); } /** * Registers the menu item submenus. * * @return void */ public function register_settings_page() { $manage_capability = $this->get_manage_capability(); $page_identifier = $this->get_page_identifier(); $admin_page_callback = $this->get_admin_page_callback(); // Get all submenu pages. $submenu_pages = $this->get_submenu_pages(); foreach ( $submenu_pages as $submenu_page ) { if ( WPSEO_Capability_Utils::current_user_can( $submenu_page[3] ) ) { $manage_capability = $submenu_page[3]; $page_identifier = $submenu_page[4]; $admin_page_callback = $submenu_page[5]; break; } } foreach ( $submenu_pages as $index => $submenu_page ) { $submenu_pages[ $index ][0] = $page_identifier; } /* * The current user has the capability to control anything. * This means that all submenus and dashboard can be shown. */ global $admin_page_hooks; add_menu_page( 'Yoast SEO: ' . __( 'Dashboard', 'wordpress-seo' ), 'Yoast SEO ' . $this->get_notification_counter(), $manage_capability, $page_identifier, $admin_page_callback, $this->get_icon_svg(), 99, ); // Wipe notification bits from hooks. // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- This is a deliberate action. $admin_page_hooks[ $page_identifier ] = 'seo'; // Add submenu items to the main menu if possible. $this->register_submenu_pages( $submenu_pages ); } /** * Returns the list of registered submenu pages. * * @return array List of registered submenu pages. */ public function get_submenu_pages() { global $wpseo_admin; $search_console_callback = null; // Account for when the available submenu pages are requested from outside the admin. if ( isset( $wpseo_admin ) ) { $google_search_console = new WPSEO_GSC(); $search_console_callback = [ $google_search_console, 'display' ]; } // Submenu pages. $submenu_pages = [ $this->get_submenu_page( __( 'Search Console', 'wordpress-seo' ), 'wpseo_search_console', $search_console_callback, ), $this->get_submenu_page( __( 'Tools', 'wordpress-seo' ), 'wpseo_tools' ), ]; /** * Filter: 'wpseo_submenu_pages' - Collects all submenus that need to be shown. * * @param array $submenu_pages List with all submenu pages. */ return (array) apply_filters( 'wpseo_submenu_pages', $submenu_pages ); } /** * Returns the notification count in HTML format. * * @return string The notification count in HTML format. */ protected function get_notification_counter() { $notification_center = Yoast_Notification_Center::get(); $notification_count = $notification_center->get_notification_count(); // Add main page. /* translators: Hidden accessibility text; %s: number of notifications. */ $notifications = sprintf( _n( '%s notification', '%s notifications', $notification_count, 'wordpress-seo' ), number_format_i18n( $notification_count ) ); return sprintf( '<span class="update-plugins count-%1$d"><span class="plugin-count" aria-hidden="true">%1$d</span><span class="screen-reader-text">%2$s</span></span>', $notification_count, $notifications ); } /** * Returns the capability that is required to manage all options. * * @return string Capability to check against. */ protected function get_manage_capability() { return 'wpseo_manage_options'; } } admin/menu/class-network-admin-menu.php 0000644 00000004667 15174712003 0014152 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Menu */ /** * Network Admin Menu handler. */ class WPSEO_Network_Admin_Menu extends WPSEO_Base_Menu { /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { // Needs the lower than default priority so other plugins can hook underneath it without issue. add_action( 'network_admin_menu', [ $this, 'register_settings_page' ], 5 ); } /** * Register the settings page for the Network settings. * * @return void */ public function register_settings_page() { if ( ! $this->check_manage_capability() ) { return; } add_menu_page( __( 'Network Settings', 'wordpress-seo' ) . ' - Yoast SEO', 'Yoast SEO', $this->get_manage_capability(), $this->get_page_identifier(), [ $this, 'network_config_page' ], $this->get_icon_svg(), ); $submenu_pages = $this->get_submenu_pages(); $this->register_submenu_pages( $submenu_pages ); } /** * Returns the list of registered submenu pages. * * @return array List of registered submenu pages. */ public function get_submenu_pages() { // Submenu pages. $submenu_pages = [ $this->get_submenu_page( __( 'General', 'wordpress-seo' ), $this->get_page_identifier(), [ $this, 'network_config_page' ], ), ]; if ( WPSEO_Utils::allow_system_file_edit() === true ) { $submenu_pages[] = $this->get_submenu_page( __( 'Edit Files', 'wordpress-seo' ), 'wpseo_files' ); } /** * Filter: 'wpseo_network_submenu_pages' - Collects all network submenus that need to be shown. * * @internal For internal Yoast SEO use only. * * @param array $submenu_pages List with all submenu pages. */ return (array) apply_filters( 'wpseo_network_submenu_pages', $submenu_pages ); } /** * Loads the form for the network configuration page. * * @return void */ public function network_config_page() { require_once WPSEO_PATH . 'admin/pages/network.php'; } /** * Checks whether the current user has capabilities to manage all options. * * @return bool True if capabilities are sufficient, false otherwise. */ protected function check_manage_capability() { return current_user_can( $this->get_manage_capability() ); } /** * Returns the capability that is required to manage all options. * * @return string Capability to check against. */ protected function get_manage_capability() { return 'wpseo_manage_network_options'; } } admin/class-yoast-input-validation.php 0000644 00000016322 15174712003 0014100 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Implements server-side user input validation. * * @since 12.0 */ class Yoast_Input_Validation { /** * The error descriptions. * * @since 12.1 * * @var array<string, string> */ private static $error_descriptions = []; /** * Check whether an option group is a Yoast SEO setting. * * The normal pattern is 'yoast' . $option_name . 'options'. * * @since 12.0 * * @param string $group_name The option group name. * * @return bool Whether or not it's an Yoast SEO option group. */ public static function is_yoast_option_group_name( $group_name ) { return ( strpos( $group_name, 'yoast' ) !== false ); } /** * Adds an error message to the document title when submitting a settings * form and errors are returned. * * Uses the WordPress `admin_title` filter in the WPSEO_Option subclasses. * * @since 12.0 * * @param string $admin_title The page title, with extra context added. * * @return string The modified or original admin title. */ public static function add_yoast_admin_document_title_errors( $admin_title ) { $errors = get_settings_errors(); $error_count = 0; foreach ( $errors as $error ) { // For now, filter the admin title only in the Yoast SEO settings pages. if ( self::is_yoast_option_group_name( $error['setting'] ) && $error['code'] !== 'settings_updated' ) { ++$error_count; } } if ( $error_count > 0 ) { return sprintf( /* translators: %1$s: amount of errors, %2$s: the admin page title */ _n( 'The form contains %1$s error. %2$s', 'The form contains %1$s errors. %2$s', $error_count, 'wordpress-seo' ), number_format_i18n( $error_count ), $admin_title, ); } return $admin_title; } /** * Checks whether a specific form input field was submitted with an invalid value. * * @since 12.1 * * @param string $error_code Must be the same slug-name used for the field variable and for `add_settings_error()`. * * @return bool Whether or not the submitted input field contained an invalid value. */ public static function yoast_form_control_has_error( $error_code ) { $errors = get_settings_errors(); foreach ( $errors as $error ) { if ( $error['code'] === $error_code ) { return true; } } return false; } /** * Sets the error descriptions. * * @since 12.1 * @deprecated 23.3 * @codeCoverageIgnore * * @param array<string, string> $descriptions An associative array of error descriptions. * For each entry, the key must be the setting variable. * * @return void */ public static function set_error_descriptions( $descriptions = [] ) { // @phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Needed for BC. _deprecated_function( __METHOD__, 'Yoast SEO 23.3' ); } /** * Gets all the error descriptions. * * @since 12.1 * @deprecated 23.3 * @codeCoverageIgnore * * @return array<string, string> An associative array of error descriptions. */ public static function get_error_descriptions() { _deprecated_function( __METHOD__, 'Yoast SEO 23.3' ); return []; } /** * Gets a specific error description. * * @since 12.1 * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string|null The error description. */ public static function get_error_description( $error_code ) { if ( ! isset( self::$error_descriptions[ $error_code ] ) ) { return null; } return self::$error_descriptions[ $error_code ]; } /** * Gets the aria-invalid HTML attribute based on the submitted invalid value. * * @since 12.1 * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string The aria-invalid HTML attribute or empty string. */ public static function get_the_aria_invalid_attribute( $error_code ) { if ( self::yoast_form_control_has_error( $error_code ) ) { return ' aria-invalid="true"'; } return ''; } /** * Gets the aria-describedby HTML attribute based on the submitted invalid value. * * @since 12.1 * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string The aria-describedby HTML attribute or empty string. */ public static function get_the_aria_describedby_attribute( $error_code ) { if ( self::yoast_form_control_has_error( $error_code ) && self::get_error_description( $error_code ) ) { return ' aria-describedby="' . esc_attr( $error_code ) . '-error-description"'; } return ''; } /** * Gets the error description wrapped in a HTML paragraph. * * @since 12.1 * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string The error description HTML or empty string. */ public static function get_the_error_description( $error_code ) { $error_description = self::get_error_description( $error_code ); if ( self::yoast_form_control_has_error( $error_code ) && $error_description ) { return '<p id="' . esc_attr( $error_code ) . '-error-description" class="yoast-input-validation__error-description">' . $error_description . '</p>'; } return ''; } /** * Adds the submitted invalid value to the WordPress `$wp_settings_errors` global. * * @since 12.1 * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * @param string $dirty_value The submitted invalid value. * * @return void */ public static function add_dirty_value_to_settings_errors( $error_code, $dirty_value ) { global $wp_settings_errors; if ( ! is_array( $wp_settings_errors ) ) { return; } foreach ( $wp_settings_errors as $index => $error ) { if ( $error['code'] === $error_code ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- This is a deliberate action. $wp_settings_errors[ $index ]['yoast_dirty_value'] = $dirty_value; } } } /** * Gets an invalid submitted value. * * @since 12.1 * @deprecated 23.3 * @codeCoverageIgnore * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string The submitted invalid input field value. */ public static function get_dirty_value( $error_code ) { // @phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Needed for BC. _deprecated_function( __METHOD__, 'Yoast SEO 23.3' ); return ''; } /** * Gets a specific invalid value message. * * @since 12.1 * @deprecated 23.3 * @codeCoverageIgnore * * @param string $error_code Code of the error set via `add_settings_error()`, normally the variable name. * * @return string The error invalid value message or empty string. */ public static function get_dirty_value_message( $error_code ) { // @phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Needed for BC. _deprecated_function( __METHOD__, 'Yoast SEO 23.3' ); return ''; } } admin/index.php 0000644 00000000046 15174712003 0007454 0 ustar 00 <?php /** * Nothing to see here. */ admin/class-admin-editor-specific-replace-vars.php 0000644 00000014543 15174712003 0016200 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Determines the editor specific replacement variables. */ class WPSEO_Admin_Editor_Specific_Replace_Vars { /** * Holds the editor specific replacements variables. * * @var array The editor specific replacement variables. */ protected $replacement_variables = [ // Posts types. 'page' => [ 'id', 'pt_single', 'pt_plural', 'parent_title' ], 'post' => [ 'id', 'term404', 'pt_single', 'pt_plural' ], // Custom post type. 'custom_post_type' => [ 'id', 'term404', 'pt_single', 'pt_plural', 'parent_title' ], // Settings - archive pages. 'custom-post-type_archive' => [ 'pt_single', 'pt_plural' ], // Taxonomies. 'category' => [ 'term_title', 'term_description', 'category_description', 'parent_title', 'term_hierarchy' ], 'post_tag' => [ 'term_title', 'term_description', 'tag_description' ], 'post_format' => [ 'term_title' ], // Custom taxonomy. 'term-in-custom-taxonomy' => [ 'term_title', 'term_description', 'category_description', 'parent_title', 'term_hierarchy' ], // Settings - special pages. 'search' => [ 'searchphrase' ], ]; /** * WPSEO_Admin_Editor_Specific_Replace_Vars constructor. */ public function __construct() { $this->add_for_page_types( [ 'page', 'post', 'custom_post_type' ], WPSEO_Custom_Fields::get_custom_fields(), ); $this->add_for_page_types( [ 'post', 'term-in-custom-taxonomy' ], WPSEO_Custom_Taxonomies::get_custom_taxonomies(), ); } /** * Retrieves the editor specific replacement variables. * * @return array The editor specific replacement variables. */ public function get() { /** * Filter: Adds the possibility to add extra editor specific replacement variables. * * @param array $replacement_variables Array of editor specific replace vars. */ $replacement_variables = apply_filters( 'wpseo_editor_specific_replace_vars', $this->replacement_variables, ); if ( ! is_array( $replacement_variables ) ) { $replacement_variables = $this->replacement_variables; } return array_filter( $replacement_variables, 'is_array' ); } /** * Retrieves the generic replacement variable names. * * Which are the replacement variables without the editor specific ones. * * @param array $replacement_variables Possibly generic replacement variables. * * @return array The generic replacement variable names. */ public function get_generic( $replacement_variables ) { $shared_variables = array_diff( $this->extract_names( $replacement_variables ), $this->get_unique_replacement_variables(), ); return array_values( $shared_variables ); } /** * Determines the page type of the current term. * * @param string $taxonomy The taxonomy name. * * @return string The page type. */ public function determine_for_term( $taxonomy ) { $replacement_variables = $this->get(); if ( array_key_exists( $taxonomy, $replacement_variables ) ) { return $taxonomy; } return 'term-in-custom-taxonomy'; } /** * Determines the page type of the current post. * * @param WP_Post $post A WordPress post instance. * * @return string The page type. */ public function determine_for_post( $post ) { if ( $post instanceof WP_Post === false ) { return 'post'; } $replacement_variables = $this->get(); if ( array_key_exists( $post->post_type, $replacement_variables ) ) { return $post->post_type; } return 'custom_post_type'; } /** * Determines the page type for a post type. * * @param string $post_type The name of the post_type. * @param string $fallback The page type to fall back to. * * @return string The page type. */ public function determine_for_post_type( $post_type, $fallback = 'custom_post_type' ) { if ( ! $this->has_for_page_type( $post_type ) ) { return $fallback; } return $post_type; } /** * Determines the page type for an archive page. * * @param string $name The name of the archive. * @param string $fallback The page type to fall back to. * * @return string The page type. */ public function determine_for_archive( $name, $fallback = 'custom-post-type_archive' ) { $page_type = $name . '_archive'; if ( ! $this->has_for_page_type( $page_type ) ) { return $fallback; } return $page_type; } /** * Adds the replavement variables for the given page types. * * @param array $page_types Page types to add variables for. * @param array $replacement_variables_to_add The variables to add. * * @return void */ protected function add_for_page_types( array $page_types, array $replacement_variables_to_add ) { if ( empty( $replacement_variables_to_add ) ) { return; } $replacement_variables_to_add = array_fill_keys( $page_types, $replacement_variables_to_add ); $replacement_variables = $this->replacement_variables; $this->replacement_variables = array_merge_recursive( $replacement_variables, $replacement_variables_to_add ); } /** * Extracts the names from the given replacements variables. * * @param array $replacement_variables Replacement variables to extract the name from. * * @return array Extracted names. */ protected function extract_names( $replacement_variables ) { $extracted_names = []; foreach ( $replacement_variables as $replacement_variable ) { if ( empty( $replacement_variable['name'] ) ) { continue; } $extracted_names[] = $replacement_variable['name']; } return $extracted_names; } /** * Returns whether the given page type has editor specific replace vars. * * @param string $page_type The page type to check. * * @return bool True if there are associated editor specific replace vars. */ protected function has_for_page_type( $page_type ) { $replacement_variables = $this->get(); return ( ! empty( $replacement_variables[ $page_type ] ) && is_array( $replacement_variables[ $page_type ] ) ); } /** * Merges all editor specific replacement variables into one array and removes duplicates. * * @return array The list of unique editor specific replacement variables. */ protected function get_unique_replacement_variables() { $merged_replacement_variables = call_user_func_array( 'array_merge', array_values( $this->get() ) ); return array_unique( $merged_replacement_variables ); } } admin/class-yoast-notifications.php 0000644 00000017205 15174712003 0013463 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Notifications */ /** * Class Yoast_Notifications. */ class Yoast_Notifications { /** * Holds the admin page's ID. * * @var string */ public const ADMIN_PAGE = 'wpseo_dashboard'; /** * Total notifications count. * * @var int */ private static $notification_count = 0; /** * All error notifications. * * @var array */ private static $errors = []; /** * Active errors. * * @var array */ private static $active_errors = []; /** * Dismissed errors. * * @var array */ private static $dismissed_errors = []; /** * All warning notifications. * * @var array */ private static $warnings = []; /** * Active warnings. * * @var array */ private static $active_warnings = []; /** * Dismissed warnings. * * @var array */ private static $dismissed_warnings = []; /** * Yoast_Notifications constructor. */ public function __construct() { $this->add_hooks(); } /** * Add hooks * * @return void */ private function add_hooks() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); if ( $page === self::ADMIN_PAGE ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } } // Needed for adminbar and Notifications page. add_action( 'admin_init', [ self::class, 'collect_notifications' ], 99 ); // Add AJAX hooks. add_action( 'wp_ajax_yoast_dismiss_notification', [ $this, 'ajax_dismiss_notification' ] ); add_action( 'wp_ajax_yoast_restore_notification', [ $this, 'ajax_restore_notification' ] ); } /** * Enqueue assets. * * @return void */ public function enqueue_assets() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_style( 'notifications' ); } /** * Handle ajax request to dismiss a notification. * * @return void */ public function ajax_dismiss_notification() { $notification = $this->get_notification_from_ajax_request(); if ( $notification ) { $notification_center = Yoast_Notification_Center::get(); $notification_center->maybe_dismiss_notification( $notification ); $this->output_ajax_response( $notification->get_type() ); } wp_die(); } /** * Handle ajax request to restore a notification. * * @return void */ public function ajax_restore_notification() { $notification = $this->get_notification_from_ajax_request(); if ( $notification ) { $notification_center = Yoast_Notification_Center::get(); $notification_center->restore_notification( $notification ); $this->output_ajax_response( $notification->get_type() ); } wp_die(); } /** * Create AJAX response data. * * @param string $type Notification type. * * @return void */ private function output_ajax_response( $type ) { $html = $this->get_view_html( $type ); // phpcs:disable WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe. echo WPSEO_Utils::format_json_encode( [ 'html' => $html, 'total' => self::get_active_notification_count(), ], ); // phpcs:enable -- Reason: WPSEO_Utils::format_json_encode is safe. } /** * Get the HTML to return in the AJAX request. * * @param string $type Notification type. * * @return bool|string */ private function get_view_html( $type ) { switch ( $type ) { case 'error': $view = 'errors'; break; case 'warning': default: $view = 'warnings'; break; } // Re-collect notifications. self::collect_notifications(); /** * Stops PHPStorm from nagging about this variable being unused. The variable is used in the view. * * @noinspection PhpUnusedLocalVariableInspection */ $notifications_data = self::get_template_variables(); ob_start(); include WPSEO_PATH . 'admin/views/partial-notifications-' . $view . '.php'; $html = ob_get_clean(); return $html; } /** * Extract the Yoast Notification from the AJAX request. * * This function does not handle nonce verification. * * @return Yoast_Notification|null A Yoast_Notification on success, null on failure. */ private function get_notification_from_ajax_request() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: This function does not handle nonce verification. if ( ! isset( $_POST['notification'] ) || ! is_string( $_POST['notification'] ) ) { return null; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: This function does not handle nonce verification. $notification_id = sanitize_text_field( wp_unslash( $_POST['notification'] ) ); if ( empty( $notification_id ) ) { return null; } $notification_center = Yoast_Notification_Center::get(); return $notification_center->get_notification_by_id( $notification_id ); } /** * Collect the notifications and group them together. * * @return void */ public static function collect_notifications() { $notification_center = Yoast_Notification_Center::get(); $notifications = $notification_center->get_sorted_notifications(); self::$notification_count = count( $notifications ); self::$errors = array_filter( $notifications, [ self::class, 'filter_error_notifications' ] ); self::$dismissed_errors = array_filter( self::$errors, [ self::class, 'filter_dismissed_notifications' ] ); self::$active_errors = array_diff( self::$errors, self::$dismissed_errors ); self::$warnings = array_filter( $notifications, [ self::class, 'filter_warning_notifications' ] ); self::$dismissed_warnings = array_filter( self::$warnings, [ self::class, 'filter_dismissed_notifications' ] ); self::$active_warnings = array_diff( self::$warnings, self::$dismissed_warnings ); } /** * Get the variables needed in the views. * * @return array */ public static function get_template_variables() { return [ 'metrics' => [ 'total' => self::$notification_count, 'active' => self::get_active_notification_count(), 'errors' => count( self::$errors ), 'warnings' => count( self::$warnings ), ], 'errors' => [ 'dismissed' => self::$dismissed_errors, 'active' => self::$active_errors, ], 'warnings' => [ 'dismissed' => self::$dismissed_warnings, 'active' => self::$active_warnings, ], ]; } /** * Get the number of active notifications. * * @return int */ public static function get_active_notification_count() { return ( count( self::$active_errors ) + count( self::$active_warnings ) ); } /** * Filter out any non-errors. * * @param Yoast_Notification $notification Notification to test. * * @return bool */ private static function filter_error_notifications( Yoast_Notification $notification ) { return $notification->get_type() === 'error'; } /** * Filter out any non-warnings. * * @param Yoast_Notification $notification Notification to test. * * @return bool */ private static function filter_warning_notifications( Yoast_Notification $notification ) { return $notification->get_type() !== 'error'; } /** * Filter out any dismissed notifications. * * @param Yoast_Notification $notification Notification to test. * * @return bool */ private static function filter_dismissed_notifications( Yoast_Notification $notification ) { return Yoast_Notification_Center::is_notification_dismissed( $notification ); } } class_alias( Yoast_Notifications::class, 'Yoast_Alerts' ); admin/class-bulk-editor-list-table.php 0000644 00000072673 15174712003 0013746 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Bulk Editor * @since 1.5.0 */ /** * Implements table for bulk editing. */ class WPSEO_Bulk_List_Table extends WP_List_Table { /** * The nonce that was passed with the request. * * @var string */ private $nonce; /** * Array of post types for which the current user has `edit_others_posts` capabilities. * * @var array */ private $all_posts; /** * Array of post types for which the current user has `edit_posts` capabilities, but not `edit_others_posts`. * * @var array */ private $own_posts; /** * Saves all the metadata into this array. * * @var array */ protected $meta_data = []; /** * The current requested page_url. * * @var string */ private $request_url = ''; /** * The current page (depending on $_GET['paged']) if current tab is for current page_type, else it will be 1. * * @var int */ private $current_page; /** * The current post filter, if is used (depending on $_GET['post_type_filter']). * * @var string */ private $current_filter; /** * The current post status, if is used (depending on $_GET['post_status']). * * @var string */ private $current_status; /** * The current sorting, if used (depending on $_GET['order'] and $_GET['orderby']). * * @var string */ private $current_order; /** * The page_type for current class instance (for example: title / description). * * @var string */ protected $page_type; /** * Based on the page_type ($this->page_type) there will be constructed an url part, for subpages and * navigation. * * @var string */ protected $page_url; /** * The settings which will be used in the __construct. * * @var array */ protected $settings; /** * Holds the pagination config. * * @var array */ protected $pagination = []; /** * Holds the sanitized data from the user input. * * @var array */ protected $input_fields = []; /** * The field in the database where meta field is saved. * * Should be set in the child class. * * @var string */ protected $target_db_field = ''; /** * Class constructor. * * @param array $args The arguments. */ public function __construct( $args = [] ) { parent::__construct( $this->settings ); $args = wp_parse_args( $args, [ 'nonce' => '', 'input_fields' => [], ], ); $this->input_fields = $args['input_fields']; if ( isset( $_SERVER['REQUEST_URI'] ) ) { $this->request_url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } $this->current_page = ( ! empty( $this->input_fields['paged'] ) ) ? $this->input_fields['paged'] : 1; $this->current_filter = ( ! empty( $this->input_fields['post_type_filter'] ) ) ? $this->input_fields['post_type_filter'] : 1; $this->current_status = ( ! empty( $this->input_fields['post_status'] ) ) ? $this->input_fields['post_status'] : 1; $this->current_order = [ 'order' => ( ! empty( $this->input_fields['order'] ) ) ? $this->input_fields['order'] : 'asc', 'orderby' => ( ! empty( $this->input_fields['orderby'] ) ) ? $this->input_fields['orderby'] : 'post_title', ]; $this->nonce = $args['nonce']; $this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}"; $this->populate_editable_post_types(); } /** * Prepares the data and renders the page. * * @return void */ public function show_page() { $this->prepare_page_navigation(); $this->prepare_items(); $this->views(); $this->display(); } /** * Used in the constructor to build a reference list of post types the current user can edit. * * @return void */ protected function populate_editable_post_types() { $post_types = get_post_types( [ 'public' => true, 'exclude_from_search' => false, ], 'object', ); $this->all_posts = []; $this->own_posts = []; if ( is_array( $post_types ) && $post_types !== [] ) { foreach ( $post_types as $post_type ) { if ( ! current_user_can( $post_type->cap->edit_posts ) ) { continue; } if ( current_user_can( $post_type->cap->edit_others_posts ) ) { $this->all_posts[] = esc_sql( $post_type->name ); } else { $this->own_posts[] = esc_sql( $post_type->name ); } } } } /** * Will show the navigation for the table like page navigation and page filter. * * @param string $which Table nav location (such as top). * * @return void */ public function display_tablenav( $which ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $post_status = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : ''; $order_by = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : ''; $order = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : ''; $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended; ?> <div class="tablenav <?php echo esc_attr( $which ); ?>"> <?php if ( $which === 'top' ) { ?> <form id="posts-filter" action="" method="get"> <input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>" /> <input type="hidden" name="page" value="wpseo_tools" /> <input type="hidden" name="tool" value="bulk-editor" /> <input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>" /> <input type="hidden" name="orderby" value="<?php echo esc_attr( $order_by ); ?>" /> <input type="hidden" name="order" value="<?php echo esc_attr( $order ); ?>" /> <input type="hidden" name="post_type_filter" value="<?php echo esc_attr( $post_type_filter ); ?>" /> <?php if ( ! empty( $post_status ) ) { ?> <input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>" /> <?php } ?> <?php } ?> <?php $this->extra_tablenav( $which ); $this->pagination( $which ); ?> <br class="clear"/> <?php if ( $which === 'top' ) { ?> </form> <?php } ?> </div> <?php } /** * This function builds the base sql subquery used in this class. * * This function takes into account the post types in which the current user can * edit all posts, and the ones the current user can only edit his/her own. * * @return string The subquery, which should always be used in $wpdb->prepare(), * passing the current user_id in as the first parameter. */ public function get_base_subquery() { global $wpdb; $all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'"; $own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'"; $post_author = esc_sql( (int) get_current_user_id() ); $subquery = "( SELECT * FROM {$wpdb->posts} WHERE post_type IN ({$all_posts_string}) UNION ALL SELECT * FROM {$wpdb->posts} WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author} ) sub_base"; return $subquery; } /** * Gets the views. * * @return array The views. */ public function get_views() { global $wpdb; $status_links = []; $states = get_post_stati( [ 'show_in_admin_all_list' => true ] ); $subquery = $this->get_base_subquery(); $total_posts = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$subquery} WHERE post_status IN (" . implode( ', ', array_fill( 0, count( $states ), '%s' ) ) . ')', $states, ), ); $post_status = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : ''; $current_link_attributes = empty( $post_status ) ? ' class="current" aria-current="page"' : ''; $localized_text = sprintf( /* translators: %s expands to the number of posts in localized format. */ _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ), number_format_i18n( $total_posts ), ); $status_links['all'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>'; $post_stati = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' ); if ( is_array( $post_stati ) && $post_stati !== [] ) { foreach ( $post_stati as $status ) { $status_name = esc_sql( $status->name ); $total = (int) $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(ID) FROM {$subquery} WHERE post_status = %s ", $status_name, ), ); if ( $total === 0 ) { continue; } $current_link_attributes = ''; if ( $status_name === $post_status ) { $current_link_attributes = ' class="current" aria-current="page"'; } $status_links[ $status_name ] = '<a href="' . esc_url( add_query_arg( [ 'post_status' => $status_name ], admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) ) . '"' . $current_link_attributes . '>' . sprintf( translate_nooped_plural( $status->label_count, $total ), number_format_i18n( $total ) ) . '</a>'; } } unset( $post_stati, $status, $status_name, $total, $current_link_attributes ); $trashed_posts = $wpdb->get_var( "SELECT COUNT(ID) FROM {$subquery} WHERE post_status IN ('trash') ", ); $current_link_attributes = ''; if ( $post_status === 'trash' ) { $current_link_attributes = 'class="current" aria-current="page"'; } $localized_text = sprintf( /* translators: %s expands to the number of trashed posts in localized format. */ _nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ), number_format_i18n( $trashed_posts ), ); $status_links['trash'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor&post_status=trash' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>'; return $status_links; } /** * Outputs extra table navigation. * * @param string $which Table nav location (such as top). * * @return void */ public function extra_tablenav( $which ) { if ( $which === 'top' ) { $post_types = get_post_types( [ 'public' => true, 'exclude_from_search' => false, ], ); $instance_type = esc_attr( $this->page_type ); if ( is_array( $post_types ) && $post_types !== [] ) { global $wpdb; echo '<div class="alignleft actions">'; $post_types = esc_sql( $post_types ); $post_types = "'" . implode( "', '", $post_types ) . "'"; $states = get_post_stati( [ 'show_in_admin_all_list' => true ] ); $states['trash'] = 'trash'; $subquery = $this->get_base_subquery(); $post_types = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT post_type FROM {$subquery} WHERE post_status IN (" . implode( ', ', array_fill( 0, count( $states ), '%s' ) ) . ') ORDER BY post_type ASC', $states, ), ); $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : ''; $selected = ( ! empty( $post_type_filter ) ) ? $post_type_filter : '-1'; $options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>'; if ( is_array( $post_types ) && $post_types !== [] ) { foreach ( $post_types as $post_type ) { $obj = get_post_type_object( $post_type->post_type ); $options .= sprintf( '<option value="%2$s" %3$s>%1$s</option>', esc_html( $obj->labels->name ), esc_attr( $post_type->post_type ), selected( $selected, $post_type->post_type, false ), ); } } printf( '<label for="%1$s" class="screen-reader-text">%2$s</label>', esc_attr( 'post-type-filter-' . $instance_type ), /* translators: Hidden accessibility text. */ esc_html__( 'Filter by content type', 'wordpress-seo' ), ); printf( '<select name="post_type_filter" id="%2$s">%1$s</select>', // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $options is properly escaped above. $options, esc_attr( 'post-type-filter-' . $instance_type ), ); submit_button( esc_html__( 'Filter', 'wordpress-seo' ), 'button', false, false, [ 'id' => 'post-query-submit' ] ); echo '</div>'; } } } /** * Gets a list of sortable columns. * * The format is: 'internal-name' => array( 'orderby', bool ). * * @return array */ public function get_sortable_columns() { return [ 'col_page_title' => [ 'post_title', true ], 'col_post_type' => [ 'post_type', false ], 'col_post_date' => [ 'post_date', false ], ]; } /** * Sets the correct pagenumber and pageurl for the navigation. * * @return void */ public function prepare_page_navigation() { $request_url = $this->request_url . $this->page_url; $current_page = $this->current_page; $current_filter = $this->current_filter; $current_status = $this->current_status; $current_order = $this->current_order; /* * If current type doesn't compare with objects page_type, then we have to unset * some vars in the requested url (which will be used for internal table urls). */ if ( isset( $this->input_fields['type'] ) && $this->input_fields['type'] !== $this->page_type ) { $request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below. $request_url = remove_query_arg( 'post_type_filter', $request_url ); $request_url = remove_query_arg( 'post_status', $request_url ); $request_url = remove_query_arg( 'orderby', $request_url ); $request_url = remove_query_arg( 'order', $request_url ); $request_url = add_query_arg( 'pages', 1, $request_url ); $current_page = 1; $current_filter = '-1'; $current_status = ''; $current_order = [ 'orderby' => 'post_title', 'order' => 'asc', ]; } $_SERVER['REQUEST_URI'] = $request_url; $_GET['paged'] = $current_page; $_REQUEST['paged'] = $current_page; $_REQUEST['post_type_filter'] = $current_filter; $_GET['post_type_filter'] = $current_filter; $_GET['post_status'] = $current_status; $_GET['orderby'] = $current_order['orderby']; $_GET['order'] = $current_order['order']; } /** * Preparing the requested pagerows and setting the needed variables. * * @return void */ public function prepare_items() { $post_type_clause = $this->get_post_type_clause(); $all_states = $this->get_all_states(); $subquery = $this->get_base_subquery(); // Setting the column headers. $this->set_column_headers(); // Count the total number of needed items and setting pagination given $total_items. $total_items = $this->count_items( $subquery, $all_states, $post_type_clause ); $this->set_pagination( $total_items ); // Getting items given $query. $query = $this->parse_item_query( $subquery, $all_states, $post_type_clause ); $this->get_items( $query ); // Get the metadata for the current items ($this->items). $this->get_meta_data(); } /** * Getting the columns for first row. * * @return array */ public function get_columns() { return $this->merge_columns(); } /** * Setting the column headers. * * @return void */ protected function set_column_headers() { $columns = $this->get_columns(); $hidden = []; $sortable = $this->get_sortable_columns(); $this->_column_headers = [ $columns, $hidden, $sortable ]; } /** * Counting total items. * * @param string $subquery SQL FROM part. * @param string $all_states SQL IN part. * @param string $post_type_clause SQL post type part. * * @return mixed */ protected function count_items( $subquery, $all_states, $post_type_clause ) { global $wpdb; return (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$subquery} WHERE post_status IN ({$all_states}) {$post_type_clause} ", ); } /** * Getting the post_type_clause filter. * * @return string */ protected function get_post_type_clause() { // Filter Block. $post_type_clause = ''; $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : ''; if ( ! empty( $post_type_filter ) && get_post_type_object( $post_type_filter ) ) { $post_types = esc_sql( $post_type_filter ); $post_type_clause = "AND post_type IN ('{$post_types}')"; } return $post_type_clause; } /** * Setting the pagination. * * Total items is the number of all visible items. * * @param int $total_items Total items counts. * * @return void */ protected function set_pagination( $total_items ) { // Calculate items per page. $per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 ); $paged = isset( $_GET['paged'] ) && is_string( $_GET['paged'] ) ? esc_sql( sanitize_text_field( wp_unslash( $_GET['paged'] ) ) ) : ''; if ( empty( $paged ) || ! is_numeric( $paged ) ) { $paged = 1; } else { $paged = (int) $paged; } if ( $paged <= 0 ) { $paged = 1; } $this->set_pagination_args( [ 'total_items' => $total_items, 'total_pages' => ceil( $total_items / $per_page ), 'per_page' => $per_page, ], ); $this->pagination = [ 'per_page' => $per_page, 'offset' => ( ( $paged - 1 ) * $per_page ), ]; } /** * Parse the query to get items from database. * * Based on given parameters there will be parse a query which will get all the pages/posts and other post_types * from the database. * * @param string $subquery SQL FROM part. * @param string $all_states SQL IN part. * @param string $post_type_clause SQL post type part. * * @return string */ protected function parse_item_query( $subquery, $all_states, $post_type_clause ) { // Order By block. $orderby = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : ''; $orderby = ! empty( $orderby ) ? esc_sql( $orderby ) : 'post_title'; $orderby = $this->sanitize_orderby( $orderby ); // Order clause. $order = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : ''; $order = ! empty( $order ) ? esc_sql( strtoupper( $order ) ) : 'ASC'; $order = $this->sanitize_order( $order ); // Get all needed results. $query = " SELECT ID, post_title, post_type, post_status, post_modified, post_date FROM {$subquery} WHERE post_status IN ({$all_states}) $post_type_clause ORDER BY {$orderby} {$order} LIMIT %d,%d "; return $query; } /** * Heavily restricts the possible columns by which a user can order the table * in the bulk editor, thereby preventing a possible CSRF vulnerability. * * @param string $orderby The column by which we want to order. * * @return string */ protected function sanitize_orderby( $orderby ) { $valid_column_names = [ 'post_title', 'post_type', 'post_date', ]; if ( in_array( $orderby, $valid_column_names, true ) ) { return $orderby; } return 'post_title'; } /** * Makes sure the order clause is always ASC or DESC for the bulk editor table, * thereby preventing a possible CSRF vulnerability. * * @param string $order Whether we want to sort ascending or descending. * * @return string SQL order string (ASC, DESC). */ protected function sanitize_order( $order ) { if ( in_array( strtoupper( $order ), [ 'ASC', 'DESC' ], true ) ) { return $order; } return 'ASC'; } /** * Getting all the items. * * @param string $query SQL query to use. * * @return void */ protected function get_items( $query ) { global $wpdb; $this->items = $wpdb->get_results( $wpdb->prepare( $query, $this->pagination['offset'], $this->pagination['per_page'], ), ); } /** * Getting all the states. * * @return string */ protected function get_all_states() { global $wpdb; $states = get_post_stati( [ 'show_in_admin_all_list' => true ] ); $states['trash'] = 'trash'; if ( ! empty( $this->input_fields['post_status'] ) ) { $requested_state = $this->input_fields['post_status']; if ( in_array( $requested_state, $states, true ) ) { $states = [ $requested_state ]; } if ( $requested_state !== 'trash' ) { unset( $states['trash'] ); } } return $wpdb->prepare( implode( ', ', array_fill( 0, count( $states ), '%s' ) ), $states, ); } /** * Based on $this->items and the defined columns, the table rows will be displayed. * * @return void */ public function display_rows() { $records = $this->items; list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); if ( ( is_array( $records ) && $records !== [] ) && ( is_array( $columns ) && $columns !== [] ) ) { foreach ( $records as $record ) { echo '<tr id="', esc_attr( 'record_' . $record->ID ), '">'; foreach ( $columns as $column_name => $column_display_name ) { $classes = ''; if ( $primary === $column_name ) { $classes .= ' has-row-actions column-primary'; } $attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name ); $column_value = $this->parse_column( $column_name, $record ); if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) { $column_value = $this->parse_page_specific_column( $column_name, $record, $attributes ); } if ( ! empty( $column_value ) ) { printf( '<td %2$s>%1$s</td>', $column_value, $attributes ); } } echo '</tr>'; } } } /** * Getting the attributes for each table cell. * * @param string $column_name Column name string. * @param array $hidden Set of hidden columns. * @param string $classes Additional CSS classes. * @param string $column_display_name Column display name string. * * @return string */ protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) { $attributes = ''; $class = [ $column_name, "column-$column_name$classes" ]; if ( in_array( $column_name, $hidden, true ) ) { $class[] = 'hidden'; } if ( ! empty( $class ) ) { $attributes = 'class="' . esc_attr( implode( ' ', $class ) ) . '"'; } $attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"'; return $attributes; } /** * Parsing the title. * * @param WP_Post $rec Post object. * * @return string */ protected function parse_page_title_column( $rec ) { $title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title; $return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) ); $post_type_object = get_post_type_object( $rec->post_type ); $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $rec->ID ); $actions = []; if ( $can_edit_post && $rec->post_status !== 'trash' ) { $actions['edit'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', esc_url( get_edit_post_link( $rec->ID, true ) ), /* translators: Hidden accessibility text; %s: post title. */ esc_attr( sprintf( __( 'Edit “%s”', 'wordpress-seo' ), $title ) ), __( 'Edit', 'wordpress-seo' ), ); } if ( $post_type_object->public ) { if ( in_array( $rec->post_status, [ 'pending', 'draft', 'future' ], true ) ) { if ( $can_edit_post ) { $actions['view'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ), /* translators: Hidden accessibility text; %s: post title. */ esc_attr( sprintf( __( 'Preview “%s”', 'wordpress-seo' ), $title ) ), __( 'Preview', 'wordpress-seo' ), ); } } elseif ( $rec->post_status !== 'trash' ) { $actions['view'] = sprintf( '<a href="%s" aria-label="%s" rel="bookmark">%s</a>', esc_url( get_permalink( $rec->ID ) ), /* translators: Hidden accessibility text; %s: post title. */ esc_attr( sprintf( __( 'View “%s”', 'wordpress-seo' ), $title ) ), __( 'View', 'wordpress-seo' ), ); } } $return .= $this->row_actions( $actions ); return $return; } /** * Parsing the column based on the $column_name. * * @param string $column_name Column name. * @param WP_Post $rec Post object. * * @return string */ protected function parse_column( $column_name, $rec ) { static $date_format; if ( ! isset( $date_format ) ) { $date_format = get_option( 'date_format' ); } switch ( $column_name ) { case 'col_page_title': $column_value = $this->parse_page_title_column( $rec ); break; case 'col_page_slug': $permalink = get_permalink( $rec->ID ); $display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink ); $column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) ); break; case 'col_post_type': $post_type = get_post_type_object( $rec->post_type ); $column_value = $post_type->labels->singular_name; break; case 'col_post_status': $post_status = get_post_status_object( $rec->post_status ); $column_value = $post_status->label; break; case 'col_post_date': $column_value = date_i18n( $date_format, strtotime( $rec->post_date ) ); break; case 'col_row_action': $column_value = sprintf( '<a href="#" role="button" class="wpseo-save" data-id="%1$s">%2$s</a> <span aria-hidden="true">|</span> <a href="#" role="button" class="wpseo-save-all">%3$s</a>', $rec->ID, esc_html__( 'Save', 'wordpress-seo' ), esc_html__( 'Save all', 'wordpress-seo' ), ); break; } if ( ! empty( $column_value ) ) { return $column_value; } } /** * Parse the field where the existing meta-data value is displayed. * * @param int $record_id Record ID. * @param string $attributes HTML attributes. * @param bool|array $values Optional values data array. * * @return string */ protected function parse_meta_data_field( $record_id, $attributes, $values = false ) { // Fill meta data if exists in $this->meta_data. $meta_data = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : []; $meta_key = WPSEO_Meta::$meta_prefix . $this->target_db_field; $meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : ''; if ( ! empty( $values ) ) { $meta_value = $values[ $meta_value ]; } $id = "wpseo-existing-$this->target_db_field-$record_id"; // $attributes correctly escaped, verified by Alexander. See WPSEO_Bulk_Description_List_Table::parse_page_specific_column. return sprintf( '<td %2$s id="%3$s">%1$s</td>', esc_html( $meta_value ), $attributes, esc_attr( $id ) ); } /** * Method for setting the meta data, which belongs to the records that will be shown on the current page. * * This method will loop through the current items ($this->items) for getting the post_id. With this data * ($needed_ids) the method will query the meta-data table for getting the title. * * @return void */ protected function get_meta_data() { $post_ids = $this->get_post_ids(); $meta_data = $this->get_meta_data_result( $post_ids ); $this->parse_meta_data( $meta_data ); // Little housekeeping. unset( $post_ids, $meta_data ); } /** * Getting all post_ids from to $this->items. * * @return array */ protected function get_post_ids() { $post_ids = []; foreach ( $this->items as $item ) { $post_ids[] = $item->ID; } return $post_ids; } /** * Getting the meta_data from database. * * @param array $post_ids Post IDs for SQL IN part. * * @return mixed */ protected function get_meta_data_result( array $post_ids ) { global $wpdb; $where = $wpdb->prepare( 'post_id IN (' . implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) ) . ')', $post_ids, ); $where .= $wpdb->prepare( ' AND meta_key = %s', WPSEO_Meta::$meta_prefix . $this->target_db_field ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- They are prepared on the lines above. return $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE {$where}" ); } /** * Setting $this->meta_data. * * @param array $meta_data Meta data set. * * @return void */ protected function parse_meta_data( $meta_data ) { foreach ( $meta_data as $row ) { $this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value; } } /** * This method will merge general array with given parameter $columns. * * @param array $columns Optional columns set. * * @return array */ protected function merge_columns( $columns = [] ) { $columns = array_merge( [ 'col_page_title' => __( 'WP Page Title', 'wordpress-seo' ), 'col_post_type' => __( 'Content Type', 'wordpress-seo' ), 'col_post_status' => __( 'Post Status', 'wordpress-seo' ), 'col_post_date' => __( 'Publication date', 'wordpress-seo' ), 'col_page_slug' => __( 'Page URL/Slug', 'wordpress-seo' ), ], $columns, ); $columns['col_row_action'] = __( 'Action', 'wordpress-seo' ); return $columns; } } admin/class-schema-person-upgrade-notification.php 0000644 00000004356 15174712003 0016335 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Notifies the user to update the Search Appearance settings when the site is set to represent a Person, * but no person (name) has been chosen. */ class WPSEO_Schema_Person_Upgrade_Notification implements WPSEO_WordPress_Integration { /** * Registers all hooks to WordPress * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'handle_notification' ] ); } /** * Handles if the notification should be added or removed. * * @return void */ public function handle_notification() { $company_or_person_user_id = WPSEO_Options::get( 'company_or_person_user_id', false ); if ( WPSEO_Options::get( 'company_or_person' ) === 'person' && empty( $company_or_person_user_id ) ) { $this->add_notification(); return; } $this->remove_notification(); } /** * Adds a notification to the notification center. * * @return void */ protected function add_notification() { $notification_center = Yoast_Notification_Center::get(); $notification_center->add_notification( $this->get_notification() ); } /** * Removes a notification to the notification center. * * @return void */ protected function remove_notification() { $notification_center = Yoast_Notification_Center::get(); $notification_center->remove_notification( $this->get_notification() ); } /** * Gets the notification object. * * @return Yoast_Notification */ protected function get_notification() { $message = sprintf( /* translators: %1$s is a link start tag to the Search Appearance settings, %2$s is the link closing tag. */ __( 'You have previously set your site to represent a person. We’ve improved our functionality around Schema and the Knowledge Graph, so you should go in and %1$scomplete those settings%2$s.', 'wordpress-seo' ), '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' ) ) . '">', '</a>', ); $notification = new Yoast_Notification( $message, [ 'type' => Yoast_Notification::WARNING, 'id' => 'wpseo-schema-person-upgrade', 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ], ); return $notification; } } admin/notifiers/interface-notification-handler.php 0000644 00000000665 15174712003 0016415 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Notifiers */ /** * Dictates the required methods for a Notification Handler implementation. */ interface WPSEO_Notification_Handler { /** * Handles the notification object. * * @param Yoast_Notification_Center $notification_center The notification center object. * * @return void */ public function handle( Yoast_Notification_Center $notification_center ); } admin/notifiers/dismissible-notification.php 0000644 00000006151 15174712003 0015345 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Notifiers */ /** * Abstract class representing a dismissible notification. */ abstract class WPSEO_Dismissible_Notification implements WPSEO_Listener, WPSEO_Notification_Handler { /** * The identifier for the notification. * * @var string */ protected $notification_identifier = ''; /** * Retrieves instance of a notification. * * @return Yoast_Notification The notification. */ abstract protected function get_notification(); /** * Listens to an argument in the request URL and triggers an action. * * @return void */ public function listen() { if ( $this->get_listener_value() !== $this->notification_identifier ) { return; } $this->dismiss(); } /** * Adds the notification if applicable, otherwise removes it. * * @param Yoast_Notification_Center $notification_center The notification center object. * * @return void */ public function handle( Yoast_Notification_Center $notification_center ) { if ( $this->is_applicable() ) { $notification = $this->get_notification(); $notification_center->add_notification( $notification ); return; } $notification_center->remove_notification_by_id( 'wpseo-' . $this->notification_identifier ); } /** * Listens to an argument in the request URL and triggers an action. * * @return void */ protected function dismiss() { $this->set_dismissal_state(); $this->redirect_to_dashboard(); } /** * Checks if a notice is applicable. * * @return bool Whether a notice should be shown or not. */ protected function is_applicable() { return $this->is_notice_dismissed() === false; } /** * Checks whether the notification has been dismissed. * * @codeCoverageIgnore * * @return bool True when notification is dismissed. */ protected function is_notice_dismissed() { return get_user_meta( get_current_user_id(), 'wpseo-remove-' . $this->notification_identifier, true ) === '1'; } /** * Retrieves the value where listener is listening for. * * @codeCoverageIgnore * * @return string|null The listener value or null if not set. */ protected function get_listener_value() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: Normally we would need to check for a nonce here but this class is not used anymore. if ( isset( $_GET['yoast_dismiss'] ) && is_string( $_GET['yoast_dismiss'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: Normally we would need to check for a nonce here but this class is not used anymore. return sanitize_text_field( wp_unslash( $_GET['yoast_dismiss'] ) ); } return null; } /** * Dismisses the notification. * * @codeCoverageIgnore * * @return void */ protected function set_dismissal_state() { update_user_meta( get_current_user_id(), 'wpseo-remove-' . $this->notification_identifier, true ); } /** * Redirects the user back to the dashboard. * * @codeCoverageIgnore * * @return void */ protected function redirect_to_dashboard() { wp_safe_redirect( admin_url( 'admin.php?page=wpseo_dashboard' ) ); exit(); } } admin/class-my-yoast-proxy.php 0000644 00000014217 15174712003 0012416 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Loads the MyYoast proxy. * * This class registers a proxy page on `admin.php`. Which is reached with the `page=PAGE_IDENTIFIER` parameter. * It will read external files and serves them like they are located locally. */ class WPSEO_MyYoast_Proxy implements WPSEO_WordPress_Integration { /** * The page identifier used in WordPress to register the MyYoast proxy page. * * @var string */ public const PAGE_IDENTIFIER = 'wpseo_myyoast_proxy'; /** * The cache control's max age. Used in the header of a successful proxy response. * * @var int */ public const CACHE_CONTROL_MAX_AGE = DAY_IN_SECONDS; /** * Registers the hooks when the user is on the right page. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { if ( ! $this->is_proxy_page() ) { return; } // Register the page for the proxy. add_action( 'admin_menu', [ $this, 'add_proxy_page' ] ); add_action( 'admin_init', [ $this, 'handle_proxy_page' ] ); } /** * Registers the proxy page. It does not actually add a link to the dashboard. * * @codeCoverageIgnore * * @return void */ public function add_proxy_page() { add_dashboard_page( '', '', 'read', self::PAGE_IDENTIFIER, '' ); } /** * Renders the requested proxy page and exits to prevent the WordPress UI from loading. * * @codeCoverageIgnore * * @return void */ public function handle_proxy_page() { $this->render_proxy_page(); // Prevent the WordPress UI from loading. exit(); } /** * Renders the requested proxy page. * * This is separated from the exits to be able to test it. * * @return void */ public function render_proxy_page() { $proxy_options = $this->determine_proxy_options(); if ( $proxy_options === [] ) { // Do not accept any other file than implemented. $this->set_header( 'HTTP/1.0 501 Requested file not implemented' ); return; } // Set the headers before serving the remote file. $this->set_header( 'Content-Type: ' . $proxy_options['content_type'] ); $this->set_header( 'Cache-Control: max-age=' . self::CACHE_CONTROL_MAX_AGE ); try { echo $this->get_remote_url_body( $proxy_options['url'] ); } catch ( Exception $e ) { /* * Reset the file headers because the loading failed. * * Note: Due to supporting PHP 5.2 `header_remove` can not be used here. * Overwrite the headers instead. */ $this->set_header( 'Content-Type: text/plain' ); $this->set_header( 'Cache-Control: max-age=0' ); $this->set_header( 'HTTP/1.0 500 ' . $e->getMessage() ); } } /** * Tries to load the given url via `wp_remote_get`. * * @codeCoverageIgnore * * @param string $url The url to load. * * @return string The body of the response. * * @throws Exception When `wp_remote_get` returned an error. * @throws Exception When the response code is not 200. */ protected function get_remote_url_body( $url ) { $response = wp_remote_get( $url ); if ( $response instanceof WP_Error ) { throw new Exception( 'Unable to retrieve file from MyYoast' ); } if ( wp_remote_retrieve_response_code( $response ) !== 200 ) { throw new Exception( 'Received unexpected response from MyYoast' ); } return wp_remote_retrieve_body( $response ); } /** * Determines the proxy options based on the file and plugin version arguments. * * When the file is known it returns an array like this: * <code> * $array = array( * 'content_type' => 'the content type' * 'url' => 'the url, possibly with the plugin version' * ) * </code> * * @return array Empty for an unknown file. See format above for known files. */ protected function determine_proxy_options() { if ( $this->get_proxy_file() === 'research-webworker' ) { return [ 'content_type' => 'text/javascript; charset=UTF-8', 'url' => 'https://my.yoast.com/api/downloads/file/analysis-worker?plugin_version=' . $this->get_plugin_version(), ]; } return []; } /** * Checks if the current page is the MyYoast proxy page. * * @codeCoverageIgnore * * @return bool True when the page request parameter equals the proxy page. */ protected function is_proxy_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; return $page === self::PAGE_IDENTIFIER; } /** * Returns the proxy file from the HTTP request parameters. * * @codeCoverageIgnore * * @return string The sanitized file request parameter or an empty string if it does not exist. */ protected function get_proxy_file() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['file'] ) && is_string( $_GET['file'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['file'] ) ); } return ''; } /** * Returns the plugin version from the HTTP request parameters. * * @codeCoverageIgnore * * @return string The sanitized plugin_version request parameter or an empty string if it does not exist. */ protected function get_plugin_version() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['plugin_version'] ) && is_string( $_GET['plugin_version'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $plugin_version = sanitize_text_field( wp_unslash( $_GET['plugin_version'] ) ); // Replace slashes to secure against requiring a file from another path. return str_replace( [ '/', '\\' ], '_', $plugin_version ); } return ''; } /** * Sets the HTTP header. * * This is a tiny helper function to enable better testing. * * @codeCoverageIgnore * * @param string $header The header to set. * * @return void */ protected function set_header( $header ) { header( $header ); } } admin/class-option-tab.php 0000644 00000004334 15174712003 0011530 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Options\Tabs */ /** * Class WPSEO_Option_Tab. */ class WPSEO_Option_Tab { /** * Name of the tab. * * @var string */ private $name; /** * Label of the tab. * * @var string */ private $label; /** * Optional arguments. * * @var array */ private $arguments; /** * WPSEO_Option_Tab constructor. * * @param string $name Name of the tab. * @param string $label Localized label of the tab. * @param array $arguments Optional arguments. */ public function __construct( $name, $label, array $arguments = [] ) { $this->name = sanitize_title( $name ); $this->label = $label; $this->arguments = $arguments; } /** * Gets the name. * * @return string The name. */ public function get_name() { return $this->name; } /** * Gets the label. * * @return string The label. */ public function get_label() { return $this->label; } /** * Retrieves whether the tab needs a save button. * * @return bool True whether the tabs needs a save button. */ public function has_save_button() { return (bool) $this->get_argument( 'save_button', true ); } /** * Retrieves whether the tab hosts beta functionalities. * * @return bool True whether the tab hosts beta functionalities. */ public function is_beta() { return (bool) $this->get_argument( 'beta', false ); } /** * Retrieves whether the tab hosts premium functionalities. * * @return bool True whether the tab hosts premium functionalities. */ public function is_premium() { return (bool) $this->get_argument( 'premium', false ); } /** * Gets the option group. * * @return string The option group. */ public function get_opt_group() { return $this->get_argument( 'opt_group' ); } /** * Retrieves the variable from the supplied arguments. * * @param string $variable Variable to retrieve. * @param string|mixed $default_value Default to use when variable not found. * * @return mixed|string The retrieved variable. */ protected function get_argument( $variable, $default_value = '' ) { return array_key_exists( $variable, $this->arguments ) ? $this->arguments[ $variable ] : $default_value; } } admin/pages/network.php 0000644 00000001701 15174712003 0011134 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $yform = Yoast_Form::get_instance(); $yform->admin_header( true, 'wpseo_ms' ); $network_tabs = new WPSEO_Option_Tabs( 'network' ); $network_tabs->add_tab( new WPSEO_Option_Tab( 'general', __( 'General', 'wordpress-seo' ) ) ); $network_tabs->add_tab( new WPSEO_Option_Tab( 'features', __( 'Features', 'wordpress-seo' ) ) ); $network_tabs->add_tab( new WPSEO_Option_Tab( 'integrations', __( 'Integrations', 'wordpress-seo' ) ) ); $network_tabs->add_tab( new WPSEO_Option_Tab( 'crawl-settings', __( 'Crawl settings', 'wordpress-seo' ), [ 'save_button' => true, ], ), ); $network_tabs->add_tab( new WPSEO_Option_Tab( 'restore-site', __( 'Restore Site', 'wordpress-seo' ), [ 'save_button' => false ] ) ); $network_tabs->display( $yform ); $yform->admin_footer(); admin/pages/tools.php 0000644 00000005510 15174712003 0010605 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } $tool_page = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['tool'] ) && is_string( $_GET['tool'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $tool_page = sanitize_text_field( wp_unslash( $_GET['tool'] ) ); } $yform = Yoast_Form::get_instance(); $yform->admin_header( false ); if ( $tool_page === '' ) { $tools = []; $tools['import-export'] = [ 'title' => __( 'Import and Export', 'wordpress-seo' ), 'desc' => __( 'Import settings from other SEO plugins and export your settings for re-use on (another) site.', 'wordpress-seo' ), ]; if ( WPSEO_Utils::allow_system_file_edit() === true && ! is_multisite() ) { $tools['file-editor'] = [ 'title' => __( 'File editor', 'wordpress-seo' ), 'desc' => __( 'This tool allows you to quickly change important files for your SEO, like your robots.txt and, if you have one, your .htaccess file.', 'wordpress-seo' ), ]; } $tools['bulk-editor'] = [ 'title' => __( 'Bulk editor', 'wordpress-seo' ), 'desc' => __( 'This tool allows you to quickly change titles and descriptions of your posts and pages without having to go into the editor for each page.', 'wordpress-seo' ), ]; echo '<p>'; printf( /* translators: %1$s expands to Yoast SEO */ esc_html__( '%1$s comes with some very powerful built-in tools:', 'wordpress-seo' ), 'Yoast SEO', ); echo '</p>'; echo '<ul class="ul-disc">'; $admin_url = admin_url( 'admin.php?page=wpseo_tools' ); foreach ( $tools as $slug => $tool ) { $href = ( ! empty( $tool['href'] ) ) ? $admin_url . $tool['href'] : add_query_arg( [ 'tool' => $slug ], $admin_url ); $attr = ( ! empty( $tool['attr'] ) ) ? $tool['attr'] : ''; echo '<li>'; echo '<strong><a href="', esc_url( $href ), '" ', esc_attr( $attr ), '>', esc_html( $tool['title'] ), '</a></strong><br/>'; echo esc_html( $tool['desc'] ); echo '</li>'; } /** * WARNING: This hook is intended for internal use only. * Don't use it in your code as it will be removed shortly. */ do_action( 'wpseo_tools_overview_list_items_internal' ); echo '</ul>'; } else { echo '<a href="', esc_url( admin_url( 'admin.php?page=wpseo_tools' ) ), '">', esc_html__( '« Back to Tools page', 'wordpress-seo' ), '</a>'; $tool_pages = [ 'bulk-editor', 'import-export' ]; if ( WPSEO_Utils::allow_system_file_edit() === true && ! is_multisite() ) { $tool_pages[] = 'file-editor'; } if ( in_array( $tool_page, $tool_pages, true ) ) { require_once WPSEO_PATH . 'admin/views/tool-' . $tool_page . '.php'; } } $yform->admin_footer( false ); admin/pages/redirects.php 0000644 00000000376 15174712003 0011436 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin * @since 19.0 */ if ( ! defined( 'WPSEO_VERSION' ) ) { header( 'Status: 403 Forbidden' ); header( 'HTTP/1.1 403 Forbidden' ); exit(); } require WPSEO_PATH . 'admin/views/redirects.php'; admin/class-option-tabs.php 0000644 00000004407 15174712003 0011714 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Options\Tabs */ /** * Class WPSEO_Option_Tabs. */ class WPSEO_Option_Tabs { /** * Tabs base. * * @var string */ private $base; /** * The tabs in this group. * * @var array */ private $tabs = []; /** * Name of the active tab. * * @var string */ private $active_tab = ''; /** * WPSEO_Option_Tabs constructor. * * @codeCoverageIgnore * * @param string $base Base of the tabs. * @param string $active_tab Currently active tab. */ public function __construct( $base, $active_tab = '' ) { $this->base = sanitize_title( $base ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $tab = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; $this->active_tab = empty( $tab ) ? $active_tab : $tab; } /** * Get the base. * * @return string */ public function get_base() { return $this->base; } /** * Add a tab. * * @param WPSEO_Option_Tab $tab Tab to add. * * @return $this */ public function add_tab( WPSEO_Option_Tab $tab ) { $this->tabs[] = $tab; return $this; } /** * Get active tab. * * @return WPSEO_Option_Tab|null Get the active tab. */ public function get_active_tab() { if ( empty( $this->active_tab ) ) { return null; } $active_tabs = array_filter( $this->tabs, [ $this, 'is_active_tab' ] ); if ( ! empty( $active_tabs ) ) { $active_tabs = array_values( $active_tabs ); if ( count( $active_tabs ) === 1 ) { return $active_tabs[0]; } } return null; } /** * Is the tab the active tab. * * @param WPSEO_Option_Tab $tab Tab to check for active tab. * * @return bool */ public function is_active_tab( WPSEO_Option_Tab $tab ) { return ( $tab->get_name() === $this->active_tab ); } /** * Get all tabs. * * @return WPSEO_Option_Tab[] */ public function get_tabs() { return $this->tabs; } /** * Display the tabs. * * @param Yoast_Form $yform Yoast Form needed in the views. * * @return void */ public function display( Yoast_Form $yform ) { $formatter = new WPSEO_Option_Tabs_Formatter(); $formatter->run( $this, $yform ); } } admin/metabox/class-metabox-null-tab.php 0000644 00000000651 15174712003 0014264 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates the HTML for a metabox tab. */ class WPSEO_Metabox_Null_Tab implements WPSEO_Metabox_Tab { /** * Returns the html for the tab link. * * @return string|null */ public function link() { return null; } /** * Returns the html for the tab content. * * @return string|null */ public function content() { return null; } } admin/metabox/class-metabox.php 0000644 00000115625 15174712003 0012560 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository; use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter; use Yoast\WP\SEO\Presenters\Admin\Meta_Fields_Presenter; /** * This class generates the metabox on the edit post / page as well as contains all page analysis functionality. */ class WPSEO_Metabox extends WPSEO_Meta { /** * Whether the social tab is enabled. * * @var bool */ private $social_is_enabled; /** * Helper to determine whether the SEO analysis is enabled. * * @var WPSEO_Metabox_Analysis_SEO */ protected $seo_analysis; /** * Helper to determine whether the readability analysis is enabled. * * @var WPSEO_Metabox_Analysis_Readability */ protected $readability_analysis; /** * Helper to determine whether the inclusive language analysis is enabled. * * @var WPSEO_Metabox_Analysis_Inclusive_Language */ protected $inclusive_language_analysis; /** * The metabox editor object. * * @var WPSEO_Metabox_Editor */ protected $editor; /** * The Metabox post. * * @var WP_Post|null */ protected $post = null; /** * Whether the advanced metadata is enabled. * * @var bool */ protected $is_advanced_metadata_enabled; /** * Class constructor. */ public function __construct() { if ( $this->is_internet_explorer() ) { add_action( 'add_meta_boxes', [ $this, 'internet_explorer_metabox' ] ); return; } add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] ); add_action( 'wp_insert_post', [ $this, 'save_postdata' ] ); add_action( 'edit_attachment', [ $this, 'save_postdata' ] ); add_action( 'add_attachment', [ $this, 'save_postdata' ] ); $this->social_is_enabled = WPSEO_Options::get( 'opengraph', false, [ 'wpseo_social' ] ) || WPSEO_Options::get( 'twitter', false, [ 'wpseo_social' ] ); $this->is_advanced_metadata_enabled = WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) || WPSEO_Options::get( 'disableadvanced_meta', null, [ 'wpseo' ] ) === false; $this->seo_analysis = new WPSEO_Metabox_Analysis_SEO(); $this->readability_analysis = new WPSEO_Metabox_Analysis_Readability(); $this->inclusive_language_analysis = new WPSEO_Metabox_Analysis_Inclusive_Language(); } /** * Checks whether the request comes from an IE 11 browser. * * @return bool Whether the request comes from an IE 11 browser. */ public static function is_internet_explorer() { if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) { return false; } $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); if ( stripos( $user_agent, 'Trident/7.0' ) === false ) { return false; } return true; } /** * Adds an alternative metabox for internet explorer users. * * @return void */ public function internet_explorer_metabox() { $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, [ $this, 'display_metabox' ] ); if ( ! is_array( $post_types ) || $post_types === [] ) { return; } $product_title = $this->get_product_title(); foreach ( $post_types as $post_type ) { add_filter( "postbox_classes_{$post_type}_wpseo_meta", [ $this, 'wpseo_metabox_class' ] ); add_meta_box( 'wpseo_meta', $product_title, [ $this, 'render_internet_explorer_notice' ], $post_type, 'normal', apply_filters( 'wpseo_metabox_prio', 'high' ), [ '__block_editor_compatible_meta_box' => true ], ); } } /** * Renders the content for the internet explorer metabox. * * @return void */ public function render_internet_explorer_notice() { $content = sprintf( /* translators: 1: Link start tag to the Firefox website, 2: Link start tag to the Chrome website, 3: Link start tag to the Edge website, 4: Link closing tag. */ esc_html__( 'The browser you are currently using is unfortunately rather dated. Since we strive to give you the best experience possible, we no longer support this browser. Instead, please use %1$sFirefox%4$s, %2$sChrome%4$s or %3$sMicrosoft Edge%4$s.', 'wordpress-seo' ), '<a href="https://www.mozilla.org/firefox/new/">', '<a href="https://www.google.com/chrome/">', '<a href="https://www.microsoft.com/windows/microsoft-edge">', '</a>', ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above. echo new Alert_Presenter( $content ); } /** * Translates text strings for use in the meta box. * * IMPORTANT: if you want to add a new string (option) somewhere, make sure you add that array key to * the main meta box definition array in the class WPSEO_Meta() as well!!!! * * @deprecated 23.5 * @codeCoverageIgnore * * @return void */ public static function translate_meta_boxes() { _deprecated_function( __METHOD__, 'Yoast SEO 23.5' ); WPSEO_Meta::$meta_fields['general']['title']['title'] = __( 'SEO title', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['general']['metadesc']['title'] = __( 'Meta description', 'wordpress-seo' ); /* translators: %s expands to the post type name. */ WPSEO_Meta::$meta_fields['advanced']['meta-robots-noindex']['title'] = __( 'Allow search engines to show this %s in search results?', 'wordpress-seo' ); if ( (string) get_option( 'blog_public' ) === '0' ) { WPSEO_Meta::$meta_fields['advanced']['meta-robots-noindex']['description'] = '<span class="error-message">' . __( 'Warning: even though you can set the meta robots setting here, the entire site is set to noindex in the sitewide privacy settings, so these settings won\'t have an effect.', 'wordpress-seo' ) . '</span>'; } /* translators: %1$s expands to Yes or No, %2$s expands to the post type name.*/ WPSEO_Meta::$meta_fields['advanced']['meta-robots-noindex']['options']['0'] = __( 'Default for %2$s, currently: %1$s', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-noindex']['options']['2'] = __( 'Yes', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-noindex']['options']['1'] = __( 'No', 'wordpress-seo' ); /* translators: %1$s expands to the post type name.*/ WPSEO_Meta::$meta_fields['advanced']['meta-robots-nofollow']['title'] = __( 'Should search engines follow links on this %1$s?', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-nofollow']['options']['0'] = __( 'Yes', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-nofollow']['options']['1'] = __( 'No', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-adv']['title'] = __( 'Meta robots advanced', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-adv']['description'] = __( 'If you want to apply advanced <code>meta</code> robots settings for this page, please define them in the following field.', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-adv']['options']['noimageindex'] = __( 'No Image Index', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-adv']['options']['noarchive'] = __( 'No Archive', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['meta-robots-adv']['options']['nosnippet'] = __( 'No Snippet', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['bctitle']['title'] = __( 'Breadcrumbs Title', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['bctitle']['description'] = __( 'Title to use for this page in breadcrumb paths', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['canonical']['title'] = __( 'Canonical URL', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['canonical']['description'] = sprintf( /* translators: 1: link open tag; 2: link close tag. */ __( 'The canonical URL that this page should point to. Leave empty to default to permalink. %1$sCross domain canonical%2$s supported too.', 'wordpress-seo' ), '<a href="https://googlewebmastercentral.blogspot.com/2009/12/handling-legitimate-cross-domain.html" target="_blank" rel="noopener">', WPSEO_Admin_Utils::get_new_tab_message() . '</a>', ); WPSEO_Meta::$meta_fields['advanced']['redirect']['title'] = __( '301 Redirect', 'wordpress-seo' ); WPSEO_Meta::$meta_fields['advanced']['redirect']['description'] = __( 'The URL that this page should redirect to.', 'wordpress-seo' ); do_action_deprecated( 'wpseo_tab_translate', [], 'Yoast SEO 23.5', '', 'WPSEO_Metabox::translate_meta_boxes is deprecated.' ); } /** * Determines whether the metabox should be shown for the passed identifier. * * By default the check is done for post types, but can also be used for taxonomies. * * @param string|null $identifier The identifier to check. * @param string $type The type of object to check. Defaults to post_type. * * @return bool Whether or not the metabox should be displayed. */ public function display_metabox( $identifier = null, $type = 'post_type' ) { return WPSEO_Utils::is_metabox_active( $identifier, $type ); } /** * Adds the Yoast SEO meta box to the edit boxes in the edit post, page, * attachment, and custom post types pages. * * @return void */ public function add_meta_box() { $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, [ $this, 'display_metabox' ] ); if ( ! is_array( $post_types ) || $post_types === [] ) { return; } $product_title = $this->get_product_title(); foreach ( $post_types as $post_type ) { add_filter( "postbox_classes_{$post_type}_wpseo_meta", [ $this, 'wpseo_metabox_class' ] ); add_meta_box( 'wpseo_meta', $product_title, [ $this, 'meta_box' ], $post_type, 'normal', apply_filters( 'wpseo_metabox_prio', 'high' ), [ '__block_editor_compatible_meta_box' => true ], ); } } /** * Adds CSS classes to the meta box. * * @param string[] $classes An array of postbox CSS classes. * * @return string[] List of classes that will be applied to the editbox container. */ public function wpseo_metabox_class( $classes ) { $classes[] = 'yoast wpseo-metabox'; return $classes; } /** * Passes variables to js for use with the post-scraper. * * @return array<string, string|array<string|int|bool>|bool|int> */ public function get_metabox_script_data() { $permalink = $this->get_permalink(); $post_formatter = new WPSEO_Metabox_Formatter( new WPSEO_Post_Metabox_Formatter( $this->get_metabox_post(), [], $permalink ), ); $values = $post_formatter->get_values(); /** This filter is documented in admin/filters/class-cornerstone-filter.php. */ $post_types = apply_filters( 'wpseo_cornerstone_post_types', WPSEO_Post_Type::get_accessible_post_types() ); if ( $values['cornerstoneActive'] && ! in_array( $this->get_metabox_post()->post_type, $post_types, true ) ) { $values['cornerstoneActive'] = false; } if ( $values['semrushIntegrationActive'] && $this->post->post_type === 'attachment' ) { $values['semrushIntegrationActive'] = 0; } if ( $values['wincherIntegrationActive'] && $this->post->post_type === 'attachment' ) { $values['wincherIntegrationActive'] = 0; } return $values; } /** * Determines whether or not the current post type has registered taxonomies. * * @return bool Whether the current post type has taxonomies. */ private function current_post_type_has_taxonomies() { $post_taxonomies = get_object_taxonomies( get_post_type() ); return ! empty( $post_taxonomies ); } /** * Determines the scope based on the post type. * This can be used by the replacevar plugin to determine if a replacement needs to be executed. * * @return string String describing the current scope. */ private function determine_scope() { if ( $this->get_metabox_post()->post_type === 'page' ) { return 'page'; } return 'post'; } /** * Outputs the meta box. * * @return void */ public function meta_box() { $this->render_hidden_fields(); $this->render_tabs(); } /** * Renders the metabox hidden fields. * * @return void */ protected function render_hidden_fields() { wp_nonce_field( 'yoast_free_metabox', 'yoast_free_metabox_nonce' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in class. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'general' ); if ( $this->is_advanced_metadata_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in class. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'advanced' ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in class. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'schema', $this->get_metabox_post()->post_type ); if ( $this->social_is_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in class. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' ); } /** * Filter: 'wpseo_content_meta_section_content' - Allow filtering the metabox content before outputting. * * @param string $post_content The metabox content string. */ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be escaped in the filter. echo apply_filters( 'wpseo_content_meta_section_content', '' ); } /** * Renders the metabox tabs. * * @return void */ protected function render_tabs() { echo '<div class="wpseo-metabox-content">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $this->get_product_title() returns a hard-coded string. printf( '<div class="wpseo-metabox-menu"><ul role="tablist" class="yoast-aria-tabs" aria-label="%s">', $this->get_product_title() ); $tabs = $this->get_tabs(); foreach ( $tabs as $tab ) { if ( $tab->name === 'premium' ) { continue; } $tab->display_link(); } echo '</ul></div>'; foreach ( $tabs as $tab ) { $tab->display_content(); } echo '</div>'; } /** * Returns the relevant metabox tabs for the current view. * * @return WPSEO_Metabox_Section[] */ private function get_tabs() { $tabs = []; $label = __( 'SEO', 'wordpress-seo' ); if ( $this->seo_analysis->is_enabled() ) { $label = '<span class="wpseo-score-icon-container" id="wpseo-seo-score-icon"></span>' . $label; } $tabs[] = new WPSEO_Metabox_Section_React( 'content', $label ); if ( $this->readability_analysis->is_enabled() ) { $tabs[] = new WPSEO_Metabox_Section_Readability(); } if ( $this->inclusive_language_analysis->is_enabled() ) { $tabs[] = new WPSEO_Metabox_Section_Inclusive_Language(); } if ( $this->is_advanced_metadata_enabled ) { $tabs[] = new WPSEO_Metabox_Section_React( 'schema', '<span class="wpseo-schema-icon"></span>' . __( 'Schema', 'wordpress-seo' ), '', ); } if ( $this->social_is_enabled ) { $tabs[] = new WPSEO_Metabox_Section_React( 'social', '<span class="dashicons dashicons-share"></span>' . __( 'Social', 'wordpress-seo' ), '', [ 'html_after' => '<div id="wpseo-section-social"></div>', ], ); } $tabs = array_merge( $tabs, $this->get_additional_tabs() ); return $tabs; } /** * Returns the metabox tabs that have been added by other plugins. * * @return WPSEO_Metabox_Section_Additional[] */ protected function get_additional_tabs() { $tabs = []; /** * Private filter: 'yoast_free_additional_metabox_sections'. * * Meant for internal use only. Allows adding additional tabs to the Yoast SEO metabox. * * @since 11.9 * * @param array[] $tabs { * An array of arrays with tab specifications. * * @type array $tab { * A tab specification. * * @type string $name The name of the tab. Used in the HTML IDs, href and aria properties. * @type string $link_content The content of the tab link. * @type string $content The content of the tab. * @type array $options { * Optional. Extra options. * * @type string $link_class Optional. The class for the tab link. * @type string $link_aria_label Optional. The aria label of the tab link. * } * } * } */ $requested_tabs = apply_filters( 'yoast_free_additional_metabox_sections', [] ); foreach ( $requested_tabs as $tab ) { if ( is_array( $tab ) && array_key_exists( 'name', $tab ) && array_key_exists( 'link_content', $tab ) && array_key_exists( 'content', $tab ) ) { $options = array_key_exists( 'options', $tab ) ? $tab['options'] : []; $tabs[] = new WPSEO_Metabox_Section_Additional( $tab['name'], $tab['link_content'], $tab['content'], $options, ); } } return $tabs; } /** * Adds a line in the meta box. * * @deprecated 23.5 * @codeCoverageIgnore * * @param string[] $meta_field_def Contains the vars based on which output is generated. * @param string $key Internal key (without prefix). * * @return string */ public function do_meta_box( $meta_field_def, $key = '' ) { _deprecated_function( __METHOD__, 'Yoast SEO 23.5' ); $content = ''; $esc_form_key = esc_attr( WPSEO_Meta::$form_prefix . $key ); $meta_value = WPSEO_Meta::get_value( $key, $this->get_metabox_post()->ID ); $class = ''; if ( isset( $meta_field_def['class'] ) && $meta_field_def['class'] !== '' ) { $class = ' ' . $meta_field_def['class']; } $placeholder = ''; if ( isset( $meta_field_def['placeholder'] ) && $meta_field_def['placeholder'] !== '' ) { $placeholder = $meta_field_def['placeholder']; } $aria_describedby = ''; $description = ''; if ( isset( $meta_field_def['description'] ) ) { $aria_describedby = ' aria-describedby="' . $esc_form_key . '-desc"'; $description = '<p id="' . $esc_form_key . '-desc" class="yoast-metabox__description">' . $meta_field_def['description'] . '</p>'; } // Add a hide_on_pages option that returns nothing when the field is rendered on a page. if ( isset( $meta_field_def['hide_on_pages'] ) && $meta_field_def['hide_on_pages'] && get_post_type() === 'page' ) { return ''; } switch ( $meta_field_def['type'] ) { case 'text': $ac = ''; if ( isset( $meta_field_def['autocomplete'] ) && $meta_field_def['autocomplete'] === false ) { $ac = 'autocomplete="off" '; } if ( $placeholder !== '' ) { $placeholder = ' placeholder="' . esc_attr( $placeholder ) . '"'; } $content .= '<input type="text"' . $placeholder . ' id="' . $esc_form_key . '" ' . $ac . 'name="' . $esc_form_key . '" value="' . esc_attr( $meta_value ) . '" class="large-text' . $class . '"' . $aria_describedby . '/>'; break; case 'url': if ( $placeholder !== '' ) { $placeholder = ' placeholder="' . esc_attr( $placeholder ) . '"'; } $content .= '<input type="url"' . $placeholder . ' id="' . $esc_form_key . '" name="' . $esc_form_key . '" value="' . esc_attr( urldecode( $meta_value ) ) . '" class="large-text' . $class . '"' . $aria_describedby . '/>'; break; case 'textarea': $rows = 3; if ( isset( $meta_field_def['rows'] ) && $meta_field_def['rows'] > 0 ) { $rows = $meta_field_def['rows']; } $content .= '<textarea class="large-text' . $class . '" rows="' . esc_attr( $rows ) . '" id="' . $esc_form_key . '" name="' . $esc_form_key . '"' . $aria_describedby . '>' . esc_textarea( $meta_value ) . '</textarea>'; break; case 'hidden': $default = ''; if ( isset( $meta_field_def['default'] ) ) { $default = sprintf( ' data-default="%s"', esc_attr( $meta_field_def['default'] ) ); } $content .= '<input type="hidden" id="' . $esc_form_key . '" name="' . $esc_form_key . '" value="' . esc_attr( $meta_value ) . '"' . $default . '/>' . "\n"; break; case 'select': if ( isset( $meta_field_def['options'] ) && is_array( $meta_field_def['options'] ) && $meta_field_def['options'] !== [] ) { $content .= '<select name="' . $esc_form_key . '" id="' . $esc_form_key . '" class="yoast' . $class . '">'; foreach ( $meta_field_def['options'] as $val => $option ) { $selected = selected( $meta_value, $val, false ); $content .= '<option ' . $selected . ' value="' . esc_attr( $val ) . '">' . esc_html( $option ) . '</option>'; } unset( $val, $option, $selected ); $content .= '</select>'; } break; case 'multiselect': if ( isset( $meta_field_def['options'] ) && is_array( $meta_field_def['options'] ) && $meta_field_def['options'] !== [] ) { // Set $meta_value as $selected_arr. $selected_arr = $meta_value; // If the multiselect field is 'meta-robots-adv' we should explode on ,. if ( $key === 'meta-robots-adv' ) { $selected_arr = explode( ',', $meta_value ); } if ( ! is_array( $selected_arr ) ) { $selected_arr = (array) $selected_arr; } $options_count = count( $meta_field_def['options'] ); $content .= '<select multiple="multiple" size="' . esc_attr( $options_count ) . '" name="' . $esc_form_key . '[]" id="' . $esc_form_key . '" class="yoast' . $class . '"' . $aria_describedby . '>'; foreach ( $meta_field_def['options'] as $val => $option ) { $selected = ''; if ( in_array( $val, $selected_arr, true ) ) { $selected = ' selected="selected"'; } $content .= '<option ' . $selected . ' value="' . esc_attr( $val ) . '">' . esc_html( $option ) . '</option>'; } $content .= '</select>'; unset( $val, $option, $selected, $selected_arr, $options_count ); } break; case 'checkbox': $checked = checked( $meta_value, 'on', false ); $expl = ( isset( $meta_field_def['expl'] ) ) ? esc_html( $meta_field_def['expl'] ) : ''; $content .= '<input type="checkbox" id="' . $esc_form_key . '" name="' . $esc_form_key . '" ' . $checked . ' value="on" class="yoast' . $class . '"' . $aria_describedby . '/> <label for="' . $esc_form_key . '">' . $expl . '</label>'; unset( $checked, $expl ); break; case 'radio': if ( isset( $meta_field_def['options'] ) && is_array( $meta_field_def['options'] ) && $meta_field_def['options'] !== [] ) { foreach ( $meta_field_def['options'] as $val => $option ) { $checked = checked( $meta_value, $val, false ); $content .= '<input type="radio" ' . $checked . ' id="' . $esc_form_key . '_' . esc_attr( $val ) . '" name="' . $esc_form_key . '" value="' . esc_attr( $val ) . '"/> <label for="' . $esc_form_key . '_' . esc_attr( $val ) . '">' . esc_html( $option ) . '</label> '; } unset( $val, $option, $checked ); } break; } $html = ''; if ( $content === '' ) { $content = apply_filters_deprecated( 'wpseo_do_meta_box_field_' . $key, [ $content, $meta_value, $esc_form_key, $meta_field_def, $key ], 'Yoast SEO 23.5', '', 'do_meta_box is deprecated' ); } if ( $content !== '' ) { $title = esc_html( $meta_field_def['title'] ); // By default, use the field title as a label element. $label = '<label for="' . $esc_form_key . '">' . $title . '</label>'; // Set the inline help and help panel, if any. $help_button = ''; $help_panel = ''; if ( isset( $meta_field_def['help'] ) && $meta_field_def['help'] !== '' ) { $help = new WPSEO_Admin_Help_Panel( $key, $meta_field_def['help-button'], $meta_field_def['help'] ); $help_button = $help->get_button_html(); $help_panel = $help->get_panel_html(); } // If it's a set of radio buttons, output proper fieldset and legend. if ( $meta_field_def['type'] === 'radio' ) { return '<fieldset><legend>' . $title . '</legend>' . $help_button . $help_panel . $content . $description . '</fieldset>'; } // If it's a single checkbox, ignore the title. if ( $meta_field_def['type'] === 'checkbox' ) { $label = ''; } // Other meta box content or form fields. if ( $meta_field_def['type'] === 'hidden' ) { $html = $content; } else { $html = $label . $description . $help_button . $help_panel . $content; } } return $html; } /** * Saves the WP SEO metadata for posts. * * {@internal $_POST parameters are validated via sanitize_post_meta().}} * * @param int $post_id Post ID. * * @return bool|void Boolean false if invalid save post request. */ public function save_postdata( $post_id ) { // Bail if this is a multisite installation and the site has been switched. if ( is_multisite() && ms_is_switched() ) { return false; } if ( $post_id === null ) { return false; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in wp_verify_none. if ( ! isset( $_POST['yoast_free_metabox_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['yoast_free_metabox_nonce'] ), 'yoast_free_metabox' ) ) { return false; } if ( wp_is_post_revision( $post_id ) ) { $post_id = wp_is_post_revision( $post_id ); } /** * Determine we're not accidentally updating a different post. * We can't use filter_input here as the ID isn't available at this point, other than in the $_POST data. */ if ( ! isset( $_POST['ID'] ) || $post_id !== (int) $_POST['ID'] ) { return false; } clean_post_cache( $post_id ); $post = get_post( $post_id ); if ( ! is_object( $post ) ) { // Non-existent post. return false; } do_action( 'wpseo_save_compare_data', $post ); $social_fields = []; if ( $this->social_is_enabled ) { $social_fields = WPSEO_Meta::get_meta_field_defs( 'social' ); } $meta_boxes = apply_filters( 'wpseo_save_metaboxes', [] ); $meta_boxes = array_merge( $meta_boxes, WPSEO_Meta::get_meta_field_defs( 'general', $post->post_type ), WPSEO_Meta::get_meta_field_defs( 'advanced' ), $social_fields, WPSEO_Meta::get_meta_field_defs( 'schema', $post->post_type ), ); foreach ( $meta_boxes as $key => $meta_box ) { // If analysis is disabled remove that analysis score value from the DB. if ( $this->is_meta_value_disabled( $key ) ) { WPSEO_Meta::delete( $key, $post_id ); continue; } $data = null; $field_name = WPSEO_Meta::$form_prefix . $key; if ( $meta_box['type'] === 'checkbox' ) { $data = isset( $_POST[ $field_name ] ) ? 'on' : 'off'; } else { if ( isset( $_POST[ $field_name ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We're preparing to do just that. $data = wp_unslash( $_POST[ $field_name ] ); // For multi-select. if ( is_array( $data ) ) { $data = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $data ); } if ( is_string( $data ) ) { $data = ( $key !== 'canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data ); } } // Reset options when no entry is present with multiselect - only applies to `meta-robots-adv` currently. if ( ! isset( $_POST[ $field_name ] ) && ( $meta_box['type'] === 'multiselect' ) ) { $data = []; } } if ( $data !== null ) { WPSEO_Meta::set_value( $key, $data, $post_id ); } } do_action( 'wpseo_saved_postdata' ); } /** * Determines if the given meta value key is disabled. * * @param string $key The key of the meta value. * * @return bool Whether the given meta value key is disabled. */ public function is_meta_value_disabled( $key ) { if ( $key === 'linkdex' && ! $this->seo_analysis->is_enabled() ) { return true; } if ( $key === 'content_score' && ! $this->readability_analysis->is_enabled() ) { return true; } if ( $key === 'inclusive_language_score' && ! $this->inclusive_language_analysis->is_enabled() ) { return true; } return false; } /** * Enqueues all the needed JS and CSS. * * @todo [JRF => whomever] Create css/metabox-mp6.css file and add it to the below allowed colors array when done. * * @return void */ public function enqueue() { global $pagenow; if ( $this->readability_analysis->is_enabled() ) { $this->editor = new WPSEO_Metabox_Editor(); $this->editor->register_hooks(); } $asset_manager = new WPSEO_Admin_Asset_Manager(); if ( self::is_post_overview( $pagenow ) ) { return; } /* Filter 'wpseo_always_register_metaboxes_on_admin' documented in wpseo-main.php */ if ( ( self::is_post_edit( $pagenow ) === false && apply_filters( 'wpseo_always_register_metaboxes_on_admin', false ) === false ) || $this->display_metabox() === false ) { return; } $post_id = get_queried_object_id(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( empty( $post_id ) && isset( $_GET['post'] ) && is_string( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. $post_id = sanitize_text_field( wp_unslash( $_GET['post'] ) ); } if ( $post_id !== 0 ) { // Enqueue files needed for upload functionality. wp_enqueue_media( [ 'post' => $post_id ] ); } $asset_manager->enqueue_style( 'metabox-css' ); if ( $this->readability_analysis->is_enabled() ) { $asset_manager->enqueue_style( 'scoring' ); } $asset_manager->enqueue_style( 'monorepo' ); $asset_manager->enqueue_style( 'ai-generator' ); $asset_manager->enqueue_style( 'ai-fix-assessments' ); $is_block_editor = WP_Screen::get()->is_block_editor(); $post_edit_handle = 'post-edit'; if ( ! $is_block_editor ) { $post_edit_handle = 'post-edit-classic'; } $asset_manager->enqueue_script( $post_edit_handle ); $asset_manager->enqueue_style( 'admin-css' ); /** * Removes the emoji script as it is incompatible with both React and any * contenteditable fields. */ remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); $asset_manager->localize_script( $post_edit_handle, 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() ); $plugins_script_data = [ 'replaceVars' => [ 'replace_vars' => $this->get_replace_vars(), 'hidden_replace_vars' => $this->get_hidden_replace_vars(), 'recommended_replace_vars' => $this->get_recommended_replace_vars(), 'scope' => $this->determine_scope(), 'has_taxonomies' => $this->current_post_type_has_taxonomies(), ], 'shortcodes' => [ 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ]; $worker_script_data = [ 'url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ), 'dependencies' => YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ), 'keywords_assessment_url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ), 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(), ]; $page_on_front = (int) get_option( 'page_on_front' ); $homepage_is_page = get_option( 'show_on_front' ) === 'page'; $is_front_page = $homepage_is_page && $page_on_front === (int) $post_id; $script_data = [ 'metabox' => $this->get_metabox_script_data(), 'isPost' => true, 'isBlockEditor' => $is_block_editor, 'postId' => $post_id, 'postStatus' => get_post_status( $post_id ), 'postType' => get_post_type( $post_id ), 'isPage' => get_post_type( $post_id ) === 'page', 'usedKeywordsNonce' => wp_create_nonce( 'wpseo-keyword-usage-and-post-types' ), 'analysis' => [ 'plugins' => $plugins_script_data, 'worker' => $worker_script_data, ], 'isFrontPage' => $is_front_page, ]; /** * The website information repository. * * @var Website_Information_Repository $repo */ $repo = YoastSEO()->classes->get( Website_Information_Repository::class ); $site_information = $repo->get_post_site_information(); $site_information->set_permalink( $this->get_permalink() ); $script_data = array_merge_recursive( $site_information->get_legacy_site_information(), $script_data ); if ( ! $is_block_editor && post_type_supports( get_post_type(), 'thumbnail' ) ) { $asset_manager->enqueue_style( 'featured-image' ); } $asset_manager->localize_script( $post_edit_handle, 'wpseoScriptData', $script_data ); } /** * Returns post in metabox context. * * @return WP_Post|array<string|int|bool> */ protected function get_metabox_post() { if ( $this->post !== null ) { return $this->post; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post'] ) && is_string( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, Sanitization happens in the validate_int function. $post_id = (int) WPSEO_Utils::validate_int( wp_unslash( $_GET['post'] ) ); $this->post = get_post( $post_id ); return $this->post; } if ( isset( $GLOBALS['post'] ) ) { $this->post = $GLOBALS['post']; return $this->post; } return []; } /** * Returns an array with shortcode tags for all registered shortcodes. * * @return string[] */ private function get_valid_shortcode_tags() { $shortcode_tags = []; foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) { $shortcode_tags[] = $tag; } return $shortcode_tags; } /** * Prepares the replace vars for localization. * * @return string[] Replace vars. */ private function get_replace_vars() { $cached_replacement_vars = []; $vars_to_cache = [ 'date', 'id', 'sitename', 'sitedesc', 'sep', 'page', 'currentdate', 'currentyear', 'currentmonth', 'currentday', 'post_year', 'post_month', 'post_day', 'name', 'author_first_name', 'author_last_name', 'permalink', 'post_content', 'category_title', 'tag', 'category', ]; foreach ( $vars_to_cache as $var ) { $cached_replacement_vars[ $var ] = wpseo_replace_vars( '%%' . $var . '%%', $this->get_metabox_post() ); } // Merge custom replace variables with the WordPress ones. return array_merge( $cached_replacement_vars, $this->get_custom_replace_vars( $this->get_metabox_post() ) ); } /** * Returns the list of replace vars that should be hidden inside the editor. * * @return string[] The hidden replace vars. */ protected function get_hidden_replace_vars() { return ( new WPSEO_Replace_Vars() )->get_hidden_replace_vars(); } /** * Prepares the recommended replace vars for localization. * * @return array<string[]> Recommended replacement variables. */ private function get_recommended_replace_vars() { $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); // What is recommended depends on the current context. $post_type = $recommended_replace_vars->determine_for_post( $this->get_metabox_post() ); return $recommended_replace_vars->get_recommended_replacevars_for( $post_type ); } /** * Gets the custom replace variables for custom taxonomies and fields. * * @param WP_Post $post The post to check for custom taxonomies and fields. * * @return array<string[]> Array containing all the replacement variables. */ private function get_custom_replace_vars( $post ) { return [ 'custom_fields' => $this->get_custom_fields_replace_vars( $post ), 'custom_taxonomies' => $this->get_custom_taxonomies_replace_vars( $post ), ]; } /** * Gets the custom replace variables for custom taxonomies. * * @param WP_Post $post The post to check for custom taxonomies. * * @return array<string[]> Array containing all the replacement variables. */ private function get_custom_taxonomies_replace_vars( $post ) { $taxonomies = get_object_taxonomies( $post, 'objects' ); $custom_replace_vars = []; foreach ( $taxonomies as $taxonomy_name => $taxonomy ) { if ( is_string( $taxonomy ) ) { // If attachment, see https://core.trac.wordpress.org/ticket/37368 . $taxonomy_name = $taxonomy; $taxonomy = get_taxonomy( $taxonomy_name ); } if ( $taxonomy->_builtin && $taxonomy->public ) { continue; } $custom_replace_vars[ $taxonomy_name ] = [ 'name' => $taxonomy->name, 'description' => $taxonomy->description, ]; } return $custom_replace_vars; } /** * Gets the custom replace variables for custom fields. * * @param WP_Post $post The post to check for custom fields. * * @return array<string[]> Array containing all the replacement variables. */ private function get_custom_fields_replace_vars( $post ) { $custom_replace_vars = []; // If no post object is passed, return the empty custom_replace_vars array. if ( ! is_object( $post ) ) { return $custom_replace_vars; } $custom_fields = get_post_custom( $post->ID ); // If $custom_fields is an empty string or generally not an array, return early. if ( ! is_array( $custom_fields ) ) { return $custom_replace_vars; } $meta = YoastSEO()->meta->for_post( $post->ID ); if ( ! $meta ) { return $custom_replace_vars; } // Simply concatenate all fields containing replace vars so we can handle them all with a single regex find. $replace_vars_fields = implode( ' ', [ $meta->presentation->title, $meta->presentation->meta_description, ], ); preg_match_all( '/%%cf_([A-Za-z0-9_]+)%%/', $replace_vars_fields, $matches ); $fields_to_include = $matches[1]; foreach ( $custom_fields as $custom_field_name => $custom_field ) { // Skip private custom fields. if ( substr( $custom_field_name, 0, 1 ) === '_' ) { continue; } // Skip custom fields that are not used, new ones will be fetched dynamically. if ( ! in_array( $custom_field_name, $fields_to_include, true ) ) { continue; } // Skip custom field values that are serialized. if ( is_serialized( $custom_field[0] ) ) { continue; } $custom_replace_vars[ $custom_field_name ] = $custom_field[0]; } return $custom_replace_vars; } /** * Checks if the page is the post overview page. * * @param string $page The page to check for the post overview page. * * @return bool Whether or not the given page is the post overview page. */ public static function is_post_overview( $page ) { return $page === 'edit.php'; } /** * Checks if the page is the post edit page. * * @param string $page The page to check for the post edit page. * * @return bool Whether or not the given page is the post edit page. */ public static function is_post_edit( $page ) { return $page === 'post.php' || $page === 'post-new.php'; } /** * Retrieves the product title. * * @return string The product title. */ protected function get_product_title() { return YoastSEO()->helpers->product->get_product_name(); } /** * Gets the permalink. * * @return string */ protected function get_permalink() { $permalink = ''; if ( is_object( $this->get_metabox_post() ) ) { $permalink = get_sample_permalink( $this->get_metabox_post()->ID ); $permalink = $permalink[0]; } return $permalink; } } admin/metabox/interface-metabox-section.php 0000644 00000000552 15174712003 0015045 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates and displays the HTML for a metabox section. */ interface WPSEO_Metabox_Section { /** * Outputs the section link. * * @return void */ public function display_link(); /** * Outputs the section content. * * @return void */ public function display_content(); } admin/metabox/class-metabox-analysis-inclusive-language.php 0000644 00000003670 15174712003 0020155 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Represents the inclusive language analysis. */ class WPSEO_Metabox_Analysis_Inclusive_Language implements WPSEO_Metabox_Analysis { /** * Whether this analysis is enabled. * * @return bool Whether or not this analysis is enabled. */ public function is_enabled() { return $this->is_globally_enabled() && $this->is_user_enabled() && $this->is_current_version_supported() && YoastSEO()->helpers->language->has_inclusive_language_support( WPSEO_Language_Utils::get_language( get_locale() ) ); } /** * Whether or not this analysis is enabled by the user. * * @return bool Whether or not this analysis is enabled by the user. */ public function is_user_enabled() { return ! get_the_author_meta( 'wpseo_inclusive_language_analysis_disable', get_current_user_id() ); } /** * Whether or not this analysis is enabled globally. * * @return bool Whether or not this analysis is enabled globally. */ public function is_globally_enabled() { return WPSEO_Options::get( 'inclusive_language_analysis_active', false ); } /** * Whether the inclusive language analysis should be loaded in Free. * * It should always be loaded when Premium is not active. If Premium is active, it depends on the version. Some Premium * versions also have inclusive language code (when it was still a Premium only feature) which would result in rendering * the analysis twice. In those cases, the analysis should be only loaded from the Premium side. * * @return bool Whether or not the inclusive language analysis should be loaded. */ private function is_current_version_supported() { $is_premium = YoastSEO()->helpers->product->is_premium(); $premium_version = YoastSEO()->helpers->product->get_premium_version(); return ! $is_premium || version_compare( $premium_version, '19.6-RC0', '>=' ) || version_compare( $premium_version, '19.2', '==' ); } } admin/metabox/class-metabox-analysis-readability.php 0000644 00000001641 15174712003 0016660 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Represents the readability analysis. */ class WPSEO_Metabox_Analysis_Readability implements WPSEO_Metabox_Analysis { /** * Whether this analysis is enabled. * * @return bool Whether or not this analysis is enabled. */ public function is_enabled() { return $this->is_globally_enabled() && $this->is_user_enabled(); } /** * Whether or not this analysis is enabled by the user. * * @return bool Whether or not this analysis is enabled by the user. */ public function is_user_enabled() { return ! get_the_author_meta( 'wpseo_content_analysis_disable', get_current_user_id() ); } /** * Whether or not this analysis is enabled globally. * * @return bool Whether or not this analysis is enabled globally. */ public function is_globally_enabled() { return WPSEO_Options::get( 'content_analysis_active', true ); } } admin/metabox/class-metabox-section-additional.php 0000644 00000005111 15174712003 0016314 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Generates and displays an additional metabox section. */ class WPSEO_Metabox_Section_Additional implements WPSEO_Metabox_Section { /** * Name of the section, used as an identifier in the HTML. * * @var string */ public $name; /** * Content of the tab's section. * * @var string */ public $content; /** * HTML to use in the tab header. * * @var string */ private $link_content; /** * Class to add to the link. * * @var string */ private $link_class; /** * Aria label to use for the link. * * @var string */ private $link_aria_label; /** * Represents the content class. * * @var string */ private $content_class; /** * Constructor. * * @param string $name The name of the section, used as an identifier in the html. * Can only contain URL safe characters. * @param string $link_content The text content of the section link. * @param string $content Optional. Content to use above the React root element. * @param array $options Optional link attributes. */ public function __construct( $name, $link_content, $content = '', array $options = [] ) { $this->name = $name; $this->content = $content; $default_options = [ 'link_class' => '', 'link_aria_label' => '', 'content_class' => 'wpseo-form', ]; $options = wp_parse_args( $options, $default_options ); $this->link_content = $link_content; $this->link_class = $options['link_class']; $this->link_aria_label = $options['link_aria_label']; $this->content_class = $options['content_class']; } /** * Outputs the section link. * * @return void */ public function display_link() { printf( '<li role="presentation"><a role="tab" href="#wpseo-meta-section-%1$s" id="wpseo-meta-tab-%1$s" aria-controls="wpseo-meta-section-%1$s" class="wpseo-meta-section-link %2$s"%3$s>%4$s</a></li>', esc_attr( $this->name ), esc_attr( $this->link_class ), ( $this->link_aria_label !== '' ) ? ' aria-label="' . esc_attr( $this->link_aria_label ) . '"' : '', $this->link_content, ); } /** * Outputs the section content. * * @return void */ public function display_content() { $html = sprintf( '<div role="tabpanel" id="wpseo-meta-section-%1$s" aria-labelledby="wpseo-meta-tab-%1$s" tabindex="0" class="wpseo-meta-section %2$s">', esc_attr( $this->name ), esc_attr( $this->content_class ), ); $html .= $this->content; $html .= '</div>'; echo $html; } } admin/metabox/class-abstract-sectioned-metabox-tab.php 0000644 00000004407 15174712003 0017073 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Base class for metabox that consist of multiple sections. */ abstract class WPSEO_Abstract_Metabox_Tab_With_Sections implements WPSEO_Metabox_Section { /** * Holds the name of the tab. * * @var string */ public $name; /** * Holds the HTML of the tab header. * * @var string */ protected $link_content; /** * Holds the name of the tab header. * * @var string */ protected $link_title; /** * Holds the classname of the tab header. * * @var string */ protected $link_class; /** * Holds the aria label of the tab header. * * @var string */ protected $link_aria_label; /** * Constructor. * * @param string $name The name of the section, used as an identifier in the html. * Can only contain URL safe characters. * @param string $link_content The text content of the section link. * @param array $options Optional link attributes. */ public function __construct( $name, $link_content, array $options = [] ) { $default_options = [ 'link_title' => '', 'link_class' => '', 'link_aria_label' => '', ]; $options = array_merge( $default_options, $options ); $this->name = $name; $this->link_content = $link_content; $this->link_title = $options['link_title']; $this->link_class = $options['link_class']; $this->link_aria_label = $options['link_aria_label']; } /** * Outputs the section link if any section has been added. * * @return void */ public function display_link() { if ( $this->has_sections() ) { printf( '<li role="presentation"><a role="tab" href="#wpseo-meta-section-%1$s" id="wpseo-meta-tab-%1$s" aria-controls="wpseo-meta-section-%1$s" class="wpseo-meta-section-link %2$s"%3$s%4$s>%5$s</a></li>', esc_attr( $this->name ), esc_attr( $this->link_class ), ( $this->link_title !== '' ) ? ' title="' . esc_attr( $this->link_title ) . '"' : '', ( $this->link_aria_label !== '' ) ? ' aria-label="' . esc_attr( $this->link_aria_label ) . '"' : '', $this->link_content, ); } } /** * Checks whether the tab has any sections. * * @return bool Whether the tab has any sections */ abstract protected function has_sections(); } admin/metabox/interface-metabox-analysis.php 0000644 00000001267 15174712003 0015230 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Describes an interface for an analysis that can either be enabled or disabled. */ interface WPSEO_Metabox_Analysis { /** * Whether this analysis is enabled. * * @return bool Whether or not this analysis is enabled. */ public function is_enabled(); /** * Whether or not this analysis is enabled by the user. * * @return bool Whether or not this analysis is enabled by the user. */ public function is_user_enabled(); /** * Whether or not this analysis is enabled globally. * * @return bool Whether or not this analysis is enabled globally. */ public function is_globally_enabled(); } admin/metabox/class-metabox-collapsible.php 0000644 00000003022 15174712003 0015032 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates the HTML for a metabox tab. */ class WPSEO_Metabox_Collapsible implements WPSEO_Metabox_Tab { /** * The collapsible's unique identifier. * * @var string */ private $name; /** * The content to be displayed inside the collapsible. * * @var string */ private $content; /** * The label. * * @var string */ private $link_content; /** * Constructor. * * @param string $name The name of the tab, used as an identifier in the html. * @param string $content The tab content. * @param string $link_content The text content of the tab link. */ public function __construct( $name, $content, $link_content ) { $this->name = $name; $this->content = $content; $this->link_content = $link_content; } /** * Returns the html for the tab link. * * @return string */ public function link() { return $this->link_content; } /** * Returns the html for the tab content. * * @return string */ public function content() { $collapsible_paper = new WPSEO_Paper_Presenter( $this->link(), null, [ 'content' => $this->content, 'collapsible' => true, 'class' => 'metabox wpseo-form wpseo-collapsible-container', 'paper_id' => 'collapsible-' . $this->name, ], ); return $collapsible_paper->get_output(); } /** * Returns the collapsible's unique identifier. * * @return string */ public function get_name() { return $this->name; } } admin/metabox/class-metabox-collapsibles-section.php 0000644 00000003745 15174712003 0016673 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates and displays a metabox tab that consists of collapsible sections. */ class WPSEO_Metabox_Collapsibles_Sections extends WPSEO_Abstract_Metabox_Tab_With_Sections { /** * Holds the tab's collapsibles. * * @var WPSEO_Metabox_Collapsible[] */ private $collapsibles = []; /** * Constructor. * * @param string $name The name of the section, used as an identifier in the html. * Can only contain URL safe characters. * @param string $link_content The text content of the section link. * @param array $collapsibles The metabox collapsibles (`WPSEO_Metabox_Collapsible[]`) to be included in the section. * @param array $options Optional link attributes. */ public function __construct( $name, $link_content, array $collapsibles = [], array $options = [] ) { parent::__construct( $name, $link_content, $options ); $this->collapsibles = $collapsibles; } /** * Outputs the section content if any tab has been added. * * @return void */ public function display_content() { if ( $this->has_sections() ) { printf( '<div id="%1$s" class="wpseo-meta-section">', esc_attr( 'wpseo-meta-section-' . $this->name ) ); echo '<div class="wpseo_content_wrapper">'; add_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_forms' ] ); add_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_a11y' ] ); foreach ( $this->collapsibles as $collapsible ) { echo wp_kses_post( $collapsible->content() ); } remove_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_forms' ] ); remove_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_a11y' ] ); echo '</div></div>'; } } /** * Checks whether the tab has any sections. * * @return bool Whether the tab has any sections */ protected function has_sections() { return ! empty( $this->collapsibles ); } } admin/metabox/class-metabox-section-react.php 0000644 00000006036 15174712003 0015311 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates and displays the React root element for a metabox section. */ class WPSEO_Metabox_Section_React implements WPSEO_Metabox_Section { /** * Name of the section, used as an identifier in the HTML. * * @var string */ public $name; /** * Content to use before the React root node. * * @var string */ public $content; /** * Content to use to display the button to open this content block. * * @var string */ private $link_content; /** * Class to add to the link. * * @var string */ private $link_class; /** * Aria label to use for the link. * * @var string */ private $link_aria_label; /** * Additional html content to be displayed within the section. * * @var string */ private $html_after; /** * Constructor. * * @param string $name The name of the section, used as an identifier in the html. * Can only contain URL safe characters. * @param string $link_content The text content of the section link. * @param string $content Optional. Content to use above the React root element. * @param array $options Optional link attributes. */ public function __construct( $name, $link_content, $content = '', array $options = [] ) { $this->name = $name; $this->content = $content; $default_options = [ 'link_class' => '', 'link_aria_label' => '', 'html_after' => '', ]; $options = wp_parse_args( $options, $default_options ); $this->link_content = $link_content; $this->link_class = $options['link_class']; $this->link_aria_label = $options['link_aria_label']; $this->html_after = $options['html_after']; } /** * Outputs the section link. * * @return void */ public function display_link() { printf( '<li role="presentation"><a role="tab" href="#wpseo-meta-section-%1$s" id="wpseo-meta-tab-%1$s" aria-controls="wpseo-meta-section-%1$s" class="wpseo-meta-section-link %2$s"%3$s>%4$s</a></li>', esc_attr( $this->name ), esc_attr( $this->link_class ), ( $this->link_aria_label !== '' ) ? ' aria-label="' . esc_attr( $this->link_aria_label ) . '"' : '', wp_kses_post( $this->link_content ), ); } /** * Outputs the section content. * * @return void */ public function display_content() { add_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_forms' ] ); add_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_a11y' ] ); printf( '<div role="tabpanel" id="wpseo-meta-section-%1$s" aria-labelledby="wpseo-meta-tab-%1$s" tabindex="0" class="wpseo-meta-section">', esc_attr( $this->name ), ); echo wp_kses_post( $this->content ); echo '<div id="wpseo-metabox-root" class="wpseo-metabox-root"></div>'; echo wp_kses_post( $this->html_after ); echo '</div>'; remove_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_forms' ] ); remove_filter( 'wp_kses_allowed_html', [ 'WPSEO_Utils', 'extend_kses_post_with_a11y' ] ); } } admin/metabox/class-metabox-section-inclusive-language.php 0000644 00000002320 15174712003 0017765 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates and displays the React root element for a metabox section. */ class WPSEO_Metabox_Section_Inclusive_Language implements WPSEO_Metabox_Section { /** * Name of the section, used as an identifier in the HTML. * * @var string */ public $name = 'inclusive-language'; /** * Outputs the section link. * * @return void */ public function display_link() { printf( '<li role="presentation"><a role="tab" href="#wpseo-meta-section-%1$s" id="wpseo-meta-tab-%1$s" aria-controls="wpseo-meta-section-%1$s" class="wpseo-meta-section-link"> <div class="wpseo-score-icon-container" id="wpseo-inclusive-language-score-icon"></div><span>%2$s</span></a></li>', esc_attr( $this->name ), esc_html__( 'Inclusive language', 'wordpress-seo' ), ); } /** * Outputs the section content. * * @return void */ public function display_content() { printf( '<div role="tabpanel" id="wpseo-meta-section-%1$s" aria-labelledby="wpseo-meta-tab-%1$s" tabindex="0" class="wpseo-meta-section">', esc_attr( $this->name ), ); echo '<div id="wpseo-metabox-inclusive-language-root" class="wpseo-metabox-root"></div>', '</div>'; } } admin/metabox/class-metabox-section-readability.php 0000644 00000002255 15174712003 0016503 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates and displays the React root element for a metabox section. */ class WPSEO_Metabox_Section_Readability implements WPSEO_Metabox_Section { /** * Name of the section, used as an identifier in the HTML. * * @var string */ public $name = 'readability'; /** * Outputs the section link. * * @return void */ public function display_link() { printf( '<li role="presentation"><a role="tab" href="#wpseo-meta-section-%1$s" id="wpseo-meta-tab-%1$s" aria-controls="wpseo-meta-section-%1$s" class="wpseo-meta-section-link"> <div class="wpseo-score-icon-container" id="wpseo-readability-score-icon"></div><span>%2$s</span></a></li>', esc_attr( $this->name ), esc_html__( 'Readability', 'wordpress-seo' ), ); } /** * Outputs the section content. * * @return void */ public function display_content() { printf( '<div role="tabpanel" id="wpseo-meta-section-%1$s" aria-labelledby="wpseo-meta-tab-%1$s" tabindex="0" class="wpseo-meta-section">', esc_attr( $this->name ), ); echo '<div id="wpseo-metabox-readability-root" class="wpseo-metabox-root"></div>', '</div>'; } } admin/metabox/class-metabox-form-tab.php 0000644 00000005442 15174712003 0014260 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates the HTML for a metabox tab. */ class WPSEO_Metabox_Form_Tab implements WPSEO_Metabox_Tab { /** * The tab identifier. * * @var string */ private $name; /** * The tab content. * * @var string */ private $content; /** * The tab link content. * * @var string */ private $link_content; /** * Additional tab content class. * * @var string */ private $tab_class; /** * Additional tab link class. * * @var string */ private $link_class; /** * Title attribute on the link span. * * @var string */ private $link_title; /** * Arial label attribute on the link span. * * @var string */ private $link_aria_label; /** * Does it contain a single tab. * * @var bool */ private $single; /** * Constructor. * * @param string $name The name of the tab, used as an identifier in the html. * @param string $content The tab content. * @param string $link_content The text content of the tab link. * @param array $options Optional link attributes. */ public function __construct( $name, $content, $link_content, array $options = [] ) { $default_options = [ 'tab_class' => '', 'link_class' => '', 'link_title' => '', 'link_aria_label' => '', 'single' => false, ]; $options = array_merge( $default_options, $options ); $this->name = $name; $this->content = $content; $this->link_content = $link_content; $this->tab_class = $options['tab_class']; $this->link_class = $options['link_class']; $this->link_title = $options['link_title']; $this->link_aria_label = $options['link_aria_label']; $this->single = $options['single']; } /** * Returns the html for the tab link. * * @return string */ public function link() { $html = '<li class="%1$s%2$s"><a class="wpseo_tablink%3$s" href="#wpseo_%1$s"%4$s%5$s>%6$s</a></li>'; if ( $this->single ) { $html = '<li class="%1$s%2$s"><span class="wpseo_tablink%3$s"%4$s%5$s>%6$s</span></li>'; } return sprintf( $html, esc_attr( $this->name ), ( $this->tab_class !== '' ) ? ' ' . esc_attr( $this->tab_class ) : '', ( $this->link_class !== '' ) ? ' ' . esc_attr( $this->link_class ) : '', ( $this->link_title !== '' ) ? ' title="' . esc_attr( $this->link_title ) . '"' : '', ( $this->link_aria_label !== '' ) ? ' aria-label="' . esc_attr( $this->link_aria_label ) . '"' : '', $this->link_content, ); } /** * Returns the html for the tab content. * * @return string */ public function content() { return sprintf( '<div id="%1$s" class="wpseotab %2$s">%3$s</div>', esc_attr( 'wpseo_' . $this->name ), esc_attr( $this->name ), $this->content, ); } } admin/metabox/class-metabox-analysis-seo.php 0000644 00000001621 15174712003 0015153 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Represents the SEO analysis. */ class WPSEO_Metabox_Analysis_SEO implements WPSEO_Metabox_Analysis { /** * Whether this analysis is enabled. * * @return bool Whether or not this analysis is enabled. */ public function is_enabled() { return $this->is_globally_enabled() && $this->is_user_enabled(); } /** * Whether or not this analysis is enabled by the user. * * @return bool Whether or not this analysis is enabled by the user. */ public function is_user_enabled() { return ! get_the_author_meta( 'wpseo_keyword_analysis_disable', get_current_user_id() ); } /** * Whether or not this analysis is enabled globally. * * @return bool Whether or not this analysis is enabled globally. */ public function is_globally_enabled() { return WPSEO_Options::get( 'keyword_analysis_active', true ); } } admin/metabox/class-metabox-editor.php 0000644 00000004415 15174712003 0014036 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Metabox */ /** * Handles all things with the metabox in combination with the WordPress editor. */ class WPSEO_Metabox_Editor { /** * Registers hooks to WordPress. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { // For the Classic editor. add_filter( 'mce_css', [ $this, 'add_css_inside_editor' ] ); // For the Block/Gutenberg editor. // See https://github.com/danielbachhuber/gutenberg-migration-guide/blob/master/filter-mce-css.md. add_action( 'enqueue_block_editor_assets', [ $this, 'add_editor_styles' ] ); add_filter( 'tiny_mce_before_init', [ $this, 'add_custom_element' ] ); } /** * Adds our inside the editor CSS file to the list of CSS files to be loaded inside the editor. * * @param string $css_files The CSS files that WordPress wants to load inside the editor. * @return string The CSS files WordPress wants to load and our CSS file. */ public function add_css_inside_editor( $css_files ) { $asset_manager = new WPSEO_Admin_Asset_Manager(); $styles = $asset_manager->special_styles(); $inside_editor = $styles['inside-editor']; $asset_location = new WPSEO_Admin_Asset_SEO_Location( WPSEO_FILE ); $url = $asset_location->get_url( $inside_editor, WPSEO_Admin_Asset::TYPE_CSS ); if ( $css_files === '' ) { $css_files = $url; } else { $css_files .= ',' . $url; } return $css_files; } /** * Enqueues the CSS to use in the TinyMCE editor. * * @return void */ public function add_editor_styles() { $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_style( 'inside-editor' ); } /** * Adds a custom element to the tinyMCE editor that we need for marking the content. * * @param array $tinymce_config The tinyMCE config as configured by WordPress. * * @return array The new tinyMCE config with our added custom elements. */ public function add_custom_element( $tinymce_config ) { if ( ! empty( $tinymce_config['custom_elements'] ) ) { $custom_elements = $tinymce_config['custom_elements']; $custom_elements .= ',~yoastmark'; } else { $custom_elements = '~yoastmark'; } $tinymce_config['custom_elements'] = $custom_elements; return $tinymce_config; } } admin/metabox/interface-metabox-tab.php 0000644 00000000533 15174712003 0014146 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Generates the HTML for a metabox tab. */ interface WPSEO_Metabox_Tab { /** * Returns the html for the tab link. * * @return string */ public function link(); /** * Returns the html for the tab content. * * @return string */ public function content(); } admin/class-yoast-dashboard-widget.php 0000644 00000007666 15174712003 0014034 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class to change or add WordPress dashboard widgets. */ class Yoast_Dashboard_Widget implements WPSEO_WordPress_Integration { /** * Holds the cache transient key. * * @var string */ public const CACHE_TRANSIENT_KEY = 'wpseo-dashboard-totals'; /** * Holds an instance of the admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Holds the dashboard statistics. * * @var WPSEO_Statistics */ protected $statistics; /** * Yoast_Dashboard_Widget constructor. * * @param WPSEO_Statistics|null $statistics WPSEO_Statistics instance. */ public function __construct( ?WPSEO_Statistics $statistics = null ) { $statistics ??= new WPSEO_Statistics(); $this->statistics = $statistics; $this->asset_manager = new WPSEO_Admin_Asset_Manager(); } /** * Register WordPress hooks. * * @return void */ public function register_hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_dashboard_assets' ] ); add_action( 'admin_init', [ $this, 'queue_dashboard_widget' ] ); } /** * Adds the dashboard widget if it should be shown. * * @return void */ public function queue_dashboard_widget() { if ( $this->show_widget() ) { add_action( 'wp_dashboard_setup', [ $this, 'add_dashboard_widget' ] ); } } /** * Adds dashboard widget to WordPress. * * @return void */ public function add_dashboard_widget() { add_filter( 'postbox_classes_dashboard_wpseo-dashboard-overview', [ $this, 'wpseo_dashboard_overview_class' ] ); wp_add_dashboard_widget( 'wpseo-dashboard-overview', /* translators: %s is the plugin name */ sprintf( __( '%s Posts Overview', 'wordpress-seo' ), 'Yoast SEO' ), [ $this, 'display_dashboard_widget' ], ); } /** * Adds CSS classes to the dashboard widget. * * @param array $classes An array of postbox CSS classes. * * @return array */ public function wpseo_dashboard_overview_class( $classes ) { $classes[] = 'yoast wpseo-dashboard-overview'; return $classes; } /** * Displays the dashboard widget. * * @return void */ public function display_dashboard_widget() { echo '<div id="yoast-seo-dashboard-widget"></div>'; } /** * Enqueues assets for the dashboard if the current page is the dashboard. * * @return void */ public function enqueue_dashboard_assets() { if ( ! $this->is_dashboard_screen() ) { return; } $this->asset_manager->localize_script( 'dashboard-widget', 'wpseoDashboardWidgetL10n', $this->localize_dashboard_script() ); $this->asset_manager->enqueue_script( 'dashboard-widget' ); $this->asset_manager->enqueue_style( 'wp-dashboard' ); $this->asset_manager->enqueue_style( 'monorepo' ); } /** * Translates strings used in the dashboard widget. * * @return array The translated strings. */ public function localize_dashboard_script() { return [ 'feed_header' => sprintf( /* translators: %1$s resolves to Yoast.com */ __( 'Latest blog posts on %1$s', 'wordpress-seo' ), 'Yoast.com', ), 'feed_footer' => __( 'Read more like this on our SEO blog', 'wordpress-seo' ), 'wp_version' => substr( $GLOBALS['wp_version'], 0, 3 ) . '-' . ( is_plugin_active( 'classic-editor/classic-editor.php' ) ? '1' : '0' ), 'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, ]; } /** * Checks if the current screen is the dashboard screen. * * @return bool Whether or not this is the dashboard screen. */ private function is_dashboard_screen() { $current_screen = get_current_screen(); return ( $current_screen instanceof WP_Screen && $current_screen->id === 'dashboard' ); } /** * Returns true when the dashboard widget should be shown. * * @return bool */ private function show_widget() { $analysis_seo = new WPSEO_Metabox_Analysis_SEO(); return $analysis_seo->is_enabled() && current_user_can( 'edit_posts' ); } } admin/class-premium-upsell-admin-block.php 0000644 00000017524 15174712003 0014617 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Promotions\Application\Promotion_Manager; /** * Class WPSEO_Premium_Upsell_Admin_Block */ class WPSEO_Premium_Upsell_Admin_Block { /** * Hook to display the block on. * * @var string */ protected $hook; /** * Identifier to use in the dismissal functionality. * * @var string */ protected $identifier = 'premium_upsell'; /** * Registers which hook the block will be displayed on. * * @param string $hook Hook to display the block on. */ public function __construct( $hook ) { $this->hook = $hook; } /** * Registers WordPress hooks. * * @return void */ public function register_hooks() { add_action( $this->hook, [ $this, 'render' ] ); } /** * Renders the upsell block. * * @return void */ public function render() { $is_woocommerce_active = ( new WooCommerce_Conditional() )->is_met(); $url = ( $is_woocommerce_active ) ? WPSEO_Shortlinker::get( 'https://yoa.st/admin-footer-upsell-woocommerce' ) : WPSEO_Shortlinker::get( 'https://yoa.st/17h' ); [ $header_text, $header_icon ] = $this->get_header( $is_woocommerce_active ); $arguments = $this->get_arguments( $is_woocommerce_active ); $now_including = [ 'Local SEO', 'News SEO', 'Video SEO', __( 'Google Docs add-on (1 seat)', 'wordpress-seo' ) ]; if ( $is_woocommerce_active ) { array_unshift( $now_including, 'Yoast SEO Premium' ); } $header_class = ( $is_woocommerce_active ) ? 'woo-header' : ''; $arguments_html = implode( '', array_map( [ $this, 'get_argument_html' ], $arguments ) ); $badge_class = ( $is_woocommerce_active ) ? 'woo-badge' : ''; $class = $this->get_html_class(); /* translators: %s expands to Yoast SEO Premium */ $button_text = $this->get_button_text( $is_woocommerce_active ); /* translators: Hidden accessibility text. */ $button_text .= '<span class="screen-reader-text">' . esc_html__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>' . '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'; $upgrade_button = sprintf( '<a id="%1$s" class="yoast-button-upsell" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2" href="%2$s" target="_blank">%3$s</a>', esc_attr( 'wpseo-' . $this->identifier . '-popup-button' ), esc_url( $url ), $button_text, ); echo '<div class="' . esc_attr( $class ) . '">'; if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { $bf_label = esc_html__( 'BLACK FRIDAY', 'wordpress-seo' ); $sale_label = esc_html__( '30% OFF', 'wordpress-seo' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped above. echo "<div class='black-friday-container'><span>$sale_label</span> <span style='margin-left: auto;'>$bf_label</span> </div>"; } echo '<div class="' . esc_attr( $class . '--container' ) . '">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in get_header() method. echo '<h2 class="' . esc_attr( $class . '--header' ) . ' ' . esc_attr( $header_class ) . ' ">' . $header_text . $header_icon . '</h2>'; echo '<div class="' . esc_attr( $class . '--subheader' ) . '">'; echo '<span style="margin-right: 8px">' . esc_html__( 'Now includes:', 'wordpress-seo' ) . '</span>'; echo '<div style="display: inline-block;">'; foreach ( $now_including as $value ) { echo '<span class="yoast-badge ' . esc_attr( $class . '--badge' ) . ' ' . esc_attr( $badge_class ) . '">' . esc_html( $value ) . '</span>'; } echo '</div>'; echo '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in $this->get_argument_html() method. echo '<ul class="' . esc_attr( $class . '--motivation' ) . '">' . $arguments_html . '</ul>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in $upgrade_button and $button_text above. echo '<p style="max-width: inherit; margin-top: 24px; margin-bottom: 0;">' . $upgrade_button . '</p>'; echo '</div>'; echo '</div>'; } /** * Formats the argument to a HTML list item. * * @param string $argument The argument to format. * * @return string Formatted argument in HTML. */ protected function get_argument_html( $argument ) { $assets_uri = trailingslashit( plugin_dir_url( WPSEO_FILE ) ); $class = $this->get_html_class(); return sprintf( '<li style="line-height: 19.5px"><img src="%1$s" alt="" width="19.5" height="19.5"><div class="%2$s">%3$s</div></li>', esc_url( $assets_uri . 'packages/js/images/icon-check-circle-green.svg' ), esc_attr( $class . '--argument' ), $argument, ); } /** * Returns the HTML base class to use. * * @return string The HTML base class. */ protected function get_html_class() { return 'yoast_' . $this->identifier; } /** * Returns the arguments based on whether WooCommerce is active. * * @param bool $is_woocommerce_active Whether WooCommerce is active. * * @return array<string> The arguments list. */ private function get_arguments( bool $is_woocommerce_active ) { $arguments = [ esc_html__( 'Generate SEO optimized metadata in seconds with AI', 'wordpress-seo' ), esc_html__( 'Make your articles visible, be seen in Google News', 'wordpress-seo' ), esc_html__( 'Built to get found by search, AI, and real users', 'wordpress-seo' ), esc_html__( 'Easy Local SEO. Show up in Google Maps results', 'wordpress-seo' ), esc_html__( 'Internal links and redirect management, easy', 'wordpress-seo' ), esc_html__( 'Access to friendly help when you need it, day or night', 'wordpress-seo' ), ]; if ( $is_woocommerce_active ) { $arguments[1] = esc_html__( 'Boost visibility for your products, from 10 or 10,000+', 'wordpress-seo' ); } return $arguments; } /** * Returns the header text and icon based on whether WooCommerce is active. * * @param bool $is_woocommerce_active Whether WooCommerce is active. * * @return array<string, string> The header text and icon. */ private function get_header( bool $is_woocommerce_active ) { $assets_uri = trailingslashit( plugin_dir_url( WPSEO_FILE ) ); if ( $is_woocommerce_active ) { $header_text = sprintf( /* translators: %s expands to Yoast WooCommerce SEO */ esc_html__( 'Upgrade to %s', 'wordpress-seo' ), 'Yoast WooCommerce SEO', ); $header_icon = sprintf( '<img src="%s" alt="" width="14" height="14" style="margin-inline-start: 8px;">', esc_url( $assets_uri . 'packages/js/images/icon-trolley.svg' ), ); } else { $header_text = sprintf( /* translators: %s expands to Yoast SEO Premium*/ esc_html__( 'Upgrade to %s', 'wordpress-seo' ), 'Yoast SEO Premium', ); $header_icon = sprintf( '<img src="%s" alt="" width="14" height="14" style="margin-inline-start: 8px;">', esc_url( $assets_uri . 'packages/js/images/icon-crown.svg' ), ); } return [ $header_text, $header_icon ]; } /** * Returns the button text based on whether WooCommerce is active. * * @param bool $is_woocommerce_active Whether WooCommerce is active. * * @return string The button text. */ private function get_button_text( bool $is_woocommerce_active ): string { if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) { return esc_html__( 'Get 30% off now!', 'wordpress-seo' ); } else { // phpcs:disable Squiz.ControlStructures.InlineIfDeclaration.NotSingleLine -- needed to add translators comments. return $is_woocommerce_active /* translators: %s expands to Yoast WooCommerce SEO */ ? sprintf( esc_html__( 'Explore %s now!', 'wordpress-seo' ), 'Yoast WooCommerce SEO' ) /* translators: %s expands to Yoast SEO Premium */ : sprintf( esc_html__( 'Explore %s now!', 'wordpress-seo' ), 'Yoast SEO Premium' ); } } } admin/taxonomy/class-taxonomy-fields-presenter.php 0000644 00000014074 15174712003 0016463 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class WPSEO_Taxonomy_Presenter. */ class WPSEO_Taxonomy_Fields_Presenter { /** * The taxonomy meta data for the current term. * * @var array */ private $tax_meta; /** * Constructs the WPSEO_Taxonomy_Fields_Presenter class. * * @param stdClass $term The current term. */ public function __construct( $term ) { $this->tax_meta = WPSEO_Taxonomy_Meta::get_term_meta( (int) $term->term_id, $term->taxonomy ); } /** * Displaying the form fields. * * @param array $fields Array with the fields that will be displayed. * * @return string */ public function html( array $fields ) { $content = ''; foreach ( $fields as $field_name => $field_configuration ) { $content .= $this->form_row( 'wpseo_' . $field_name, $field_configuration ); } return $content; } /** * Create a row in the form table. * * @param string $field_name Variable the row controls. * @param array $field_configuration Array with the field configuration. * * @return string */ private function form_row( $field_name, array $field_configuration ) { $esc_field_name = esc_attr( $field_name ); $options = (array) $field_configuration['options']; if ( ! empty( $field_configuration['description'] ) ) { $options['description'] = $field_configuration['description']; } $label = $this->get_label( $field_configuration['label'], $esc_field_name ); $field = $this->get_field( $field_configuration['type'], $esc_field_name, $this->get_field_value( $field_name ), $options ); $help_content = ( $field_configuration['options']['help'] ?? '' ); $help_button_text = ( $field_configuration['options']['help-button'] ?? '' ); $help = new WPSEO_Admin_Help_Panel( $field_name, $help_button_text, $help_content ); return $this->parse_row( $label, $help, $field ); } /** * Generates the html for the given field config. * * @param string $field_type The fieldtype, e.g: text, checkbox, etc. * @param string $field_name The name of the field. * @param string $field_value The value of the field. * @param array $options Array with additional options. * * @return string */ private function get_field( $field_type, $field_name, $field_value, array $options ) { $class = $this->get_class( $options ); $field = ''; $description = ''; $aria_describedby = ''; if ( ! empty( $options['description'] ) ) { $aria_describedby = ' aria-describedby="' . $field_name . '-desc"'; $description = '<p id="' . $field_name . '-desc" class="yoast-metabox__description">' . $options['description'] . '</p>'; } switch ( $field_type ) { case 'div': $field .= '<div id="' . $field_name . '"></div>'; break; case 'url': $field .= '<input name="' . $field_name . '" id="' . $field_name . '" ' . $class . ' type="url" value="' . esc_attr( urldecode( $field_value ) ) . '" size="40"' . $aria_describedby . '/>'; break; case 'text': $field .= '<input name="' . $field_name . '" id="' . $field_name . '" ' . $class . ' type="text" value="' . esc_attr( $field_value ) . '" size="40"' . $aria_describedby . '/>'; break; case 'checkbox': $field .= '<input name="' . $field_name . '" id="' . $field_name . '" type="checkbox" ' . checked( $field_value ) . $aria_describedby . '/>'; break; case 'textarea': $rows = 3; if ( ! empty( $options['rows'] ) ) { $rows = $options['rows']; } $field .= '<textarea class="large-text" rows="' . esc_attr( $rows ) . '" id="' . $field_name . '" name="' . $field_name . '"' . $aria_describedby . '>' . esc_textarea( $field_value ) . '</textarea>'; break; case 'select': if ( is_array( $options ) && $options !== [] ) { $field .= '<select name="' . $field_name . '" id="' . $field_name . '"' . $aria_describedby . '>'; $select_options = ( array_key_exists( 'options', $options ) ) ? $options['options'] : $options; foreach ( $select_options as $option => $option_label ) { $selected = selected( $option, $field_value, false ); $field .= '<option ' . $selected . ' value="' . esc_attr( $option ) . '">' . esc_html( $option_label ) . '</option>'; } unset( $option, $option_label, $selected ); $field .= '</select>'; } break; case 'hidden': $field .= '<input name="' . $field_name . '" id="hidden_' . $field_name . '" type="hidden" value="' . esc_attr( $field_value ) . '" />'; break; } return $field . $description; } /** * Getting the value for given field_name. * * @param string $field_name The fieldname to get the value for. * * @return string */ private function get_field_value( $field_name ) { if ( isset( $this->tax_meta[ $field_name ] ) && $this->tax_meta[ $field_name ] !== '' ) { return $this->tax_meta[ $field_name ]; } return ''; } /** * Getting the class attributes if $options contains a class key. * * @param array $options The array with field options. * * @return string */ private function get_class( array $options ) { if ( ! empty( $options['class'] ) ) { return ' class="' . esc_attr( $options['class'] ) . '"'; } return ''; } /** * Getting the label HTML. * * @param string $label The label value. * @param string $field_name The target field. * * @return string */ private function get_label( $label, $field_name ) { if ( $label !== '' ) { return '<label for="' . $field_name . '">' . esc_html( $label ) . '</label>'; } return ''; } /** * Returns the HTML for the row which contains label, help and the field. * * @param string $label The html for the label if there was a label set. * @param WPSEO_Admin_Help_Panel $help The help panel to render in this row. * @param string $field The html for the field. * * @return string */ private function parse_row( $label, WPSEO_Admin_Help_Panel $help, $field ) { if ( $label !== '' || $help !== '' ) { return $label . $help->get_button_html() . $help->get_panel_html() . $field; } return $field; } } admin/taxonomy/class-taxonomy.php 0000644 00000034557 15174712003 0013222 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository; use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter; /** * Class that handles the edit boxes on taxonomy edit pages. */ class WPSEO_Taxonomy { /** * The current active taxonomy. * * @var string */ private $taxonomy = ''; /** * Holds the metabox SEO analysis instance. * * @var WPSEO_Metabox_Analysis_SEO */ private $analysis_seo; /** * Holds the metabox readability analysis instance. * * @var WPSEO_Metabox_Analysis_Readability */ private $analysis_readability; /** * Holds the metabox inclusive language analysis instance. * * @var WPSEO_Metabox_Analysis_Inclusive_Language */ private $analysis_inclusive_language; /** * Class constructor. */ public function __construct() { $this->taxonomy = $this::get_taxonomy(); add_action( 'edit_term', [ $this, 'update_term' ], 99, 3 ); add_action( 'init', [ $this, 'custom_category_descriptions_allow_html' ] ); add_action( 'admin_init', [ $this, 'admin_init' ] ); if ( self::is_term_overview( $GLOBALS['pagenow'] ) ) { new WPSEO_Taxonomy_Columns(); } $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO(); $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability(); $this->analysis_inclusive_language = new WPSEO_Metabox_Analysis_Inclusive_Language(); } /** * Add hooks late enough for taxonomy object to be available for checks. * * @return void */ public function admin_init() { $taxonomy = get_taxonomy( $this->taxonomy ); if ( empty( $taxonomy ) || empty( $taxonomy->public ) || ! $this->show_metabox() ) { return; } // Adds custom category description editor. Needs a hook that runs before the description field. add_action( "{$this->taxonomy}_term_edit_form_top", [ $this, 'custom_category_description_editor' ] ); add_action( sanitize_text_field( $this->taxonomy ) . '_edit_form', [ $this, 'term_metabox' ], 90, 1 ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); } /** * Show the SEO inputs for term. * * @param stdClass|WP_Term $term Term to show the edit boxes for. * * @return void */ public function term_metabox( $term ) { if ( WPSEO_Metabox::is_internet_explorer() ) { $this->show_internet_explorer_notice(); return; } $metabox = new WPSEO_Taxonomy_Metabox( $this->taxonomy, $term ); $metabox->display(); } /** * Renders the content for the internet explorer metabox. * * @return void */ private function show_internet_explorer_notice() { $product_title = YoastSEO()->helpers->product->get_product_name(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $product_title is hardcoded. printf( '<div id="wpseo_meta" class="postbox yoast wpseo-taxonomy-metabox-postbox"><h2><span>%1$s</span></h2>', $product_title ); echo '<div class="inside">'; $content = sprintf( /* translators: 1: Link start tag to the Firefox website, 2: Link start tag to the Chrome website, 3: Link start tag to the Edge website, 4: Link closing tag. */ esc_html__( 'The browser you are currently using is unfortunately rather dated. Since we strive to give you the best experience possible, we no longer support this browser. Instead, please use %1$sFirefox%4$s, %2$sChrome%4$s or %3$sMicrosoft Edge%4$s.', 'wordpress-seo' ), '<a href="https://www.mozilla.org/firefox/new/">', '<a href="https://www.google.com/chrome/">', '<a href="https://www.microsoft.com/windows/microsoft-edge">', '</a>', ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above. echo new Alert_Presenter( $content ); echo '</div></div>'; } /** * Queue assets for taxonomy screens. * * @since 1.5.0 * * @return void */ public function admin_enqueue_scripts() { $pagenow = $GLOBALS['pagenow']; if ( ! ( self::is_term_edit( $pagenow ) || self::is_term_overview( $pagenow ) ) ) { return; } $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_style( 'monorepo' ); $tag_id = $this::get_tag_id(); if ( self::is_term_edit( $pagenow ) && $tag_id !== null ) { wp_enqueue_media(); // Enqueue files needed for upload functionality. $asset_manager->enqueue_style( 'metabox-css' ); if ( $this->analysis_readability->is_enabled() ) { $asset_manager->enqueue_style( 'scoring' ); } $asset_manager->enqueue_style( 'ai-generator' ); $asset_manager->enqueue_script( 'term-edit' ); /** * Remove the emoji script as it is incompatible with both React and any * contenteditable fields. */ remove_action( 'admin_print_scripts', 'print_emoji_detection_script' ); $asset_manager->localize_script( 'term-edit', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() ); $script_data = [ 'analysis' => [ 'plugins' => [ 'replaceVars' => [ 'replace_vars' => $this->get_replace_vars(), 'recommended_replace_vars' => $this->get_recommended_replace_vars(), 'scope' => $this->determine_scope(), ], 'shortcodes' => [ 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), 'wpseo_filter_shortcodes_nonce' => wp_create_nonce( 'wpseo-filter-shortcodes' ), ], ], 'worker' => [ 'url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ), 'dependencies' => YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ), 'keywords_assessment_url' => YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ), 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(), ], ], 'metabox' => $this->localize_term_scraper_script( $tag_id ), 'isTerm' => true, 'postId' => $tag_id, 'postType' => $this->get_taxonomy(), 'usedKeywordsNonce' => wp_create_nonce( 'wpseo-keyword-usage' ), ]; /** * The website information repository. * * @var Website_Information_Repository $repo */ $repo = YoastSEO()->classes->get( Website_Information_Repository::class ); $term_information = $repo->get_term_site_information(); $term_information->set_term( get_term_by( 'id', $tag_id, $this::get_taxonomy() ) ); $script_data = array_merge_recursive( $term_information->get_legacy_site_information(), $script_data ); $asset_manager->localize_script( 'term-edit', 'wpseoScriptData', $script_data ); } if ( self::is_term_overview( $pagenow ) ) { $asset_manager->enqueue_script( 'edit-page' ); $asset_manager->enqueue_style( 'edit-page' ); } } /** * Update the taxonomy meta data on save. * * @param int $term_id ID of the term to save data for. * @param int $tt_id The taxonomy_term_id for the term. * @param string $taxonomy The taxonomy the term belongs to. * * @return void */ public function update_term( $term_id, $tt_id, $taxonomy ) { // Bail if this is a multisite installation and the site has been switched. if ( is_multisite() && ms_is_switched() ) { return; } /* Create post array with only our values. */ $new_meta_data = []; foreach ( WPSEO_Taxonomy_Meta::$defaults_per_term as $key => $default ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce is already checked by WordPress before executing this action. if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: $data is getting sanitized later. $data = wp_unslash( $_POST[ $key ] ); $new_meta_data[ $key ] = ( $key !== 'wpseo_canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data ); } // If analysis is disabled remove that analysis score value from the DB. if ( $this->is_meta_value_disabled( $key ) ) { $new_meta_data[ $key ] = ''; } } // Saving the values. WPSEO_Taxonomy_Meta::set_values( $term_id, $taxonomy, $new_meta_data ); } /** * Determines if the given meta value key is disabled. * * @param string $key The key of the meta value. * @return bool Whether the given meta value key is disabled. */ public function is_meta_value_disabled( $key ) { if ( $key === 'wpseo_linkdex' && ! $this->analysis_seo->is_enabled() ) { return true; } if ( $key === 'wpseo_content_score' && ! $this->analysis_readability->is_enabled() ) { return true; } if ( $key === 'wpseo_inclusive_language_score' && ! $this->analysis_inclusive_language->is_enabled() ) { return true; } return false; } /** * Allows post-kses-filtered HTML in term descriptions. * * @return void */ public function custom_category_descriptions_allow_html() { remove_filter( 'term_description', 'wp_kses_data' ); remove_filter( 'pre_term_description', 'wp_filter_kses' ); add_filter( 'term_description', 'wp_kses_post' ); add_filter( 'pre_term_description', 'wp_filter_post_kses' ); } /** * Output the WordPress editor. * * @return void */ public function custom_category_description_editor() { wp_editor( '', 'description' ); } /** * Pass variables to js for use with the term-scraper. * * @param int $term_id The ID of the term to localize the script for. * * @return array */ public function localize_term_scraper_script( $term_id ) { $term = get_term_by( 'id', $term_id, $this::get_taxonomy() ); $taxonomy = get_taxonomy( $term->taxonomy ); $term_formatter = new WPSEO_Metabox_Formatter( new WPSEO_Term_Metabox_Formatter( $taxonomy, $term ), ); return $term_formatter->get_values(); } /** * Pass some variables to js for replacing variables. * * @return array */ public function localize_replace_vars_script() { return [ 'replace_vars' => $this->get_replace_vars(), 'recommended_replace_vars' => $this->get_recommended_replace_vars(), 'scope' => $this->determine_scope(), ]; } /** * Determines the scope based on the current taxonomy. * This can be used by the replacevar plugin to determine if a replacement needs to be executed. * * @return string String decribing the current scope. */ private function determine_scope() { $taxonomy = $this::get_taxonomy(); if ( $taxonomy === 'category' ) { return 'category'; } if ( $taxonomy === 'post_tag' ) { return 'tag'; } return 'term'; } /** * Determines if a given page is the term overview page. * * @param string $page The string to check for the term overview page. * * @return bool */ public static function is_term_overview( $page ) { return $page === 'edit-tags.php'; } /** * Determines if a given page is the term edit page. * * @param string $page The string to check for the term edit page. * * @return bool */ public static function is_term_edit( $page ) { return $page === 'term.php'; } /** * Function to get the labels for the current taxonomy. * * @return object|null Labels for the current taxonomy or null if the taxonomy is not set. */ public static function get_labels() { $term = self::get_taxonomy(); if ( $term !== '' ) { $taxonomy = get_taxonomy( $term ); return $taxonomy->labels; } return null; } /** * Retrieves a template. * Check if metabox for current taxonomy should be displayed. * * @return bool */ private function show_metabox() { $option_key = 'display-metabox-tax-' . $this->taxonomy; return WPSEO_Options::get( $option_key ); } /** * Getting the taxonomy from the URL. * * @return string */ private static function get_taxonomy() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['taxonomy'] ) && is_string( $_GET['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ); } return ''; } /** * Get the current tag ID from the GET parameters. * * @return int|null the tag ID if it exists, null otherwise. */ private static function get_tag_id() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['tag_ID'] ) && is_string( $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer. $tag_id = (int) wp_unslash( $_GET['tag_ID'] ); if ( $tag_id > 0 ) { return $tag_id; } } return null; } /** * Prepares the replace vars for localization. * * @return array<string, string> The replacement variables. */ private function get_replace_vars() { $term_id = $this::get_tag_id(); $term = get_term_by( 'id', $term_id, $this::get_taxonomy() ); $cached_replacement_vars = []; $vars_to_cache = [ 'date', 'id', 'sitename', 'sitedesc', 'sep', 'page', 'term_title', 'term_description', 'term_hierarchy', 'category_description', 'tag_description', 'searchphrase', 'currentyear', ]; foreach ( $vars_to_cache as $var ) { $cached_replacement_vars[ $var ] = wpseo_replace_vars( '%%' . $var . '%%', $term ); } return $cached_replacement_vars; } /** * Prepares the recommended replace vars for localization. * * @return array<string> The recommended replacement variables. */ private function get_recommended_replace_vars() { $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); $taxonomy = $this::get_taxonomy(); if ( $taxonomy === '' ) { return []; } // What is recommended depends on the current context. $page_type = $recommended_replace_vars->determine_for_term( $taxonomy ); return $recommended_replace_vars->get_recommended_replacevars_for( $page_type ); } /** * Returns an array with shortcode tags for all registered shortcodes. * * @return array<string> Array with shortcode tags. */ private function get_valid_shortcode_tags() { $shortcode_tags = []; foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) { $shortcode_tags[] = $tag; } return $shortcode_tags; } } admin/taxonomy/class-taxonomy-columns.php 0000644 00000016007 15174712003 0014666 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ use Yoast\WP\SEO\Helpers\Score_Icon_Helper; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * This class adds columns to the taxonomy table. */ class WPSEO_Taxonomy_Columns { /** * The SEO analysis. * * @var WPSEO_Metabox_Analysis_SEO */ private $analysis_seo; /** * The readability analysis. * * @var WPSEO_Metabox_Analysis_Readability */ private $analysis_readability; /** * The current taxonomy. * * @var string */ private $taxonomy; /** * Holds the Indexable_Repository. * * @var Indexable_Repository */ protected $indexable_repository; /** * Holds the Score_Icon_Helper. * * @var Score_Icon_Helper */ protected $score_icon_helper; /** * WPSEO_Taxonomy_Columns constructor. */ public function __construct() { $this->taxonomy = $this->get_taxonomy(); if ( ! empty( $this->taxonomy ) ) { add_filter( 'manage_edit-' . $this->taxonomy . '_columns', [ $this, 'add_columns' ] ); add_filter( 'manage_' . $this->taxonomy . '_custom_column', [ $this, 'parse_column' ], 10, 3 ); } $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO(); $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability(); $this->indexable_repository = YoastSEO()->classes->get( Indexable_Repository::class ); $this->score_icon_helper = YoastSEO()->helpers->score_icon; } /** * Adds an SEO score column to the terms table, right after the description column. * * @param array $columns Current set columns. * * @return array */ public function add_columns( array $columns ) { if ( $this->display_metabox( $this->taxonomy ) === false ) { return $columns; } $new_columns = []; foreach ( $columns as $column_name => $column_value ) { $new_columns[ $column_name ] = $column_value; if ( $column_name === 'description' && $this->analysis_seo->is_enabled() ) { $new_columns['wpseo-score'] = '<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="' . esc_attr__( 'SEO score', 'wordpress-seo' ) . '"><span class="yoast-column-seo-score yoast-column-header-has-tooltip"><span class="screen-reader-text">' . __( 'SEO score', 'wordpress-seo' ) . '</span></span></span>'; } if ( $column_name === 'description' && $this->analysis_readability->is_enabled() ) { $new_columns['wpseo-score-readability'] = '<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="' . esc_attr__( 'Readability score', 'wordpress-seo' ) . '"><span class="yoast-column-readability yoast-column-header-has-tooltip"><span class="screen-reader-text">' . __( 'Readability score', 'wordpress-seo' ) . '</span></span></span>'; } } return $new_columns; } /** * Parses the column. * * @param string $content The current content of the column. * @param string $column_name The name of the column. * @param int $term_id ID of requested taxonomy. * * @return string */ public function parse_column( $content, $column_name, $term_id ) { switch ( $column_name ) { case 'wpseo-score': return $this->get_score_value( $term_id ); case 'wpseo-score-readability': return $this->get_score_readability_value( $term_id ); } return $content; } /** * Retrieves the taxonomy from the $_GET or $_POST variable. * * @return string|null The current taxonomy or null when it is not set. */ public function get_current_taxonomy() { // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( ! empty( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'POST' ) { if ( isset( $_POST['taxonomy'] ) && is_string( $_POST['taxonomy'] ) ) { return sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ); } } elseif ( isset( $_GET['taxonomy'] ) && is_string( $_GET['taxonomy'] ) ) { return sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended return null; } /** * Returns the posted/get taxonomy value if it is set. * * @return string|null */ private function get_taxonomy() { // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( wp_doing_ajax() ) { if ( isset( $_POST['taxonomy'] ) && is_string( $_POST['taxonomy'] ) ) { return sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ); } } elseif ( isset( $_GET['taxonomy'] ) && is_string( $_GET['taxonomy'] ) ) { return sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended return null; } /** * Parses the value for the score column. * * @param int $term_id ID of requested term. * * @return string */ private function get_score_value( $term_id ) { $indexable = $this->indexable_repository->find_by_id_and_type( (int) $term_id, 'term' ); return $this->score_icon_helper->for_seo( $indexable, '', __( 'Term is set to noindex.', 'wordpress-seo' ) ); } /** * Parses the value for the readability score column. * * @param int $term_id ID of the requested term. * * @return string The HTML for the readability score indicator. */ private function get_score_readability_value( $term_id ) { $score = (int) WPSEO_Taxonomy_Meta::get_term_meta( $term_id, $this->taxonomy, 'content_score' ); return $this->score_icon_helper->for_readability( $score ); } /** * Check if the taxonomy is indexable. * * @param mixed $term The current term. * * @return bool Whether the term is indexable. */ private function is_indexable( $term ) { // When the no_index value is not empty and not default, check if its value is index. $no_index = WPSEO_Taxonomy_Meta::get_term_meta( $term->term_id, $this->taxonomy, 'noindex' ); // Check if the default for taxonomy is empty (this will be index). if ( ! empty( $no_index ) && $no_index !== 'default' ) { return ( $no_index === 'index' ); } if ( is_object( $term ) ) { $no_index_key = 'noindex-tax-' . $term->taxonomy; // If the option is false, this means we want to index it. return WPSEO_Options::get( $no_index_key, false ) === false; } return true; } /** * Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by * choice of the admin or because the taxonomy is not public. * * @since 7.0 * * @param string|null $taxonomy Optional. The taxonomy to test, defaults to the current taxonomy. * * @return bool Whether the meta box (and associated columns etc) should be hidden. */ private function display_metabox( $taxonomy = null ) { $current_taxonomy = $this->get_current_taxonomy(); if ( ! isset( $taxonomy ) && ! empty( $current_taxonomy ) ) { $taxonomy = $current_taxonomy; } return WPSEO_Utils::is_metabox_active( $taxonomy, 'taxonomy' ); } } admin/taxonomy/class-taxonomy-metabox.php 0000644 00000014006 15174712003 0014642 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * This class generates the metabox on the edit term page. */ class WPSEO_Taxonomy_Metabox { /** * The term currently being edited. * * @var WP_Term */ private $term; /** * The term's taxonomy. * * @var string */ private $taxonomy; /** * Whether or not the social tab is enabled for this metabox. * * @var bool */ private $is_social_enabled; /** * Helper to determine whether or not the SEO analysis is enabled. * * @var WPSEO_Metabox_Analysis_SEO */ protected $seo_analysis; /** * Helper to determine whether or not the readability analysis is enabled. * * @var WPSEO_Metabox_Analysis_Readability */ protected $readability_analysis; /** * Helper to determine whether or not the inclusive language analysis is enabled. * * @var WPSEO_Metabox_Analysis_Inclusive_Language */ protected $inclusive_language_analysis; /** * The constructor. * * @param string $taxonomy The taxonomy. * @param stdClass $term The term. */ public function __construct( $taxonomy, $term ) { $this->term = $term; $this->taxonomy = $taxonomy; $this->is_social_enabled = WPSEO_Options::get( 'opengraph', false ) || WPSEO_Options::get( 'twitter', false ); $this->seo_analysis = new WPSEO_Metabox_Analysis_SEO(); $this->readability_analysis = new WPSEO_Metabox_Analysis_Readability(); $this->inclusive_language_analysis = new WPSEO_Metabox_Analysis_Inclusive_Language(); } /** * Shows the Yoast SEO metabox for the term. * * @return void */ public function display() { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $this->get_product_title() returns a hard-coded string. printf( '<div id="wpseo_meta" class="postbox yoast wpseo-taxonomy-metabox-postbox"><h2><span>%1$s</span></h2>', $this->get_product_title() ); echo '<div class="inside">'; $this->render_hidden_fields(); $this->render_tabs(); echo '</div>'; echo '</div>'; } /** * Renders the metabox hidden fields. * * @return void */ protected function render_hidden_fields() { $fields_presenter = new WPSEO_Taxonomy_Fields_Presenter( $this->term ); $field_definitions = new WPSEO_Taxonomy_Fields(); echo $fields_presenter->html( $field_definitions->get( 'content' ) ); if ( WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) || WPSEO_Options::get( 'disableadvanced_meta' ) === false ) { echo $fields_presenter->html( $field_definitions->get( 'settings' ) ); } if ( $this->is_social_enabled ) { echo $fields_presenter->html( $field_definitions->get( 'social' ) ); } } /** * Renders the metabox tabs. * * @return void */ protected function render_tabs() { echo '<div class="wpseo-metabox-content">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: $this->get_product_title() returns a hard-coded string. printf( '<div class="wpseo-metabox-menu"><ul role="tablist" class="yoast-aria-tabs" aria-label="%s">', $this->get_product_title() ); $tabs = $this->get_tabs(); foreach ( $tabs as $tab ) { $tab->display_link(); } echo '</ul></div>'; foreach ( $tabs as $tab ) { $tab->display_content(); } echo '</div>'; } /** * Returns the relevant metabox sections for the current view. * * @return WPSEO_Metabox_Section[] */ private function get_tabs() { $tabs = []; $label = __( 'SEO', 'wordpress-seo' ); if ( $this->seo_analysis->is_enabled() ) { $label = '<span class="wpseo-score-icon-container" id="wpseo-seo-score-icon"></span>' . $label; } $tabs[] = new WPSEO_Metabox_Section_React( 'content', $label ); if ( $this->readability_analysis->is_enabled() ) { $tabs[] = new WPSEO_Metabox_Section_Readability(); } if ( $this->inclusive_language_analysis->is_enabled() ) { $tabs[] = new WPSEO_Metabox_Section_Inclusive_Language(); } if ( $this->is_social_enabled ) { $tabs[] = new WPSEO_Metabox_Section_React( 'social', '<span class="dashicons dashicons-share"></span>' . __( 'Social', 'wordpress-seo' ), '', [ 'html_after' => '<div id="wpseo-section-social"></div>', ], ); } $tabs = array_merge( $tabs, $this->get_additional_tabs() ); return $tabs; } /** * Returns the metabox tabs that have been added by other plugins. * * @return WPSEO_Metabox_Section_Additional[] */ protected function get_additional_tabs() { $tabs = []; /** * Private filter: 'yoast_free_additional_taxonomy_metabox_sections'. * * Meant for internal use only. Allows adding additional tabs to the Yoast SEO metabox for taxonomies. * * @param array[] $tabs { * An array of arrays with tab specifications. * * @type array $tab { * A tab specification. * * @type string $name The name of the tab. Used in the HTML IDs, href and aria properties. * @type string $link_content The content of the tab link. * @type string $content The content of the tab. * @type array $options { * Optional. Extra options. * * @type string $link_class Optional. The class for the tab link. * @type string $link_aria_label Optional. The aria label of the tab link. * } * } * } */ $requested_tabs = apply_filters( 'yoast_free_additional_taxonomy_metabox_sections', [] ); foreach ( $requested_tabs as $tab ) { if ( is_array( $tab ) && array_key_exists( 'name', $tab ) && array_key_exists( 'link_content', $tab ) && array_key_exists( 'content', $tab ) ) { $options = array_key_exists( 'options', $tab ) ? $tab['options'] : []; $tabs[] = new WPSEO_Metabox_Section_Additional( $tab['name'], $tab['link_content'], $tab['content'], $options, ); } } return $tabs; } /** * Retrieves the product title. * * @return string The product title. */ protected function get_product_title() { return YoastSEO()->helpers->product->get_product_name(); } } admin/taxonomy/class-taxonomy-fields.php 0000644 00000012040 15174712003 0014445 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Class WPSEO_Taxonomy_Tab. * * Contains the basics for each class extending this one. */ class WPSEO_Taxonomy_Fields { /** * Returns the taxonomy fields. * * @param string $field_group The field group. * * @return array */ public function get( $field_group ) { $fields = []; switch ( $field_group ) { case 'content': $fields = $this->get_content_fields(); break; case 'settings': $fields = $this->get_settings_fields(); break; case 'social': $fields = $this->get_social_fields(); break; } return $this->filter_hidden_fields( $fields ); } /** * Returns array with the fields for the general tab. * * @return array */ protected function get_content_fields() { $fields = [ 'title' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'desc' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'linkdex' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'content_score' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'inclusive_language_score' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'focuskw' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'is_cornerstone' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], ]; /** * Filter: 'wpseo_taxonomy_content_fields' - Adds the possibility to register additional content fields. * * @param array $additional_fields The additional fields. */ $additional_fields = apply_filters( 'wpseo_taxonomy_content_fields', [] ); return array_merge( $fields, $additional_fields ); } /** * Returns array with the fields for the settings tab. * * @return array */ protected function get_settings_fields() { return [ 'noindex' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'bctitle' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => ( WPSEO_Options::get( 'breadcrumbs-enable' ) !== true ), ], 'canonical' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], ]; } /** * Returning the fields for the social media tab. * * @return array */ protected function get_social_fields() { $fields = []; if ( WPSEO_Options::get( 'opengraph', false ) === true ) { $fields = [ 'opengraph-title' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'opengraph-description' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'opengraph-image' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'opengraph-image-id' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], ]; } if ( WPSEO_Options::get( 'twitter', false ) === true ) { $fields = array_merge( $fields, [ 'twitter-title' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'twitter-description' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'twitter-image' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], 'twitter-image-id' => [ 'label' => '', 'description' => '', 'type' => 'hidden', 'options' => '', 'hide' => false, ], ], ); } return $fields; } /** * Filter the hidden fields. * * @param array $fields Array with the form fields that has will be filtered. * * @return array */ protected function filter_hidden_fields( array $fields ) { foreach ( $fields as $field_name => $field_options ) { if ( ! empty( $field_options['hide'] ) ) { unset( $fields[ $field_name ] ); } } return $fields; } } admin/interface-collection.php 0000644 00000000401 15174712003 0012431 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * Interface that represents a collection. */ interface WPSEO_Collection { /** * Returns the collection data. * * @return array The collection data. */ public function get(); } admin/admin-settings-changed-listener.php 0000644 00000004615 15174712003 0014513 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin */ /** * A WordPress integration that listens for whether the SEO changes have been saved successfully. */ class WPSEO_Admin_Settings_Changed_Listener implements WPSEO_WordPress_Integration { /** * Have the Yoast SEO settings been saved. * * @var bool */ private static $settings_saved = false; /** * Registers all hooks to WordPress. * * @return void */ public function register_hooks() { add_action( 'admin_init', [ $this, 'intercept_save_update_notification' ] ); } /** * Checks and overwrites the wp_settings_errors global to determine whether the Yoast SEO settings have been saved. * * @return void */ public function intercept_save_update_notification() { global $pagenow; if ( $pagenow !== 'admin.php' || ! YoastSEO()->helpers->current_page->is_yoast_seo_page() ) { return; } // Variable name is the same as the global that is set by get_settings_errors. $wp_settings_errors = get_settings_errors(); foreach ( $wp_settings_errors as $key => $wp_settings_error ) { if ( ! $this->is_settings_updated_notification( $wp_settings_error ) ) { continue; } self::$settings_saved = true; unset( $wp_settings_errors[ $key ] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- Overwrite the global with the list excluding the Changed saved message. $GLOBALS['wp_settings_errors'] = $wp_settings_errors; break; } } /** * Checks whether the settings notification is a settings_updated notification. * * @param array $wp_settings_error The settings object. * * @return bool Whether this is a settings updated settings notification. */ public function is_settings_updated_notification( $wp_settings_error ) { return ! empty( $wp_settings_error['code'] ) && $wp_settings_error['code'] === 'settings_updated'; } /** * Get whether the settings have successfully been saved * * @return bool Whether the settings have successfully been saved. */ public function have_settings_been_saved() { return self::$settings_saved; } /** * Renders a success message if the Yoast SEO settings have been saved. * * @return void */ public function show_success_message() { if ( $this->have_settings_been_saved() ) { echo '<p class="wpseo-message"><span class="dashicons dashicons-yes"></span>', esc_html__( 'Settings saved.', 'wordpress-seo' ), '</p>'; } } } admin/class-bulk-description-editor-list-table.php 0000644 00000004065 15174712003 0016255 0 ustar 00 <?php /** * WPSEO plugin file. * * @package WPSEO\Admin\Bulk Editor * @since 1.5.0 */ /** * Implements table for bulk description editing. */ class WPSEO_Bulk_Description_List_Table extends WPSEO_Bulk_List_Table { /** * Current type for this class will be (meta) description. * * @var string */ protected $page_type = 'description'; /** * Settings with are used in __construct. * * @var array */ protected $settings = [ 'singular' => 'wpseo_bulk_description', 'plural' => 'wpseo_bulk_descriptions', 'ajax' => true, ]; /** * The field in the database where meta field is saved. * * @var string */ protected $target_db_field = 'metadesc'; /** * The columns shown on the table. * * @return array */ public function get_columns() { $columns = [ 'col_existing_yoast_seo_metadesc' => __( 'Existing Yoast Meta Description', 'wordpress-seo' ), 'col_new_yoast_seo_metadesc' => __( 'New Yoast Meta Description', 'wordpress-seo' ), ]; return $this->merge_columns( $columns ); } /** * Parse the metadescription. * * @param string $column_name Column name. * @param object $record Data object. * @param string $attributes HTML attributes. * * @return string */ protected function parse_page_specific_column( $column_name, $record, $attributes ) { switch ( $column_name ) { case 'col_new_yoast_seo_metadesc': return sprintf( '<textarea id="%1$s" name="%1$s" class="wpseo-new-metadesc" data-id="%2$s" aria-labelledby="col_new_yoast_seo_metadesc"></textarea>', esc_attr( 'wpseo-new-metadesc-' . $record->ID ), esc_attr( $record->ID ), ); case 'col_existing_yoast_seo_metadesc': // @todo Inconsistent return/echo behavior R. // I traced the escaping of the attributes to WPSEO_Bulk_List_Table::column_attributes. Alexander. // The output of WPSEO_Bulk_List_Table::parse_meta_data_field is properly escaped. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->parse_meta_data_field( $record->ID, $attributes ); break; } } } images/icon-seo-analysis.svg 0000644 00000002352 15174712003 0012071 0 ustar 00 <svg width="42" height="42" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true" focusable="false"><g clip-path="url(#clip0_657_2290)"><mask id="a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="42" height="42"><path d="M42 0H0v42h42V0z" fill="#fff"/></mask><g mask="url(#a)"><mask id="b" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-3" y="-3" width="48" height="48"><path d="M44.1-2.1H-2.1v46.2h46.2V-2.1z" fill="#fff"/></mask><g mask="url(#b)"><path fill="url(#pattern0_657_2290)" d="M-2.234-2.217h46.368v46.368H-2.234z"/></g></g><path d="M25.41 7.35h-8.954a4.775 4.775 0 0 0-4.557 4.641v17.963a4.653 4.653 0 0 0 4.611 4.696h8.9a4.653 4.653 0 0 0 4.696-4.611V11.99a4.794 4.794 0 0 0-4.696-4.64zm2.591 21.95a3.233 3.233 0 0 1-3.208 3.233H17.16a3.236 3.236 0 0 1-3.209-3.234V12.617a3.233 3.233 0 0 1 3.21-3.234h7.63a3.236 3.236 0 0 1 3.21 3.234v16.682z" fill="#fff"/><path d="M20.975 23.906a2.936 2.936 0 1 0 0-5.871 2.936 2.936 0 0 0 0 5.871zm0-7.22a2.936 2.936 0 1 0 0-5.871 2.936 2.936 0 0 0 0 5.871zm0 14.499a2.936 2.936 0 1 0 0-5.871 2.936 2.936 0 0 0 0 5.871z" fill="#fff"/></g><defs><clipPath id="clip0_657_2290"><rect width="42" height="42" rx="6" fill="#fff"/></clipPath></defs></svg>