Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/integrations.tar

breadcrumbs-integration.php000066600000004000151130361070012062 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * Adds customizations to the front end for breadcrumbs.
 */
class Breadcrumbs_Integration implements Integration_Interface {

	/**
	 * The breadcrumbs presenter.
	 *
	 * @var Breadcrumbs_Presenter
	 */
	private $presenter;

	/**
	 * The meta tags context memoizer.
	 *
	 * @var Meta_Tags_Context_Memoizer
	 */
	private $context_memoizer;

	/**
	 * Breadcrumbs integration constructor.
	 *
	 * @param Helpers_Surface            $helpers          The helpers.
	 * @param WPSEO_Replace_Vars         $replace_vars     The replace vars.
	 * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
	 */
	public function __construct(
		Helpers_Surface $helpers,
		WPSEO_Replace_Vars $replace_vars,
		Meta_Tags_Context_Memoizer $context_memoizer
	) {
		$this->context_memoizer        = $context_memoizer;
		$this->presenter               = new Breadcrumbs_Presenter();
		$this->presenter->helpers      = $helpers;
		$this->presenter->replace_vars = $replace_vars;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * Registers the `wpseo_breadcrumb` shortcode.
	 *
	 * @codeCoverageIgnore
	 */
	public function register_hooks() {
		\add_shortcode( 'wpseo_breadcrumb', [ $this, 'render' ] );
	}

	/**
	 * Renders the breadcrumbs.
	 *
	 * @return string The rendered breadcrumbs.
	 */
	public function render() {
		$context = $this->context_memoizer->for_current_page();

		/** This filter is documented in src/integrations/front-end-integration.php */
		$presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );

		$this->presenter->presentation = $presentation;

		return $this->presenter->present();
	}
}
primary-category.php000066600000003716151130361070010563 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use stdClass;
use WP_Error;
use WP_Post;
use WPSEO_Primary_Term;
use Yoast\WP\SEO\Conditionals\Primary_Category_Conditional;

/**
 * Adds customizations to the front end for the primary category.
 */
class Primary_Category implements Integration_Interface {

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case only when on the frontend, the post overview, post edit or new post admin page.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Primary_Category_Conditional::class ];
	}

	/**
	 * Registers a filter to change a post's primary category.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'post_link_category', [ $this, 'post_link_category' ], 10, 3 );
	}

	/**
	 * Filters post_link_category to change the category to the chosen category by the user.
	 *
	 * @param stdClass     $category   The category that is now used for the post link.
	 * @param array|null   $categories This parameter is not used.
	 * @param WP_Post|null $post       The post in question.
	 *
	 * @return array|object|WP_Error|null The category we want to use for the post link.
	 */
	public function post_link_category( $category, $categories = null, $post = null ) {
		$post = \get_post( $post );
		if ( $post === null ) {
			return $category;
		}

		$primary_category = $this->get_primary_category( $post );
		if ( $primary_category !== false && $primary_category !== $category->cat_ID ) {
			$category = \get_category( $primary_category );
		}

		return $category;
	}

	/**
	 * Get the id of the primary category.
	 *
	 * @codeCoverageIgnore It justs wraps a dependency.
	 *
	 * @param WP_Post $post The post in question.
	 *
	 * @return int Primary category id.
	 */
	protected function get_primary_category( $post ) {
		$primary_term = new WPSEO_Primary_Term( 'category', $post->ID );

		return $primary_term->get_primary_term();
	}
}
settings-integration.php000066600000050677151130361070011456 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use WP_Post_Type;
use WP_Taxonomy;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Admin_Editor_Specific_Replace_Vars;
use WPSEO_Admin_Recommended_Replace_Vars;
use WPSEO_Option_Titles;
use WPSEO_Options;
use WPSEO_Replace_Vars;
use WPSEO_Shortlinker;
use WPSEO_Sitemaps_Router;
use Yoast\WP\SEO\Conditionals\Settings_Conditional;
use Yoast\WP\SEO\Config\Schema_Types;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Schema\Article_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Integrations\Admin\Social_Profiles_Helper;

/**
 * Class Settings_Integration.
 */
class Settings_Integration implements Integration_Interface {

	const PAGE = 'wpseo_settings';

	/**
	 * Holds the included WordPress options.
	 *
	 * @var string[]
	 */
	const WP_OPTIONS = [ 'blogname', 'blogdescription' ];

	/**
	 * Holds the allowed option groups.
	 *
	 * @var array
	 */
	const ALLOWED_OPTION_GROUPS = [ 'wpseo', 'wpseo_titles', 'wpseo_social' ];

	/**
	 * Holds the disallowed settings, per option group.
	 *
	 * Note: these are the settings that hold Objects.
	 *
	 * @var array
	 */
	const DISALLOWED_SETTINGS = [
		'wpseo'        => [
			'myyoast-oauth',
			'semrush_tokens',
			'custom_taxonomy_slugs',
			'zapier_subscription',
			'import_cursors',
			'workouts_data',
			'configuration_finished_steps',
			'importing_completed',
			'wincher_tokens',
			'least_readability_ignore_list',
			'least_seo_score_ignore_list',
			'most_linked_ignore_list',
			'least_linked_ignore_list',
			'indexables_page_reading_list',
		],
		'wpseo_titles' => [
			'company_logo_meta',
			'person_logo_meta',
		],
	];

	/**
	 * Holds the WPSEO_Admin_Asset_Manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * Holds the WPSEO_Replace_Vars.
	 *
	 * @var WPSEO_Replace_Vars
	 */
	protected $replace_vars;

	/**
	 * Holds the Schema_Types.
	 *
	 * @var Schema_Types
	 */
	protected $schema_types;

	/**
	 * Holds the Current_Page_Helper.
	 *
	 * @var Current_Page_Helper
	 */
	protected $current_page_helper;

	/**
	 * Holds the Post_Type_Helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * Holds the Taxonomy_Helper.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy_helper;

	/**
	 * Holds the Product_Helper.
	 *
	 * @var Product_Helper
	 */
	protected $product_helper;

	/**
	 * Holds the Woocommerce_Helper.
	 *
	 * @var Woocommerce_Helper
	 */
	protected $woocommerce_helper;

	/**
	 * Holds the Article_Helper.
	 *
	 * @var Article_Helper
	 */
	protected $article_helper;

	/**
	 * Holds the Social_Profiles_Helper.
	 *
	 * @var Social_Profiles_Helper
	 */
	protected $social_profiles_helper;

	/**
	 * Constructs Settings_Integration.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager          The WPSEO_Admin_Asset_Manager.
	 * @param WPSEO_Replace_Vars        $replace_vars           The WPSEO_Replace_Vars.
	 * @param Schema_Types              $schema_types           The Schema_Types.
	 * @param Current_Page_Helper       $current_page_helper    The Current_Page_Helper.
	 * @param Post_Type_Helper          $post_type_helper       The Post_Type_Helper.
	 * @param Taxonomy_Helper           $taxonomy_helper        The Taxonomy_Helper.
	 * @param Product_Helper            $product_helper         The Product_Helper.
	 * @param Woocommerce_Helper        $woocommerce_helper     The Woocommerce_Helper.
	 * @param Article_Helper            $article_helper         The Article_Helper.
	 * @param Social_Profiles_Helper    $social_profiles_helper The Social_Profiles_Helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		WPSEO_Replace_Vars $replace_vars,
		Schema_Types $schema_types,
		Current_Page_Helper $current_page_helper,
		Post_Type_Helper $post_type_helper,
		Taxonomy_Helper $taxonomy_helper,
		Product_Helper $product_helper,
		Woocommerce_Helper $woocommerce_helper,
		Article_Helper $article_helper,
		Social_Profiles_Helper $social_profiles_helper
	) {
		$this->asset_manager          = $asset_manager;
		$this->replace_vars           = $replace_vars;
		$this->schema_types           = $schema_types;
		$this->current_page_helper    = $current_page_helper;
		$this->taxonomy_helper        = $taxonomy_helper;
		$this->post_type_helper       = $post_type_helper;
		$this->product_helper         = $product_helper;
		$this->woocommerce_helper     = $woocommerce_helper;
		$this->article_helper         = $article_helper;
		$this->social_profiles_helper = $social_profiles_helper;
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Settings_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		// Add page.
		\add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] );
		\add_filter( 'admin_menu', [ $this, 'add_settings_saved_page' ] );

		// Are we saving the settings?
		if ( $this->current_page_helper->get_current_admin_page() === 'options.php' ) {
			$post_action = \filter_input( \INPUT_POST, 'action', \FILTER_SANITIZE_STRING );
			$option_page = \filter_input( \INPUT_POST, 'option_page', \FILTER_SANITIZE_STRING );

			if ( $post_action === 'update' && $option_page === self::PAGE ) {
				\add_action( 'admin_init', [ $this, 'register_setting' ] );
				\add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX );
			}

			return;
		}

		// Are we on the settings page?
		if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) {
			\add_action( 'admin_init', [ $this, 'register_setting' ] );
			\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
			\add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX );
		}
	}

	/**
	 * Registers the different options under the setting.
	 *
	 * @return void
	 */
	public function register_setting() {
		foreach ( WPSEO_Options::$options as $name => $instance ) {
			if ( \in_array( $name, self::ALLOWED_OPTION_GROUPS, true ) ) {
				\register_setting( self::PAGE, $name );
			}
		}
		// Only register WP options when the user is allowed to manage them.
		if ( \current_user_can( 'manage_options' ) ) {
			foreach ( self::WP_OPTIONS as $name ) {
				\register_setting( self::PAGE, $name );
			}
		}
	}

	/**
	 * Adds the page.
	 *
	 * @param array $pages The pages.
	 *
	 * @return array The pages.
	 */
	public function add_page( $pages ) {
		\array_splice(
			$pages,
			1,
			0,
			[
				[
					'wpseo_dashboard',
					'',
					\__( 'Settings', 'wordpress-seo' ),
					'wpseo_manage_options',
					self::PAGE,
					[ $this, 'display_page' ],
				],
			]
		);

		return $pages;
	}

	/**
	 * Adds a dummy page.
	 *
	 * Because the options route NEEDS to redirect to something.
	 *
	 * @param array $pages The pages.
	 *
	 * @return array The pages.
	 */
	public function add_settings_saved_page( $pages ) {
		\add_submenu_page(
			null,
			'',
			null,
			'wpseo_manage_options',
			self::PAGE . '_saved',
			static function () {
				// Add success indication to HTML response.
				$success = empty( \get_settings_errors() ) ? 'true' : 'false';
				echo \esc_html( "{{ yoast-success: $success }}" );
			}
		);

		return $pages;
	}

	/**
	 * Displays the page.
	 */
	public function display_page() {
		echo '<div id="yoast-seo-settings"></div>';
	}

	/**
	 * Enqueues the assets.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		// Remove the emoji script as it is incompatible with both React and any contenteditable fields.
		\remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
		\wp_enqueue_media();
		$this->asset_manager->enqueue_script( 'new-settings' );
		$this->asset_manager->enqueue_style( 'new-settings' );
		$this->asset_manager->localize_script( 'new-settings', 'wpseoScriptData', $this->get_script_data() );
	}

	/**
	 * Removes all current WP notices.
	 *
	 * @return void
	 */
	public function remove_notices() {
		\remove_all_actions( 'admin_notices' );
		\remove_all_actions( 'user_admin_notices' );
		\remove_all_actions( 'network_admin_notices' );
		\remove_all_actions( 'all_admin_notices' );
	}

	/**
	 * Creates the script data.
	 *
	 * @return array The script data.
	 */
	protected function get_script_data() {
		$default_settings       = $this->get_default_settings();
		$settings               = $this->get_settings( $default_settings );
		$post_types             = $this->post_type_helper->get_public_post_types( 'objects' );
		$taxonomies             = $this->taxonomy_helper->get_public_taxonomies( 'objects' );
		$transformed_post_types = $this->transform_post_types( $post_types );

		return [
			'settings'             => $this->transform_settings( $settings ),
			'defaultSettings'      => $default_settings,
			'disabledSettings'     => $this->get_disabled_settings( $settings ),
			'endpoint'             => \admin_url( 'options.php' ),
			'nonce'                => \wp_create_nonce( self::PAGE . '-options' ),
			'separators'           => WPSEO_Option_Titles::get_instance()->get_separator_options_for_display(),
			'replacementVariables' => $this->get_replacement_variables(),
			'schema'               => $this->get_schema( $transformed_post_types ),
			'preferences'          => $this->get_preferences(),
			'linkParams'           => WPSEO_Shortlinker::get_query_params(),
			'postTypes'            => $transformed_post_types,
			'taxonomies'           => $this->transform_taxonomies( $taxonomies, \array_keys( $transformed_post_types ) ),
			'fallbacks'            => $this->get_fallbacks(),
		];
	}

	/**
	 * Retrieves the preferences.
	 *
	 * @return array The preferences.
	 */
	protected function get_preferences() {
		$shop_page_id             = $this->woocommerce_helper->get_shop_page_id();
		$homepage_is_latest_posts = \get_option( 'show_on_front' ) === 'posts';
		$page_on_front            = \get_option( 'page_on_front' );
		$page_for_posts           = \get_option( 'page_for_posts' );

		if ( empty( $page_on_front ) ) {
			$page_on_front = $page_for_posts;
		}

		return [
			'isPremium'                     => $this->product_helper->is_premium(),
			'isRtl'                         => \is_rtl(),
			'isNetworkAdmin'                => \is_network_admin(),
			'isMainSite'                    => \is_main_site(),
			'isWooCommerceActive'           => $this->woocommerce_helper->is_active(),
			'isLocalSeoActive'              => (bool) \defined( 'WPSEO_LOCAL_FILE' ),
			'siteUrl'                       => \get_bloginfo( 'url' ),
			'sitemapUrl'                    => WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ),
			'hasWooCommerceShopPage'        => $shop_page_id !== -1,
			'editWooCommerceShopPageUrl'    => \get_edit_post_link( $shop_page_id, 'js' ),
			'wooCommerceShopPageSettingUrl' => \get_admin_url( null, 'admin.php?page=wc-settings&tab=products' ),
			'homepageIsLatestPosts'         => $homepage_is_latest_posts,
			'homepagePageEditUrl'           => \get_edit_post_link( $page_on_front, 'js' ),
			'homepagePostsEditUrl'          => \get_edit_post_link( $page_for_posts, 'js' ),
			'editUserUrl'                   => \admin_url( 'user-edit.php' ),
			'generalSettingsUrl'            => \admin_url( 'options-general.php' ),
			'companyOrPersonMessage'        => \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ),
			'currentUserId'                 => \get_current_user_id(),
			'canEditUsers'                  => \current_user_can( 'edit_users' ),
			'canManageOptions'              => \current_user_can( 'manage_options' ),
			'pluginUrl'                     => \plugins_url( '', \WPSEO_FILE ),
		];
	}

	/**
	 * Retrieves the default settings.
	 *
	 * @return array The default settings.
	 */
	protected function get_default_settings() {
		$defaults = [];

		// Add Yoast settings.
		foreach ( WPSEO_Options::$options as $option_name => $instance ) {
			if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
				$option_instance          = WPSEO_Options::get_option_instance( $option_name );
				$defaults[ $option_name ] = ( $option_instance ) ? $option_instance->get_defaults() : [];
			}
		}
		// Add WP settings.
		foreach ( self::WP_OPTIONS as $option_name ) {
			$defaults[ $option_name ] = '';
		}
		// Add person social profiles.
		$defaults['person_social_profiles'] = $this->social_profiles_helper->get_person_social_profiles( false );

		// Remove disallowed settings.
		foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) {
			foreach ( $disallowed_settings as $disallowed_setting ) {
				unset( $defaults[ $option_name ][ $disallowed_setting ] );
			}
		}

		return $defaults;
	}

	/**
	 * Retrieves the settings and their values.
	 *
	 * @param array $default_settings The default settings.
	 *
	 * @return array The settings.
	 */
	protected function get_settings( $default_settings ) {
		$settings = [];

		// Add Yoast settings.
		foreach ( WPSEO_Options::$options as $option_name => $instance ) {
			if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
				$settings[ $option_name ] = \array_merge( $default_settings[ $option_name ], WPSEO_Options::get_option( $option_name ) );
			}
		}
		// Add WP settings.
		foreach ( self::WP_OPTIONS as $option_name ) {
			$settings[ $option_name ] = \get_option( $option_name );
		}
		// Add person social profiles.
		$person_id                          = ( $settings['wpseo_titles']['company_or_person'] === 'person' ) ? $settings['wpseo_titles']['company_or_person_user_id'] : false;
		$settings['person_social_profiles'] = $this->social_profiles_helper->get_person_social_profiles( $person_id );

		// Remove disallowed settings.
		foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) {
			foreach ( $disallowed_settings as $disallowed_setting ) {
				unset( $settings[ $option_name ][ $disallowed_setting ] );
			}
		}

		return $settings;
	}

	/**
	 * Transforms setting values.
	 *
	 * @param array $settings The settings.
	 *
	 * @return array The settings.
	 */
	protected function transform_settings( $settings ) {
		if ( isset( $settings['wpseo_titles']['breadcrumbs-sep'] ) ) {
			/**
			 * The breadcrumbs separator default value is the HTML entity `&raquo;`.
			 * Which does not get decoded in our JS, while it did in our Yoast form. Decode it here as an exception.
			 */
			$settings['wpseo_titles']['breadcrumbs-sep'] = \html_entity_decode(
				$settings['wpseo_titles']['breadcrumbs-sep'],
				( \ENT_NOQUOTES | \ENT_HTML5 ),
				'UTF-8'
			);

			return $settings;
		}

		return $settings;
	}

	/**
	 * Retrieves the disabled settings.
	 *
	 * @param array $settings The settings.
	 *
	 * @return array The settings.
	 */
	protected function get_disabled_settings( $settings ) {
		$disabled_settings = [];

		foreach ( WPSEO_Options::$options as $option_name => $instance ) {
			if ( ! \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
				continue;
			}

			$disabled_settings[ $option_name ] = [];
			$option_instance                   = WPSEO_Options::get_option_instance( $option_name );
			if ( $option_instance === false ) {
				continue;
			}
			foreach ( $settings[ $option_name ] as $setting_name => $setting_value ) {
				if ( $option_instance->is_disabled( $setting_name ) ) {
					$disabled_settings[ $option_name ][ $setting_name ] = true;
				}
			}
		}

		return $disabled_settings;
	}

	/**
	 * Retrieves the replacement variables.
	 *
	 * @return array The replacement variables.
	 */
	protected function get_replacement_variables() {
		$recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
		$specific_replace_vars    = new WPSEO_Admin_Editor_Specific_Replace_Vars();
		$replacement_variables    = $this->replace_vars->get_replacement_variables_with_labels();

		return [
			'variables'   => $replacement_variables,
			'recommended' => $recommended_replace_vars->get_recommended_replacevars(),
			'specific'    => $specific_replace_vars->get(),
			'shared'      => $specific_replace_vars->get_generic( $replacement_variables ),
		];
	}

	/**
	 * Retrieves the schema.
	 *
	 * @param array $post_types The post types.
	 *
	 * @return array The schema.
	 */
	protected function get_schema( array $post_types ) {
		$schema = [];

		foreach ( $this->schema_types->get_article_type_options() as $article_type ) {
			$schema['articleTypes'][ $article_type['value'] ] = [
				'label' => $article_type['name'],
				'value' => $article_type['value'],
			];
		}

		foreach ( $this->schema_types->get_page_type_options() as $page_type ) {
			$schema['pageTypes'][ $page_type['value'] ] = [
				'label' => $page_type['name'],
				'value' => $page_type['value'],
			];
		}

		$schema['articleTypeDefaults'] = [];
		$schema['pageTypeDefaults']    = [];
		foreach ( $post_types as $name => $post_type ) {
			$schema['articleTypeDefaults'][ $name ] = WPSEO_Options::get_default( 'wpseo_titles', "schema-article-type-$name" );
			$schema['pageTypeDefaults'][ $name ]    = WPSEO_Options::get_default( 'wpseo_titles', "schema-page-type-$name" );
		}

		return $schema;
	}

	/**
	 * Transforms the post types, to represent them.
	 *
	 * @param WP_Post_Type[] $post_types The WP_Post_Type array to transform.
	 *
	 * @return array The post types.
	 */
	protected function transform_post_types( $post_types ) {
		$transformed = [];
		foreach ( $post_types as $name => $post_type ) {
			$transformed[ $name ] = [
				'name'                 => $post_type->name,
				'route'                => $this->get_route( $post_type->name, $post_type->rewrite, $post_type->rest_base ),
				'label'                => $post_type->label,
				'singularLabel'        => $post_type->labels->singular_name,
				'hasArchive'           => $this->post_type_helper->has_archive( $post_type ),
				'hasSchemaArticleType' => $this->article_helper->is_article_post_type( $post_type->name ),
				'menuPosition'         => $post_type->menu_position,
			];
		}

		\uasort( $transformed, [ $this, 'compare_post_types' ] );

		return $transformed;
	}

	/**
	 * Compares two post types.
	 *
	 * @param array $a The first post type.
	 * @param array $b The second post type.
	 *
	 * @return int The order.
	 */
	protected function compare_post_types( $a, $b ) {
		if ( $a['menuPosition'] === null && $b['menuPosition'] !== null ) {
			return 1;
		}
		if ( $a['menuPosition'] !== null && $b['menuPosition'] === null ) {
			return -1;
		}

		if ( $a['menuPosition'] === null && $b['menuPosition'] === null ) {
			// No position specified, order alphabetically by label.
			return \strnatcmp( $a['label'], $b['label'] );
		}

		return ( ( $a['menuPosition'] < $b['menuPosition'] ) ? -1 : 1 );
	}

	/**
	 * Transforms the taxonomies, to represent them.
	 *
	 * @param WP_Taxonomy[] $taxonomies      The WP_Taxonomy array to transform.
	 * @param string[]      $post_type_names The post type names.
	 *
	 * @return array The taxonomies.
	 */
	protected function transform_taxonomies( $taxonomies, $post_type_names ) {
		$transformed = [];
		foreach ( $taxonomies as $name => $taxonomy ) {
			$transformed[ $name ] = [
				'name'          => $taxonomy->name,
				'route'         => $this->get_route( $taxonomy->name, $taxonomy->rewrite, $taxonomy->rest_base ),
				'label'         => $taxonomy->label,
				'singularLabel' => $taxonomy->labels->singular_name,
				'postTypes'     => \array_filter(
					$taxonomy->object_type,
					static function ( $object_type ) use ( $post_type_names ) {
						return \in_array( $object_type, $post_type_names, true );
					}
				),
			];
		}

		\uasort(
			$transformed,
			static function ( $a, $b ) {
				return \strnatcmp( $a['label'], $b['label'] );
			}
		);

		return $transformed;
	}

	/**
	 * Gets the route from a name, rewrite and rest_base.
	 *
	 * @param string $name      The name.
	 * @param array  $rewrite   The rewrite data.
	 * @param string $rest_base The rest base.
	 *
	 * @return string The route.
	 */
	protected function get_route( $name, $rewrite, $rest_base ) {
		$route = $name;
		if ( isset( $rewrite['slug'] ) ) {
			$route = $rewrite['slug'];
		}
		if ( ! empty( $rest_base ) ) {
			$route = $rest_base;
		}
		// Always strip leading slashes.
		while ( substr( $route, 0, 1 ) === '/' ) {
			$route = substr( $route, 1 );
		}

		return $route;
	}

	/**
	 * Retrieves the fallbacks.
	 *
	 * @return array The fallbacks.
	 */
	protected function get_fallbacks() {
		$site_logo_id = \get_option( 'site_logo' );
		if ( ! $site_logo_id ) {
			$site_logo_id = \get_theme_mod( 'custom_logo' );
		}
		if ( ! $site_logo_id ) {
			$site_logo_id = '0';
		}

		return [
			'siteLogoId' => $site_logo_id,
		];
	}
}
front-end/theme-titles.php000066600000002272151130361070011561 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Notify the user by giving a deprecated notice.
 */
class Theme_Titles implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'thematic_doctitle', [ $this, 'title' ], 15 );
		\add_filter( 'woo_title', [ $this, 'title' ], 99 );
	}

	/**
	 * Filters the title for woo_title and the thematic_doctitle.
	 *
	 * @deprecated 14.0
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $title The title.
	 *
	 * @return string The title.
	 */
	public function title( $title ) {
		\_deprecated_function(
			__METHOD__,
			'WPSEO 14.0',
			\esc_html__(
				'a theme that has proper title-tag theme support, or adapt your theme to have that support',
				'wordpress-seo'
			)
		);

		return $title;
	}
}
front-end/category-term-description.php000066600000002434151130361070014260 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Adds support for shortcodes to category and term descriptions.
 */
class Category_Term_Description implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'category_description', [ $this, 'add_shortcode_support' ] );
		\add_filter( 'term_description', [ $this, 'add_shortcode_support' ] );
	}

	/**
	 * Adds shortcode support to category and term descriptions.
	 *
	 * This methods wrap in output buffering to prevent shortcodes that echo stuff
	 * instead of return from breaking things.
	 *
	 * @param string $description String to add shortcodes in.
	 *
	 * @return string Content with shortcodes filtered out.
	 */
	public function add_shortcode_support( $description ) {
		\ob_start();
		$description = \do_shortcode( $description );
		\ob_end_clean();

		return $description;
	}
}
front-end/wp-robots-integration.php000066600000013010151130361070013422 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\WP_Robots_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Robots_Presenter;

/**
 * Class WP_Robots_Integration
 *
 * @package Yoast\WP\SEO\Integrations\Front_End
 */
class WP_Robots_Integration implements Integration_Interface {

	/**
	 * The meta tags context memoizer.
	 *
	 * @var Meta_Tags_Context_Memoizer
	 */
	protected $context_memoizer;

	/**
	 * Sets the dependencies for this integration.
	 *
	 * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
	 */
	public function __construct( Meta_Tags_Context_Memoizer $context_memoizer ) {
		$this->context_memoizer = $context_memoizer;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		/**
		 * Allow control of the `wp_robots` filter by prioritizing our hook 10 less than max.
		 * Use the `wpseo_robots` filter to filter the Yoast robots output, instead of WordPress core.
		 */
		\add_filter( 'wp_robots', [ $this, 'add_robots' ], ( \PHP_INT_MAX - 10 ) );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [
			Front_End_Conditional::class,
			WP_Robots_Conditional::class,
		];
	}

	/**
	 * Adds our robots tag value to the WordPress robots tag output.
	 *
	 * @param array $robots The current robots data.
	 *
	 * @return array The robots data.
	 */
	public function add_robots( $robots ) {
		if ( ! \is_array( $robots ) ) {
			return $this->get_robots_value();
		}

		$merged_robots   = \array_merge( $robots, $this->get_robots_value() );
		$filtered_robots = $this->enforce_robots_congruence( $merged_robots );
		$sorted_robots   = $this->sort_robots( $filtered_robots );

		// Filter all falsy-null robot values.
		return \array_filter( $sorted_robots );
	}

	/**
	 * Retrieves the robots key-value pairs.
	 *
	 * @return array The robots key-value pairs.
	 */
	protected function get_robots_value() {
		$context = $this->context_memoizer->for_current_page();

		$robots_presenter               = new Robots_Presenter();
		$robots_presenter->presentation = $context->presentation;
		return $this->format_robots( $robots_presenter->get() );
	}

	/**
	 * Formats our robots fields, to match the pattern WordPress is using.
	 *
	 * Our format: `[ 'index' => 'noindex', 'max-image-preview' => 'max-image-preview:large', ... ]`
	 * WordPress format: `[ 'noindex' => true, 'max-image-preview' => 'large', ... ]`
	 *
	 * @param array $robots Our robots value.
	 *
	 * @return array The formatted robots.
	 */
	protected function format_robots( $robots ) {
		foreach ( $robots as $key => $value ) {
			// When the entry represents for example: max-image-preview:large.
			$colon_position = \strpos( $value, ':' );
			if ( $colon_position !== false ) {
				$robots[ $key ] = \substr( $value, ( $colon_position + 1 ) );

				continue;
			}

			// When index => noindex, we want a separate noindex as entry in array.
			if ( \strpos( $value, 'no' ) === 0 ) {
				$robots[ $key ]   = false;
				$robots[ $value ] = true;

				continue;
			}

			// When the key is equal to the value, just make its value a boolean.
			if ( $key === $value ) {
				$robots[ $key ] = true;
			}
		}

		return $robots;
	}

	/**
	 * Ensures all other possible robots values are congruent with nofollow and or noindex.
	 *
	 * WordPress might add some robot values again.
	 * When the page is set to noindex we want to filter out these values.
	 *
	 * @param array $robots The robots.
	 *
	 * @return array The filtered robots.
	 */
	protected function enforce_robots_congruence( $robots ) {
		if ( ! empty( $robots['nofollow'] ) ) {
			$robots['follow'] = null;
		}
		if ( ! empty( $robots['noarchive'] ) ) {
			$robots['archive'] = null;
		}
		if ( ! empty( $robots['noimageindex'] ) ) {
			$robots['imageindex'] = null;

			// `max-image-preview` should set be to `none` when `noimageindex` is present.
			// Using `isset` rather than `! empty` here so that in the rare case of `max-image-preview`
			// being equal to an empty string due to filtering, its value would still be set to `none`.
			if ( isset( $robots['max-image-preview'] ) ) {
				$robots['max-image-preview'] = 'none';
			}
		}
		if ( ! empty( $robots['nosnippet'] ) ) {
			$robots['snippet'] = null;
		}
		if ( ! empty( $robots['noindex'] ) ) {
			$robots['index']             = null;
			$robots['imageindex']        = null;
			$robots['noimageindex']      = null;
			$robots['archive']           = null;
			$robots['noarchive']         = null;
			$robots['snippet']           = null;
			$robots['nosnippet']         = null;
			$robots['max-snippet']       = null;
			$robots['max-image-preview'] = null;
			$robots['max-video-preview'] = null;
		}

		return $robots;
	}

	/**
	 * Sorts the robots array.
	 *
	 * @param array $robots The robots array.
	 *
	 * @return array The sorted robots array.
	 */
	protected function sort_robots( $robots ) {
		\uksort(
			$robots,
			static function ( $a, $b ) {
				$order = [
					'index'             => 0,
					'noindex'           => 1,
					'follow'            => 2,
					'nofollow'          => 3,
				];
				$ai    = isset( $order[ $a ] ) ? $order[ $a ] : 4;
				$bi    = isset( $order[ $b ] ) ? $order[ $b ] : 4;

				return ( $ai - $bi );
			}
		);

		return $robots;
	}
}
front-end/rss-footer-embed.php000066600000012334151130361070012332 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class RSS_Footer_Embed.
 */
class RSS_Footer_Embed implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Sets the required helpers.
	 *
	 * @codeCoverageIgnore It only handles dependencies.
	 *
	 * @param Options_Helper $options The options helper.
	 */
	public function __construct( Options_Helper $options ) {
		$this->options = $options;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'the_content_feed', [ $this, 'embed_rssfooter' ] );
		\add_filter( 'the_excerpt_rss', [ $this, 'embed_rssfooter_excerpt' ] );
	}

	/**
	 * Adds the RSS footer (or header) to the full RSS feed item.
	 *
	 * @param string $content Feed item content.
	 *
	 * @return string
	 */
	public function embed_rssfooter( $content ) {
		if ( ! $this->include_rss_footer( 'full' ) ) {
			return $content;
		}

		return $this->embed_rss( $content );
	}

	/**
	 * Adds the RSS footer (or header) to the excerpt RSS feed item.
	 *
	 * @param string $content Feed item excerpt.
	 *
	 * @return string
	 */
	public function embed_rssfooter_excerpt( $content ) {
		if ( ! $this->include_rss_footer( 'excerpt' ) ) {
			return $content;
		}

		return $this->embed_rss( \wpautop( $content ) );
	}

	/**
	 * Checks if the RSS footer should included.
	 *
	 * @param string $context The context of the RSS content.
	 *
	 * @return bool Whether or not the RSS footer should included.
	 */
	protected function include_rss_footer( $context ) {
		if ( ! \is_feed() ) {
			return false;
		}

		/**
		 * Filter: 'wpseo_include_rss_footer' - Allow the RSS footer to be dynamically shown/hidden.
		 *
		 * @api boolean $show_embed Indicates if the RSS footer should be shown or not.
		 *
		 * @param string $context The context of the RSS content - 'full' or 'excerpt'.
		 */
		if ( ! \apply_filters( 'wpseo_include_rss_footer', true, $context ) ) {
			return false;
		}

		return $this->is_configured();
	}

	/**
	 * Checks if the RSS feed fields are configured.
	 *
	 * @return bool True when one of the fields has a value.
	 */
	protected function is_configured() {
		return ( $this->options->get( 'rssbefore', '' ) !== '' || $this->options->get( 'rssafter', '' ) );
	}

	/**
	 * Adds the RSS footer and/or header to an RSS feed item.
	 *
	 * @param string $content Feed item content.
	 *
	 * @return string The content to add.
	 */
	protected function embed_rss( $content ) {
		$before  = $this->rss_replace_vars( $this->options->get( 'rssbefore', '' ) );
		$after   = $this->rss_replace_vars( $this->options->get( 'rssafter', '' ) );
		$content = $before . $content . $after;

		return $content;
	}

	/**
	 * Replaces the possible RSS variables with their actual values.
	 *
	 * @param string $content The RSS content that should have the variables replaced.
	 *
	 * @return string
	 */
	protected function rss_replace_vars( $content ) {
		if ( $content === '' ) {
			return $content;
		}

		$replace_vars = $this->get_replace_vars( $this->get_link_template(), \get_post() );

		$content = \stripslashes( \trim( $content ) );
		$content = \str_ireplace( \array_keys( $replace_vars ), \array_values( $replace_vars ), $content );

		return \wpautop( $content );
	}

	/**
	 * Retrieves the replacement variables.
	 *
	 * @codeCoverageIgnore It just contains too much WordPress functions.
	 *
	 * @param string $link_template The link template.
	 * @param mixed  $post          The post to use.
	 *
	 * @return array The replacement variables.
	 */
	protected function get_replace_vars( $link_template, $post ) {
		$author_link = '';
		if ( \is_object( $post ) ) {
			$author_link = \sprintf( $link_template, \esc_url( \get_author_posts_url( $post->post_author ) ), \esc_html( \get_the_author() ) );
		}

		return [
			'%%AUTHORLINK%%'   => $author_link,
			'%%POSTLINK%%'     => \sprintf( $link_template, \esc_url( \get_permalink() ), \esc_html( \get_the_title() ) ),
			'%%BLOGLINK%%'     => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) ),
			'%%BLOGDESCLINK%%' => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) . ' - ' . \esc_html( \get_bloginfo( 'description' ) ) ),
		];
	}

	/**
	 * Retrieves the link template.
	 *
	 * @return string The link template.
	 */
	protected function get_link_template() {
		/**
		 * Filter: 'nofollow_rss_links' - Allow the developer to determine whether or not to follow the links in
		 * the bits Yoast SEO adds to the RSS feed, defaults to true.
		 *
		 * @api bool $unsigned Whether or not to follow the links in RSS feed, defaults to true.
		 *
		 * @since 1.4.20
		 */
		if ( \apply_filters( 'nofollow_rss_links', true ) ) {
			return '<a rel="nofollow" href="%1$s">%2$s</a>';
		}

		return '<a href="%1$s">%2$s</a>';
	}
}
front-end/robots-txt-integration.php000066600000014174151130361070013627 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use WPSEO_Sitemaps_Router;
use Yoast\WP\SEO\Conditionals\Robots_Txt_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Robots_Txt_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Robots_Txt_Presenter;

/**
 * Handles adding the sitemap to the `robots.txt`.
 */
class Robots_Txt_Integration implements Integration_Interface {

	/**
	 * Holds the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Holds the robots txt helper.
	 *
	 * @var Robots_Txt_Helper
	 */
	protected $robots_txt_helper;

	/**
	 * Holds the robots txt presenter.
	 *
	 * @var Robots_Txt_Presenter
	 */
	protected $robots_txt_presenter;

	/**
	 * Sets the helpers.
	 *
	 * @param Options_Helper    $options_helper Options helper.
	 * @param Robots_Txt_Helper $robots_txt_helper Robots txt helper.
	 */
	public function __construct( Options_Helper $options_helper, Robots_Txt_Helper $robots_txt_helper ) {
		$this->options_helper       = $options_helper;
		$this->robots_txt_helper    = $robots_txt_helper;
		$this->robots_txt_presenter = new Robots_Txt_Presenter( $robots_txt_helper );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Robots_Txt_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'robots_txt', [ $this, 'filter_robots' ], 99999 );
	}

	/**
	 * Filters the robots.txt output.
	 *
	 * @param string $robots_txt The robots.txt output from WordPress.
	 *
	 * @return string Filtered robots.txt output.
	 */
	public function filter_robots( $robots_txt ) {
		$robots_txt = $this->remove_default_robots( $robots_txt );
		$this->maybe_add_xml_sitemap();

		/**
		 * Filter: 'wpseo_should_add_subdirectory_multisite_xml_sitemaps' - Disabling this filter removes subdirectory sites from xml sitemaps.
		 *
		 * @since 19.8
		 *
		 * @param bool $show Whether to display multisites in the xml sitemaps.
		 */
		if ( \apply_filters( 'wpseo_should_add_subdirectory_multisite_xml_sitemaps', true ) ) {
			$this->add_subdirectory_multisite_xml_sitemaps();
		}

		/**
		 * Allow registering custom robots rules to be outputted within the Yoast content block in robots.txt.
		 *
		 * @param Robots_Txt_Helper $robots_txt_helper The Robots_Txt_Helper object.
		 */
		\do_action( 'Yoast\WP\SEO\register_robots_rules', $this->robots_txt_helper );

		return \trim( $robots_txt . PHP_EOL . $this->robots_txt_presenter->present() . PHP_EOL );
	}

	/**
	 * Replaces the default WordPress robots.txt output.
	 *
	 * @param string $robots_txt Input robots.txt.
	 *
	 * @return string
	 */
	protected function remove_default_robots( $robots_txt ) {
		return \str_replace(
			"User-agent: *\nDisallow: /wp-admin/\nAllow: /wp-admin/admin-ajax.php\n",
			'',
			$robots_txt
		);
	}

	/**
	 * Adds XML sitemap reference to robots.txt.
	 *
	 * @return void
	 */
	protected function maybe_add_xml_sitemap() {
		// If the XML sitemap is disabled, bail.
		if ( ! $this->options_helper->get( 'enable_xml_sitemap', false ) ) {
			return;
		}
		$this->robots_txt_helper->add_sitemap( \esc_url( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ) );
	}

	/**
	 * Adds subdomain multisite' XML sitemap references to robots.txt.
	 *
	 * @return void
	 */
	protected function add_subdirectory_multisite_xml_sitemaps() {
		// If not on a multisite subdirectory, bail.
		if ( ! \is_multisite() || \is_subdomain_install() ) {
			return;
		}

		$sitemaps_enabled = $this->get_xml_sitemaps_enabled();

		foreach ( $sitemaps_enabled as $blog_id => $is_sitemap_enabled ) {
			if ( ! $is_sitemap_enabled ) {
				continue;
			}
			$this->robots_txt_helper->add_sitemap( \esc_url( \get_home_url( $blog_id, 'sitemap_index.xml' ) ) );
		}
	}

	/**
	 * Retrieves whether the XML sitemaps are enabled, keyed by blog ID.
	 *
	 * @return array
	 */
	protected function get_xml_sitemaps_enabled() {
		$is_allowed = $this->is_sitemap_allowed();
		$blog_ids   = $this->get_blog_ids();
		$is_enabled = [];
		foreach ( $blog_ids as $blog_id ) {
			$is_enabled[ $blog_id ] = $is_allowed && $this->is_sitemap_enabled_for( $blog_id );
		}

		return $is_enabled;
	}

	/**
	 * Retrieves whether the sitemap is allowed on a sub site.
	 *
	 * @return bool
	 */
	protected function is_sitemap_allowed() {
		$options = \get_network_option( null, 'wpseo_ms' );
		if ( ! $options || ! isset( $options['allow_enable_xml_sitemap'] ) ) {
			// Default is enabled.
			return true;
		}

		return (bool) $options['allow_enable_xml_sitemap'];
	}

	/**
	 * Retrieves whether the sitemap is enabled on a site.
	 *
	 * @param int $blog_id The blog ID.
	 *
	 * @return bool
	 */
	protected function is_sitemap_enabled_for( $blog_id ) {
		if ( ! $this->is_yoast_active_on( $blog_id ) ) {
			return false;
		}

		$options = \get_blog_option( $blog_id, 'wpseo' );
		if ( ! $options || ! isset( $options['enable_xml_sitemap'] ) ) {
			// Default is enabled.
			return true;
		}

		return (bool) $options['enable_xml_sitemap'];
	}

	/**
	 * Determines whether Yoast SEO is active.
	 *
	 * @param int $blog_id The blog ID.
	 *
	 * @return bool
	 */
	protected function is_yoast_active_on( $blog_id ) {
		return \in_array( 'wordpress-seo/wp-seo.php', (array) \get_blog_option( $blog_id, 'active_plugins', [] ), true ) || $this->is_yoast_active_for_network();
	}

	/**
	 * Determines whether Yoast SEO is active for the entire network.
	 *
	 * @return bool
	 */
	protected function is_yoast_active_for_network() {
		$plugins = \get_network_option( null, 'active_sitewide_plugins' );
		if ( isset( $plugins['wordpress-seo/wp-seo.php'] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Retrieves the blog IDs of public, "active" sites on the network.
	 *
	 * @return array
	 */
	protected function get_blog_ids() {
		$criteria = [
			'archived'   => 0,
			'deleted'    => 0,
			'public'     => 1,
			'spam'       => 0,
			'fields'     => 'ids',
			'network_id' => \get_current_network_id(),
		];

		return \get_sites( $criteria );
	}
}
front-end/feed-improvements.php000066600000010677151130361070012616 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Surfaces\Meta_Surface;

/**
 * Class Feed_Improvements
 */
class Feed_Improvements implements Integration_Interface {

	/**
	 * Holds the options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Holds the meta helper surface.
	 *
	 * @var Meta_Surface
	 */
	private $meta;

	/**
	 * Canonical_Header constructor.
	 *
	 * @codeCoverageIgnore It only sets depedencies.
	 *
	 * @param Options_Helper $options The options helper.
	 * @param Meta_Surface   $meta    The meta surface.
	 */
	public function __construct(
		Options_Helper $options,
		Meta_Surface $meta
	) {
		$this->options = $options;
		$this->meta    = $meta;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Registers hooks to WordPress.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'get_bloginfo_rss', [ $this, 'filter_bloginfo_rss' ], 10, 2 );
		\add_filter( 'document_title_separator', [ $this, 'filter_document_title_separator' ] );

		\add_action( 'do_feed_rss', [ $this, 'handle_rss_feed' ], 9 );
		\add_action( 'do_feed_rss2', [ $this, 'send_canonical_header' ], 9 );
		\add_action( 'do_feed_rss2', [ $this, 'add_robots_headers' ], 9 );
	}

	/**
	 * Filter `bloginfo_rss` output to give the URL for what's being shown instead of just always the homepage.
	 *
	 * @param string $show The output so far.
	 * @param string $what What is being shown.
	 *
	 * @return string
	 */
	public function filter_bloginfo_rss( $show, $what ) {
		if ( $what === 'url' ) {
			return $this->get_url_for_queried_object( $show );
		}

		return $show;
	}

	/**
	 * Makes sure send canonical header always runs, because this RSS hook does not support the for_comments parameter
	 *
	 * @return void
	 */
	public function handle_rss_feed() {
		$this->send_canonical_header( false );
	}

	/**
	 * Adds a canonical link header to the main canonical URL for the requested feed object. If it is not a comment
	 * feed.
	 *
	 * @param bool $for_comments If the RRS feed is meant for a comment feed.
	 *
	 * @return void
	 */
	public function send_canonical_header( $for_comments ) {

		if ( $for_comments || \headers_sent() ) {
			return;
		}

		$url = $this->get_url_for_queried_object( $this->meta->for_home_page()->canonical );
		if ( ! empty( $url ) ) {
			\header( \sprintf( 'Link: <%s>; rel="canonical"', $url ), false );
		}
	}

	/**
	 * Adds noindex, follow tag for comment feeds.
	 *
	 * @param bool $for_comments If the RSS feed is meant for a comment feed.
	 *
	 * @return void
	 */
	public function add_robots_headers( $for_comments ) {
		if ( $for_comments && ! \headers_sent() ) {
			\header( 'X-Robots-Tag: noindex, follow', true );
		}
	}

	/**
	 * Makes sure the title separator set in Yoast SEO is used for all feeds.
	 *
	 * @param string $separator The separator from WordPress.
	 *
	 * @return string The separator from Yoast SEO's settings.
	 */
	public function filter_document_title_separator( $separator ) {
		return \html_entity_decode( $this->options->get_title_separator() );
	}

	/**
	 * Determines the main URL for the queried object.
	 *
	 * @param string $url The URL determined so far.
	 *
	 * @return string The canonical URL for the queried object.
	 */
	protected function get_url_for_queried_object( $url = '' ) {
		$queried_object = \get_queried_object();
		// Don't call get_class with null. This gives a warning.
		$class = ( $queried_object !== null ) ? \get_class( $queried_object ) : null;

		switch ( $class ) {
			// Post type archive feeds.
			case 'WP_Post_Type':
				$url = $this->meta->for_post_type_archive( $queried_object->name )->canonical;
				break;
			// Post comment feeds.
			case 'WP_Post':
				$url = $this->meta->for_post( $queried_object->ID )->canonical;
				break;
			// Term feeds.
			case 'WP_Term':
				$url = $this->meta->for_term( $queried_object->term_id )->canonical;
				break;
			// Author feeds.
			case 'WP_User':
				$url = $this->meta->for_author( $queried_object->ID )->canonical;
				break;
			// This would be NULL on the home page and on date archive feeds.
			case null:
				$url = $this->meta->for_home_page()->canonical;
				break;
			default:
				break;
		}

		return $url;
	}
}
front-end/handle-404.php000066600000005127151130361070010717 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper;

/**
 * Handles intercepting requests.
 */
class Handle_404 implements Integration_Interface {

	/**
	 * The WP Query wrapper.
	 *
	 * @var WP_Query_Wrapper
	 */
	private $query_wrapper;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'pre_handle_404', [ $this, 'handle_404' ] );
	}

	/**
	 * Handle_404 constructor.
	 *
	 * @codeCoverageIgnore Handles dependencies.
	 *
	 * @param WP_Query_Wrapper $query_wrapper The query wrapper.
	 */
	public function __construct( WP_Query_Wrapper $query_wrapper ) {
		$this->query_wrapper = $query_wrapper;
	}

	/**
	 * Handles the 404 status code.
	 *
	 * @param bool $handled Whether we've handled the request.
	 *
	 * @return bool True if it's 404.
	 */
	public function handle_404( $handled ) {
		if ( ! $this->is_feed_404() ) {
			return $handled;
		}

		$this->set_404();
		$this->set_headers();

		\add_filter( 'old_slug_redirect_url', '__return_false' );
		\add_filter( 'redirect_canonical', '__return_false' );

		return true;
	}

	/**
	 * If there are no posts in a feed, make it 404 instead of sending an empty RSS feed.
	 *
	 * @return bool True if it's 404.
	 */
	protected function is_feed_404() {
		if ( ! \is_feed() ) {
			return false;
		}

		$wp_query = $this->query_wrapper->get_query();

		// Don't 404 if the query contains post(s) or an object.
		if ( $wp_query->posts || $wp_query->get_queried_object() ) {
			return false;
		}

		// Don't 404 if it isn't archive or singular.
		if ( ! $wp_query->is_archive() && ! $wp_query->is_singular() ) {
			return false;
		}

		return true;
	}

	/**
	 * Sets the 404 status code.
	 */
	protected function set_404() {
		$wp_query          = $this->query_wrapper->get_query();
		$wp_query->is_feed = false;
		$wp_query->set_404();
		$this->query_wrapper->set_query( $wp_query );
	}

	/**
	 * Sets the headers for http.
	 *
	 * @codeCoverageIgnore
	 */
	protected function set_headers() {
		// Overwrite Content-Type header.
		if ( ! \headers_sent() ) {
			\header( 'Content-Type: ' . \get_option( 'html_type' ) . '; charset=' . \get_option( 'blog_charset' ) );
		}

		\status_header( 404 );
		\nocache_headers();
	}
}
front-end/force-rewrite-title.php000066600000010630151130361070013046 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper;

/**
 * Class Force_Rewrite_Title.
 */
class Force_Rewrite_Title implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Toggle indicating whether output buffering has been started.
	 *
	 * @var bool
	 */
	private $ob_started = false;

	/**
	 * The WP Query wrapper.
	 *
	 * @var WP_Query_Wrapper
	 */
	private $wp_query;

	/**
	 * Sets the helpers.
	 *
	 * @codeCoverageIgnore It just handles dependencies.
	 *
	 * @param Options_Helper   $options  Options helper.
	 * @param WP_Query_Wrapper $wp_query WP query wrapper.
	 */
	public function __construct( Options_Helper $options, WP_Query_Wrapper $wp_query ) {
		$this->options  = $options;
		$this->wp_query = $wp_query;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function register_hooks() {
		// When the option is disabled.
		if ( ! $this->options->get( 'forcerewritetitle', false ) ) {
			return;
		}

		// For WordPress versions below 4.4.
		if ( \current_theme_supports( 'title-tag' ) ) {
			return;
		}

		\add_action( 'template_redirect', [ $this, 'force_rewrite_output_buffer' ], 99999 );
		\add_action( 'wp_footer', [ $this, 'flush_cache' ], -1 );
	}

	/**
	 * Used in the force rewrite functionality this retrieves the output, replaces the title with the proper SEO
	 * title and then flushes the output.
	 *
	 * @return bool
	 */
	public function flush_cache() {
		if ( $this->ob_started !== true ) {
			return false;
		}

		$content = $this->get_buffered_output();

		$old_wp_query = $this->wp_query->get_query();

		\wp_reset_query();

		// When the file has the debug mark.
		if ( \preg_match( '/(?\'before\'.*)<!-- This site is optimized with the Yoast SEO.*<!-- \/ Yoast SEO( Premium)? plugin. -->(?\'after\'.*)/is', $content, $matches ) ) {
			$content = $this->replace_titles_from_content( $content, $matches );

			unset( $matches );
		}

		// phpcs:ignore WordPress.WP.GlobalVariablesOverride -- The query gets reset here with the original query.
		$GLOBALS['wp_query'] = $old_wp_query;

		// phpcs:ignore WordPress.Security.EscapeOutput -- The output should already have been escaped, we are only filtering it.
		echo $content;

		return true;
	}

	/**
	 * Starts the output buffer so it can later be fixed by flush_cache().
	 */
	public function force_rewrite_output_buffer() {
		$this->ob_started = true;
		$this->start_output_buffering();
	}

	/**
	 * Replaces the titles from the parts that contains a title.
	 *
	 * @param string $content          The content to remove the titles from.
	 * @param array  $parts_with_title The parts containing a title.
	 *
	 * @return string The modified content.
	 */
	protected function replace_titles_from_content( $content, $parts_with_title ) {
		if ( isset( $parts_with_title['before'] ) && \is_string( $parts_with_title['before'] ) ) {
			$content = $this->replace_title( $parts_with_title['before'], $content );
		}

		if ( isset( $parts_with_title['after'] ) ) {
			$content = $this->replace_title( $parts_with_title['after'], $content );
		}

		return $content;
	}

	/**
	 * Removes the title from the part that contains the title and put this modified part back
	 * into the content.
	 *
	 * @param string $part_with_title The part with the title that needs to be replaced.
	 * @param string $content         The entire content.
	 *
	 * @return string The altered content.
	 */
	protected function replace_title( $part_with_title, $content ) {
		$part_without_title = \preg_replace( '/<title.*?\/title>/i', '', $part_with_title );

		return \str_replace( $part_with_title, $part_without_title, $content );
	}

	/**
	 * Starts the output buffering.
	 *
	 * @codeCoverageIgnore
	 */
	protected function start_output_buffering() {
		\ob_start();
	}

	/**
	 * Retrieves the buffered output.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return false|string The buffered output.
	 */
	protected function get_buffered_output() {
		return \ob_get_clean();
	}
}
front-end/backwards-compatibility.php000066600000003301151130361070013757 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Adds actions that were previously called and are now deprecated.
 */
class Backwards_Compatibility implements Integration_Interface {

	/**
	 * Represents the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Backwards_Compatibility constructor
	 *
	 * @param Options_Helper $options The options helper.
	 */
	public function __construct( Options_Helper $options ) {
		$this->options = $options;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->options->get( 'opengraph' ) === true ) {
			\add_action( 'wpseo_head', [ $this, 'call_wpseo_opengraph' ], 30 );
		}
		if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) {
			\add_action( 'wpseo_head', [ $this, 'call_wpseo_twitter' ], 40 );
		}
	}

	/**
	 * Calls the old wpseo_opengraph action.
	 *
	 * @return void
	 */
	public function call_wpseo_opengraph() {
		\do_action_deprecated( 'wpseo_opengraph', [], '14.0', 'wpseo_frontend_presenters' );
	}

	/**
	 * Calls the old wpseo_twitter action.
	 *
	 * @return void
	 */
	public function call_wpseo_twitter() {
		\do_action_deprecated( 'wpseo_twitter', [], '14.0', 'wpseo_frontend_presenters' );
	}
}
front-end/open-graph-oembed.php000066600000006115151130361070012446 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use WP_Post;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Surfaces\Meta_Surface;

/**
 * Class Open_Graph_OEmbed.
 */
class Open_Graph_OEmbed implements Integration_Interface {

	/**
	 * The meta surface.
	 *
	 * @var Meta_Surface
	 */
	private $meta;

	/**
	 * The oEmbed data.
	 *
	 * @var array
	 */
	private $data;

	/**
	 * The post ID for the current post.
	 *
	 * @var int
	 */
	private $post_id;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class, Open_Graph_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'oembed_response_data', [ $this, 'set_oembed_data' ], 10, 2 );
	}

	/**
	 * Open_Graph_OEmbed constructor.
	 *
	 * @param Meta_Surface $meta The meta surface.
	 */
	public function __construct( Meta_Surface $meta ) {
		$this->meta = $meta;
	}

	/**
	 * Callback function to pass to the oEmbed's response data that will enable
	 * support for using the image and title set by the WordPress SEO plugin's fields. This
	 * address the concern where some social channels/subscribed use oEmebed data over Open Graph data
	 * if both are present.
	 *
	 * @link https://developer.wordpress.org/reference/hooks/oembed_response_data/ for hook info.
	 *
	 * @param array   $data The oEmbed data.
	 * @param WP_Post $post The current Post object.
	 *
	 * @return array An array of oEmbed data with modified values where appropriate.
	 */
	public function set_oembed_data( $data, $post ) {
		// Data to be returned.
		$this->data    = $data;
		$this->post_id = $post->ID;

		$this->set_title();
		$this->set_description();
		$this->set_image();

		return $this->data;
	}

	/**
	 * Sets the OpenGraph title if configured.
	 */
	protected function set_title() {
		$opengraph_title = $this->meta->for_post( $this->post_id )->open_graph_title;

		if ( ! empty( $opengraph_title ) ) {
			$this->data['title'] = $opengraph_title;
		}
	}

	/**
	 * Sets the OpenGraph description if configured.
	 */
	protected function set_description() {
		$opengraph_description = $this->meta->for_post( $this->post_id )->open_graph_description;

		if ( ! empty( $opengraph_description ) ) {
			$this->data['description'] = $opengraph_description;
		}
	}

	/**
	 * Sets the image if it has been configured.
	 */
	protected function set_image() {
		$images = $this->meta->for_post( $this->post_id )->open_graph_images;
		$image  = \reset( $images );

		if ( empty( $image ) ) {
			return;
		}

		if ( ! isset( $image['url'] ) ) {
			return;
		}

		$this->data['thumbnail_url'] = $image['url'];

		if ( isset( $image['width'] ) ) {
			$this->data['thumbnail_width'] = $image['width'];
		}

		if ( isset( $image['height'] ) ) {
			$this->data['thumbnail_height'] = $image['height'];
		}
	}
}
front-end/redirects.php000066600000015707151130361070011150 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Meta_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class Redirects.
 */
class Redirects implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * The meta helper.
	 *
	 * @var Meta_Helper
	 */
	protected $meta;

	/**
	 * The current page helper.
	 *
	 * @var Current_Page_Helper
	 */
	protected $current_page;

	/**
	 * The redirect helper.
	 *
	 * @var Redirect_Helper
	 */
	private $redirect;

	/**
	 * The URL helper.
	 *
	 * @var Url_Helper
	 */
	private $url;

	/**
	 * Holds the WP_Query variables we should get rid of.
	 *
	 * @var string[]
	 */
	private $date_query_variables = [
		'year',
		'm',
		'monthnum',
		'day',
		'hour',
		'minute',
		'second',
	];

	/**
	 * Sets the helpers.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param Options_Helper      $options      Options helper.
	 * @param Meta_Helper         $meta         Meta helper.
	 * @param Current_Page_Helper $current_page The current page helper.
	 * @param Redirect_Helper     $redirect     The redirect helper.
	 * @param Url_Helper          $url          The URL helper.
	 */
	public function __construct( Options_Helper $options, Meta_Helper $meta, Current_Page_Helper $current_page, Redirect_Helper $redirect, Url_Helper $url ) {
		$this->options      = $options;
		$this->meta         = $meta;
		$this->current_page = $current_page;
		$this->redirect     = $redirect;
		$this->url          = $url;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'wp', [ $this, 'archive_redirect' ] );
		\add_action( 'wp', [ $this, 'page_redirect' ], 99 );
		\add_action( 'wp', [ $this, 'category_redirect' ] );
		\add_action( 'template_redirect', [ $this, 'attachment_redirect' ], 1 );
		\add_action( 'template_redirect', [ $this, 'disable_date_queries' ] );
	}

	/**
	 * Disable date queries, if they're disabled in Yoast SEO settings, to prevent indexing the wrong things.
	 *
	 * @return void
	 */
	public function disable_date_queries() {
		if ( $this->options->get( 'disable-date', false ) ) {
			$exploded_url                    = \explode( '?', $this->url->recreate_current_url(), 2 );
			list( $base_url, $query_string ) = \array_pad( $exploded_url, 2, '' );
			\parse_str( $query_string, $query_vars );
			foreach ( $this->date_query_variables as $variable ) {
				if ( \in_array( $variable, \array_keys( $query_vars ), true ) ) {
					$this->do_date_redirect( $query_vars, $base_url );
				}
			}
		}
	}

	/**
	 * When certain archives are disabled, this redirects those to the homepage.
	 */
	public function archive_redirect() {
		if ( $this->need_archive_redirect() ) {
			$this->redirect->do_safe_redirect( \get_bloginfo( 'url' ), 301 );
		}
	}

	/**
	 * Based on the redirect meta value, this function determines whether it should redirect the current post / page.
	 */
	public function page_redirect() {
		if ( ! $this->current_page->is_simple_page() ) {
			return;
		}

		$post = \get_post();
		if ( ! \is_object( $post ) ) {
			return;
		}

		$redirect = $this->meta->get_value( 'redirect', $post->ID );
		if ( $redirect === '' ) {
			return;
		}

		$this->redirect->do_safe_redirect( $redirect, 301 );
	}

	/**
	 * If the option to disable attachment URLs is checked, this performs the redirect to the attachment.
	 */
	public function attachment_redirect() {
		if ( ! $this->current_page->is_attachment() ) {
			return;
		}

		if ( $this->options->get( 'disable-attachment', false ) === false ) {
			return;
		}

		$url = $this->get_attachment_url();
		if ( empty( $url ) ) {
			return;
		}

		$this->redirect->do_unsafe_redirect( $url, 301 );
	}

	/**
	 * Checks if certain archive pages are disabled to determine if a archive redirect is needed.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return bool Whether or not to redirect an archive page.
	 */
	protected function need_archive_redirect() {
		if ( $this->options->get( 'disable-date', false ) && $this->current_page->is_date_archive() ) {
			return true;
		}

		if ( $this->options->get( 'disable-author', false ) && $this->current_page->is_author_archive() ) {
			return true;
		}

		if ( $this->options->get( 'disable-post_format', false ) && $this->current_page->is_post_format_archive() ) {
			return true;
		}

		return false;
	}

	/**
	 * Retrieves the attachment url for the current page.
	 *
	 * @codeCoverageIgnore It wraps WordPress functions.
	 *
	 * @return string The attachment url.
	 */
	protected function get_attachment_url() {
		/**
		 * Allows the developer to change the target redirection URL for attachments.
		 *
		 * @api string $attachment_url The attachment URL for the queried object.
		 * @api object $queried_object The queried object.
		 *
		 * @since 7.5.3
		 */
		return \apply_filters(
			'wpseo_attachment_redirect_url',
			\wp_get_attachment_url( \get_queried_object_id() ),
			\get_queried_object()
		);
	}

	/**
	 * Redirects away query variables that shouldn't work.
	 *
	 * @param array  $query_vars The query variables in the current URL.
	 * @param string $base_url   The base URL without query string.
	 *
	 * @return void
	 */
	private function do_date_redirect( $query_vars, $base_url ) {
		foreach ( $this->date_query_variables as $variable ) {
			unset( $query_vars[ $variable ] );
		}
		$url = $base_url;
		if ( \count( $query_vars ) > 0 ) {
			$url .= '?' . \http_build_query( $query_vars );
		}

		$this->redirect->do_safe_redirect( $url, 301 );
	}

	/**
	 * Strips `cat=-1` from the URL and redirects to the resulting URL.
	 */
	public function category_redirect() {
		/**
		 * Allows the developer to keep cat=-1 GET parameters
		 *
		 * @since 19.9
		 *
		 * @param bool $remove_cat_parameter Whether to remove the `cat=-1` GET parameter. Default true.
		 */
		$should_remove_parameter = \apply_filters( 'wpseo_remove_cat_parameter', true );

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved.
		if ( $should_remove_parameter && isset( $_GET['cat'] ) && $_GET['cat'] === '-1' ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved.
			unset( $_GET['cat'] );
			if ( isset( $_SERVER['REQUEST_URI'] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is just a replace and the data is never saved.
				$_SERVER['REQUEST_URI'] = \remove_query_arg( 'cat' );
			}
			$this->redirect->do_safe_redirect( $this->url->recreate_current_url(), 301, 'Stripping cat=-1 from the URL' );
		}
	}
}
front-end/schema-accessibility-feature.php000066600000004462151130361070014676 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use Yoast\WP\SEO\Generators\Schema\Article;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Adds the table of contents accessibility feature to the article piece with a fallback to the webpage piece.
 */
class Schema_Accessibility_Feature implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_schema_webpage', [ $this, 'maybe_add_accessibility_feature' ], 10, 4 );
		\add_filter( 'wpseo_schema_article', [ $this, 'add_accessibility_feature' ], 10, 2 );
	}

	/**
	 * Adds the accessibility feature to the webpage if there is no article.
	 *
	 * @param array                   $piece         The graph piece.
	 * @param Meta_Tags_Context       $context       The context.
	 * @param Abstract_Schema_Piece   $the_generator The current schema generator.
	 * @param Abstract_Schema_Piece[] $generators    The schema generators.
	 *
	 * @return array The graph piece.
	 */
	public function maybe_add_accessibility_feature( $piece, $context, $the_generator, $generators ) {
		foreach ( $generators as $generator ) {
			if ( \is_a( $generator, Article::class ) && $generator->is_needed() ) {
				return $piece;
			}
		}

		return $this->add_accessibility_feature( $piece, $context );
	}

	/**
	 * Adds the accessibility feature to a schema graph piece.
	 *
	 * @param array             $piece   The schema piece.
	 * @param Meta_Tags_Context $context The context.
	 *
	 * @return array The graph piece.
	 */
	public function add_accessibility_feature( $piece, $context ) {
		if ( empty( $context->blocks['yoast-seo/table-of-contents'] ) ) {
			return $piece;
		}

		if ( isset( $piece['accessibilityFeature'] ) ) {
			$piece['accessibilityFeature'][] = 'tableOfContents';
		}
		else {
			$piece['accessibilityFeature'] = [
				'tableOfContents',
			];
		}
		return $piece;
	}
}
front-end/comment-link-fixer.php000066600000007123151130361070012665 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Helpers\Robots_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class Comment_Link_Fixer.
 */
class Comment_Link_Fixer implements Integration_Interface {

	/**
	 * The redirects helper.
	 *
	 * @var Redirect_Helper
	 */
	protected $redirect;

	/**
	 * The robots helper.
	 *
	 * @var Robots_Helper
	 */
	protected $robots;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Comment_Link_Fixer constructor.
	 *
	 * @codeCoverageIgnore It only sets depedencies.
	 *
	 * @param Redirect_Helper $redirect The redirect helper.
	 * @param Robots_Helper   $robots   The robots helper.
	 */
	public function __construct(
		Redirect_Helper $redirect, Robots_Helper $robots
	) {
		$this->redirect = $redirect;
		$this->robots   = $robots;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->clean_reply_to_com() ) {
			\add_filter( 'comment_reply_link', [ $this, 'remove_reply_to_com' ] );
			\add_action( 'template_redirect', [ $this, 'replytocom_redirect' ], 1 );
		}

		// When users view a reply to a comment, this URL parameter is set. These should never be indexed separately.
		if ( $this->has_replytocom_parameter() ) {
			\add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] );
		}
	}

	/**
	 * Checks if the url contains the ?replytocom query parameter.
	 *
	 * @codeCoverageIgnore Wraps the filter input.
	 *
	 * @return string The value of replytocom.
	 */
	protected function has_replytocom_parameter() {
		return \filter_input( \INPUT_GET, 'replytocom' );
	}

	/**
	 * Removes the ?replytocom variable from the link, replacing it with a #comment-<number> anchor.
	 *
	 * @todo Should this function also allow for relative urls ?
	 *
	 * @param string $link The comment link as a string.
	 *
	 * @return string The modified link.
	 */
	public function remove_reply_to_com( $link ) {
		return \preg_replace( '`href=(["\'])(?:.*(?:\?|&|&#038;)replytocom=(\d+)#respond)`', 'href=$1#comment-$2', $link );
	}

	/**
	 * Redirects out the ?replytocom variables.
	 *
	 * @return bool True when redirect has been done.
	 */
	public function replytocom_redirect() {
		if ( isset( $_GET['replytocom'] ) && \is_singular() ) {
			$url          = \get_permalink( $GLOBALS['post']->ID );
			$hash         = \sanitize_text_field( \wp_unslash( $_GET['replytocom'] ) );
			$query_string = '';
			if ( isset( $_SERVER['QUERY_STRING'] ) ) {
				$query_string = \remove_query_arg( 'replytocom', \sanitize_text_field( \wp_unslash( $_SERVER['QUERY_STRING'] ) ) );
			}
			if ( ! empty( $query_string ) ) {
				$url .= '?' . $query_string;
			}
			$url .= '#comment-' . $hash;

			$this->redirect->do_safe_redirect( $url, 301 );

			return true;
		}

		return false;
	}

	/**
	 * Checks whether we can allow the feature that removes ?replytocom query parameters.
	 *
	 * @codeCoverageIgnore It just wraps a call to a filter.
	 *
	 * @return bool True to remove, false not to remove.
	 */
	private function clean_reply_to_com() {
		/**
		 * Filter: 'wpseo_remove_reply_to_com' - Allow disabling the feature that removes ?replytocom query parameters.
		 *
		 * @param bool $return True to remove, false not to remove.
		 */
		return (bool) \apply_filters( 'wpseo_remove_reply_to_com', true );
	}
}
front-end/indexing-controls.php000066600000005253151130361070012625 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Front_End;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Robots_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class Indexing_Controls.
 */
class Indexing_Controls implements Integration_Interface {

	/**
	 * The robots helper.
	 *
	 * @var Robots_Helper
	 */
	protected $robots;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * The constructor.
	 *
	 * @codeCoverageIgnore Sets the dependencies.
	 *
	 * @param Robots_Helper $robots The robots helper.
	 */
	public function __construct( Robots_Helper $robots ) {
		$this->robots = $robots;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function register_hooks() {
		// The option `blog_public` is set in Settings > Reading > Search Engine Visibility.
		if ( (string) \get_option( 'blog_public' ) === '0' ) {
			\add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] );
		}

		\add_action( 'template_redirect', [ $this, 'noindex_robots' ] );
		\add_filter( 'loginout', [ $this, 'nofollow_link' ] );
		\add_filter( 'register', [ $this, 'nofollow_link' ] );

		// Remove actions that we will handle through our wpseo_head call, and probably change the output of.
		\remove_action( 'wp_head', 'rel_canonical' );
		\remove_action( 'wp_head', 'index_rel_link' );
		\remove_action( 'wp_head', 'start_post_rel_link' );
		\remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
		\remove_action( 'wp_head', 'noindex', 1 );
	}

	/**
	 * Sends a Robots HTTP header preventing URL from being indexed in the search results while allowing search engines
	 * to follow the links in the object at the URL.
	 *
	 * @return bool Boolean indicating whether the noindex header was sent.
	 */
	public function noindex_robots() {
		if ( ! \is_robots() ) {
			return false;
		}

		return $this->set_robots_header();
	}

	/**
	 * Adds rel="nofollow" to a link, only used for login / registration links.
	 *
	 * @param string $input The link element as a string.
	 *
	 * @return string
	 */
	public function nofollow_link( $input ) {
		return \str_replace( '<a ', '<a rel="nofollow" ', $input );
	}

	/**
	 * Sets the x-robots-tag to noindex follow.
	 *
	 * @codeCoverageIgnore Too difficult to test.
	 *
	 * @return bool
	 */
	protected function set_robots_header() {
		if ( \headers_sent() === false ) {
			\header( 'X-Robots-Tag: noindex, follow', true );

			return true;
		}

		return false;
	}
}
uninstall-integration.php000066600000001756151130361070011621 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\No_Conditionals;

/**
 * Class to manage the integration with the WP uninstall flow.
 */
class Uninstall_Integration implements Integration_Interface {

	use No_Conditionals;

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'uninstall_' . \WPSEO_BASENAME, [ $this, 'wpseo_uninstall' ] );
	}

	/**
	 * Performs all necessary actions that should happen upon plugin uninstall.
	 *
	 * @return void
	 */
	public function wpseo_uninstall() {
		$this->clear_import_statuses();
	}

	/**
	 * Clears the persistent import statuses.
	 *
	 * @return void
	 */
	public function clear_import_statuses() {
		$yoast_options = \get_site_option( 'wpseo' );

		if ( isset( $yoast_options['importing_completed'] ) ) {
			$yoast_options['importing_completed'] = [];

			\update_site_option( 'wpseo', $yoast_options );
		}
	}
}
blocks/abstract-dynamic-block.php000066600000002365151130361070013056 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Dynamic_Block class.
 */
abstract class Dynamic_Block implements Integration_Interface {

	/**
	 * The name of the block.
	 *
	 * @var string
	 */
	protected $block_name;

	/**
	 * The editor script for the block.
	 *
	 * @var string
	 */
	protected $script;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'init', [ $this, 'register_block' ], 11 );
	}

	/**
	 * Registers the block.
	 *
	 * @return void
	 */
	public function register_block() {
		\register_block_type(
			'yoast-seo/' . $this->block_name,
			[
				'editor_script'   => $this->script,
				'render_callback' => [ $this, 'present' ],
				'attributes'      => [
					'className' => [
						'default' => '',
						'type'    => 'string',
					],
				],
			]
		);
	}

	/**
	 * Presents the block output. This is abstract because in the loop we need to be able to build the data for the
	 * presenter in the last moment.
	 *
	 * @param array $attributes The block attributes.
	 *
	 * @return string The block output.
	 */
	abstract public function present( $attributes );
}
blocks/block-categories.php000066600000003477151130361070011763 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use Yoast\WP\SEO\Helpers\Wordpress_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Internal_Linking_Category block class.
 */
class Internal_Linking_Category implements Integration_Interface {

	/**
	 * Represents the WordPress helper.
	 *
	 * @var Wordpress_Helper
	 */
	protected $wordpress_helper;

	/**
	 * Internal_Linking_Category constructor.
	 *
	 * @param Wordpress_Helper $wordpress_helper The WordPress helper.
	 */
	public function __construct( Wordpress_Helper $wordpress_helper ) {
		$this->wordpress_helper = $wordpress_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		$wordpress_version = $this->wordpress_helper->get_wordpress_version();

		// The 'block_categories' filter has been deprecated in WordPress 5.8 and replaced by 'block_categories_all'.
		if ( \version_compare( $wordpress_version, '5.8-beta0', '<' ) ) {
			\add_filter( 'block_categories', [ $this, 'add_block_categories' ] );
		}
		else {
			\add_filter( 'block_categories_all', [ $this, 'add_block_categories' ] );
		}
	}

	/**
	 * Adds Yoast block categories.
	 *
	 * @param array $categories The categories.
	 * @return array The filtered categories.
	 */
	public function add_block_categories( $categories ) {
		$categories[] = [
			'slug'  => 'yoast-structured-data-blocks',
			'title' => \sprintf(
				/* translators: %1$s expands to Yoast. */
				\__( '%1$s Structured Data Blocks', 'wordpress-seo' ),
				'Yoast'
			),
		];
		$categories[] = [
			'slug'  => 'yoast-internal-linking-blocks',
			'title' => \sprintf(
				/* translators: %1$s expands to Yoast. */
				\__( '%1$s Internal Linking Blocks', 'wordpress-seo' ),
				'Yoast'
			),
		];

		return $categories;
	}
}
blocks/structured-data-blocks.php000066600000022552151130361070013127 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class to load assets required for structured data blocks.
 */
class Structured_Data_Blocks implements Integration_Interface {

	use No_Conditionals;

	/**
	 * An instance of the WPSEO_Admin_Asset_Manager class.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * An instance of the image helper class.
	 *
	 * @var Image_Helper
	 */
	protected $image_helper;

	/**
	 * The image caches per post.
	 *
	 * @var array
	 */
	protected $caches = [];

	/**
	 * The used cache keys per post.
	 *
	 * @var array
	 */
	protected $used_caches = [];

	/**
	 * Whether or not we've registered our shutdown function.
	 *
	 * @var bool
	 */
	protected $registered_shutdown_function = false;

	/**
	 * Structured_Data_Blocks constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
	 * @param Image_Helper              $image_helper  The image helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Image_Helper $image_helper
	) {
		$this->asset_manager = $asset_manager;
		$this->image_helper  = $image_helper;
	}

	/**
	 * Registers hooks for Structured Data Blocks with WordPress.
	 */
	public function register_hooks() {
		\add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] );
		$this->register_blocks();
	}

	/**
	 * Registers the blocks.
	 *
	 * @return void
	 */
	public function register_blocks() {
		\register_block_type(
			'yoast/faq-block',
			[
				'render_callback' => [ $this, 'optimize_faq_images' ],
				'attributes'      => [
					'className' => [
						'default' => '',
						'type'    => 'string',
					],
					'questions' => [
						'type' => 'array',
					],
					'additionalListCssClasses' => [
						'type' => 'string',
					],
				],
			]
		);
		\register_block_type(
			'yoast/how-to-block',
			[
				'render_callback' => [ $this, 'optimize_how_to_images' ],
				'attributes'      => [
					'hasDuration' => [
						'type' => 'boolean',
					],
					'days' => [
						'type' => 'string',
					],
					'hours' => [
						'type' => 'string',
					],
					'minutes' => [
						'type' => 'string',
					],
					'description' => [
						'type'     => 'array',
						'source'   => 'children',
						'selector' => '.schema-how-to-description',
					],
					'jsonDescription' => [
						'type' => 'string',
					],
					'steps' => [
						'type' => 'array',
					],
					'additionalListCssClasses' => [
						'type' => 'string',
					],
					'unorderedList' => [
						'type' => 'boolean',
					],
					'durationText' => [
						'type' => 'string',
					],
					'defaultDurationText' => [
						'type' => 'string',
					],
				],
			]
		);
	}

	/**
	 * Enqueue Gutenberg block assets for backend editor.
	 */
	public function enqueue_block_editor_assets() {
		/**
		 * Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely.
		 *
		 * @api bool If false, our structured data blocks won't show.
		 */
		if ( ! \apply_filters( 'wpseo_enable_structured_data_blocks', true ) ) {
			return;
		}

		$this->asset_manager->enqueue_script( 'structured-data-blocks' );
		$this->asset_manager->enqueue_style( 'structured-data-blocks' );
	}

	/**
	 * Optimizes images in the FAQ blocks.
	 *
	 * @param array  $attributes The attributes.
	 * @param string $content    The content.
	 *
	 * @return string The content with images optimized.
	 */
	public function optimize_faq_images( $attributes, $content ) {
		if ( ! isset( $attributes['questions'] ) ) {
			return $content;
		}

		return $this->optimize_images( $attributes['questions'], 'answer', $content );
	}

	/**
	 * Optimizes images in the How-To blocks.
	 *
	 * @param array  $attributes The attributes.
	 * @param string $content    The content.
	 *
	 * @return string The content with images optimized.
	 */
	public function optimize_how_to_images( $attributes, $content ) {
		if ( ! isset( $attributes['steps'] ) ) {
			return $content;
		}

		return $this->optimize_images( $attributes['steps'], 'text', $content );
	}

	/**
	 * Optimizes images in structured data blocks.
	 *
	 * @param array  $elements The list of elements from the block attributes.
	 * @param string $key      The key in the data to iterate over.
	 * @param string $content  The content.
	 *
	 * @return string The content with images optimized.
	 */
	private function optimize_images( $elements, $key, $content ) {
		global $post;
		if ( ! $post ) {
			return $content;
		}

		$this->add_images_from_attributes_to_used_cache( $post->ID, $elements, $key );

		// Then replace all images with optimized versions in the content.
		$content = \preg_replace_callback(
			'/<img[^>]+>/',
			function ( $matches ) {
				\preg_match( '/src="([^"]+)"/', $matches[0], $src_matches );
				if ( ! $src_matches || ! isset( $src_matches[1] ) ) {
					return $matches[0];
				}
				$attachment_id = $this->attachment_src_to_id( $src_matches[1] );
				if ( $attachment_id === 0 ) {
					return $matches[0];
				}
				$image_size  = 'full';
				$image_style = [ 'style' => 'max-width: 100%; height: auto;' ];
				\preg_match( '/style="[^"]*width:\s*(\d+)px[^"]*"/', $matches[0], $style_matches );
				if ( $style_matches && isset( $style_matches[1] ) ) {
					$width     = (int) $style_matches[1];
					$meta_data = \wp_get_attachment_metadata( $attachment_id );
					if ( isset( $meta_data['height'] ) && isset( $meta_data['width'] ) && $meta_data['height'] > 0 && $meta_data['width'] > 0 ) {
						$aspect_ratio = ( $meta_data['height'] / $meta_data['width'] );
						$height       = ( $width * $aspect_ratio );
						$image_size   = [ $width, $height ];
					}
					$image_style = '';
				}

				/**
				 * Filter: 'wpseo_structured_data_blocks_image_size' - Allows adjusting the image size in structured data blocks.
				 *
				 * @since 18.2
				 *
				 * @param string|int[] $image_size     The image size. Accepts any registered image size name, or an array of width and height values in pixels (in that order).
				 * @param int          $attachment_id  The id of the attachment.
				 * @param string       $attachment_src The attachment src.
				 */
				$image_size = \apply_filters(
					'wpseo_structured_data_blocks_image_size',
					$image_size,
					$attachment_id,
					$src_matches[1]
				);
				$image_html = \wp_get_attachment_image(
					$attachment_id,
					$image_size,
					false,
					$image_style
				);

				if ( empty( $image_html ) ) {
					return $matches[0];
				}

				return $image_html;
			},
			$content
		);

		if ( ! $this->registered_shutdown_function ) {
			\register_shutdown_function( [ $this, 'maybe_save_used_caches' ] );
			$this->registered_shutdown_function = true;
		}

		return $content;
	}

	/**
	 * If the caches of structured data block images have been changed, saves them.
	 *
	 * @return void
	 */
	public function maybe_save_used_caches() {
		foreach ( $this->used_caches as $post_id => $used_cache ) {
			if ( isset( $this->caches[ $post_id ] ) && $used_cache === $this->caches[ $post_id ] ) {
				continue;
			}
			\update_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', $used_cache );
		}
	}

	/**
	 * Converts an attachment src to an attachment ID.
	 *
	 * @param string $src The attachment src.
	 *
	 * @return int The attachment ID. 0 if none was found.
	 */
	private function attachment_src_to_id( $src ) {
		global $post;

		if ( isset( $this->used_caches[ $post->ID ][ $src ] ) ) {
			return $this->used_caches[ $post->ID ][ $src ];
		}

		$cache = $this->get_cache_for_post( $post->ID );
		if ( isset( $cache[ $src ] ) ) {
			$this->used_caches[ $post->ID ][ $src ] = $cache[ $src ];
			return $cache[ $src ];
		}

		$this->used_caches[ $post->ID ][ $src ] = $this->image_helper->get_attachment_by_url( $src );
		return $this->used_caches[ $post->ID ][ $src ];
	}

	/**
	 * Returns the cache from postmeta for a given post.
	 *
	 * @param int $post_id The post ID.
	 *
	 * @return array The images cache.
	 */
	private function get_cache_for_post( $post_id ) {
		if ( isset( $this->caches[ $post_id ] ) ) {
			return $this->caches[ $post_id ];
		}

		$cache = \get_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', true );
		if ( ! $cache ) {
			$cache = [];
		}

		$this->caches[ $post_id ] = $cache;
		return $cache;
	}

	/**
	 * Adds any images that have their ID in the block attributes to the cache.
	 *
	 * @param int    $post_id  The post ID.
	 * @param array  $elements The elements.
	 * @param string $key      The key in the elements we should loop over.
	 *
	 * @return void
	 */
	private function add_images_from_attributes_to_used_cache( $post_id, $elements, $key ) {
		// First grab all image IDs from the attributes.
		$images = [];
		foreach ( $elements as $element ) {
			if ( ! isset( $element[ $key ] ) ) {
				continue;
			}
			foreach ( $element[ $key ] as $part ) {
				if ( ! \is_array( $part ) || ! isset( $part['type'] ) || $part['type'] !== 'img' ) {
					continue;
				}

				if ( ! isset( $part['key'] ) || ! isset( $part['props']['src'] ) ) {
					continue;
				}

				$images[ $part['props']['src'] ] = (int) $part['key'];
			}
		}

		if ( isset( $this->used_caches[ $post_id ] ) ) {
			$this->used_caches[ $post_id ] = \array_merge( $this->used_caches[ $post_id ], $images );
		}
		else {
			$this->used_caches[ $post_id ] = $images;
		}
	}
}
blocks/breadcrumbs-block.php000066600000007176151130361070012127 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Helpers\Request_Helper;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * Siblings block class
 */
class Breadcrumbs_Block extends Dynamic_Block {

	/**
	 * The name of the block.
	 *
	 * @var string
	 */
	protected $block_name = 'breadcrumbs';

	/**
	 * The editor script for the block.
	 *
	 * @var string
	 */
	protected $script = 'yoast-seo-dynamic-blocks';

	/**
	 * The Meta_Tags_Context_Memoizer.
	 *
	 * @var Meta_Tags_Context_Memoizer
	 */
	private $context_memoizer;

	/**
	 * The Replace vars helper.
	 *
	 * @var WPSEO_Replace_Vars
	 */
	private $replace_vars;

	/**
	 * The helpers surface.
	 *
	 * @var Helpers_Surface
	 */
	private $helpers;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	private $indexable_repository;

	/**
	 * The request helper.
	 *
	 * @var Request_Helper
	 */
	private $request_helper;

	/**
	 * Siblings_Block constructor.
	 *
	 * @param Meta_Tags_Context_Memoizer $context_memoizer     The context.
	 * @param WPSEO_Replace_Vars         $replace_vars         The replace variable helper.
	 * @param Helpers_Surface            $helpers              The Helpers surface.
	 * @param Indexable_Repository       $indexable_repository The indexable repository.
	 * @param Request_Helper             $request_helper       The request helper.
	 */
	public function __construct(
		Meta_Tags_Context_Memoizer $context_memoizer,
		WPSEO_Replace_Vars $replace_vars,
		Helpers_Surface $helpers,
		Indexable_Repository $indexable_repository,
		Request_Helper $request_helper
	) {
		$this->context_memoizer     = $context_memoizer;
		$this->replace_vars         = $replace_vars;
		$this->helpers              = $helpers;
		$this->indexable_repository = $indexable_repository;
		$this->request_helper       = $request_helper;
	}

	/**
	 * Presents the breadcrumbs output for the current page or the available post_id.
	 *
	 * @param array $attributes The block attributes.
	 *
	 * @return string The block output.
	 */
	public function present( $attributes ) {
		$presenter = new Breadcrumbs_Presenter();
		// $this->context_memoizer->for_current_page only works on the frontend. To render the right breadcrumb in the
		// editor, we need the repository.
		if ( $this->request_helper->is_rest_request() || \is_admin() ) {
			$post_id = \get_the_ID();
			if ( $post_id ) {
				$indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' );

				if ( ! $indexable ) {
					$post      = \get_post( $post_id );
					$indexable = $this->indexable_repository->query()->create(
						[
							'object_id'        => $post_id,
							'object_type'      => 'post',
							'object_sub_type'  => $post->post_type,
						]
					);
				}

				$context = $this->context_memoizer->get( $indexable, 'Post_Type' );
			}
		}
		if ( ! isset( $context ) ) {
			$context = $this->context_memoizer->for_current_page();
		}

		/** This filter is documented in src/integrations/front-end-integration.php */
		$presentation            = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
		$presenter->presentation = $presentation;
		$presenter->replace_vars = $this->replace_vars;
		$presenter->helpers      = $this->helpers;
		$class_name              = 'yoast-breadcrumbs';

		if ( ! empty( $attributes['className'] ) ) {
			$class_name .= ' ' . \esc_attr( $attributes['className'] );
		}

		return '<div class="' . $class_name . '">' . $presenter->present() . '</div>';
	}
}
feature-flag-integration.php000066600000005672151130361070012153 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Feature_Flag_Conditional;

/**
 * Gathers all feature flags and surfaces them to the JavaScript side of the plugin.
 */
class Feature_Flag_Integration implements Integration_Interface {

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * All of the feature flag conditionals.
	 *
	 * @var Feature_Flag_Conditional[]
	 */
	protected $feature_flags;

	/**
	 * Feature_Flag_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager    The admin asset manager.
	 * @param Feature_Flag_Conditional  ...$feature_flags All of the known feature flag conditionals.
	 */
	public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Feature_Flag_Conditional ...$feature_flags ) {
		$this->asset_manager = $asset_manager;
		$this->feature_flags = $feature_flags;
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return string[] The conditionals based on which this loadable should be active.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'add_feature_flags' ] );
	}

	/**
	 * Gathers all the feature flags and injects them into the JavaScript.
	 *
	 * @return void
	 */
	public function add_feature_flags() {
		$enabled_features = $this->get_enabled_features();
		// Localize under both names for BC.
		$this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeatureFlags', $enabled_features );
		$this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeaturesL10n', $enabled_features );
	}

	/**
	 * Returns an array of all enabled feature flags.
	 *
	 * @return string[] The array of enabled features.
	 */
	public function get_enabled_features() {
		$enabled_features = [];
		foreach ( $this->feature_flags as $feature_flag ) {
			if ( $feature_flag->is_met() ) {
				$enabled_features[] = $feature_flag->get_feature_name();
			}
		}

		return $this->filter_enabled_features( $enabled_features );
	}

	/**
	 * Runs the list of enabled feature flags through a filter.
	 *
	 * @param string[] $enabled_features The list of currently enabled feature flags.
	 *
	 * @return string[] The (possibly adapted) list of enabled features.
	 */
	protected function filter_enabled_features( $enabled_features ) {
		/**
		 * Filters the list of currently enabled feature flags.
		 *
		 * @param string[] $enabled_features The current list of enabled feature flags.
		 */
		$filtered_enabled_features = \apply_filters( 'wpseo_enable_feature', $enabled_features );

		if ( ! \is_array( $filtered_enabled_features ) ) {
			$filtered_enabled_features = $enabled_features;
		}

		return $filtered_enabled_features;
	}
}
abstract-exclude-post-type.php000066600000002177151130361070012461 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

/**
 * Abstract class for excluding certain post types from being indexed.
 */
abstract class Abstract_Exclude_Post_Type implements Integration_Interface {

	/**
	 * Initializes the integration.
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_indexable_excluded_post_types', [ $this, 'exclude_post_types' ] );
	}

	/**
	 * Exclude the post type from the indexable table.
	 *
	 * @param array $excluded_post_types The excluded post types.
	 *
	 * @return array The excluded post types, including the specific post type.
	 */
	public function exclude_post_types( $excluded_post_types ) {
		return \array_merge( $excluded_post_types, $this->get_post_type() );
	}

	/**
	 * This integration is only active when the child class's conditionals are met.
	 *
	 * @return array|string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * Returns the names of the post types to be excluded.
	 * To be used in the wpseo_indexable_excluded_post_types filter.
	 *
	 * @return array The names of the post types.
	 */
	abstract public function get_post_type();
}
duplicate-post-integration.php000066600000001534151130361070012537 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\No_Conditionals;

/**
 * Class to manage the integration with Yoast Duplicate Post.
 */
class Duplicate_Post_Integration implements Integration_Interface {

	use No_Conditionals;

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'duplicate_post_excludelist_filter', [ $this, 'exclude_zapier_meta' ] );
	}

	/**
	 * Filters out the Zapier meta when you copy a post with Yoast Duplicate Post.
	 *
	 * @param array $meta_excludelist The current excludelist of meta fields.
	 *
	 * @return array The updated excludelist.
	 */
	public function exclude_zapier_meta( $meta_excludelist ) {
		$meta_excludelist[] = 'zapier_trigger_sent';
		return $meta_excludelist;
	}
}
estimated-reading-time.php000066600000002127151130361070011602 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional;

/**
 * Estimated reading time class.
 */
class Estimated_Reading_Time implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Estimated_Reading_Time_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_metabox_entries_general', [ $this, 'add_estimated_reading_time_hidden_fields' ] );
	}

	/**
	 * Adds an estimated-reading-time hidden field.
	 *
	 * @param array $field_defs The $fields_defs.
	 *
	 * @return array
	 */
	public function add_estimated_reading_time_hidden_fields( $field_defs ) {
		if ( \is_array( $field_defs ) ) {
			$field_defs['estimated-reading-time-minutes'] = [
				'type'  => 'hidden',
				'title' => 'estimated-reading-time-minutes',
			];
		}

		return $field_defs;
	}
}
integration-interface.php000066600000000652151130361070011542 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Loadable_Interface;

/**
 * An interface for registering integrations with WordPress.
 *
 * @codeCoverageIgnore It represents an interface.
 */
interface Integration_Interface extends Loadable_Interface {

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks();
}
third-party/elementor.php000066600000057665151130361070011542 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Elementor\Controls_Manager;
use Elementor\Core\DocumentTypes\PageBase;
use WP_Post;
use WP_Screen;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Admin_Recommended_Replace_Vars;
use WPSEO_Language_Utils;
use WPSEO_Meta;
use WPSEO_Metabox_Analysis_Readability;
use WPSEO_Metabox_Analysis_SEO;
use WPSEO_Metabox_Formatter;
use WPSEO_Post_Metabox_Formatter;
use WPSEO_Replace_Vars;
use WPSEO_Shortlinker;
use WPSEO_Utils;
use Yoast\WP\SEO\Actions\Alert_Dismissal_Action;
use Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Meta_Fields_Presenter;

/**
 * Integrates the Yoast SEO metabox in the Elementor editor.
 */
class Elementor implements Integration_Interface {

	/**
	 * The identifier for the elementor tab.
	 */
	const YOAST_TAB = 'yoast-tab';

	/**
	 * Represents the post.
	 *
	 * @var WP_Post|null
	 */
	protected $post;

	/**
	 * Represents the admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * Represents the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * Represents the capability helper.
	 *
	 * @var Capability_Helper
	 */
	protected $capability;

	/**
	 * Holds whether the socials are enabled.
	 *
	 * @var bool
	 */
	protected $social_is_enabled;

	/**
	 * Holds whether the advanced settings are enabled.
	 *
	 * @var bool
	 */
	protected $is_advanced_metadata_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;

	/**
	 * Represents the estimated_reading_time_conditional.
	 *
	 * @var Estimated_Reading_Time_Conditional
	 */
	protected $estimated_reading_time_conditional;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Elementor_Edit_Conditional::class ];
	}

	/**
	 * Constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager          $asset_manager                      The asset manager.
	 * @param Options_Helper                     $options                            The options helper.
	 * @param Capability_Helper                  $capability                         The capability helper.
	 * @param Estimated_Reading_Time_Conditional $estimated_reading_time_conditional The Estimated Reading Time
	 *                                                                               conditional.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Options_Helper $options,
		Capability_Helper $capability,
		Estimated_Reading_Time_Conditional $estimated_reading_time_conditional
	) {
		$this->asset_manager = $asset_manager;
		$this->options       = $options;
		$this->capability    = $capability;

		$this->seo_analysis                       = new WPSEO_Metabox_Analysis_SEO();
		$this->readability_analysis               = new WPSEO_Metabox_Analysis_Readability();
		$this->social_is_enabled                  = $this->options->get( 'opengraph', false ) || $this->options->get( 'twitter', false );
		$this->is_advanced_metadata_enabled       = $this->capability->current_user_can( 'wpseo_edit_advanced_metadata' ) || $this->options->get( 'disableadvanced_meta' ) === false;
		$this->estimated_reading_time_conditional = $estimated_reading_time_conditional;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'wp_ajax_wpseo_elementor_save', [ $this, 'save_postdata' ] );

		// We need to delay the post type lookup to give other plugins a chance to register custom post types.
		\add_action( 'init', [ $this, 'register_elementor_hooks' ], \PHP_INT_MAX );
	}

	/**
	 * Registers our Elementor hooks.
	 */
	public function register_elementor_hooks() {
		if ( ! $this->display_metabox( $this->get_metabox_post()->post_type ) ) {
			return;
		}

		\add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'init' ] );

		// We are too late for elementor/init. We should see if we can be on time, or else this workaround works (we do always get the "else" though).
		if ( ! \did_action( 'elementor/init' ) ) {
			\add_action( 'elementor/init', [ $this, 'add_yoast_panel_tab' ] );
		}
		else {
			$this->add_yoast_panel_tab();
		}
		\add_action( 'elementor/documents/register_controls', [ $this, 'register_document_controls' ] );
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function init() {
		$this->asset_manager->register_assets();
		$this->enqueue();
		$this->render_hidden_fields();
	}

	/**
	 * Register a panel tab slug, in order to allow adding controls to this tab.
	 */
	public function add_yoast_panel_tab() {
		Controls_Manager::add_tab( $this::YOAST_TAB, 'Yoast SEO' );
	}

	/**
	 * Register additional document controls.
	 *
	 * @param PageBase $document The PageBase document.
	 */
	public function register_document_controls( $document ) {
		// PageBase is the base class for documents like `post` `page` and etc.
		if ( ! $document instanceof PageBase || ! $document::get_property( 'has_elements' ) ) {
			return;
		}

		// This is needed to get the tab to appear, but will be overwritten in the JavaScript.
		$document->start_controls_section(
			'yoast_temporary_section',
			[
				'label' => 'Yoast SEO',
				'tab'   => self::YOAST_TAB,
			]
		);

		$document->end_controls_section();
	}

	// Below is mostly copied from `class-metabox.php`. That constructor has side-effects we do not need.

	/**
	 * 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 );
	}

	/**
	 * Saves the WP SEO metadata for posts.
	 *
	 * Outputs JSON via wp_send_json then stops code execution.
	 *
	 * {@internal $_POST parameters are validated via sanitize_post_meta().}}
	 *
	 * @return void
	 */
	public function save_postdata() {
		global $post;

		$post_id = \filter_input( \INPUT_POST, 'post_id', \FILTER_SANITIZE_NUMBER_INT );

		if ( ! \current_user_can( 'edit_post', $post_id ) ) {
			\wp_send_json_error( 'Forbidden', 403 );
		}

		\check_ajax_referer( 'wpseo_elementor_save', '_wpseo_elementor_nonce' );

		// Bail if this is a multisite installation and the site has been switched.
		if ( \is_multisite() && \ms_is_switched() ) {
			\wp_send_json_error( 'Switched multisite', 409 );
		}

		\clean_post_cache( $post_id );
		// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
		$post = \get_post( $post_id );

		if ( ! \is_object( $post ) ) {
			// Non-existent post.
			\wp_send_json_error( 'Post not found', 400 );
		}

		\do_action( 'wpseo_save_compare_data', $post );

		// Initialize meta, amongst other things it registers sanitization.
		WPSEO_Meta::init();

		$social_fields = [];
		if ( $this->social_is_enabled ) {
			$social_fields = WPSEO_Meta::get_meta_field_defs( 'social', $post->post_type );
		}

		// The below methods use the global post so make sure it is setup.
		\setup_postdata( $post );
		$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', $post->post_type ),
			$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 -- Reason: Sanitized through sanitize_post_meta.
					$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 );
			}
		}

		// Saving the WP post to save the slug.
		$slug = \filter_input( \INPUT_POST, WPSEO_Meta::$form_prefix . 'slug', \FILTER_SANITIZE_STRING );
		if ( $post->post_name !== $slug ) {
			$post_array              = $post->to_array();
			$post_array['post_name'] = $slug;

			$save_successful = \wp_insert_post( $post_array );
			if ( \is_wp_error( $save_successful ) ) {
				\wp_send_json_error( 'Slug not saved', 400 );
			}

			// Update the post object to ensure we have the actual slug.
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Updating the post is needed to get the current slug.
			$post = \get_post( $post_id );
			if ( ! \is_object( $post ) ) {
				\wp_send_json_error( 'Updated slug not found', 400 );
			}
		}

		\do_action( 'wpseo_saved_postdata' );

		// Output the slug, because it is processed by WP and we need the actual slug again.
		\wp_send_json_success( [ 'slug' => $post->post_name ] );
	}

	/**
	 * 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;
		}

		return false;
	}

	/**
	 * Enqueues all the needed JS and CSS.
	 *
	 * @return void
	 */
	public function enqueue() {
		$post_id = \get_queried_object_id();
		if ( empty( $post_id ) ) {
			$post_id = \sanitize_text_field( \filter_input( \INPUT_GET, 'post' ) );
		}

		if ( $post_id !== 0 ) {
			// Enqueue files needed for upload functionality.
			\wp_enqueue_media( [ 'post' => $post_id ] );
		}

		$this->asset_manager->enqueue_style( 'admin-global' );
		$this->asset_manager->enqueue_style( 'metabox-css' );
		$this->asset_manager->enqueue_style( 'scoring' );
		$this->asset_manager->enqueue_style( 'select2' );
		$this->asset_manager->enqueue_style( 'monorepo' );
		$this->asset_manager->enqueue_style( 'admin-css' );
		$this->asset_manager->enqueue_style( 'elementor' );

		$this->asset_manager->enqueue_script( 'admin-global' );
		$this->asset_manager->enqueue_script( 'elementor' );

		$this->asset_manager->localize_script( 'elementor', 'wpseoAdminGlobalL10n', \YoastSEO()->helpers->wincher->get_admin_global_links() );
		$this->asset_manager->localize_script( 'elementor', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() );
		$this->asset_manager->localize_script( 'elementor', 'wpseoFeaturesL10n', WPSEO_Utils::retrieve_enabled_features() );

		$plugins_script_data = [
			'replaceVars' => [
				'no_parent_text'           => \__( '(no parent)', 'wordpress-seo' ),
				'replace_vars'             => $this->get_replace_vars(),
				'recommended_replace_vars' => $this->get_recommended_replace_vars(),
				'hidden_replace_vars'      => $this->get_hidden_replace_vars(),
				'scope'                    => $this->determine_scope(),
				'has_taxonomies'           => $this->current_post_type_has_taxonomies(),
			],
			'shortcodes'  => [
				'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ),
				'wpseo_shortcode_tags'          => $this->get_valid_shortcode_tags(),
			],
		];

		$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(),
			// We need to make the feature flags separately available inside of the analysis web worker.
			'enabled_features'        => WPSEO_Utils::retrieve_enabled_features(),
		];

		$alert_dismissal_action = \YoastSEO()->classes->get( Alert_Dismissal_Action::class );
		$dismissed_alerts       = $alert_dismissal_action->all_dismissed();

		$script_data = [
			'media'                    => [ 'choose_image' => \__( 'Use Image', 'wordpress-seo' ) ],
			'metabox'                  => $this->get_metabox_script_data(),
			'userLanguageCode'         => WPSEO_Language_Utils::get_language( \get_user_locale() ),
			'isPost'                   => true,
			'isBlockEditor'            => WP_Screen::get()->is_block_editor(),
			'isElementorEditor'        => true,
			'postStatus'               => \get_post_status( $post_id ),
			'analysis'                 => [
				'plugins' => $plugins_script_data,
				'worker'  => $worker_script_data,
			],
			'dismissedAlerts'          => $dismissed_alerts,
			'webinarIntroElementorUrl' => WPSEO_Shortlinker::get( 'https://yoa.st/webinar-intro-elementor' ),
		];

		if ( \post_type_supports( $this->get_metabox_post()->post_type, 'thumbnail' ) ) {
			$this->asset_manager->enqueue_style( 'featured-image' );

			$script_data['featuredImage'] = [
				'featured_image_notice' => \__( 'SEO issue: The featured image should be at least 200 by 200 pixels to be picked up by Facebook and other social media sites.', 'wordpress-seo' ),
			];
		}

		$this->asset_manager->localize_script( 'elementor', 'wpseoScriptData', $script_data );
		$this->asset_manager->enqueue_user_language_script();
	}

	/**
	 * Renders the metabox hidden fields.
	 *
	 * @return void
	 */
	protected function render_hidden_fields() {
		// Wrap in a form with an action and post_id for the submit.
		\printf(
			'<form id="yoast-form" method="post" action="%1$s"><input type="hidden" name="action" value="wpseo_elementor_save" /><input type="hidden" id="post_ID" name="post_id" value="%2$s" />',
			\esc_url( \admin_url( 'admin-ajax.php' ) ),
			\esc_attr( $this->get_metabox_post()->ID )
		);

		\wp_nonce_field( 'wpseo_elementor_save', '_wpseo_elementor_nonce' );
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
		echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'general' );

		if ( $this->is_advanced_metadata_enabled ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
			echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'advanced' );
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
		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 -- Reason: Meta_Fields_Presenter->present is considered safe.
			echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' );
		}

		\printf(
			'<input type="hidden" id="%1$s" name="%1$s" value="%2$s" />',
			\esc_attr( WPSEO_Meta::$form_prefix . 'slug' ),
			\esc_attr( $this->get_post_slug() )
		);

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be escaped in the filter.
		echo \apply_filters( 'wpseo_elementor_hidden_fields', '' );

		echo '</form>';
	}

	/**
	 * Returns the slug for the post being edited.
	 *
	 * @return string
	 */
	protected function get_post_slug() {
		$post = $this->get_metabox_post();

		// In case get_metabox_post returns null for whatever reason.
		if ( ! $post instanceof WP_Post ) {
			return '';
		}

		// Drafts might not have a post_name unless the slug has been manually changed.
		// In this case we get it using get_sample_permalink.
		if ( ! $post->post_name ) {
			$sample = \get_sample_permalink( $post );

			// Since get_sample_permalink runs through filters, ensure that it has the expected return value.
			if ( \is_array( $sample ) && \count( $sample ) === 2 && \is_string( $sample[1] ) ) {
				return $sample[1];
			}
		}

		return $post->post_name;
	}

	/**
	 * Returns post in metabox context.
	 *
	 * @return WP_Post|null
	 */
	protected function get_metabox_post() {
		if ( $this->post !== null ) {
			return $this->post;
		}

		$post = \filter_input( \INPUT_GET, 'post' );
		if ( ! empty( $post ) ) {
			$post_id = (int) WPSEO_Utils::validate_int( $post );

			$this->post = \get_post( $post_id );

			return $this->post;
		}

		if ( isset( $GLOBALS['post'] ) ) {
			$this->post = $GLOBALS['post'];

			return $this->post;
		}

		return null;
	}

	/**
	 * Passes variables to js for use with the post-scraper.
	 *
	 * @return array
	 */
	protected function get_metabox_script_data() {
		$permalink = '';

		if ( \is_object( $this->get_metabox_post() ) ) {
			$permalink = \get_sample_permalink( $this->get_metabox_post()->ID );
			$permalink = $permalink[0];
		}

		$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', \YoastSEO()->helpers->post_type->get_accessible_post_types() );
		if ( $values['cornerstoneActive'] && ! \in_array( $this->get_metabox_post()->post_type, $post_types, true ) ) {
			$values['cornerstoneActive'] = false;
		}

		return $values;
	}

	/**
	 * Prepares the replace vars for localization.
	 *
	 * @return array Replace vars.
	 */
	protected function get_replace_vars() {
		$cached_replacement_vars = [];

		$vars_to_cache = [
			'date',
			'id',
			'sitename',
			'sitedesc',
			'sep',
			'page',
			'currentyear',
			'currentdate',
			'currentmonth',
			'currentday',
			'tag',
			'category',
			'category_title',
			'primary_category',
			'pt_single',
			'pt_plural',
			'modified',
			'name',
			'user_description',
			'pagetotal',
			'pagenumber',
			'post_year',
			'post_month',
			'post_day',
			'author_first_name',
			'author_last_name',
			'permalink',
			'post_content',
		];

		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() ) );
	}

	/**
	 * Prepares the recommended replace vars for localization.
	 *
	 * @return array Recommended replacement variables.
	 */
	protected 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 );
	}

	/**
	 * 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();
	}

	/**
	 * 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 Array containing all the replacement variables.
	 */
	protected 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 Array containing all the replacement variables.
	 */
	protected 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 Array containing all the replacement variables.
	 */
	protected 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 );

		// Simply concatenate all fields containing replace vars so we can handle them all with a single regex find.
		$replace_vars_fields = implode(
			' ',
			[
				YoastSEO()->meta->for_post( $post->ID )->presentation->title,
				YoastSEO()->meta->for_post( $post->ID )->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;
	}

	/**
	 * 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.
	 */
	protected function determine_scope() {
		if ( $this->get_metabox_post()->post_type === 'page' ) {
			return 'page';
		}

		return 'post';
	}

	/**
	 * Determines whether or not the current post type has registered taxonomies.
	 *
	 * @return bool Whether the current post type has taxonomies.
	 */
	protected function current_post_type_has_taxonomies() {
		$post_taxonomies = \get_object_taxonomies( $this->get_metabox_post()->post_type );

		return ! empty( $post_taxonomies );
	}

	/**
	 * Returns an array with shortcode tags for all registered shortcodes.
	 *
	 * @return array
	 */
	protected function get_valid_shortcode_tags() {
		$shortcode_tags = [];

		foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) {
			$shortcode_tags[] = $tag;
		}

		return $shortcode_tags;
	}
}
third-party/amp.php000066600000002723151130361070010306 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Front_End_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * AMP integration.
 */
class AMP implements Integration_Interface {

	/**
	 * The front end integration.
	 *
	 * @var Front_End_Integration
	 */
	protected $front_end;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Constructs the AMP integration
	 *
	 * @param Front_End_Integration $front_end The front end integration.
	 */
	public function __construct( Front_End_Integration $front_end ) {
		$this->front_end = $front_end;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'amp_post_template_head', [ $this, 'remove_amp_meta_output' ], 0 );
		\add_action( 'amp_post_template_head', [ $this->front_end, 'call_wpseo_head' ], 9 );
	}

	/**
	 * Removes amp meta output.
	 *
	 * @return void
	 */
	public function remove_amp_meta_output() {
		\remove_action( 'amp_post_template_head', 'amp_post_template_add_title' );
		\remove_action( 'amp_post_template_head', 'amp_post_template_add_canonical' );
		\remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
	}
}
third-party/coauthors-plus.php000066600000010113151130361070012511 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WP_User;
use Yoast\WP\SEO\Conditionals\Third_Party\CoAuthors_Plus_Activated_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\CoAuthors_Plus_Flag_Conditional;
use Yoast\WP\SEO\Config\Schema_Types;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
use Yoast\WP\SEO\Generators\Schema\Third_Party\CoAuthor;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;

/**
 * Integrates the multiple authors capability from CoAuthors Plus into Yoast SEO schema.
 */
class CoAuthors_Plus implements Integration_Interface {

	/**
	 * The helpers surface.
	 *
	 * @var Helpers_Surface
	 */
	protected $helpers;

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_schema_graph', [ $this, 'filter_graph' ], 11, 2 );
		\add_filter( 'wpseo_schema_author', [ $this, 'filter_author_graph' ], 11, 4 );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [
			CoAuthors_Plus_Activated_Conditional::class,
			CoAuthors_Plus_Flag_Conditional::class,
		];
	}

	/**
	 * CoAuthors_Plus constructor.
	 *
	 * @codeCoverageIgnore It only sets dependencies.
	 *
	 * @param Helpers_Surface $helpers The helper surface.
	 */
	public function __construct( Helpers_Surface $helpers ) {
		$this->helpers = $helpers;
	}

	/**
	 * Filters the graph output to add authors.
	 *
	 * @param array                   $data                   The schema graph.
	 * @param Meta_Tags_Context       $context                The context object.
	 * @param Abstract_Schema_Piece   $graph_piece_generator  The graph piece generator.
	 * @param Abstract_Schema_Piece[] $graph_piece_generators The graph piece generators.
	 *
	 * @return array The (potentially altered) schema graph.
	 */
	public function filter_author_graph( $data, $context, $graph_piece_generator, $graph_piece_generators ) {
		if ( ! isset( $data['image']['url'] ) ) {
			return $data;
		}

		if ( isset( $data['image']['@id'] ) ) {
			$data['image']['@id'] .= \md5( $data['image']['url'] );
		}

		if ( isset( $data['logo']['@id'] ) ) {
			$data['logo']['@id'] .= \md5( $data['image']['url'] );
		}

		return $data;
	}

	/**
	 * Filters the graph output to add authors.
	 *
	 * @param array             $data    The schema graph.
	 * @param Meta_Tags_Context $context Context object.
	 *
	 * @return array The (potentially altered) schema graph.
	 */
	public function filter_graph( $data, $context ) {
		if ( ! \is_singular() ) {
			return $data;
		}

		if ( ! \function_exists( '\get_coauthors' ) ) {
			return $data;
		}

		/**
		 * Contains the authors from the CoAuthors Plus plugin.
		 *
		 * @var WP_User[] $author_objects
		 */
		$author_objects = \get_coauthors( $context->post->ID );
		if ( \count( $author_objects ) <= 1 ) {
			return $data;
		}

		$ids = [];

		// Add the authors to the schema.
		foreach ( $author_objects as $author ) {
			if ( $author->ID === (int) $context->post->post_author ) {
				continue;
			}
			$author_generator          = new CoAuthor();
			$author_generator->context = $context;
			$author_generator->helpers = $this->helpers;
			$author_data               = $author_generator->generate_from_user_id( $author->ID );
			if ( ! empty( $author_data ) ) {
				$ids[] = [ '@id' => $author_data['@id'] ];
			}
		}

		$schema_types  = new Schema_Types();
		$article_types = $schema_types->get_article_type_options_values();

		// Change the author reference to reference our multiple authors.
		$add_to_graph = false;
		foreach ( $data as $key => $piece ) {
			if ( \in_array( $piece['@type'], $article_types, true ) ) {
				$data[ $key ]['author'] = \array_merge( [ $piece['author'] ], $ids );
				$add_to_graph           = true;
				break;
			}
		}

		if ( $add_to_graph ) {
			if ( ! empty( $author_data ) ) {
				if ( $context->site_represents !== 'person' || $author->ID !== $context->site_user_id ) {
					$data[] = $author_data;
				}
			}
		}

		return $data;
	}
}
third-party/wincher-publish.php000066600000010742151130361070012634 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WP_Post;
use Yoast\WP\SEO\Actions\Wincher\Wincher_Account_Action;
use Yoast\WP\SEO\Actions\Wincher\Wincher_Keyphrases_Action;
use Yoast\WP\SEO\Conditionals\Wincher_Automatically_Track_Conditional;
use Yoast\WP\SEO\Conditionals\Wincher_Conditional;
use Yoast\WP\SEO\Conditionals\Wincher_Enabled_Conditional;
use Yoast\WP\SEO\Conditionals\Wincher_Token_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Handles automatically tracking published posts with Wincher.
 */
class Wincher_Publish implements Integration_Interface {

	/**
	 * The Wincher enabled conditional.
	 *
	 * @var Wincher_Enabled_Conditional
	 */
	protected $wincher_enabled;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The Wincher keyphrases action handler.
	 *
	 * @var Wincher_Keyphrases_Action
	 */
	protected $keyphrases_action;

	/**
	 * The Wincher account action handler.
	 *
	 * @var Wincher_Account_Action
	 */
	protected $account_action;

	/**
	 * Wincher publish constructor.
	 *
	 * @param Wincher_Enabled_Conditional $wincher_enabled   The WPML WPSEO conditional.
	 * @param Options_Helper              $options_helper    The options helper.
	 * @param Wincher_Keyphrases_Action   $keyphrases_action The keyphrases action class.
	 * @param Wincher_Account_Action      $account_action    The account action class.
	 */
	public function __construct(
		Wincher_Enabled_Conditional $wincher_enabled,
		Options_Helper $options_helper,
		Wincher_Keyphrases_Action $keyphrases_action,
		Wincher_Account_Action $account_action
	) {
		$this->wincher_enabled   = $wincher_enabled;
		$this->options_helper    = $options_helper;
		$this->keyphrases_action = $keyphrases_action;
		$this->account_action    = $account_action;
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		/**
		 * Called in the REST API when submitting the post copy in the Block Editor.
		 * Runs the republishing of the copy onto the original.
		 */
		\add_action( 'rest_after_insert_post', [ $this, 'track_after_rest_api_request' ] );

		/**
		 * Called by `wp_insert_post()` when submitting the post copy, which runs in two cases:
		 * - In the Classic Editor, where there's only one request that updates everything.
		 * - In the Block Editor, only when there are custom meta boxes.
		 */
		\add_action( 'wp_insert_post', [ $this, 'track_after_post_request' ], \PHP_INT_MAX, 2 );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * This integration should only be active when the feature is enabled, a token is available and automatically tracking is enabled.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [
			Wincher_Conditional::class,
			Wincher_Enabled_Conditional::class,
			Wincher_Automatically_Track_Conditional::class,
			Wincher_Token_Conditional::class,
		];
	}

	/**
	 * Determines whether the current request is a REST request.
	 *
	 * @return bool Whether the request is a REST request.
	 */
	public function is_rest_request() {
		return \defined( 'REST_REQUEST' ) && \REST_REQUEST;
	}

	/**
	 * Sends the keyphrases associated with the post to Wincher for automatic tracking.
	 *
	 * @param WP_Post $post The post to extract the keyphrases from.
	 *
	 * @return void
	 */
	public function track_request( $post ) {
		if ( ! $post instanceof WP_Post ) {
			return;
		}

		// Filter out empty entries.
		$keyphrases = \array_filter( $this->keyphrases_action->collect_keyphrases_from_post( $post ) );

		if ( ! empty( $keyphrases ) ) {
			$this->keyphrases_action->track_keyphrases( $keyphrases, $this->account_action->check_limit() );
		}
	}

	/**
	 * Republishes the original post with the passed post, when using the Block Editor.
	 *
	 * @param WP_Post $post The copy's post object.
	 *
	 * @return void
	 */
	public function track_after_rest_api_request( $post ) {
		$this->track_request( $post );
	}

	/**
	 * Republishes the original post with the passed post, when using the Classic Editor.
	 *
	 * Runs also in the Block Editor to save the custom meta data only when there
	 * are custom meta boxes.
	 *
	 * @param int     $post_id The copy's post ID.
	 * @param WP_Post $post    The copy's post object.
	 *
	 * @return void
	 */
	public function track_after_post_request( $post_id, $post ) {
		if ( $this->is_rest_request() ) {
			return;
		}

		$this->track_request( $post );
	}
}
third-party/the-events-calendar.php000066600000002447151130361070013365 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional;
use Yoast\WP\SEO\Conditionals\The_Events_Calendar_Conditional;
use Yoast\WP\SEO\Generators\Schema\Third_Party\Events_Calendar_Schema;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class The_Events_Calendar
 */
class The_Events_Calendar implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class, The_Events_Calendar_Conditional::class, Open_Graph_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_schema_graph_pieces', [ $this, 'add_graph_pieces' ], 11, 2 );
	}

	/**
	 * Adds the events graph pieces to the schema collector.
	 *
	 * @param array  $pieces  The current graph pieces.
	 * @param string $context The current context.
	 *
	 * @return array Extended graph pieces.
	 */
	public function add_graph_pieces( $pieces, $context ) {
		$pieces[] = new Events_Calendar_Schema( $context );
		return $pieces;
	}
}
third-party/woocommerce.php000066600000022515151130361070012051 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Pagination_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * WooCommerce integration.
 */
class WooCommerce implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * The WPSEO Replace Vars object.
	 *
	 * @var WPSEO_Replace_Vars
	 */
	private $replace_vars;

	/**
	 * The memoizer for the meta tags context.
	 *
	 * @var Meta_Tags_Context_Memoizer
	 */
	protected $context_memoizer;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	private $repository;

	/**
	 * The pagination helper.
	 *
	 * @var Pagination_Helper
	 */
	protected $pagination_helper;

	/**
	 * The WooCommerce helper.
	 *
	 * @var Woocommerce_Helper
	 */
	private $woocommerce_helper;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ WooCommerce_Conditional::class, Front_End_Conditional::class ];
	}

	/**
	 * WooCommerce constructor.
	 *
	 * @param Options_Helper             $options            The options helper.
	 * @param WPSEO_Replace_Vars         $replace_vars       The replace vars helper.
	 * @param Meta_Tags_Context_Memoizer $context_memoizer   The meta tags context memoizer.
	 * @param Indexable_Repository       $repository         The indexable repository.
	 * @param Pagination_Helper          $pagination_helper  The paginataion helper.
	 * @param Woocommerce_Helper         $woocommerce_helper The WooCommerce helper.
	 */
	public function __construct(
		Options_Helper $options,
		WPSEO_Replace_Vars $replace_vars,
		Meta_Tags_Context_Memoizer $context_memoizer,
		Indexable_Repository $repository,
		Pagination_Helper $pagination_helper,
		Woocommerce_Helper $woocommerce_helper
	) {
		$this->options            = $options;
		$this->replace_vars       = $replace_vars;
		$this->context_memoizer   = $context_memoizer;
		$this->repository         = $repository;
		$this->pagination_helper  = $pagination_helper;
		$this->woocommerce_helper = $woocommerce_helper;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_frontend_page_type_simple_page_id', [ $this, 'get_page_id' ] );
		\add_filter( 'wpseo_breadcrumb_indexables', [ $this, 'add_shop_to_breadcrumbs' ] );

		\add_filter( 'wpseo_title', [ $this, 'title' ], 10, 2 );
		\add_filter( 'wpseo_metadesc', [ $this, 'description' ], 10, 2 );
		\add_filter( 'wpseo_canonical', [ $this, 'canonical' ], 10, 2 );
		\add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 10, 3 );
	}

	/**
	 * Returns the correct canonical when WooCommerce is enabled.
	 *
	 * @param string                      $canonical    The current canonical.
	 * @param Indexable_Presentation|null $presentation The indexable presentation.
	 *
	 * @return string The correct canonical.
	 */
	public function canonical( $canonical, $presentation = null ) {
		if ( ! $this->woocommerce_helper->is_shop_page() ) {
			return $canonical;
		}

		$url = $this->get_shop_paginated_link( 'curr', $presentation );

		if ( $url ) {
			return $url;
		}

		return $canonical;
	}

	/**
	 * Returns correct adjacent pages when WooCommerce is enabled.
	 *
	 * @param string                      $link         The current link.
	 * @param string                      $rel          Link relationship, prev or next.
	 * @param Indexable_Presentation|null $presentation The indexable presentation.
	 *
	 * @return string The correct link.
	 */
	public function adjacent_rel_url( $link, $rel, $presentation = null ) {
		if ( ! $this->woocommerce_helper->is_shop_page() ) {
			return $link;
		}

		if ( $rel !== 'next' && $rel !== 'prev' ) {
			return $link;
		}

		$url = $this->get_shop_paginated_link( $rel, $presentation );

		if ( $url ) {
			return $url;
		}

		return $link;
	}

	/**
	 * Adds a breadcrumb for the shop page.
	 *
	 * @param Indexable[] $indexables The array with indexables.
	 *
	 * @return Indexable[] The indexables to be shown in the breadcrumbs, with the shop page added.
	 */
	public function add_shop_to_breadcrumbs( $indexables ) {
		$shop_page_id = $this->woocommerce_helper->get_shop_page_id();

		if ( ! is_int( $shop_page_id ) || $shop_page_id < 1 ) {
			return $indexables;
		}

		foreach ( $indexables as $index => $indexable ) {
			if ( $indexable->object_type === 'post-type-archive' && $indexable->object_sub_type === 'product' ) {
				$indexables[ $index ] = $this->repository->find_by_id_and_type( $shop_page_id, 'post' );
			}
		}

		return $indexables;
	}

	/**
	 * Returns the ID of the WooCommerce shop page when the currently opened page is the shop page.
	 *
	 * @param int $page_id The page id.
	 *
	 * @return int The Page ID of the shop.
	 */
	public function get_page_id( $page_id ) {
		if ( ! $this->woocommerce_helper->is_shop_page() ) {
			return $page_id;
		}

		return $this->woocommerce_helper->get_shop_page_id();
	}

	/**
	 * Handles the title.
	 *
	 * @param string                      $title        The title.
	 * @param Indexable_Presentation|null $presentation The indexable presentation.
	 *
	 * @return string The title to use.
	 */
	public function title( $title, $presentation = null ) {
		$presentation = $this->ensure_presentation( $presentation );

		if ( $presentation->model->title ) {
			return $title;
		}

		if ( ! $this->woocommerce_helper->is_shop_page() ) {
			return $title;
		}

		if ( ! \is_archive() ) {
			return $title;
		}

		$shop_page_id = $this->woocommerce_helper->get_shop_page_id();
		if ( $shop_page_id < 1 ) {
			return $title;
		}

		$product_template_title = $this->get_product_template( 'title-product', $shop_page_id );
		if ( $product_template_title ) {
			return $product_template_title;
		}

		return $title;
	}

	/**
	 * Handles the meta description.
	 *
	 * @param string                      $description  The title.
	 * @param Indexable_Presentation|null $presentation The indexable presentation.
	 *
	 * @return string The description to use.
	 */
	public function description( $description, $presentation = null ) {
		$presentation = $this->ensure_presentation( $presentation );

		if ( $presentation->model->description ) {
			return $description;
		}

		if ( ! $this->woocommerce_helper->is_shop_page() ) {
			return $description;
		}

		if ( ! \is_archive() ) {
			return $description;
		}

		$shop_page_id = $this->woocommerce_helper->get_shop_page_id();
		if ( $shop_page_id < 1 ) {
			return $description;
		}

		$product_template_description = $this->get_product_template( 'metadesc-product', $shop_page_id );
		if ( $product_template_description ) {
			return $product_template_description;
		}

		return $description;
	}

	/**
	 * Uses template for the given option name and replace the replacement variables on it.
	 *
	 * @param string $option_name  The option name to get the template for.
	 * @param string $shop_page_id The page id to retrieve template for.
	 *
	 * @return string The rendered value.
	 */
	protected function get_product_template( $option_name, $shop_page_id ) {
		$template = $this->options->get( $option_name );
		$page     = \get_post( $shop_page_id );

		return $this->replace_vars->replace( $template, $page );
	}

	/**
	 * Get paginated link for shop page.
	 *
	 * @param string                      $rel          Link relationship, prev or next or curr.
	 * @param Indexable_Presentation|null $presentation The indexable presentation.
	 *
	 * @return string|null The link.
	 */
	protected function get_shop_paginated_link( $rel, $presentation = null ) {
		$presentation = $this->ensure_presentation( $presentation );

		$permalink = $presentation->permalink;
		if ( ! $permalink ) {
			return null;
		}

		$current_page = \max( 1, $this->pagination_helper->get_current_archive_page_number() );

		if ( $rel === 'curr' && $current_page === 1 ) {
			return $permalink;
		}

		if ( $rel === 'curr' && $current_page > 1 ) {
			return $this->pagination_helper->get_paginated_url( $permalink, $current_page );
		}

		if ( $rel === 'prev' && $current_page === 2 ) {
			return $permalink;
		}

		if ( $rel === 'prev' && $current_page > 2 ) {
			return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page - 1 ) );
		}

		if ( $rel === 'next' && $current_page < $this->pagination_helper->get_number_of_archive_pages() ) {
			return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page + 1 ) );
		}

		return null;
	}

	/**
	 * Ensures a presentation is available.
	 *
	 * @param Indexable_Presentation $presentation The indexable presentation.
	 *
	 * @return Indexable_Presentation The presentation, taken from the current page if the input was invalid.
	 */
	protected function ensure_presentation( $presentation ) {
		if ( \is_a( $presentation, Indexable_Presentation::class ) ) {
			return $presentation;
		}

		$context = $this->context_memoizer->for_current_page();

		return $context->presentation;
	}
}
third-party/w3-total-cache.php000066600000001474151130361070012246 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Third_Party\W3_Total_Cache_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * W3 Total Cache integration.
 */
class W3_Total_Cache implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ W3_Total_Cache_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * On successful update/add of the taxonomy meta option, flush the W3TC cache.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'add_option_wpseo_taxonomy_meta', 'w3tc_objectcache_flush' );
		\add_action( 'update_option_wpseo_taxonomy_meta', 'w3tc_objectcache_flush' );
	}
}
third-party/web-stories.php000066600000006036151130361070011775 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Web_Stories_Conditional;
use Yoast\WP\SEO\Integrations\Front_End_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;

/**
 * Web Stories integration.
 */
class Web_Stories implements Integration_Interface {

	/**
	 * The front end integration.
	 *
	 * @var Front_End_Integration
	 */
	protected $front_end;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Web_Stories_Conditional::class ];
	}

	/**
	 * Constructs the Web Stories integration
	 *
	 * @param Front_End_Integration $front_end The front end integration.
	 */
	public function __construct( Front_End_Integration $front_end ) {
		$this->front_end = $front_end;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		// If the title presenter was not removed, we will disable the web stories metadata because Yoast SEO will output the title already.
		if ( ! $this->front_end->should_title_presenter_be_removed() ) {
			\add_action( 'web_stories_enable_metadata', '__return_false' );
		}
		\add_action( 'web_stories_enable_schemaorg_metadata', '__return_false' );
		\add_action( 'web_stories_enable_open_graph_metadata', '__return_false' );
		\add_action( 'web_stories_enable_twitter_metadata', '__return_false' );

		\add_action( 'web_stories_story_head', [ $this, 'web_stories_story_head' ], 1 );
		\add_filter( 'wpseo_schema_article_type', [ $this, 'filter_schema_article_type' ], 10, 2 );
		\add_filter( 'wpseo_metadesc', [ $this, 'filter_meta_description' ], 10, 2 );
	}

	/**
	 * Hooks into web story <head> generation to modify output.
	 *
	 * @return void
	 */
	public function web_stories_story_head() {
		\remove_action( 'web_stories_story_head', 'rel_canonical' );
		\add_action( 'web_stories_story_head', [ $this->front_end, 'call_wpseo_head' ], 9 );
	}

	/**
	 * Filters the meta description for stories.
	 *
	 * @param string                 $description  The description sentence.
	 * @param Indexable_Presentation $presentation The presentation of an indexable.
	 * @return string The description sentence.
	 */
	public function filter_meta_description( $description, $presentation ) {
		if ( $description || $presentation->model->object_sub_type !== 'web-story' ) {
			return $description;
		}

		return \get_the_excerpt( $presentation->model->object_id );
	}

	/**
	 * Filters Article type for Web Stories.
	 *
	 * @param string|string[] $type      The Article type.
	 * @param Indexable       $indexable The indexable.
	 * @return string|string[] Article type.
	 */
	public function filter_schema_article_type( $type, $indexable ) {
		if ( $indexable->object_sub_type !== 'web-story' ) {
			return $type;
		}

		if ( \is_string( $type ) && $type === 'None' ) {
			return 'Article';
		}

		return $type;
	}
}
third-party/wordproof.php000066600000016046151130361070011555 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WPSEO_Admin_Asset;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Wordproof_Integration_Active_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Wordproof_Plugin_Inactive_Conditional;
use Yoast\WP\SEO\Config\Wordproof_App_Config;
use Yoast\WP\SEO\Config\Wordproof_Translations;
use Yoast\WP\SEO\Helpers\Wordproof_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use YoastSEO_Vendor\WordProof\SDK\Helpers\CertificateHelper;
use YoastSEO_Vendor\WordProof\SDK\Helpers\PostMetaHelper;
use YoastSEO_Vendor\WordProof\SDK\WordPressSDK;

/**
 * Class WordProof
 *
 * @package Yoast\WP\SEO\Integrations\Third_Party
 */
class Wordproof implements Integration_Interface {

	/**
	 * The Yoast meta key used to save if a post shiould be timestamped.
	 *
	 * @var string
	 */
	protected $post_meta_key = '_yoast_wpseo_wordproof_timestamp';

	/**
	 * The WordProof helper instance.
	 *
	 * @var Wordproof_Helper
	 */
	protected $wordproof;

	/**
	 * Asset manager instance.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * The WordProof integration constructor.
	 *
	 * @param Wordproof_Helper          $wordproof     The WordProof helper instance.
	 * @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO admin asset manager instance.
	 */
	public function __construct( Wordproof_Helper $wordproof, WPSEO_Admin_Asset_Manager $asset_manager = null ) {
		if ( ! $asset_manager ) {
			$asset_manager = new WPSEO_Admin_Asset_Manager();
		}

		$this->asset_manager = $asset_manager;
		$this->wordproof     = $wordproof;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [
			Wordproof_Plugin_Inactive_Conditional::class,
			Non_Multisite_Conditional::class,
			Wordproof_Integration_Active_Conditional::class,
		];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		/**
		 * Used to initialize the WordProof WordPress SDK.
		 */
		\add_action( 'init', [ $this, 'sdk_setup' ], 11 );

		/**
		 * Enqueue the wordproof assets.
		 */
		\add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 10, 0 );

		/**
		 * Add async to the wordproof scripts.
		 */
		\add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 );

		/**
		 * Removes the post meta timestamp key for the old privacy page.
		 */
		\add_action( 'update_option_wp_page_for_privacy_policy', [ $this, 'disable_timestamp_for_previous_legal_page' ], 10, 2 );

		/**
		 * Removes the post meta timestamp key for the old terms and conditions page.
		 */
		\add_action( 'update_option_woocommerce_terms_page_id', [ $this, 'disable_timestamp_for_previous_legal_page' ], 10, 2 );

		/**
		 * Called by the WordProof WordPress SDK to determine if the post should be timestamped.
		 */
		\add_filter( 'wordproof_timestamp_post_meta_key_overrides', [ $this, 'add_post_meta_key' ] );

		/**
		 * Called by the WordProof WordPress SDK to determine if the post should be automatically timestamped.
		 */
		\add_filter( 'wordproof_timestamp_post_types', [ $this, 'wordproof_timestamp_post_types' ] );

		/**
		 * Called by the WordProof WordPress SDK to determine if the certificate should be shown.
		 */
		\add_filter( 'wordproof_timestamp_show_certificate', [ $this, 'show_certificate' ], 10, 2 );

		/**
		 * Called by WPSEO_Meta to add extra meta fields to the ones defined there.
		 */
		\add_filter( 'add_extra_wpseo_meta_fields', [ $this, 'add_meta_field' ] );
	}

	/**
	 * Initializes the WordProof WordPress SDK.
	 */
	public function sdk_setup() {

		$config       = new Wordproof_App_Config();
		$translations = new Wordproof_Translations();

		WordPressSDK::getInstance( $config, $translations )
			->certificate()
			->initialize();
	}

	/**
	 * Removes the WordProof timestamp post meta if a legal page is changed.
	 *
	 * @param int $old_post_id The old post id.
	 * @param int $new_post_id The new post id.
	 */
	public function disable_timestamp_for_previous_legal_page( $old_post_id, $new_post_id ) {

		if ( $old_post_id !== $new_post_id ) {
			\delete_post_meta( $old_post_id, '_yoast_wpseo_wordproof_timestamp' );
		}
	}

	/**
	 * Return the Yoast post meta key for the SDK to determine if the post should be timestamped.
	 *
	 * @param array $meta_keys The array containing meta keys that should be used.
	 * @return array
	 */
	public function add_post_meta_key( $meta_keys ) {
		return [ $this->post_meta_key ];
	}

	/**
	 * Return an empty array to disable automatically timestamping selected post types.
	 *
	 * @param array $post_types The array containing post types that should be automatically timestamped.
	 * @return array
	 */
	public function wordproof_timestamp_post_types( $post_types ) {
		return [];
	}

	/**
	 * This filters hides the certificate if the Yoast post meta key is not set to true.
	 *
	 * @param bool    $value If the certificate should be shown.
	 * @param WP_Post $post  The post object of the post for which to determine the certificate should be shown.
	 * @return bool|null
	 */
	public function show_certificate( $value, $post ) {
		if ( ! $value ) {
			return $value;
		}

		if ( ! $this->wordproof->integration_is_active() ) {
			return false;
		}

		return \boolval( PostMetaHelper::get( $post->ID, $this->post_meta_key ) );
	}

	/**
	 * Adds the WordProof integration toggle to the array.
	 *
	 * @param array $fields The currently registered meta fields.
	 *
	 * @return array A new array with meta fields.
	 */
	public function add_meta_field( $fields ) {
		$fields['advanced']['wordproof_timestamp'] = [
			'type'          => 'hidden',
			'title'         => '',
			'default_value' => '',
			'description'   => '0',
		];

		return $fields;
	}

	/**
	 * Enqueue the uikit script.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		if ( CertificateHelper::show() ) {
			$flat_version = $this->asset_manager->flatten_version( \WPSEO_VERSION );

			/**
			 * We are using the Admin asset manager to register and enqueue a file served for all visitors,
			 * authenticated and unauthenticated users.
			 */
			$script = new WPSEO_Admin_Asset(
				[
					'name'    => 'wordproof-uikit',
					'src'     => 'wordproof-uikit.js',
					'version' => $flat_version,
				]
			);

			$this->asset_manager->register_script( $script );
			$this->asset_manager->enqueue_script( 'wordproof-uikit' );
		}
	}

	/**
	 * Adds async to the wordproof-uikit script.
	 *
	 * @param string $tag    The script tag for the enqueued script.
	 * @param string $handle The script's registered handle.
	 * @param string $src    The script's source URL.
	 *
	 * @return string The script's tag.
	 *
	 * @phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
	 */
	public function add_async_to_script( $tag, $handle, $src ) {
		if ( $handle !== WPSEO_Admin_Asset_Manager::PREFIX . 'wordproof-uikit' ) {
			return $tag;
		}

		return "<script src={$src} async></script>";
	}
}
third-party/wpml.php000066600000003526151130361070010512 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * WPML integration.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Known false positive with acronyms. Fix expected in YoastCS 3.x.
 */
class WPML implements Integration_Interface {

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'wpseo_home_url', [ $this, 'filter_home_url_before' ] );
		\add_filter( 'home_url', [ $this, 'filter_home_url_after' ], 100 );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ WPML_Conditional::class ];
	}

	/**
	 * Adds a filter to WPML's wpml_get_home_url filter to ensure we get the unmanipulated home URL.
	 */
	public function filter_home_url_before() {
		\add_filter( 'wpml_get_home_url', [ $this, 'wpml_get_home_url' ], 10, 2 );
	}

	/**
	 * Removes the wpml_get_home_url filter to return the WPML, language-enriched home URL.
	 *
	 * @param string $home_url The filtered home URL.
	 *
	 * @return string The unfiltered home URL.
	 */
	public function filter_home_url_after( $home_url ) {
		\remove_filter( 'wpml_get_home_url', [ $this, 'wpml_get_home_url' ], 10 );

		return $home_url;
	}

	/**
	 * Returns the original URL instead of the language-enriched URL.
	 * This method gets automatically triggered by the wpml_get_home_url filter.
	 *
	 * @param string $home_url The url altered by WPML. Unused.
	 * @param string $url      The url that isn't altered by WPML.
	 *
	 * @return string The original url.
	 */
	public function wpml_get_home_url( $home_url, $url ) {
		return $url;
	}
}
third-party/exclude-woocommerce-post-types.php000066600000001613151130361070015621 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Integrations\Abstract_Exclude_Post_Type;

/**
 * Excludes certain WooCommerce-specific post types from the indexable table.
 *
 * Posts with these post types will not be saved to the indexable table.
 */
class Exclude_WooCommerce_Post_Types extends Abstract_Exclude_Post_Type {

	/**
	 * This integration is only active when the WooCommerce plugin
	 * is installed and activated.
	 *
	 * @return array|string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ WooCommerce_Conditional::class ];
	}

	/**
	 * Returns the names of the post types to be excluded.
	 * To be used in the wpseo_indexable_excluded_post_types filter.
	 *
	 * @return array The names of the post types.
	 */
	public function get_post_type() {
		return [ 'shop_order' ];
	}
}
third-party/jetpack.php000066600000001504151130361070011146 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\Jetpack_Conditional;
use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Jetpack integration.
 */
class Jetpack implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class, Jetpack_Conditional::class, Open_Graph_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'jetpack_enable_open_graph', '__return_false' );
	}
}
third-party/woocommerce-permalinks.php000066600000005642151130361070014216 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * The permalink watcher.
 */
class Woocommerce_Permalinks implements Integration_Interface {

	/**
	 * Represents the indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ WooCommerce_Conditional::class, Migrations_Conditional::class ];
	}

	/**
	 * Constructor.
	 *
	 * @param Indexable_Helper $indexable_helper Indexable Helper.
	 */
	public function __construct( Indexable_Helper $indexable_helper ) {
		$this->indexable_helper = $indexable_helper;
	}

	/**
	 * Registers the hooks.
	 *
	 * @codeCoverageIgnore
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_post_types_reset_permalinks', [ $this, 'filter_product_from_post_types' ] );
		\add_action( 'update_option_woocommerce_permalinks', [ $this, 'reset_woocommerce_permalinks' ], 10, 2 );
	}

	/**
	 * Filters the product post type from the post type.
	 *
	 * @param array $post_types The post types to filter.
	 *
	 * @return array The filtered post types.
	 */
	public function filter_product_from_post_types( $post_types ) {
		unset( $post_types['product'] );

		return $post_types;
	}

	/**
	 * Resets the indexables for WooCommerce based on the changed permalink fields.
	 *
	 * @param array $old_value The old value.
	 * @param array $new_value The new value.
	 */
	public function reset_woocommerce_permalinks( $old_value, $new_value ) {
		$changed_options = \array_diff( $old_value, $new_value );

		if ( \array_key_exists( 'product_base', $changed_options ) ) {
			$this->indexable_helper->reset_permalink_indexables( 'post', 'product' );
		}

		if ( \array_key_exists( 'attribute_base', $changed_options ) ) {
			$attribute_taxonomies = $this->get_attribute_taxonomies();

			foreach ( $attribute_taxonomies as $attribute_name ) {
				$this->indexable_helper->reset_permalink_indexables( 'term', $attribute_name );
			}
		}

		if ( \array_key_exists( 'category_base', $changed_options ) ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', 'product_cat' );
		}

		if ( \array_key_exists( 'tag_base', $changed_options ) ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', 'product_tag' );
		}
	}

	/**
	 * Retrieves the taxonomies based on the attributes.
	 *
	 * @return array The taxonomies.
	 */
	protected function get_attribute_taxonomies() {
		$taxonomies = [];
		foreach ( \wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
			$taxonomies[] = \wc_attribute_taxonomy_name( $attribute_taxonomy->attribute_name );
		}

		$taxonomies = \array_filter( $taxonomies );

		return $taxonomies;
	}
}
third-party/wpml-wpseo-notification.php000066600000006576151130361070014341 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
 * Adds a notification to the dashboard if the WPML plugin is installed,
 * but the Yoast SEO Multilingual plugin (a glue plugin to make Yoast SEO and WPML work nicely together)
 * is not.
 */
class WPML_WPSEO_Notification implements Integration_Interface {

	/**
	 * The notification ID.
	 *
	 * @internal
	 */
	const NOTIFICATION_ID = 'wpml-wpseo-not-installed';

	/**
	 * The short link helper.
	 *
	 * @var Short_Link_Helper
	 */
	protected $short_link_helper;

	/**
	 * The notification center.
	 *
	 * @var Yoast_Notification_Center
	 */
	protected $notification_center;

	/**
	 * The WPML WPSEO conditional.
	 *
	 * @var WPML_WPSEO_Conditional
	 */
	protected $wpml_wpseo_conditional;

	/**
	 * WPML WPSEO notification constructor.
	 *
	 * @param Short_Link_Helper         $short_link_helper      The short link helper.
	 * @param Yoast_Notification_Center $notification_center    The notification center.
	 * @param WPML_WPSEO_Conditional    $wpml_wpseo_conditional The WPML WPSEO conditional.
	 */
	public function __construct(
		Short_Link_Helper $short_link_helper,
		Yoast_Notification_Center $notification_center,
		WPML_WPSEO_Conditional $wpml_wpseo_conditional
	) {
		$this->short_link_helper      = $short_link_helper;
		$this->notification_center    = $notification_center;
		$this->wpml_wpseo_conditional = $wpml_wpseo_conditional;
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_notices', [ $this, 'notify_not_installed' ] );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * This integration should only be active when WPML is installed and activated.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ WPML_Conditional::class ];
	}

	/**
	 * Notify the user that the Yoast SEO Multilingual plugin is not installed
	 * (when the WPML plugin is installed).
	 *
	 * Remove the notification again when it is installed.
	 *
	 * @return void
	 */
	public function notify_not_installed() {
		if ( ! $this->wpml_wpseo_conditional->is_met() ) {
			$this->notification_center->add_notification( $this->get_notification() );
			return;
		}
		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}

	/**
	 * Generates the notification to show to the user when WPML is installed,
	 * but the Yoast SEO Multilingual plugin is not.
	 *
	 * @return Yoast_Notification The notification.
	 */
	protected function get_notification() {
		return new Yoast_Notification(
			\sprintf(
				/* translators: %1$s expands to an opening anchor tag, %2$s expands to an closing anchor tag. */
				\__( 'We notice that you have installed WPML. To make sure your canonical URLs are set correctly, %1$sinstall and activate the Yoast SEO Multilingual add-on%2$s as well!', 'wordpress-seo' ),
				'<a href="' . \esc_url( $this->short_link_helper->get( 'https://yoa.st/wpml-yoast-seo' ) ) . '" target="_blank">',
				'</a>'
			),
			[
				'id'   => self::NOTIFICATION_ID,
				'type' => Yoast_Notification::WARNING,
			]
		);
	}
}
third-party/bbpress.php000066600000002651151130361070011171 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * BbPress integration.
 */
class BbPress implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * BbPress constructor.
	 *
	 * @codeCoverageIgnore It only sets dependencies.
	 *
	 * @param Options_Helper $options The options helper.
	 */
	public function __construct( Options_Helper $options ) {
		$this->options = $options;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->options->get( 'breadcrumbs-enable' ) !== true ) {
			return;
		}

		/**
		 * If breadcrumbs are active (which they supposedly are if the users has enabled this settings,
		 * there's no reason to have bbPress breadcrumbs as well.
		 *
		 * {@internal The class itself is only loaded when the template tag is encountered
		 *            via the template tag function in the wpseo-functions.php file.}}
		 */
		\add_filter( 'bbp_get_breadcrumb', '__return_false' );
	}
}
third-party/wordproof-integration-toggle.php000066600000011355151130361070015353 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Wordproof_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Feature_Toggle;

/**
 * Class WordProofIntegrationToggle.
 *
 * @package Yoast\WP\SEO\Integrations\Third_Party
 */
class Wordproof_Integration_Toggle implements Integration_Interface {

	/**
	 * The WordProof helper instance.
	 *
	 * @var Wordproof_Helper
	 */
	protected $wordproof;

	/**
	 * The WordProof integration toggle constructor.
	 *
	 * @param Wordproof_Helper $wordproof The WordProof helper instance.
	 */
	public function __construct( Wordproof_Helper $wordproof ) {
		$this->wordproof = $wordproof;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		/**
		 * Called by Yoast_Integration_Toggles to add extra toggles to the ones defined there.
		 */
		\add_filter( 'wpseo_integration_toggles', [ $this, 'add_integration_toggle' ] );

		/**
		 * Update the default wordproof_integration_active depending if the integration is disabled or not.
		 */
		\add_filter( 'wpseo_option_wpseo_defaults', [ $this, 'default_values' ] );

		/**
		 * Add extra text after the integration toggle if the toggle is disabled.
		 */
		\add_action( 'Yoast\WP\SEO\admin_integration_after', [ $this, 'after_integration_toggle' ] );

		/**
		 * Add extra text after the network integration toggle if the toggle is disabled.
		 */
		\add_action( 'Yoast\WP\SEO\admin_network_integration_after', [ $this, 'after_network_integration_toggle' ] );
	}

	/**
	 * Adds the WordProof integration toggle to the array.
	 *
	 * @param array $integration_toggles The integration toggles array.
	 *
	 * @return array The updated integration toggles array.
	 */
	public function add_integration_toggle( $integration_toggles ) {
		if ( \is_array( $integration_toggles ) ) {
			$integration_toggles[] = (object) [
				/* translators: %s expands to WordProof */
				'name'            => \sprintf( \__( '%s integration', 'wordpress-seo' ), 'WordProof' ),
				'setting'         => 'wordproof_integration_active',
				'label'           => \sprintf(
				/* translators: %s expands to WordProof */
					\__( '%1$s can be used to timestamp your privacy page.', 'wordpress-seo' ),
					'WordProof'
				),
				/* translators: %s expands to WordProof */
				'read_more_label' => \sprintf( \__( 'Read more about how %s works.', 'wordpress-seo' ), 'WordProof ' ),
				'read_more_url'   => 'https://yoa.st/wordproof-integration',
				'order'           => 16,
				'disabled'        => $this->wordproof->integration_is_disabled(),
				'new'             => true,
			];
		}

		return $integration_toggles;
	}

	/**
	 * Set the default WordProof integration option value depending if the integration is disabled or not.
	 *
	 * @param array $defaults Array containing default wpseo options.
	 *
	 * @return array
	 */
	public function default_values( $defaults ) {
		if ( $this->wordproof->integration_is_disabled() ) {
			$defaults['wordproof_integration_active'] = false;
		}

		return $defaults;
	}

	/**
	 * Add an explainer when the integration toggle is disabled.
	 *
	 * @param Yoast_Feature_Toggle $integration The integration toggle class.
	 */
	public function after_integration_toggle( $integration ) {
		if ( $integration->setting === 'wordproof_integration_active' ) {
			if ( $integration->disabled ) {

				$conditional = $this->wordproof->integration_is_disabled( true );

				if ( $conditional === 'Non_Multisite_Conditional' ) {
					echo '<p>' . \sprintf(
						/* translators: %s expands to WordProof */
						\esc_html__( 'Currently, the %s integration is not available for multisites.', 'wordpress-seo' ),
						'WordProof'
					) . '</p>';
				}

				if ( $conditional === 'Wordproof_Plugin_Inactive_Conditional' ) {
					echo '<p>' . \esc_html__( 'The WordProof Timestamp plugin needs to be disabled before you can activate this integration.', 'wordpress-seo' ) . '</p>';
				}
			}
		}
	}

	/**
	 * Add an explainer when the network integration toggle is disabled.
	 *
	 * @param Yoast_Feature_Toggle $integration The integration toggle class.
	 */
	public function after_network_integration_toggle( $integration ) {
		if ( $integration->setting === 'wordproof_integration_active' ) {
			if ( $integration->disabled ) {
				echo '<p>' . \sprintf(
					/* translators: %s expands to WordProof */
					\esc_html__( 'Currently, the %s integration is not available for multisites.', 'wordpress-seo' ),
					'WordProof'
				) . '</p>';
			}
		}
	}
}
third-party/exclude-elementor-post-types.php000066600000001650151130361070015275 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional;
use Yoast\WP\SEO\Integrations\Abstract_Exclude_Post_Type;

/**
 * Excludes certain Elementor-specific post types from the indexable table.
 *
 * Posts with these post types will not be saved to the indexable table.
 */
class Exclude_Elementor_Post_Types extends Abstract_Exclude_Post_Type {

	/**
	 * This integration is only active when the Elementor plugin
	 * is installed and activated.
	 *
	 * @return array|string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ Elementor_Activated_Conditional::class ];
	}

	/**
	 * Returns the names of the post types to be excluded.
	 * To be used in the wpseo_indexable_excluded_post_types filter.
	 *
	 * @return array The names of the post types.
	 */
	public function get_post_type() {
		return [ 'elementor_library' ];
	}
}
third-party/woocommerce-post-edit.php000066600000002740151130361070013755 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WP_Post;
use Yoast\WP\SEO\Conditionals\Admin\Post_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * A WooCommerce integration that runs in the post editor.
 */
class WooCommerce_Post_Edit implements Integration_Interface {

	/**
	 * Register the hooks for this integration to work.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_post_edit_values', [ $this, 'remove_meta_description_date' ], 10, 2 );
	}

	/**
	 * Only run this integration when WooCommerce is active and the user is in the post editor.
	 *
	 * @return string[] The conditionals that should be met before this integration is loaded.
	 */
	public static function get_conditionals() {
		return [ WooCommerce_Conditional::class, Post_Conditional::class ];
	}

	/**
	 * Don't show the date in the Google preview for WooCommerce products,
	 * since Google does not show dates for product pages in the search results.
	 *
	 * @param array   $values Key-value map of variables we enqueue in the JavaScript of the post editor.
	 * @param WP_Post $post   The post currently opened in the editor.
	 *
	 * @return array The values, where the `metaDescriptionDate` is set to the empty string.
	 */
	public function remove_meta_description_date( $values, $post ) {
		if ( $post->post_type === 'product' ) {
			$values['metaDescriptionDate'] = '';
		}

		return $values;
	}
}
third-party/wincher.php000066600000006654151130361070011177 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Wincher_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Feature_Toggle;

/**
 * Adds the Wincher integration.
 */
class Wincher implements Integration_Interface {

	/**
	 * The Wincher helper instance.
	 *
	 * @var Wincher_Helper
	 */
	protected $wincher;

	/**
	 * The Wincher integration toggle constructor.
	 *
	 * @param Wincher_Helper $wincher The Wincher helper instance.
	 */
	public function __construct( Wincher_Helper $wincher ) {
		$this->wincher = $wincher;
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		/**
		 * Called by Yoast_Integration_Toggles to add extra toggles to the ones defined there.
		 */
		\add_filter( 'wpseo_integration_toggles', [ $this, 'add_integration_toggle' ] );

		/**
		 * Called in dashboard/integrations.php to put additional content after the toggle.
		 */
		\add_action( 'Yoast\WP\SEO\admin_integration_after', [ $this, 'after_integration_toggle' ] );

		/**
		 * Add extra text after the network integration toggle if the toggle is disabled.
		 */
		\add_action( 'Yoast\WP\SEO\admin_network_integration_after', [ $this, 'after_network_integration_toggle' ] );
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Adds the Wincher integration toggle to the $integration_toggles array.
	 *
	 * @param array $integration_toggles The integration toggles array.
	 *
	 * @return array The updated integration toggles array.
	 */
	public function add_integration_toggle( $integration_toggles ) {
		if ( \is_array( $integration_toggles ) ) {
			$integration_toggles[] = (object) [
				/* translators: %s: 'Wincher' */
				'name'     => \sprintf( \__( '%s integration', 'wordpress-seo' ), 'Wincher' ),
				'setting'  => 'wincher_integration_active',
				'label'    => \sprintf(
				/* translators: %s: 'Wincher' */
					\__( 'The %s integration offers the option to track specific keyphrases and gain insights in their positions.', 'wordpress-seo' ),
					'Wincher'
				),
				'order'    => 11,
				'disabled' => \is_multisite(),
			];
		}

		return $integration_toggles;
	}

	/**
	 * Adds the disabled note when the integration toggle is disabled.
	 *
	 * @param Yoast_Feature_Toggle $integration The integration toggle class.
	 */
	public function after_integration_toggle( $integration ) {
		if ( $integration->setting === 'wincher_integration_active' ) {

			require \WPSEO_PATH . 'admin/views/tabs/metas/paper-content/integrations/wincher.php';

			if ( \is_multisite() ) {
				$this->get_disabled_note();
			}
		}
	}

	/**
	 * Adds the disabled note to the network integration toggle.
	 *
	 * @param Yoast_Feature_Toggle $integration The integration toggle class.
	 */
	public function after_network_integration_toggle( $integration ) {
		if ( $integration->setting === 'wincher_integration_active' ) {
			$this->get_disabled_note();
		}
	}

	/**
	 * Outputs the disabled note.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function get_disabled_note() {
		echo '<p>', \sprintf(
		/* translators: %s expands to Wincher */
			\esc_html__( 'Currently, the %s integration is not available for multisites.', 'wordpress-seo' ),
			'Wincher'
		), '</p>';
	}
}
schema-blocks.php000066600000010146151130361070007773 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Schema_Blocks_Conditional;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;

/**
 * Loads schema block templates into Gutenberg.
 */
class Schema_Blocks implements Integration_Interface {

	/**
	 * The registered templates.
	 *
	 * @var string[]
	 */
	protected $templates = [];

	/**
	 * Contains the asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * Represents the schema blocks conditional.
	 *
	 * @var Schema_Blocks_Conditional
	 */
	protected $blocks_conditional;

	/**
	 * Represents the short link helper.
	 *
	 * @var Short_Link_Helper
	 */
	protected $short_link_helper;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [
			Schema_Blocks_Conditional::class,
		];
	}

	/**
	 * Schema_Blocks constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager      The asset manager.
	 * @param Schema_Blocks_Conditional $blocks_conditional The schema blocks conditional.
	 * @param Short_Link_Helper         $short_link_helper  The short link helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Schema_Blocks_Conditional $blocks_conditional,
		Short_Link_Helper $short_link_helper
	) {
		$this->asset_manager      = $asset_manager;
		$this->blocks_conditional = $blocks_conditional;
		$this->short_link_helper  = $short_link_helper;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'enqueue_block_editor_assets', [ $this, 'load' ] );
		\add_action( 'enqueue_block_editor_assets', [ $this, 'load_translations' ] );
		\add_action( 'admin_enqueue_scripts', [ $this, 'output' ] );
	}

	/**
	 * Registers a schema template.
	 *
	 * @param string $template The template to be registered.
	 *                         If starting with a / is assumed to be an absolute path.
	 *                         If not starting with a / is assumed to be relative to WPSEO_PATH.
	 *
	 * @return void
	 */
	public function register_template( $template ) {
		if ( \substr( $template, 0, 1 ) !== '/' ) {
			$template = \WPSEO_PATH . '/' . $template;
		}

		$this->templates[] = $template;
	}

	/**
	 * Loads all schema block templates and the required JS library for them.
	 *
	 * @return void
	 */
	public function load() {
		$this->asset_manager->enqueue_script( 'schema-blocks' );
		$this->asset_manager->enqueue_style( 'schema-blocks' );

		$this->asset_manager->localize_script(
			'schema-blocks',
			'yoastSchemaBlocks',
			[
				'requiredLink'    => $this->short_link_helper->build( 'https://yoa.st/required-fields' ),
				'recommendedLink' => $this->short_link_helper->build( 'https://yoa.st/recommended-fields' ),
			]
		);
	}

	/**
	 * Outputs the set templates.
	 */
	public function output() {
		if ( ! $this->asset_manager->is_script_enqueued( 'schema-blocks' ) ) {
			return;
		}

		$templates = [];

		// When the schema blocks feature flag is enabled, use the registered templates.
		if ( $this->blocks_conditional->is_met() ) {
			$templates = $this->templates;
		}

		/**
		 * Filter: 'wpseo_load_schema_templates' - Allow adding additional schema templates.
		 *
		 * @param array $templates The templates to filter.
		 */
		$templates = \apply_filters( 'wpseo_load_schema_templates', $templates );
		if ( ! \is_array( $templates ) || empty( $templates ) ) {
			return;
		}

		foreach ( $templates as $template ) {
			if ( ! \file_exists( $template ) ) {
				continue;
			}
			// `.schema` and other suffixes become Schema (root) templates.
			$type = ( \substr( $template, -10 ) === '.block.php' ) ? 'block' : 'schema';
			echo '<script type="text/' . \esc_html( $type ) . '-template">';
			include $template;
			echo '</script>';
		}
	}

	/**
	 * Loads the translations and localizes the schema-blocks script file.
	 *
	 * @codeCoverageIgnore
	 * @deprecated 18.0
	 */
	public function load_translations() {
		\_deprecated_function( __FUNCTION__, '18.0' );
	}
}
alerts/abstract-dismissable-alert.php000066600000001645151130361070013763 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Alerts;

use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Dismissable_Alert class.
 */
abstract class Abstract_Dismissable_Alert implements Integration_Interface {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_allowed_dismissable_alerts', [ $this, 'register_dismissable_alert' ] );
	}

	/**
	 * Registers the dismissable alert.
	 *
	 * @param string[] $allowed_dismissable_alerts The allowed dismissable alerts.
	 *
	 * @return string[] The allowed dismissable alerts.
	 */
	public function register_dismissable_alert( $allowed_dismissable_alerts ) {
		$allowed_dismissable_alerts[] = $this->alert_identifier;

		return $allowed_dismissable_alerts;
	}
}
alerts/webinar-promo-notification.php000066600000000445151130361070014020 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Alerts;

/**
 * Webinar_Promo_Notification class.
 */
class Webinar_Promo_Notification extends Abstract_Dismissable_Alert {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier = 'webinar-promo-notification';
}
exclude-oembed-cache-post-type.php000066600000001466151130361070013152 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;

/**
 * Excludes certain oEmbed Cache-specific post types from the indexable table.
 *
 * Posts with these post types will not be saved to the indexable table.
 */
class Exclude_Oembed_Cache_Post_Type extends Abstract_Exclude_Post_Type {

	/**
	 * This integration is only active when the database migrations have been run.
	 *
	 * @return array|string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Returns the names of the post types to be excluded.
	 * To be used in the wpseo_indexable_excluded_post_types filter.
	 *
	 * @return array The names of the post types.
	 */
	public function get_post_type() {
		return [ 'oembed_cache' ];
	}
}
admin/crawl-settings-integration.php000066600000030675151130361070013650 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Utils;
use WPSEO_Option;
use WPSEO_Option_Tab;
use WPSEO_Option_Tabs;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Form;

/**
 * Crawl_Settings_Integration class
 */
class Crawl_Settings_Integration implements Integration_Interface {

	/**
	 * Holds the settings + labels for the head clean up piece.
	 *
	 * @var array
	 */
	private $basic_settings;

	/**
	 * Holds the settings + labels for the feeds clean up.
	 *
	 * @var array
	 */
	private $feed_settings;

	/**
	 * Holds the settings + labels for permalink cleanup settings.
	 *
	 * @var array
	 */
	private $permalink_cleanup_settings;

	/**
	 * Holds the settings + labels for search cleanup settings.
	 *
	 * @var array
	 */
	private $search_cleanup_settings;

	/**
	 * Holds the settings + labels for unused resources settings.
	 *
	 * @var array
	 */
	private $unused_resources_settings;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	private $product_helper;

	/**
	 * Crawl_Settings_Integration constructor.
	 *
	 * @param Product_Helper $product_helper The product helper.
	 */
	public function __construct(
		Product_Helper $product_helper
	) {
		$this->product_helper = $product_helper;
	}

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * In this case: when on an admin page.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Registers an action to add a new tab to the General page.
	 */
	public function register_hooks() {
		$this->register_setting_labels();

		\add_action( 'wpseo_settings_tabs_dashboard', [ $this, 'add_crawl_settings_tab' ] );
		if ( ! $this->product_helper->is_premium() || ! $this->is_premium_upgraded() ) {
			\add_action( 'wpseo_settings_tab_crawl_cleanup_internal', [ $this, 'add_crawl_settings_tab_content' ] );
			\add_action( 'wpseo_settings_tab_crawl_cleanup_network', [ $this, 'add_crawl_settings_tab_content_network' ] );
		}
	}

	/**
	 * Checks if Premium is installed and upgraded to the right version.
	 *
	 * @return bool Whether Premium is installed and upgraded to the right version.
	 */
	public function is_premium_upgraded() {
		$premium_version = $this->product_helper->get_premium_version();

		return $premium_version !== null && \version_compare( $premium_version, '18.6-RC1', '>=' );
	}

	/**
	 * Connects the settings to their labels.
	 *
	 * @return void
	 */
	private function register_setting_labels() {
		$this->feed_settings = [
			'remove_feed_global'            => \__( 'Global feed', 'wordpress-seo' ),
			'remove_feed_global_comments'   => \__( 'Global comment feeds', 'wordpress-seo' ),
			'remove_feed_post_comments'     => \__( 'Post comments feeds', 'wordpress-seo' ),
			'remove_feed_authors'           => \__( 'Post authors feeds', 'wordpress-seo' ),
			'remove_feed_post_types'        => \__( 'Post type feeds', 'wordpress-seo' ),
			'remove_feed_categories'        => \__( 'Category feeds', 'wordpress-seo' ),
			'remove_feed_tags'              => \__( 'Tag feeds', 'wordpress-seo' ),
			'remove_feed_custom_taxonomies' => \__( 'Custom taxonomy feeds', 'wordpress-seo' ),
			'remove_feed_search'            => \__( 'Search results feeds', 'wordpress-seo' ),
			'remove_atom_rdf_feeds'         => \__( 'Atom/RDF feeds', 'wordpress-seo' ),
		];

		$this->basic_settings = [
			'remove_shortlinks'        => \__( 'Shortlinks', 'wordpress-seo' ),
			'remove_rest_api_links'    => \__( 'REST API links', 'wordpress-seo' ),
			'remove_rsd_wlw_links'     => \__( 'RSD / WLW links', 'wordpress-seo' ),
			'remove_oembed_links'      => \__( 'oEmbed links', 'wordpress-seo' ),
			'remove_generator'         => \__( 'Generator tag', 'wordpress-seo' ),
			'remove_pingback_header'   => \__( 'Pingback HTTP header', 'wordpress-seo' ),
			'remove_powered_by_header' => \__( 'Powered by HTTP header', 'wordpress-seo' ),
		];

		$this->permalink_cleanup_settings = [
			'clean_campaign_tracking_urls' => \__( 'Campaign tracking URL parameters', 'wordpress-seo' ),
			'clean_permalinks'             => \__( 'Unregistered URL parameters', 'wordpress-seo' ),
		];

		$this->search_cleanup_settings = [
			'search_cleanup'              => \__( 'Filter search terms', 'wordpress-seo' ),
			'search_cleanup_emoji'        => \__( 'Filter searches with emojis and other special characters', 'wordpress-seo' ),
			'search_cleanup_patterns'     => \__( 'Filter searches with common spam patterns', 'wordpress-seo' ),
			'deny_search_crawling'        => \__( 'Prevent search engines from crawling site search URLs', 'wordpress-seo' ),
			'redirect_search_pretty_urls' => \__( 'Redirect pretty URLs for search pages to raw format', 'wordpress-seo' ),
		];

		$this->unused_resources_settings = [
			'remove_emoji_scripts'  => \__( 'Emoji scripts', 'wordpress-seo' ),
			'deny_wp_json_crawling' => \__( 'Prevent search engines from crawling /wp-json/', 'wordpress-seo' ),
		];
	}

	/**
	 * Adds a dedicated tab in the General sub-page.
	 *
	 * @param WPSEO_Option_Tabs $dashboard_tabs Object representing the tabs of the General sub-page.
	 */
	public function add_crawl_settings_tab( $dashboard_tabs ) {
		$premium = $this->product_helper->is_premium() && $this->is_premium_upgraded();

		$dashboard_tabs->add_tab(
			new WPSEO_Option_Tab(
				'crawl-settings',
				\__( 'Crawl settings', 'wordpress-seo' ),
				[
					'save_button' => $premium,
					'beta'        => $premium,
					'premium'     => ! $premium,
				]
			)
		);
	}

	/**
	 * Adds content to the Crawl Cleanup tab.
	 *
	 * @param Yoast_Form $yform The yoast form object.
	 */
	public function add_crawl_settings_tab_content( $yform ) {
		$this->add_crawl_settings( $yform, false );
	}

	/**
	 * Adds content to the Crawl Cleanup network tab.
	 *
	 * @param Yoast_Form $yform The yoast form object.
	 */
	public function add_crawl_settings_tab_content_network( $yform ) {
		$this->add_crawl_settings( $yform, true );
	}

	/**
	 * Print the settings sections.
	 *
	 * @param Yoast_Form $yform      The Yoast form class.
	 * @param bool       $is_network Whether we're on the network site.
	 *
	 * @return void
	 */
	private function add_crawl_settings( $yform, $is_network ) {
		$this->display_premium_upsell_btn();

		echo '<div class="yoast-crawl-settings-disabled">';

		$this->print_toggles( $this->basic_settings, $yform, $is_network, \__( 'Basic crawl settings', 'wordpress-seo' ), \__( 'Remove links added by WordPress to the header and &lt;head&gt;.', 'wordpress-seo' ) );
		$this->print_toggles( $this->feed_settings, $yform, $is_network, \__( 'Feed crawl settings', 'wordpress-seo' ), \__( "Remove feed links added by WordPress that aren't needed for this site.", 'wordpress-seo' ) );
		$this->print_toggles( $this->unused_resources_settings, $yform, $is_network, \__( 'Remove unused resources', 'wordpress-seo' ), \__( 'WordPress loads lots of resources, some of which your site might not need. If you’re not using these, removing them can speed up your pages and save resources.', 'wordpress-seo' ) );

		$first_search_setting    = \array_slice( $this->search_cleanup_settings, 0, 1 );
		$rest_search_settings    = \array_slice( $this->search_cleanup_settings, 1 );
		$search_settings_toggles = [
			'off' => \__( 'Disabled', 'wordpress-seo' ),
			'on'  => \__( 'Enabled', 'wordpress-seo' ),
		];

		$this->print_toggles( $first_search_setting, $yform, $is_network, \__( 'Search cleanup settings', 'wordpress-seo' ), \__( 'Clean up and filter searches to prevent search spam.', 'wordpress-seo' ), $search_settings_toggles );

		if ( ! $is_network ) {
			echo '<div class="yoast-crawl-single-setting">';
			$yform->number(
				'search_character_limit_free',
				\__( 'Max number of characters to allow in searches', 'wordpress-seo' ),
				[
					'min'      => 1,
					'max'      => 1000,
					'disabled' => true,
				]
			);
			$yform->hidden( 'search_character_limit', 'search_character_limit' );
			echo '</div>';
		}

		$this->print_toggles( $rest_search_settings, $yform, $is_network, '', '', $search_settings_toggles );

		$this->print_toggles( $this->permalink_cleanup_settings, $yform, $is_network, \__( 'Permalink cleanup settings', 'wordpress-seo' ), \__( 'Remove unwanted URL parameters from your URLs.', 'wordpress-seo' ) );

		if ( ! $is_network ) {
			echo '<div class="yoast-crawl-single-setting">';
			$yform->textinput(
				'clean_permalinks_extra_variables_free',
				\__( 'Additional URL parameters to allow', 'wordpress-seo' ),
				[
					'disabled' => true,
				]
			);
			$yform->hidden( 'clean_permalinks_extra_variables', 'clean_permalinks_extra_variables' );
			echo '<p class="desc label yoast-extra-variables-label-free">';
			\esc_html_e( 'Please use a comma to separate multiple URL parameters.', 'wordpress-seo' );
			echo '</p>';
			echo '</div>';
		}

		echo '</div>';
	}

	/**
	 * Prints a list of toggles for an array of settings with labels.
	 *
	 * @param array      $settings    The settings being displayed.
	 * @param Yoast_Form $yform       The Yoast form class.
	 * @param bool       $is_network  Whether we're on the network site.
	 * @param string     $title       Optional title for the settings being displayed.
	 * @param string     $description Optional description of the settings being displayed.
	 * @param array      $toggles     Optional naming of the toggle buttons.
	 *
	 * @return void
	 */
	private function print_toggles( array $settings, Yoast_Form $yform, $is_network, $title = '', $description = '', $toggles = [] ) {
		if ( ! empty( $title ) ) {
			echo '<h3 class="yoast-crawl-settings-free">', \esc_html( $title ), '</h3>';
		}
		if ( ! $is_network && ! empty( $description ) ) {
			echo '<p class="yoast-crawl-settings-explanation-free">', \esc_html( $description ), '</p>';
		}

		if ( empty( $toggles ) ) {
			$toggles = [
				'off' => \__( 'Keep', 'wordpress-seo' ),
				'on'  => \__( 'Remove', 'wordpress-seo' ),
			];
		}

		$setting_prefix = '';

		if ( $is_network ) {
			$setting_prefix = WPSEO_Option::ALLOW_KEY_PREFIX;
			// NOTE: the off/on labels here are flipped from their actual would-be values in premium for cosmetic reasons and limitations with disabled toggles.
			$toggles = [
				// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO.
				'on'  => \__( 'Allow Control', 'wordpress-seo' ),
				// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO.
				'off' => \__( 'Disable', 'wordpress-seo' ),
			];
		}
		foreach ( $settings as $setting => $label ) {
			$attr = [
				'disabled'                => true,
				'preserve_disabled_value' => true,
			];
			if ( $this->should_feature_be_disabled_multisite( $setting ) ) {
				$attr['preserve_disabled_value'] = false;
			}
			$yform->toggle_switch(
				$setting_prefix . $setting,
				$toggles,
				$label,
				'',
				$attr
			);
			if ( $setting === 'remove_feed_global_comments' && ! $is_network ) {
				echo '<p class="yoast-crawl-settings-help yoast-crawl-settings-help-free ">';
				echo \esc_html__( 'By removing Global comments feed, Post comments feeds will be removed too.', 'wordpress-seo' );
				echo '</p>';
			}
			if ( $this->should_feature_be_disabled_multisite( $setting ) ) {
				echo '<p class="disabled">';
				\esc_html_e( 'This feature is not available for multisites.', 'wordpress-seo' );
				echo '</p>';
			}
		}
	}

	/**
	 * Displays the Premium upsell button.
	 */
	public function display_premium_upsell_btn() {
		echo '<a class="yoast-button-upsell" href="';
		echo \esc_url( WPSEO_Shortlinker::get( 'http://yoa.st/crawl-settings-upsell' ) );
		echo '" target="_blank" style=" margin-top: 16px; margin-bottom: 16px; ">';

		$button_msg = ( $this->product_helper->is_premium() && ! $this->is_premium_upgraded() ) ? \esc_html__( 'Upgrade Premium', 'wordpress-seo' ) : \esc_html__( 'Unlock with Premium', 'wordpress-seo' );
		// phpcs:ignore WordPress.Security.EscapeOutput -- Already escaped.
		echo $button_msg
					// phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly.
					. WPSEO_Admin_Utils::get_new_tab_message();
		echo '<span aria-hidden="true" class="yoast-button-upsell__caret"></span></a>';
	}

	/**
	 * Checks if the feature should be disabled due to the site being a multisite.
	 *
	 * @param string $setting The setting to be displayed.
	 *
	 * @return bool
	 */
	protected function should_feature_be_disabled_multisite( $setting ) {
		return (
			\in_array( $setting, [ 'deny_search_crawling', 'deny_wp_json_crawling' ], true )
			&& \is_multisite()
		);
	}
}
admin/background-indexing-integration.php000066600000013671151130361070014621 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
use Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action;
use Yoast\WP\SEO\Conditionals\Get_Request_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Class Background_Indexing_Integration.
 *
 * @package Yoast\WP\SEO\Integrations\Admin
 */
class Background_Indexing_Integration implements Integration_Interface {

	/**
	 * The post indexing action.
	 *
	 * @var Indexable_Post_Indexation_Action
	 */
	protected $post_indexation;

	/**
	 * The term indexing action.
	 *
	 * @var Indexable_Term_Indexation_Action
	 */
	protected $term_indexation;

	/**
	 * The post type archive indexing action.
	 *
	 * @var Indexable_Post_Type_Archive_Indexation_Action
	 */
	protected $post_type_archive_indexation;

	/**
	 * Represents the general indexing.
	 *
	 * @var Indexable_General_Indexation_Action
	 */
	protected $general_indexation;

	/**
	 * Represents the indexing completed action.
	 *
	 * @var Indexable_Indexing_Complete_Action
	 */
	protected $complete_indexation_action;

	/**
	 * The post link indexing action.
	 *
	 * @var Post_Link_Indexing_Action
	 */
	protected $post_link_indexing_action;

	/**
	 * The term link indexing action.
	 *
	 * @var Term_Link_Indexing_Action
	 */
	protected $term_link_indexing_action;

	/**
	 * Represents the indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * Returns the conditionals based on which this integration should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [
			Yoast_Admin_And_Dashboard_Conditional::class,
			Migrations_Conditional::class,
			Get_Request_Conditional::class,
		];
	}

	/**
	 * Shutdown_Indexing_Integration constructor.
	 *
	 * @param Indexable_Post_Indexation_Action              $post_indexation              The post indexing action.
	 * @param Indexable_Term_Indexation_Action              $term_indexation              The term indexing action.
	 * @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation The post type archive indexing action.
	 * @param Indexable_General_Indexation_Action           $general_indexation           The general indexing action.
	 * @param Indexable_Indexing_Complete_Action            $complete_indexation_action   The complete indexing action.
	 * @param Post_Link_Indexing_Action                     $post_link_indexing_action    The post indexing action.
	 * @param Term_Link_Indexing_Action                     $term_link_indexing_action    The term indexing action.
	 * @param Indexing_Helper                               $indexing_helper              The indexing helper.
	 */
	public function __construct(
		Indexable_Post_Indexation_Action $post_indexation,
		Indexable_Term_Indexation_Action $term_indexation,
		Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation,
		Indexable_General_Indexation_Action $general_indexation,
		Indexable_Indexing_Complete_Action $complete_indexation_action,
		Post_Link_Indexing_Action $post_link_indexing_action,
		Term_Link_Indexing_Action $term_link_indexing_action,
		Indexing_Helper $indexing_helper
	) {
		$this->post_indexation              = $post_indexation;
		$this->term_indexation              = $term_indexation;
		$this->post_type_archive_indexation = $post_type_archive_indexation;
		$this->general_indexation           = $general_indexation;
		$this->complete_indexation_action   = $complete_indexation_action;
		$this->post_link_indexing_action    = $post_link_indexing_action;
		$this->term_link_indexing_action    = $term_link_indexing_action;
		$this->indexing_helper              = $indexing_helper;
	}

	/**
	 * Register hooks.
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'register_shutdown_indexing' ], 10 );
	}

	/**
	 * Enqueues the required scripts.
	 *
	 * @return void
	 */
	public function register_shutdown_indexing() {
		if ( $this->should_index_on_shutdown( $this->get_shutdown_limit() ) ) {
			\register_shutdown_function( [ $this, 'index' ] );
		}
	}

	/**
	 * Run a single indexing pass of each indexing action. Intended for use as a shutdown function.
	 *
	 * @return void
	 */
	public function index() {
		$this->post_indexation->index();
		$this->term_indexation->index();
		$this->general_indexation->index();
		$this->post_type_archive_indexation->index();
		$this->post_link_indexing_action->index();
		$this->term_link_indexing_action->index();
		$this->complete_indexation_action->complete();
	}

	/**
	 * Retrieves the shutdown limit. This limit is the amount of indexables that is generated in the background.
	 *
	 * @return int The shutdown limit.
	 */
	protected function get_shutdown_limit() {
		/**
		 * Filter 'wpseo_shutdown_indexation_limit' - Allow filtering the number of objects that can be indexed during shutdown.
		 *
		 * @api int The maximum number of objects indexed.
		 */
		return \apply_filters( 'wpseo_shutdown_indexation_limit', 25 );
	}

	/**
	 * Determine whether background indexation should be performed.
	 *
	 * @param int $shutdown_limit The shutdown limit used to determine whether indexation should be run.
	 *
	 * @return bool Should background indexation be performed.
	 */
	public function should_index_on_shutdown( $shutdown_limit ) {
		$total = $this->indexing_helper->get_limited_filtered_unindexed_count( $shutdown_limit );

		return ( $total > 0 && $total < $shutdown_limit );
	}
}
admin/indexing-tool-integration.php000066600000020242151130361070013447 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\No_Tool_Selected_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Error_Presenter;
use Yoast\WP\SEO\Presenters\Admin\Indexing_List_Item_Presenter;
use Yoast\WP\SEO\Routes\Importing_Route;
use Yoast\WP\SEO\Routes\Indexing_Route;
use Yoast\WP\SEO\Services\Importing\Importable_Detector_Service;

/**
 * Class Indexing_Tool_Integration. Bridge to the Javascript indexing tool on Yoast SEO Tools page.
 *
 * @package Yoast\WP\SEO\Integrations\Admin
 */
class Indexing_Tool_Integration implements Integration_Interface {

	/**
	 * Represents the admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * Represents the indexables helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * The short link helper.
	 *
	 * @var Short_Link_Helper
	 */
	protected $short_link_helper;

	/**
	 * Represents the indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	protected $product_helper;

	/**
	 * The Importable Detector service.
	 *
	 * @var Importable_Detector_Service
	 */
	protected $importable_detector;

	/**
	 * The Importing Route class.
	 *
	 * @var Importing_Route
	 */
	protected $importing_route;

	/**
	 * Returns the conditionals based on which this integration should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [
			Migrations_Conditional::class,
			No_Tool_Selected_Conditional::class,
			Yoast_Tools_Page_Conditional::class,
		];
	}

	/**
	 * Indexing_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager   $asset_manager       The admin asset manager.
	 * @param Indexable_Helper            $indexable_helper    The indexable helper.
	 * @param Short_Link_Helper           $short_link_helper   The short link helper.
	 * @param Indexing_Helper             $indexing_helper     The indexing helper.
	 * @param WPSEO_Addon_Manager         $addon_manager       The addon manager.
	 * @param Product_Helper              $product_helper      The product helper.
	 * @param Importable_Detector_Service $importable_detector The importable detector.
	 * @param Importing_Route             $importing_route     The importing route.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Indexable_Helper $indexable_helper,
		Short_Link_Helper $short_link_helper,
		Indexing_Helper $indexing_helper,
		WPSEO_Addon_Manager $addon_manager,
		Product_Helper $product_helper,
		Importable_Detector_Service $importable_detector,
		Importing_Route $importing_route
	) {
		$this->asset_manager       = $asset_manager;
		$this->indexable_helper    = $indexable_helper;
		$this->short_link_helper   = $short_link_helper;
		$this->indexing_helper     = $indexing_helper;
		$this->addon_manager       = $addon_manager;
		$this->product_helper      = $product_helper;
		$this->importable_detector = $importable_detector;
		$this->importing_route     = $importing_route;
	}

	/**
	 * Register hooks.
	 */
	public function register_hooks() {
		\add_action( 'wpseo_tools_overview_list_items_internal', [ $this, 'render_indexing_list_item' ], 10 );
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ], 10 );
	}

	/**
	 * Enqueues the required scripts.
	 *
	 * @return void
	 */
	public function enqueue_scripts() {
		$this->asset_manager->enqueue_script( 'indexation' );
		$this->asset_manager->enqueue_style( 'admin-css' );
		$this->asset_manager->enqueue_style( 'monorepo' );

		$data = [
			'disabled'                    => ! $this->indexable_helper->should_index_indexables(),
			'amount'                      => $this->indexing_helper->get_filtered_unindexed_count(),
			'firstTime'                   => ( $this->indexing_helper->is_initial_indexing() === true ),
			'errorMessage'                => $this->render_indexing_error(),
			'restApi'                     => [
				'root'                => \esc_url_raw( \rest_url() ),
				'indexing_endpoints'  => $this->get_indexing_endpoints(),
				'importing_endpoints' => $this->get_importing_endpoints(),
				'nonce'               => \wp_create_nonce( 'wp_rest' ),
			],
		];

		/**
		 * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process.
		 *
		 * @param array $data The indexing data to adapt.
		 */
		$data = \apply_filters( 'wpseo_indexing_data', $data );

		$this->asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data );
	}

	/**
	 * The error to show if optimization failed.
	 *
	 * @return string The error to show if optimization failed.
	 */
	protected function render_indexing_error() {
		$presenter = new Indexing_Error_Presenter(
			$this->short_link_helper,
			$this->product_helper,
			$this->addon_manager
		);

		return $presenter->present();
	}

	/**
	 * Determines if the site has a valid Premium subscription.
	 *
	 * @return bool If the site has a valid Premium subscription.
	 */
	protected function has_valid_premium_subscription() {
		return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG );
	}

	/**
	 * Renders the indexing list item.
	 *
	 * @return void
	 */
	public function render_indexing_list_item() {
		if ( \current_user_can( 'manage_options' ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The output is correctly escaped in the presenter.
			echo new Indexing_List_Item_Presenter( $this->short_link_helper );
		}
	}

	/**
	 * Retrieves a list of the indexing endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_indexing_endpoints() {
		$endpoints = [
			'prepare'            => Indexing_Route::FULL_PREPARE_ROUTE,
			'terms'              => Indexing_Route::FULL_TERMS_ROUTE,
			'posts'              => Indexing_Route::FULL_POSTS_ROUTE,
			'archives'           => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE,
			'general'            => Indexing_Route::FULL_GENERAL_ROUTE,
			'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE,
			'post_link'          => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE,
			'term_link'          => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE,
		];

		$endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints );

		$endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE;

		return $endpoints;
	}

	/**
	 * Retrieves a list of the importing endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_importing_endpoints() {
		$available_actions   = $this->importable_detector->detect_importers();
		$importing_endpoints = [];

		foreach ( $available_actions as $plugin => $types ) {
			foreach ( $types as $type ) {
				$importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type );
			}
		}

		return $importing_endpoints;
	}

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @deprecated 15.3
	 * @codeCoverageIgnore
	 *
	 * @param int $unindexed_count The total number of unindexed indexables.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_unindexed_indexables_count( $unindexed_count = 0 ) {
		\_deprecated_function( __METHOD__, 'WPSEO 15.3' );

		return $this->indexing_helper->get_unindexed_count();
	}

	/**
	 * Returns the total number of unindexed objects and applies a filter for third party integrations.
	 *
	 * @deprecated 15.3
	 * @codeCoverageIgnore
	 *
	 * @param int $unindexed_count The total number of unindexed objects.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_unindexed_count( $unindexed_count = 0 ) {
		\_deprecated_function( __METHOD__, 'WPSEO 15.3' );

		return $this->indexing_helper->get_filtered_unindexed_count();
	}
}
admin/indexables-exclude-taxonomy-integration.php000066600000002455151130361070016316 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;

/**
 * Indexables_Exclude_Taxonomy_Integration class
 */
class Indexables_Exclude_Taxonomy_Integration implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * Indexables_Exclude_Taxonomy_Integration constructor.
	 *
	 * @param Options_Helper $options_helper The options helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_indexable_excluded_taxonomies', [ $this, 'exclude_taxonomies_for_indexation' ] );
	}

	/**
	 * Exclude the taxonomy from the indexable table.
	 *
	 * @param array $excluded_taxonomies The excluded taxonomies.
	 *
	 * @return array The excluded post types, including the specific post type.
	 */
	public function exclude_taxonomies_for_indexation( $excluded_taxonomies ) {
		if ( $this->options_helper->get( 'disable-post_format', false ) ) {
			return \array_merge( $excluded_taxonomies, [ 'post_format' ] );
		}

		return $excluded_taxonomies;
	}
}
admin/menu-badge-integration.php000066600000001624151130361070012676 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Menu_Badge_Integration class.
 */
class Menu_Badge_Integration implements Integration_Interface {

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'admin_enqueue_scripts', [ $this, 'add_inline_styles' ] );
	}

	/**
	 * Renders the migration error.
	 *
	 * @return void
	 */
	public function add_inline_styles() {
		$custom_css = 'ul.wp-submenu span.yoast-premium-badge::after, #wpadminbar span.yoast-premium-badge::after { content:"' .
			\__( 'Premium', 'wordpress-seo' ) . '"}';
		\wp_add_inline_style( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global', $custom_css );
	}
}
admin/old-configuration-integration.php000066600000003121151130361070014307 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Old_Configuration_Integration class
 */
class Old_Configuration_Integration implements Integration_Interface {

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'admin_menu', [ $this, 'add_submenu_page' ], 11 );
		\add_action( 'admin_init', [ $this, 'redirect_to_new_configuration' ] );
	}

	/**
	 * Adds the old configuration submenu page.
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array the filtered submenu pages.
	 */
	public function add_submenu_page( $submenu_pages ) {
		\add_submenu_page(
			null,
			\__( 'Old Configuration Wizard', 'wordpress-seo' ),
			null,
			'manage_options',
			'wpseo_configurator',
			[ $this, 'render_page' ]
		);

		return $submenu_pages;
	}

	/**
	 * Renders the old configuration page.
	 */
	public function render_page() {
		// This page is never to be displayed.
	}

	/**
	 * Redirects from the old configuration page to the new configuration page.
	 */
	public function redirect_to_new_configuration() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Data is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_configurator' ) {
			return;
		}
		\wp_safe_redirect( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ), 302, 'Yoast SEO' );
		exit;
	}
}
admin/deactivated-premium-integration.php000066600000010404151130361070014617 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Non_Multisite_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;

/**
 * Deactivated_Premium_Integration class
 */
class Deactivated_Premium_Integration implements Integration_Interface {

	/**
	 * The options' helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class, Non_Multisite_Conditional::class ];
	}

	/**
	 * First_Time_Configuration_Notice_Integration constructor.
	 *
	 * @param Options_Helper            $options_helper      The options helper.
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
	 */
	public function __construct(
		Options_Helper $options_helper,
		WPSEO_Admin_Asset_Manager $admin_asset_manager
	) {
		$this->options_helper      = $options_helper;
		$this->admin_asset_manager = $admin_asset_manager;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'admin_notices', [ $this, 'premium_deactivated_notice' ] );
		\add_action( 'wp_ajax_dismiss_premium_deactivated_notice', [ $this, 'dismiss_premium_deactivated_notice' ] );
	}

	/**
	 * Shows a notice if premium is installed but not activated.
	 *
	 * @return void
	 */
	public function premium_deactivated_notice() {
		global $pagenow;
		if ( $pagenow === 'update.php' ) {
			return;
		}

		if ( $this->options_helper->get( 'dismiss_premium_deactivated_notice', false ) === true ) {
			return;
		}

		$premium_file = 'wordpress-seo-premium/wp-seo-premium.php';

		if ( ! \current_user_can( 'activate_plugin', $premium_file ) ) {
			return;
		}

		if ( $this->premium_is_installed_not_activated( $premium_file ) ) {
			$this->admin_asset_manager->enqueue_style( 'monorepo' );

			$content = \sprintf(
				/* translators: 1: Yoast SEO Premium 2: Link start tag to activate premium, 3: Link closing tag. */
				\__( 'You\'ve installed %1$s but it\'s not activated yet. %2$sActivate %1$s now!%3$s', 'wordpress-seo' ),
				'Yoast SEO Premium',
				'<a href="' . \esc_url(
					\wp_nonce_url(
						\self_admin_url( 'plugins.php?action=activate&plugin=' . $premium_file ),
						'activate-plugin_' . $premium_file
					)
				) . '">',
				'</a>'
			);
            // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above.
			echo new Notice_Presenter(
				/* translators: 1: Yoast SEO Premium */
				\sprintf( \__( 'Activate %1$s!', 'wordpress-seo' ), 'Yoast SEO Premium' ),
				$content,
				'support-team.svg',
				null,
				true,
				'yoast-premium-deactivated-notice'
			);
            // phpcs:enable

			// Enable permanently dismissing the notice.
			echo "<script>
                function dismiss_premium_deactivated_notice(){
                    var data = {
                    'action': 'dismiss_premium_deactivated_notice',
                    };

                    jQuery.post( ajaxurl, data, function( response ) {
                        jQuery( '#yoast-premium-deactivated-notice' ).hide();
                    });
                }

                jQuery( document ).ready( function() {
                    jQuery( 'body' ).on( 'click', '#yoast-premium-deactivated-notice .notice-dismiss', function() {
                        dismiss_premium_deactivated_notice();
                    } );
                } );
            </script>";
		}
	}

	/**
	 * Dismisses the premium deactivated notice.
	 *
	 * @return bool
	 */
	public function dismiss_premium_deactivated_notice() {
		return $this->options_helper->set( 'dismiss_premium_deactivated_notice', true );
	}

	/**
	 * Returns whether or not premium is installed and not activated.
	 *
	 * @param string $premium_file The premium file.
	 *
	 * @return bool Whether or not premium is installed and not activated.
	 */
	protected function premium_is_installed_not_activated( $premium_file ) {
		return (
			! \defined( 'WPSEO_PREMIUM_FILE' )
			&& \file_exists( \WP_PLUGIN_DIR . '/' . $premium_file )
		);
	}
}
admin/link-count-columns-integration.php000066600000016760151130361070014442 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WP_Query;
use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
use Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Should_Index_Links_Conditional;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Link_Count_Columns_Integration class.
 */
class Link_Count_Columns_Integration implements Integration_Interface {

	/**
	 * Partial column name.
	 *
	 * @var string
	 */
	const COLUMN_LINKED = 'linked';

	/**
	 * Partial column name.
	 *
	 * @var string
	 */
	const COLUMN_LINKS = 'links';

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * The database object.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * The post link builder.
	 *
	 * @var Post_Link_Indexing_Action
	 */
	protected $post_link_indexing_action;

	/**
	 * The admin columns cache.
	 *
	 * @var Admin_Columns_Cache_Integration
	 */
	protected $admin_columns_cache;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Posts_Overview_Or_Ajax_Conditional::class,
			Should_Index_Links_Conditional::class,
		];
	}

	/**
	 * Link_Count_Columns_Integration constructor
	 *
	 * @param Post_Type_Helper                $post_type_helper          The post type helper.
	 * @param wpdb                            $wpdb                      The wpdb object.
	 * @param Post_Link_Indexing_Action       $post_link_indexing_action The post link indexing action.
	 * @param Admin_Columns_Cache_Integration $admin_columns_cache       The admin columns cache.
	 */
	public function __construct(
		Post_Type_Helper $post_type_helper,
		wpdb $wpdb,
		Post_Link_Indexing_Action $post_link_indexing_action,
		Admin_Columns_Cache_Integration $admin_columns_cache
	) {
		$this->post_type_helper          = $post_type_helper;
		$this->wpdb                      = $wpdb;
		$this->post_link_indexing_action = $post_link_indexing_action;
		$this->admin_columns_cache       = $admin_columns_cache;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'posts_clauses', [ $this, 'order_by_links' ], 1, 2 );
		\add_filter( 'posts_clauses', [ $this, 'order_by_linked' ], 1, 2 );

		\add_action( 'admin_init', [ $this, 'register_init_hooks' ] );

		// Adds a filter to exclude the attachments from the link count.
		\add_filter( 'wpseo_link_count_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] );
	}

	/**
	 * Register hooks that need to be registered after `init` due to all post types not yet being registered.
	 */
	public function register_init_hooks() {
		$public_post_types = \apply_filters( 'wpseo_link_count_post_types', $this->post_type_helper->get_accessible_post_types() );

		if ( ! \is_array( $public_post_types ) || empty( $public_post_types ) ) {
			return;
		}

		foreach ( $public_post_types as $post_type ) {
			\add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_post_columns' ] );
			\add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
			\add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] );
		}
	}

	/**
	 * Adds the columns for the post overview.
	 *
	 * @param array $columns Array with columns.
	 *
	 * @return array The extended array with columns.
	 */
	public function add_post_columns( $columns ) {
		if ( ! \is_array( $columns ) ) {
			return $columns;
		}

		$columns[ 'wpseo-' . self::COLUMN_LINKS ] = \sprintf(
			'<span class="yoast-linked-to yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
			\esc_attr__( 'Number of outgoing internal links in this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ),
			\esc_html__( 'Outgoing internal links', 'wordpress-seo' )
		);

		if ( $this->post_link_indexing_action->get_total_unindexed() === 0 ) {
			$columns[ 'wpseo-' . self::COLUMN_LINKED ] = \sprintf(
				'<span class="yoast-linked-from yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
				\esc_attr__( 'Number of internal links linking to this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ),
				\esc_html__( 'Received internal links', 'wordpress-seo' )
			);
		}

		return $columns;
	}

	/**
	 * Modifies the query pieces to allow ordering column by links to post.
	 *
	 * @param array    $pieces Array of Query pieces.
	 * @param WP_Query $query  The Query on which to apply.
	 *
	 * @return array
	 */
	public function order_by_linked( $pieces, $query ) {
		if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKED ) {
			return $pieces;
		}

		return $this->build_sort_query_pieces( $pieces, $query, 'incoming_link_count' );
	}

	/**
	 * Modifies the query pieces to allow ordering column by links to post.
	 *
	 * @param array    $pieces Array of Query pieces.
	 * @param WP_Query $query  The Query on which to apply.
	 *
	 * @return array
	 */
	public function order_by_links( $pieces, $query ) {
		if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKS ) {
			return $pieces;
		}

		return $this->build_sort_query_pieces( $pieces, $query, 'link_count' );
	}

	/**
	 * Builds the pieces for a sorting query.
	 *
	 * @param array    $pieces Array of Query pieces.
	 * @param WP_Query $query  The Query on which to apply.
	 * @param string   $field  The field in the table to JOIN on.
	 *
	 * @return array Modified Query pieces.
	 */
	protected function build_sort_query_pieces( $pieces, $query, $field ) {
		// We only want our code to run in the main WP query.
		if ( ! $query->is_main_query() ) {
			return $pieces;
		}

		// Get the order query variable - ASC or DESC.
		$order = \strtoupper( $query->get( 'order' ) );

		// Make sure the order setting qualifies. If not, set default as ASC.
		if ( ! \in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
			$order = 'ASC';
		}

		$table = Model::get_table_name( 'Indexable' );

		$pieces['join']   .= " LEFT JOIN $table AS yoast_indexable ON yoast_indexable.object_id = {$this->wpdb->posts}.ID AND yoast_indexable.object_type = 'post' ";
		$pieces['orderby'] = "yoast_indexable.$field $order, FIELD( {$this->wpdb->posts}.post_status, 'publish' ) $order, {$pieces['orderby']}";

		return $pieces;
	}

	/**
	 * 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.
	 */
	public function column_content( $column_name, $post_id ) {
		$indexable = $this->admin_columns_cache->get_indexable( $post_id );
		// Nothing to output if we don't have the value.
		if ( $indexable === false ) {
			return;
		}

		switch ( $column_name ) {
			case 'wpseo-' . self::COLUMN_LINKS:
				echo (int) $indexable->link_count;
				return;
			case 'wpseo-' . self::COLUMN_LINKED:
				if ( \get_post_status( $post_id ) === 'publish' ) {
					echo (int) $indexable->incoming_link_count;
				}
		}
	}

	/**
	 * Sets the sortable columns.
	 *
	 * @param array $columns Array with sortable columns.
	 *
	 * @return array The extended array with sortable columns.
	 */
	public function column_sort( array $columns ) {
		$columns[ 'wpseo-' . self::COLUMN_LINKS ]  = 'wpseo-' . self::COLUMN_LINKS;
		$columns[ 'wpseo-' . self::COLUMN_LINKED ] = 'wpseo-' . self::COLUMN_LINKED;

		return $columns;
	}
}
admin/first-time-configuration-notice-integration.php000066600000017011151130361070017076 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;

/**
 * First_Time_Configuration_Notice_Integration class
 */
class First_Time_Configuration_Notice_Integration implements Integration_Interface {

	/**
	 * The options' helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	private $indexing_helper;

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * Whether we show the alternate mesage.
	 *
	 * @var bool
	 */
	private $show_alternate_message;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * First_Time_Configuration_Notice_Integration constructor.
	 *
	 * @param Options_Helper            $options_helper      The options helper.
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
	 */
	public function __construct(
		Options_Helper $options_helper,
		Indexing_Helper $indexing_helper,
		WPSEO_Admin_Asset_Manager $admin_asset_manager
	) {
		$this->options_helper         = $options_helper;
		$this->indexing_helper        = $indexing_helper;
		$this->admin_asset_manager    = $admin_asset_manager;
		$this->show_alternate_message = false;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'wp_ajax_dismiss_first_time_configuration_notice', [ $this, 'dismiss_first_time_configuration_notice' ] );
		\add_action( 'admin_notices', [ $this, 'first_time_configuration_notice' ] );
	}

	/**
	 * Dismisses the First-time configuration notice.
	 *
	 * @return bool
	 */
	public function dismiss_first_time_configuration_notice() {
		return $this->options_helper->set( 'dismiss_configuration_workout_notice', true );
	}

	/**
	 * Determines whether and where the "First-time SEO Configuration" admin notice should be displayed.
	 *
	 * @return bool Whether the "First-time SEO Configuration" admin notice should be displayed.
	 */
	public function should_display_first_time_configuration_notice() {
		if ( ! $this->options_helper->get( 'dismiss_configuration_workout_notice', false ) === false ) {
			return false;
		}

		if ( ! $this->user_can_do_first_time_configuration() ) {
			return false;
		}

		if ( ! $this->on_wpseo_admin_page_or_dashboard() ) {
			return false;
		}

		if ( $this->is_first_time_configuration_finished() ) {
			return false;
		}

		if ( $this->options_helper->get( 'first_time_install', false ) !== false ) {
			return ! $this->are_site_representation_name_and_logo_set() || $this->indexing_helper->get_unindexed_count() > 0;
		}

		if ( $this->indexing_helper->is_initial_indexing() === false ) {
			return false;
		}

		if ( $this->indexing_helper->is_finished_indexables_indexing() === true ) {
			return false;
		}

		$this->show_alternate_message = true;

		return ! $this->are_site_representation_name_and_logo_set();
	}

	/**
	 * Displays an admin notice when the first-time configuration has not been finished yet.
	 *
	 * @return void
	 */
	public function first_time_configuration_notice() {
		if ( ! $this->should_display_first_time_configuration_notice() ) {
			return;
		}

		$this->admin_asset_manager->enqueue_style( 'monorepo' );

		$title = ( ! $this->show_alternate_message ) ? \__( 'First-time SEO configuration', 'wordpress-seo' ) : \__( 'SEO configuration', 'wordpress-seo' );
		if ( ! $this->show_alternate_message ) {
			$content = \sprintf(
				/* translators: 1: Link start tag to the first-time configuration, 2: Yoast SEO, 3: Link closing tag. */
				\__( 'Get started quickly with the %1$s%2$s First-time configuration%3$s and configure Yoast SEO with the optimal SEO settings for your site!', 'wordpress-seo' ),
				'<a href="' . \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ) . '">',
				'Yoast SEO',
				'</a>'
			);
		}
		else {
			$content = \sprintf(
				/* translators: 1: Link start tag to the first-time configuration, 2: Link closing tag. */
				\__( 'We noticed that you haven\'t fully configured Yoast SEO yet. Optimize your SEO settings even further by using our improved %1$s First-time configuration%2$s.', 'wordpress-seo' ),
				'<a href="' . \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ) . '">',
				'</a>'
			);
		}

		$notice = new Notice_Presenter(
			$title,
			$content,
			'mirrored_fit_bubble_woman_1_optim.svg',
			null,
			true,
			'yoast-first-time-configuration-notice'
		);

		//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe.
		echo $notice->present();

		// Enable permanently dismissing the notice.
		echo "<script>
			function dismiss_first_time_configuration_notice(){
				var data = {
				'action': 'dismiss_first_time_configuration_notice',
				};

				jQuery.post( ajaxurl, data, function( response ) {
					jQuery( '#yoast-first-time-configuration-notice' ).hide();
				});
			}

			jQuery( document ).ready( function() {
				jQuery( 'body' ).on( 'click', '#yoast-first-time-configuration-notice .notice-dismiss', function() {
					dismiss_first_time_configuration_notice();
				} );
			} );
			</script>";
	}

	/**
	 * Whether the user can do the first-time configuration.
	 *
	 * @return bool Whether the current user can do the first-time configuration.
	 */
	private function user_can_do_first_time_configuration() {
		return \current_user_can( 'wpseo_manage_options' );
	}

	/**
	 * Whether the user is currently visiting one of our admin pages or the WordPress dashboard.
	 *
	 * @return bool Whether the current page is a Yoast SEO admin page
	 */
	private function on_wpseo_admin_page_or_dashboard() {
		$pagenow = $GLOBALS['pagenow'];

		// Show on the WP Dashboard.
		if ( $pagenow === 'index.php' ) {
			return true;
		}

		$page_from_get = \filter_input( \INPUT_GET, 'page' );

		// Show on Yoast SEO pages, with some exceptions.
		if ( $pagenow === 'admin.php' && \strpos( $page_from_get, 'wpseo' ) === 0 ) {
			$exceptions = [
				'wpseo_installation_successful',
				'wpseo_installation_successful_free',
			];

			if ( ! \in_array( $page_from_get, $exceptions, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Whether all steps of the first-time configuration have been finished.
	 *
	 * @return bool Whether the first-time configuration has been finished.
	 */
	private function is_first_time_configuration_finished() {
		$configuration_finished_steps = $this->options_helper->get( 'configuration_finished_steps', [] );

		return \count( $configuration_finished_steps ) === 3;
	}

	/**
	 * Whether the site representation name and logo have been set.
	 *
	 * @return bool  Whether the site representation name and logo have been set.
	 */
	private function are_site_representation_name_and_logo_set() {
		$company_or_person = $this->options_helper->get( 'company_or_person', '' );

		if ( $company_or_person === '' ) {
			return false;
		}

		if ( $company_or_person === 'company' ) {
			return ! empty( $this->options_helper->get( 'company_name' ) )
					&& ! empty( $this->options_helper->get( 'company_logo', '' ) );
		}

		return ! empty( $this->options_helper->get( 'company_or_person_user_id' ) )
				&& ! empty( $this->options_helper->get( 'person_logo', '' ) );
	}
}
admin/social-profiles-helper.php000066600000020354151130361070012722 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Class Social_Profiles_Helper.
 */
class Social_Profiles_Helper {

	/**
	 * The fields for the person social profiles payload.
	 *
	 * @var array
	 */
	private $person_social_profile_fields = [
		'facebook'   => 'get_non_valid_url',
		'instagram'  => 'get_non_valid_url',
		'linkedin'   => 'get_non_valid_url',
		'myspace'    => 'get_non_valid_url',
		'pinterest'  => 'get_non_valid_url',
		'soundcloud' => 'get_non_valid_url',
		'tumblr'     => 'get_non_valid_url',
		'twitter'    => 'get_non_valid_twitter',
		'youtube'    => 'get_non_valid_url',
		'wikipedia'  => 'get_non_valid_url',
	];

	/**
	 * The fields for the organization social profiles payload.
	 *
	 * @var array
	 */
	private $organization_social_profile_fields = [
		'facebook_site'     => 'get_non_valid_url',
		'twitter_site'      => 'get_non_valid_twitter',
		'other_social_urls' => 'get_non_valid_url_array',
	];

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Social_Profiles_Helper constructor.
	 *
	 * @param Options_Helper $options_helper The WPSEO options helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

	/**
	 * Gets the person social profiles stored in the database.
	 *
	 * @param int $person_id The id of the person.
	 *
	 * @return array The person's social profiles.
	 */
	public function get_person_social_profiles( $person_id ) {
		$social_profiles_fields = \array_keys( $this->person_social_profile_fields );
		$person_social_profiles = \array_combine( $social_profiles_fields, \array_fill( 0, \count( $social_profiles_fields ), '' ) );

		// If no person has been selected, $person_id is set to false.
		if ( \is_numeric( $person_id ) ) {
			foreach ( \array_keys( $person_social_profiles ) as $field_name ) {
				$value = \get_user_meta( $person_id, $field_name, true );
				// If $person_id is an integer but does not represent a valid user, get_user_meta returns false.
				if ( ! \is_bool( $value ) ) {
					$person_social_profiles[ $field_name ] = $value;
				}
			}
		}

		return $person_social_profiles;
	}

	/**
	 * Stores the values for the person's social profiles.
	 *
	 * @param int   $person_id       The id of the person to edit.
	 * @param array $social_profiles The array of the person's social profiles to be set.
	 *
	 * @return string[] An array of field names which failed to be saved in the db.
	 */
	public function set_person_social_profiles( $person_id, $social_profiles ) {
		$failures = [];

		// First validate all social profiles, before even attempting to save them.
		foreach ( $this->person_social_profile_fields as $field_name => $validation_method ) {
			if ( ! isset( $social_profiles[ $field_name ] ) ) {
				$failures[] = $field_name;
				continue;
			}

			if ( $social_profiles[ $field_name ] === '' ) {
				continue;
			}

			$value_to_validate = $social_profiles[ $field_name ];
			$failures          = \array_merge( $failures, \call_user_func( [ $this, $validation_method ], $value_to_validate, $field_name ) );
		}

		if ( ! empty( $failures ) ) {
			return $failures;
		}

		// All social profiles look good, now let's try to save them.
		foreach ( $this->person_social_profile_fields as $field_name => $validation_method ) {
			$social_profiles[ $field_name ] = $this->sanitize_social_field( $social_profiles[ $field_name ] );
			\update_user_meta( $person_id, $field_name, $social_profiles[ $field_name ] );
		}

		return $failures;
	}

	/**
	 * Stores the values for the organization's social profiles.
	 *
	 * @param array $social_profiles An array with the social profiles values to be saved in the db.
	 *
	 * @return string[] An array of field names which failed to be saved in the db.
	 */
	public function set_organization_social_profiles( $social_profiles ) {
		$failures = [];

		// First validate all social profiles, before even attempting to save them.
		foreach ( $this->organization_social_profile_fields as $field_name => $validation_method ) {
			if ( ! isset( $social_profiles[ $field_name ] ) ) {
				$failures[] = $field_name;
				continue;
			}
			$social_profiles[ $field_name ] = $this->sanitize_social_field( $social_profiles[ $field_name ] );

			$value_to_validate = $social_profiles[ $field_name ];
			$failures          = \array_merge( $failures, \call_user_func( [ $this, $validation_method ], $value_to_validate, $field_name ) );
		}

		if ( ! empty( $failures ) ) {
			return $failures;
		}

		// All social profiles look good, now let's try to save them.
		foreach ( \array_keys( $this->organization_social_profile_fields ) as $field_name ) {
			// Remove empty strings in Other Social URLs.
			if ( $field_name === 'other_social_urls' ) {
				$other_social_urls = \array_filter(
					$social_profiles[ $field_name ],
					static function( $other_social_url ) {
						return $other_social_url !== '';
					}
				);

				$social_profiles[ $field_name ] = \array_values( $other_social_urls );
			}

			$result = $this->options_helper->set( $field_name, $social_profiles[ $field_name ] );
			if ( ! $result ) {
				/**
				 * The value for Twitter might have been sanitised from URL to username.
				 * If so, $result will be false. We should check if the option value is part of the received value.
				 */
				if ( $field_name === 'twitter_site' ) {
					$current_option = $this->options_helper->get( $field_name );
					if ( ! \strpos( $social_profiles[ $field_name ], 'twitter.com/' . $current_option ) ) {
						$failures[] = $field_name;
					}
				}
				else {
					$failures[] = $field_name;
				}
			}
		}

		if ( ! empty( $failures ) ) {
			return $failures;
		}

		return [];
	}

	/**
	 * Returns a sanitized social field.
	 *
	 * @param string|array $social_field The social field to sanitize.
	 *
	 * @return string|array The sanitized social field.
	 */
	protected function sanitize_social_field( $social_field ) {
		if ( \is_array( $social_field ) ) {
			foreach ( $social_field as $key => $value ) {
				$social_field[ $key ] = \sanitize_text_field( $value );
			}

			return $social_field;
		}

		return \sanitize_text_field( $social_field );
	}

	/**
	 * Checks if the current user has the capability to edit a specific user.
	 *
	 * @param int $person_id The id of the person to edit.
	 *
	 * @return bool
	 */
	public function can_edit_profile( $person_id ) {
		return \current_user_can( 'edit_user', $person_id );
	}

	/**
	 * Checks if url is not valid and returns the name of the setting if it's not.
	 *
	 * @param string $url         The url to be validated.
	 * @param string $url_setting The name of the setting to be updated with the url.
	 *
	 * @return array An array with the setting that the non-valid url is about to update.
	 */
	protected function get_non_valid_url( $url, $url_setting ) {
		if ( $this->options_helper->is_social_url_valid( $url ) ) {
			return [];
		}

		return [ $url_setting ];
	}

	/**
	 * Checks if urls in an array are not valid and return the name of the setting if one of them is not, including the non-valid url's index in the array
	 *
	 * @param array  $urls         The urls to be validated.
	 * @param string $urls_setting The name of the setting to be updated with the urls.
	 *
	 * @return array An array with the settings that the non-valid urls are about to update, suffixed with a dash-separated index of the positions of those settings, eg. other_social_urls-2.
	 */
	protected function get_non_valid_url_array( $urls, $urls_setting ) {
		$non_valid_url_array = [];

		foreach ( $urls as $key => $url ) {
			if ( ! $this->options_helper->is_social_url_valid( $url ) ) {
				$non_valid_url_array[] = $urls_setting . '-' . $key;
			}
		}

		return $non_valid_url_array;
	}

	/**
	 * Checks if the twitter value is not valid and returns the name of the setting if it's not.
	 *
	 * @param array  $twitter_site    The twitter value to be validated.
	 * @param string $twitter_setting The name of the twitter setting to be updated with the value.
	 *
	 * @return array An array with the setting that the non-valid twitter value is about to update.
	 */
	protected function get_non_valid_twitter( $twitter_site, $twitter_setting ) {
		if ( $this->options_helper->is_twitter_id_valid( $twitter_site, false ) ) {
			return [];
		}

		return [ $twitter_setting ];
	}
}
admin/installation-success-integration.php000066600000007602151130361070015043 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Installation_Success_Integration class
 */
class Installation_Success_Integration implements Integration_Interface {

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	protected $product_helper;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Installation_Success_Integration constructor.
	 *
	 * @param Options_Helper $options_helper The options helper.
	 * @param Product_Helper $product_helper The product helper.
	 */
	public function __construct( Options_Helper $options_helper, Product_Helper $product_helper ) {
		$this->options_helper = $options_helper;
		$this->product_helper = $product_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'admin_menu', [ $this, 'add_submenu_page' ], 9 );
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
		\add_action( 'admin_init', [ $this, 'maybe_redirect' ] );
	}

	/**
	 * Redirects to the installation success page if an installation has just occurred.
	 *
	 * @return void
	 */
	public function maybe_redirect() {
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return;
		}

		if ( ! $this->options_helper->get( 'should_redirect_after_install_free', false ) ) {
			return;
		}
		$this->options_helper->set( 'should_redirect_after_install_free', false );

		if ( ! empty( $this->options_helper->get( 'activation_redirect_timestamp_free', 0 ) ) ) {
			return;
		}
		$this->options_helper->set( 'activation_redirect_timestamp_free', \time() );

		// phpcs:ignore WordPress.Security.NonceVerification -- This is not a form.
		if ( isset( $_REQUEST['activate-multi'] ) && $_REQUEST['activate-multi'] === 'true' ) {
			return;
		}

		if ( $this->product_helper->is_premium() ) {
			return;
		}

		if ( \is_network_admin() || \is_plugin_active_for_network( \WPSEO_BASENAME ) ) {
			return;
		}

		\wp_safe_redirect( \admin_url( 'admin.php?page=wpseo_installation_successful_free' ), 302, 'Yoast SEO' );
		$this->terminate_execution();
	}

	/**
	 * Adds the installation success submenu page.
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array the filtered submenu pages.
	 */
	public function add_submenu_page( $submenu_pages ) {
		\add_submenu_page(
			null,
			\__( 'Installation Successful', 'wordpress-seo' ),
			null,
			'manage_options',
			'wpseo_installation_successful_free',
			[ $this, 'render_page' ]
		);

		return $submenu_pages;
	}

	/**
	 * Enqueue assets on the Installation success page.
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_installation_successful_free' ) {
			return;
		}

		$asset_manager = new WPSEO_Admin_Asset_Manager();
		$asset_manager->enqueue_script( 'installation-success' );
		$asset_manager->enqueue_style( 'tailwind' );
		$asset_manager->enqueue_style( 'monorepo' );

		$asset_manager->localize_script(
			'installation-success',
			'wpseoInstallationSuccess',
			[
				'pluginUrl'                 => \esc_url( \plugins_url( '', \WPSEO_FILE ) ),
				'firstTimeConfigurationUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ),
			]
		);
	}

	/**
	 * Renders the installation success page.
	 */
	public function render_page() {
		echo '<div id="wpseo-installation-successful-free" class="yoast"></div>';
	}

	/**
	 * Wrap the `exit` function to make unit testing easier.
	 */
	public function terminate_execution() {
		exit;
	}
}
admin/disable-concatenate-scripts-integration.php000066600000001756151130361070016252 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Disable_Concatenate_Scripts_Integration class.
 */
class Disable_Concatenate_Scripts_Integration implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * In this case: when on an admin page.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Registers an action to disable script concatenation.
	 */
	public function register_hooks() {
		\add_action( 'wp_print_scripts', [ $this, 'disable_concatenate_scripts' ] );
	}

	/**
	 * Due to bugs in the 5.5 core release concatenate scripts is causing errors.
	 *
	 * Because of this we disable it.
	 *
	 * @return void
	 */
	public function disable_concatenate_scripts() {
		global $concatenate_scripts;

		$concatenate_scripts = false;
	}
}
admin/integrations-page.php000066600000016104151130361070011770 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use WPSEO_Plugin_Availability;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Jetpack_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Integrations_Page class
 */
class Integrations_Page implements Integration_Interface {

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Workouts_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
	 * @param Options_Helper            $options_helper      The options helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $admin_asset_manager,
		Options_Helper $options_helper
	) {
		$this->admin_asset_manager = $admin_asset_manager;
		$this->options_helper      = $options_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 10 );
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
	}

	/**
	 * Adds the integrations submenu page.
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array The filtered submenu pages.
	 */
	public function add_submenu_page( $submenu_pages ) {
		$integrations_page = [
			'wpseo_dashboard',
			'',
			\__( 'Integrations', 'wordpress-seo' ),
			'wpseo_manage_options',
			'wpseo_integrations',
			[ $this, 'render_target' ],
		];

		\array_splice( $submenu_pages, 1, 0, [ $integrations_page ] );
		return $submenu_pages;
	}

	/**
	 * Enqueue the integrations app.
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_integrations' ) {
			return;
		}

		$this->admin_asset_manager->enqueue_style( 'admin-css' );
		$this->admin_asset_manager->enqueue_style( 'tailwind' );
		$this->admin_asset_manager->enqueue_style( 'monorepo' );

		$this->admin_asset_manager->enqueue_script( 'integrations-page' );

		$elementor_conditional   = new Elementor_Activated_Conditional();
		$jetpack_conditional     = new Jetpack_Conditional();
		$woocommerce_conditional = new WooCommerce_Conditional();

		$woocommerce_seo_file = 'wpseo-woocommerce/wpseo-woocommerce.php';
		$acf_seo_file         = 'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php';
		$acf_seo_file_github  = 'yoast-acf-analysis/yoast-acf-analysis.php';
		$algolia_file         = 'wp-search-with-algolia/algolia.php';
		$old_algolia_file     = 'search-by-algolia-instant-relevant-results/algolia.php';

		$wpseo_plugin_availability_checker = new WPSEO_Plugin_Availability();
		$woocommerce_seo_installed         = \file_exists( \WP_PLUGIN_DIR . '/' . $woocommerce_seo_file );
		$woocommerce_seo_active            = $wpseo_plugin_availability_checker->is_active( $woocommerce_seo_file );
		$woocommerce_active                = $woocommerce_conditional->is_met();
		$acf_seo_installed                 = \file_exists( \WP_PLUGIN_DIR . '/' . $acf_seo_file );
		$acf_seo_github_installed          = \file_exists( \WP_PLUGIN_DIR . '/' . $acf_seo_file_github );
		$acf_seo_active                    = $wpseo_plugin_availability_checker->is_active( $acf_seo_file );
		$acf_seo_github_active             = $wpseo_plugin_availability_checker->is_active( $acf_seo_file_github );
		$acf_active                        = \class_exists( 'acf' );
		$algolia_active                    = $wpseo_plugin_availability_checker->is_active( $algolia_file );
		$old_algolia_active                = $wpseo_plugin_availability_checker->is_active( $old_algolia_file );

		$woocommerce_seo_activate_url = \wp_nonce_url(
			\self_admin_url( 'plugins.php?action=activate&plugin=' . $woocommerce_seo_file ),
			'activate-plugin_' . $woocommerce_seo_file
		);

		if ( $acf_seo_installed ) {
			$acf_seo_activate_url = \wp_nonce_url(
				\self_admin_url( 'plugins.php?action=activate&plugin=' . $acf_seo_file ),
				'activate-plugin_' . $acf_seo_file
			);
		}
		else {
			$acf_seo_activate_url = \wp_nonce_url(
				\self_admin_url( 'plugins.php?action=activate&plugin=' . $acf_seo_file_github ),
				'activate-plugin_' . $acf_seo_file_github
			);
		}

		$acf_seo_install_url = \wp_nonce_url(
			\self_admin_url( 'update.php?action=install-plugin&plugin=acf-content-analysis-for-yoast-seo' ),
			'install-plugin_acf-content-analysis-for-yoast-seo'
		);

		$this->admin_asset_manager->localize_script(
			'integrations-page',
			'wpseoIntegrationsData',
			[
				'semrush_integration_active'     => $this->options_helper->get( 'semrush_integration_active', true ),
				'allow_semrush_integration'      => $this->options_helper->get( 'allow_semrush_integration_active', true ),
				'zapier_integration_active'      => $this->options_helper->get( 'zapier_integration_active', false ),
				'allow_zapier_integration'       => $this->options_helper->get( 'allow_zapier_integration_active', true ),
				'algolia_integration_active'     => $this->options_helper->get( 'algolia_integration_active', false ),
				'allow_algolia_integration'      => $this->options_helper->get( 'allow_algolia_integration_active', true ),
				'wincher_integration_active'     => $this->options_helper->get( 'wincher_integration_active', true ),
				'allow_wincher_integration'      => null,
				'wordproof_integration_active'   => $this->options_helper->get( 'wordproof_integration_active', true ),
				'allow_wordproof_integration'    => null,
				'elementor_integration_active'   => $elementor_conditional->is_met(),
				'jetpack_integration_active'     => $jetpack_conditional->is_met(),
				'woocommerce_seo_installed'      => $woocommerce_seo_installed,
				'woocommerce_seo_active'         => $woocommerce_seo_active,
				'woocommerce_active'             => $woocommerce_active,
				'woocommerce_seo_activate_url'   => $woocommerce_seo_activate_url,
				'acf_seo_installed'              => $acf_seo_installed || $acf_seo_github_installed,
				'acf_seo_active'                 => $acf_seo_active || $acf_seo_github_active,
				'acf_active'                     => $acf_active,
				'acf_seo_activate_url'           => $acf_seo_activate_url,
				'acf_seo_install_url'            => $acf_seo_install_url,
				'algolia_active'                 => $algolia_active || $old_algolia_active,
				'is_multisite'                   => \is_multisite(),
				'plugin_url'                     => \plugins_url( '', \WPSEO_FILE ),
			]
		);
	}

	/**
	 * Renders the target for the React to mount to.
	 */
	public function render_target() {
		?>
		<div class="wrap yoast wpseo-admin-page page-wpseo">
			<div class="wp-header-end" style="height: 0; width: 0;"></div>
			<div id="wpseo-integrations"></div>
		</div>
		<?php
	}
}
admin/indexables-page-integration.php000066600000016633151130361070013730 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use WPSEO_Addon_Manager;

use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Indexables_Page_Conditional;
use Yoast\WP\SEO\Helpers\Indexables_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Error_Presenter;
use Yoast\WP\SEO\Routes\Indexing_Route;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
/**
 * Indexables_Page_Integration class
 */
class Indexables_Page_Integration implements Integration_Interface {

	/**
	 * The shortlinker.
	 *
	 * @var WPSEO_Shortlinker
	 */
	private $shortlinker;

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * The short link helper.
	 *
	 * @var Short_Link_Helper
	 */
	protected $short_link_helper;

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The indexables page helper.
	 *
	 * @var Indexables_Page_Helper
	 */
	private $indexables_page_helper;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	private $product_helper;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Indexables_Page_Conditional::class,
		];
	}

	/**
	 * Indexables_Page_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager    The admin asset manager.
	 * @param WPSEO_Addon_Manager       $addon_manager          The addon manager.
	 * @param WPSEO_Shortlinker         $shortlinker            The shortlinker.
	 * @param Short_Link_Helper         $short_link_helper      The short link helper.
	 * @param Indexables_Page_Helper    $indexables_page_helper The indexables page helper.
	 * @param Options_Helper            $options_helper         The options helper.
	 * @param Product_Helper            $product_helper         The product helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $admin_asset_manager,
		WPSEO_Addon_Manager $addon_manager,
		WPSEO_Shortlinker $shortlinker,
		Short_Link_Helper $short_link_helper,
		Indexables_Page_Helper $indexables_page_helper,
		Options_Helper $options_helper,
		Product_Helper $product_helper
	) {
		$this->admin_asset_manager    = $admin_asset_manager;
		$this->addon_manager          = $addon_manager;
		$this->shortlinker            = $shortlinker;
		$this->short_link_helper      = $short_link_helper;
		$this->indexables_page_helper = $indexables_page_helper;
		$this->options_helper         = $options_helper;
		$this->product_helper         = $product_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
	}

	/**
	 * Enqueues assets for the Indexables Page.
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_dashboard' || \is_network_admin() ) {
			return;
		}

		$this->admin_asset_manager->enqueue_script( 'indexables-page' );
		$this->admin_asset_manager->enqueue_style( 'tailwind' );
		$this->admin_asset_manager->enqueue_style( 'monorepo' );
		$this->admin_asset_manager->enqueue_script( 'indexation' );

		$data = [
			'disabled'     => ! \YoastSEO()->helpers->indexable->should_index_indexables(),
			'amount'       => \YoastSEO()->helpers->indexing->get_filtered_unindexed_count(),
			// Forcing this to 0 to not display the initial alert notice.
			'firstTime'    => '0',
			'errorMessage' => $this->render_indexing_error(),
			'restApi'      => [
				'root'               => \esc_url_raw( \rest_url() ),
				'indexing_endpoints' => $this->get_endpoints(),
				'nonce'              => \wp_create_nonce( 'wp_rest' ),
			],
		];

		/**
		 * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process.
		 *
		 * @param array $data The indexing data to adapt.
		 */
		$data = \apply_filters( 'wpseo_indexing_data', $data );

		$this->admin_asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data );

		$this->admin_asset_manager->localize_script(
			'indexables-page',
			'wpseoIndexablesPageData',
			[
				'environment'              => \wp_get_environment_type(),
				'listSize'                 => $this->indexables_page_helper->get_indexables_list_size(),
				'isLinkSuggestionsEnabled' => $this->indexables_page_helper->get_link_suggestions_enabled(),
				'isPremium'                => $this->product_helper->is_premium(),
				'ignoreLists'              => [
					'least_readability' => $this->options_helper->get( 'least_readability_ignore_list', [] ),
					'least_seo_score'   => $this->options_helper->get( 'least_seo_score_ignore_list', [] ),
					'most_linked'       => $this->options_helper->get( 'most_linked_ignore_list', [] ),
					'least_linked'      => $this->options_helper->get( 'least_linked_ignore_list', [] ),
				],
				'shortlinks'               => [
					'orphanedContent'         => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-orphaned-content' ),
					'cornerstoneContent'      => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-cornerstone-content' ),
					'recommendedReadingOne'   => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-recommended-reading-1' ),
					'recommendedReadingTwo'   => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-recommended-reading-2' ),
					'recommendedReadingThree' => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-recommended-reading-3' ),
					'recommendedReadingFour'  => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-recommended-reading-4' ),
					'recommendedReadingFive'  => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-recommended-reading-5' ),
					'internalLinks'           => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-internal-linking-suggestions' ),
					'getPremium'              => $this->shortlinker->build_shortlink( 'https://yoa.st/indexables-get-premium' ),
				],
			]
		);
	}

	/**
	 * Retrieves a list of the endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_endpoints() {
		$endpoints = [
			'prepare'            => Indexing_Route::FULL_PREPARE_ROUTE,
			'terms'              => Indexing_Route::FULL_TERMS_ROUTE,
			'posts'              => Indexing_Route::FULL_POSTS_ROUTE,
			'archives'           => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE,
			'general'            => Indexing_Route::FULL_GENERAL_ROUTE,
			'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE,
			'post_link'          => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE,
			'term_link'          => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE,
		];

		$endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints );

		$endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE;

		return $endpoints;
	}

	/**
	 * The error to show if optimization failed.
	 *
	 * @return string The error to show if optimization failed.
	 */
	protected function render_indexing_error() {
		$presenter = new Indexing_Error_Presenter(
			$this->short_link_helper,
			$this->product_helper,
			$this->addon_manager
		);

		return $presenter->present();
	}
}
admin/addon-installation/dialog-integration.php000066600000006634151130361070015723 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on.

namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation;

use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional;
use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Represents the Addon installation feature.
 */
class Dialog_Integration implements Integration_Interface {

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The addons.
	 *
	 * @var array
	 */
	protected $owned_addons;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Licenses_Page_Conditional::class,
			Addon_Installation_Conditional::class,
		];
	}

	/**
	 * Addon_Installation constructor.
	 *
	 * @param WPSEO_Addon_Manager $addon_manager The addon manager.
	 */
	public function __construct( WPSEO_Addon_Manager $addon_manager ) {
		$this->addon_manager = $addon_manager;
	}

	/**
	 * Registers all hooks to WordPress.
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'start_addon_installation' ] );
	}

	/**
	 * Starts the addon installation flow.
	 *
	 * @return void
	 */
	public function start_addon_installation() {
		// Only show the dialog when we explicitly want to see it.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: This is not a form.
		if ( ! isset( $_GET['install'] ) || $_GET['install'] !== 'true' ) {
			return;
		}

		$this->bust_myyoast_addon_information_cache();
		$this->owned_addons = $this->get_owned_addons();

		if ( \count( $this->owned_addons ) > 0 ) {
			\add_action( 'admin_enqueue_scripts', [ $this, 'show_modal' ] );
		}
		else {
			\add_action( 'admin_notices', [ $this, 'throw_no_owned_addons_warning' ] );
		}
	}

	/**
	 * Throws a no owned addons warning.
	 *
	 * @return void
	 */
	public function throw_no_owned_addons_warning() {
		echo '<div class="notice notice-warning"><p>' .
			\sprintf(
				/* translators: %1$s expands to Yoast SEO */
				\esc_html__(
					'No %1$s plugins have been installed. You don\'t seem to own any active subscriptions.',
					'wordpress-seo'
				),
				'Yoast SEO'
			) .
			'</p></div>';
	}

	/**
	 * Shows the modal.
	 *
	 * @return void
	 */
	public function show_modal() {
		\wp_localize_script(
			WPSEO_Admin_Asset_Manager::PREFIX . 'addon-installation',
			'wpseoAddonInstallationL10n',
			[
				'addons' => $this->owned_addons,
				'nonce'  => \wp_create_nonce( 'wpseo_addon_installation' ),
			]
		);

		$asset_manager = new WPSEO_Admin_Asset_Manager();
		$asset_manager->enqueue_script( 'addon-installation' );
	}

	/**
	 * Retrieves a list of owned addons for the site in MyYoast.
	 *
	 * @return array List of owned addons with slug as key and name as value.
	 */
	protected function get_owned_addons() {
		$owned_addons = [];

		foreach ( $this->addon_manager->get_myyoast_site_information()->subscriptions as $addon ) {
			$owned_addons[] = $addon->product->name;
		}

		return $owned_addons;
	}

	/**
	 * Bust the site information transients to have fresh data.
	 *
	 * @return void
	 */
	protected function bust_myyoast_addon_information_cache() {
		$this->addon_manager->remove_site_information_transients();
	}
}
admin/addon-installation/installation-integration.php000066600000013272151130361070017161 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on.

namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation;

use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action;
use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action;
use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional;
use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Activation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Already_Installed_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Installation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Activate_Plugins_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Install_Plugins_Exception;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Represents the Addon installation feature.
 */
class Installation_Integration implements Integration_Interface {

	/**
	 * The installation action.
	 *
	 * @var Addon_Install_Action
	 */
	protected $addon_install_action;

	/**
	 * The activation action.
	 *
	 * @var Addon_Activate_Action
	 */
	protected $addon_activate_action;

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Licenses_Page_Conditional::class,
			Addon_Installation_Conditional::class,
		];
	}

	/**
	 * Addon_Installation constructor.
	 *
	 * @param WPSEO_Addon_Manager   $addon_manager         The addon manager.
	 * @param Addon_Activate_Action $addon_activate_action The addon activate action.
	 * @param Addon_Install_Action  $addon_install_action  The addon install action.
	 */
	public function __construct(
		WPSEO_Addon_Manager $addon_manager,
		Addon_Activate_Action $addon_activate_action,
		Addon_Install_Action $addon_install_action
	) {
		$this->addon_manager         = $addon_manager;
		$this->addon_activate_action = $addon_activate_action;
		$this->addon_install_action  = $addon_install_action;
	}

	/**
	 * Registers all hooks to WordPress.
	 */
	public function register_hooks() {
		\add_action( 'wpseo_install_and_activate_addons', [ $this, 'install_and_activate_addons' ] );
	}

	/**
	 * Installs and activates missing addons.
	 *
	 * @return void
	 */
	public function install_and_activate_addons() {
		if ( \filter_input( \INPUT_GET, 'action' ) !== 'install' ) {
			return;
		}

		\check_admin_referer( 'wpseo_addon_installation', 'nonce' );

		echo '<div class="wrap yoast wpseo_table_page">';

		\printf(
			'<h1 id="wpseo-title" class="yoast-h1">%s</h1>',
			\esc_html__( 'Installing and activating addons', 'wordpress-seo' )
		);

		$licensed_addons = $this->addon_manager->get_myyoast_site_information()->subscriptions;

		foreach ( $licensed_addons as $addon ) {
			\printf( '<p><strong>%s</strong></p>', \esc_html( $addon->product->name ) );

			list( $installed, $output ) = $this->install_addon( $addon->product->slug, $addon->product->download );

			if ( $installed ) {
				$activation_output = $this->activate_addon( $addon->product->slug );

				$output = \array_merge( $output, $activation_output );
			}

			echo '<p>';
			echo \implode( '<br />', \array_map( 'esc_html', $output ) );
			echo '</p>';
		}

		\printf(
			/* translators: %1$s expands to an anchor tag to the admin premium page, %2$s expands to Yoast SEO Premium, %3$s expands to a closing anchor tag */
			\esc_html__( '%1$s Continue to %2$s%3$s', 'wordpress-seo' ),
			'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_licenses' ) ) . '">',
			'Yoast SEO Premium',
			'</a>'
		);

		echo '</div>';

		exit;
	}

	/**
	 * Activates an addon.
	 *
	 * @param string $addon_slug The addon to activate.
	 *
	 * @return array The output of the activation.
	 */
	public function activate_addon( $addon_slug ) {
		$output = [];

		try {
			$this->addon_activate_action->activate_addon( $addon_slug );

			/* Translators: %s expands to the name of the addon. */
			$output[] = \__( 'Addon activated.', 'wordpress-seo' );
		} catch ( User_Cannot_Activate_Plugins_Exception $exception ) {
			$output[] = \__( 'You are not allowed to activate plugins.', 'wordpress-seo' );
		} catch ( Addon_Activation_Error_Exception $exception ) {
			$output[] = \sprintf(
				/* Translators:%s expands to the error message. */
				\__( 'Addon activation failed because of an error: %s.', 'wordpress-seo' ),
				$exception->getMessage()
			);
		}

		return $output;
	}

	/**
	 * Installs an addon.
	 *
	 * @param string $addon_slug     The slug of the addon to install.
	 * @param string $addon_download The download URL of the addon.
	 *
	 * @return array The installation success state and the output of the installation.
	 */
	public function install_addon( $addon_slug, $addon_download ) {
		$installed = false;
		$output    = [];

		try {
			$installed = $this->addon_install_action->install_addon( $addon_slug, $addon_download );
		} catch ( Addon_Already_Installed_Exception $exception ) {
			/* Translators: %s expands to the name of the addon. */
			$output[] = \__( 'Addon installed.', 'wordpress-seo' );

			$installed = true;
		} catch ( User_Cannot_Install_Plugins_Exception $exception ) {
			$output[] = \__( 'You are not allowed to install plugins.', 'wordpress-seo' );
		} catch ( Addon_Installation_Error_Exception $exception ) {
			$output[] = \sprintf(
				/* Translators: %s expands to the error message. */
				\__( 'Addon installation failed because of an error: %s.', 'wordpress-seo' ),
				$exception->getMessage()
			);
		}

		return [ $installed, $output ];
	}
}
admin/admin-columns-cache-integration.php000066600000016762151130361070014512 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WP_Post;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Admin_Columns_Cache_Integration class.
 */
class Admin_Columns_Cache_Integration implements Integration_Interface {

	/**
	 * Cache of indexables.
	 *
	 * @var Indexable[]
	 */
	protected $indexable_cache = [];

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case: only when on an admin page.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Admin_Columns_Cache_Integration constructor.
	 *
	 * @param Indexable_Repository $indexable_repository The indexable repository.
	 */
	public function __construct( Indexable_Repository $indexable_repository ) {
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Registers the appropriate actions and filters to fill the cache with
	 * indexables on admin pages.
	 *
	 * This cache is used in showing the Yoast SEO columns on the posts overview
	 * page (e.g. keyword score, incoming link count, etc.)
	 */
	public function register_hooks() {
		// Hook into tablenav to calculate links and linked.
		\add_action( 'manage_posts_extra_tablenav', [ $this, 'maybe_fill_cache' ] );
	}

	/**
	 * Makes sure we calculate all values in one query by filling our cache beforehand.
	 *
	 * @param string $target Extra table navigation location which is triggered.
	 */
	public function maybe_fill_cache( $target ) {
		if ( $target === 'top' ) {
			$this->fill_cache();
		}
	}

	/**
	 * Fills the cache of indexables for all known post IDs.
	 */
	public function fill_cache() {
		global $wp_query;

		// No need to continue building a cache if the main query did not return anything to cache.
		if ( empty( $wp_query->posts ) ) {
			return;
		}

		$posts    = $wp_query->posts;
		$post_ids = [];

		// Post lists return a list of objects.
		if ( isset( $posts[0] ) && \is_a( $posts[0], 'WP_Post' ) ) {
			$post_ids = \wp_list_pluck( $posts, 'ID' );
		}
		elseif ( isset( $posts[0] ) && \is_object( $posts[0] ) ) {
			$post_ids = $this->get_current_page_page_ids( $posts );
		}
		elseif ( ! empty( $posts ) ) {
			// Page list returns an array of post IDs.
			$post_ids = \array_keys( $posts );
		}

		if ( empty( $post_ids ) ) {
			return;
		}

		if ( isset( $posts[0] ) && ! \is_a( $posts[0], WP_Post::class ) ) {
			// Prime the post caches as core would to avoid duplicate queries.
			// This needs to be done as this executes before core does.
			\_prime_post_caches( $post_ids );
		}

		$indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_ids, 'post', false );

		foreach ( $indexables as $indexable ) {
			$this->indexable_cache[ $indexable->object_id ] = $indexable;
		}
	}

	/**
	 * Returns the indexable for a given post ID.
	 *
	 * @param int $post_id The post ID.
	 *
	 * @return Indexable|false The indexable. False if none could be found.
	 */
	public function get_indexable( $post_id ) {
		if ( ! \array_key_exists( $post_id, $this->indexable_cache ) ) {
			$this->indexable_cache[ $post_id ] = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' );
		}
		return $this->indexable_cache[ $post_id ];
	}

	/**
	 * Gets all the page IDs set to be shown on the current page.
	 * This is copied over with some changes from WP_Posts_List_Table::_display_rows_hierarchical.
	 *
	 * @param array $pages The pages, each containing an ID and post_parent.
	 *
	 * @return array The IDs of all pages shown on the current page.
	 */
	private function get_current_page_page_ids( $pages ) {
		global $per_page;
		$pagenum = isset( $_REQUEST['paged'] ) ? \absint( $_REQUEST['paged'] ) : 0;
		$pagenum = \max( 1, $pagenum );

		/*
		 * Arrange pages into two parts: top level pages and children_pages
		 * children_pages is two dimensional array, eg.
		 * children_pages[10][] contains all sub-pages whose parent is 10.
		 * It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations
		 * If searching, ignore hierarchy and treat everything as top level
		 */
		if ( empty( $_REQUEST['s'] ) ) {
			$top_level_pages = [];
			$children_pages  = [];
			$pages_map       = [];

			foreach ( $pages as $page ) {

				// Catch and repair bad pages.
				if ( $page->post_parent === $page->ID ) {
					$page->post_parent = 0;
				}

				if ( $page->post_parent === 0 ) {
					$top_level_pages[] = $page;
				}
				else {
					$children_pages[ $page->post_parent ][] = $page;
				}
				$pages_map[ $page->ID ] = $page;
			}

			$pages = &$top_level_pages;
		}

		$count      = 0;
		$start      = ( ( $pagenum - 1 ) * $per_page );
		$end        = ( $start + $per_page );
		$to_display = [];

		foreach ( $pages as $page ) {
			if ( $count >= $end ) {
				break;
			}

			if ( $count >= $start ) {
				$to_display[] = $page->ID;
			}

			++$count;

			$this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map );
		}

		// If it is the last pagenum and there are orphaned pages, display them with paging as well.
		if ( isset( $children_pages ) && $count < $end ) {
			foreach ( $children_pages as $orphans ) {
				foreach ( $orphans as $op ) {
					if ( $count >= $end ) {
						break;
					}

					if ( $count >= $start ) {
						$to_display[] = $op->ID;
					}

					++$count;
				}
			}
		}

		return $to_display;
	}

	/**
	 * Adds all child pages due to be shown on the current page to the $to_display array.
	 * Copied over with some changes from WP_Posts_List_Table::_page_rows.
	 *
	 * @param array $children_pages The full map of child pages.
	 * @param int   $count          The number of pages already processed.
	 * @param int   $parent_id      The id of the parent that's currently being processed.
	 * @param int   $start          The number at which the current overview starts.
	 * @param int   $end            The number at which the current overview ends.
	 * @param int   $to_display     The page IDs to be shown.
	 * @param int   $pages_map      A map of page ID to an object with ID and post_parent.
	 *
	 * @return void
	 */
	private function get_child_page_ids( &$children_pages, &$count, $parent_id, $start, $end, &$to_display, &$pages_map ) {
		if ( ! isset( $children_pages[ $parent_id ] ) ) {
			return;
		}

		foreach ( $children_pages[ $parent_id ] as $page ) {
			if ( $count >= $end ) {
				break;
			}

			// If the page starts in a subtree, print the parents.
			if ( $count === $start && $page->post_parent > 0 ) {
				$my_parents = [];
				$my_parent  = $page->post_parent;
				while ( $my_parent ) {
					// Get the ID from the list or the attribute if my_parent is an object.
					$parent_id = $my_parent;
					if ( \is_object( $my_parent ) ) {
						$parent_id = $my_parent->ID;
					}

					$my_parent    = $pages_map[ $parent_id ];
					$my_parents[] = $my_parent;
					if ( ! $my_parent->post_parent ) {
						break;
					}
					$my_parent = $my_parent->post_parent;
				}
				while ( $my_parent = \array_pop( $my_parents ) ) {
					$to_display[] = $my_parent->ID;
				}
			}

			if ( $count >= $start ) {
				$to_display[] = $page->ID;
			}

			++$count;

			$this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map );
		}

		unset( $children_pages[ $parent_id ] ); // Required in order to keep track of orphans.
	}
}
admin/helpscout-beacon.php000066600000030346151130361070011607 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Tracking_Server_Data;
use WPSEO_Utils;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Config\Migration_Status;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Integrations\Settings_Integration;

/**
 * Class WPSEO_HelpScout
 */
class HelpScout_Beacon implements Integration_Interface {

	/**
	 * The id for the beacon.
	 *
	 * @var string
	 */
	protected $beacon_id = '2496aba6-0292-489c-8f5d-1c0fba417c2f';

	/**
	 * The id for the beacon for users that have tracking on.
	 *
	 * @var string
	 */
	protected $beacon_id_tracking_users = '6b8e74c5-aa81-4295-b97b-c2a62a13ea7f';

	/**
	 * The products the beacon is loaded for.
	 *
	 * @var array
	 */
	protected $products = [];

	/**
	 * Whether to asks the user's consent before loading in HelpScout.
	 *
	 * @var bool
	 */
	protected $ask_consent = true;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * The array of pages we need to show the beacon on with their respective beacon IDs.
	 *
	 * @var array
	 */
	protected $pages_ids;

	/**
	 * The array of pages we need to show the beacon on.
	 *
	 * @var array
	 */
	protected $base_pages = [
		'wpseo_dashboard',
		Settings_Integration::PAGE,
		'wpseo_titles',
		'wpseo_search_console',
		'wpseo_social',
		'wpseo_tools',
		'wpseo_licenses',
		'wpseo_workouts',
	];

	/**
	 * The current admin page
	 *
	 * @var string
	 */
	protected $page;

	/**
	 * The asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * The migration status object.
	 *
	 * @var Migration_Status
	 */
	protected $migration_status;

	/**
	 * Headless_Rest_Endpoints_Enabled_Conditional constructor.
	 *
	 * @param Options_Helper            $options          The options helper.
	 * @param WPSEO_Admin_Asset_Manager $asset_manager    The asset manager.
	 * @param Migration_Status          $migration_status The migrations status.
	 */
	public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status ) {
		$this->options          = $options;
		$this->asset_manager    = $asset_manager;
		$this->ask_consent      = ! $this->options->get( 'tracking' );
		$this->page             = \filter_input( \INPUT_GET, 'page', \FILTER_SANITIZE_STRING );
		$this->migration_status = $migration_status;

		foreach ( $this->base_pages as $page ) {
			if ( $this->ask_consent ) {
				// We want to be able to show surveys to people who have tracking on, so we give them a different beacon.
				$this->pages_ids[ $page ] = $this->beacon_id_tracking_users;
			}
			else {
				$this->pages_ids[ $page ] = $this->beacon_id;
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] );
		\add_action( 'admin_footer', [ $this, 'output_beacon_js' ] );
	}

	/**
	 * Enqueues the HelpScout script.
	 */
	public function enqueue_help_scout_script() {
		// Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon.
		$this->filter_settings();
		if ( ! $this->is_beacon_page() ) {
			return;
		}

		$this->asset_manager->enqueue_script( 'help-scout-beacon' );
	}

	/**
	 * Outputs a small piece of javascript for the beacon.
	 */
	public function output_beacon_js() {
		if ( ! $this->is_beacon_page() ) {
			return;
		}

		\printf(
			'<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>',
			( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon',
			\esc_html( $this->pages_ids[ $this->page ] ),
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode.
			WPSEO_Utils::format_json_encode( (array) $this->get_session_data() )
		);
	}

	/**
	 * Checks if the current page is a page containing the beacon.
	 *
	 * @return bool
	 */
	private function is_beacon_page() {
		$return = false;
		if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) {
			$return = true;
		}

		/**
		 * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon.
		 *
		 * @api bool - Whether we show the beacon or not.
		 */
		return \apply_filters( 'wpseo_helpscout_show_beacon', $return );
	}

	/**
	 * Retrieves the identifying data.
	 *
	 * @return string The data to pass as identifying data.
	 */
	protected function get_session_data() {
		// Short-circuit if we can get the needed data from a transient.
		$transient_data = \get_transient( 'yoast_beacon_session_data' );

		if ( \is_array( $transient_data ) ) {
			return WPSEO_Utils::format_json_encode( $transient_data );
		}

		$current_user = \wp_get_current_user();

		// Do not make these strings translatable! They are for our support agents, the user won't see them!
		$data = \array_merge(
			[
				'name'               => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ),
				'email'              => $current_user->user_email,
				'Languages'          => $this->get_language_settings(),
			],
			$this->get_server_info(),
			[
				'WordPress Version'    => $this->get_wordpress_version(),
				'Active theme'         => $this->get_theme_info(),
				'Active plugins'       => $this->get_active_plugins(),
				'Must-use and dropins' => $this->get_mustuse_and_dropins(),
				'Indexables status'    => $this->get_indexables_status(),
			]
		);

		if ( ! empty( $this->products ) ) {
			$addon_manager = new WPSEO_Addon_Manager();
			foreach ( $this->products as $product ) {
				$subscription = $addon_manager->get_subscription( $product );

				if ( ! $subscription ) {
					continue;
				}

				$data[ $subscription->product->name ] = $this->get_product_info( $subscription );
			}
		}

		// Store the data in a transient for 5 minutes to prevent overhead on every backend pageload.
		\set_transient( 'yoast_beacon_session_data', $data, ( 5 * \MINUTE_IN_SECONDS ) );

		return WPSEO_Utils::format_json_encode( $data );
	}

	/**
	 * Returns basic info about the server software.
	 *
	 * @return array
	 */
	private function get_server_info() {
		$server_tracking_data = new WPSEO_Tracking_Server_Data();
		$server_data          = $server_tracking_data->get();
		$server_data          = $server_data['server'];

		$fields_to_use = [
			'Server IP'        => 'ip',
			'PHP Version'      => 'PhpVersion',
			'cURL Version'     => 'CurlVersion',
		];

		$server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . ' (SSL Support ' . $server_data['CurlVersion']['sslSupport'] . ')';

		$server_info = [];

		foreach ( $fields_to_use as $label => $field_to_use ) {
			if ( isset( $server_data[ $field_to_use ] ) ) {
				$server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] );
			}
		}

		// Get the memory limits for the server and, if different, from WordPress as well.
		$memory_limit                 = \ini_get( 'memory_limit' );
		$server_info['Memory limits'] = 'Server memory limit: ' . $memory_limit;

		if ( $memory_limit !== \WP_MEMORY_LIMIT ) {
			$server_info['Memory limits'] .= ', WP_MEMORY_LIMIT: ' . \WP_MEMORY_LIMIT;
		}

		if ( $memory_limit !== \WP_MAX_MEMORY_LIMIT ) {
			$server_info['Memory limits'] .= ', WP_MAX_MEMORY_LIMIT: ' . \WP_MAX_MEMORY_LIMIT;
		}

		return $server_info;
	}

	/**
	 * Returns info about the Yoast SEO plugin version and license.
	 *
	 * @param object $plugin The plugin.
	 *
	 * @return string The product info.
	 */
	private function get_product_info( $plugin ) {
		if ( empty( $plugin ) ) {
			return '';
		}

		$product_info = \sprintf(
			'Expiration date %1$s',
			$plugin->expiry_date
		);

		return $product_info;
	}

	/**
	 * Returns the WordPress version + a suffix about the multisite status.
	 *
	 * @return string The WordPress version string.
	 */
	private function get_wordpress_version() {
		global $wp_version;

		$wordpress_version = $wp_version;
		if ( \is_multisite() ) {
			$wordpress_version .= ' (multisite: yes)';
		}
		else {
			$wordpress_version .= ' (multisite: no)';
		}

		return $wordpress_version;
	}

	/**
	 * Returns information about the current theme.
	 *
	 * @return string The theme info as string.
	 */
	private function get_theme_info() {
		$theme = \wp_get_theme();

		$theme_info = \sprintf(
			'%1$s (Version %2$s, %3$s)',
			\esc_html( $theme->display( 'Name' ) ),
			\esc_html( $theme->display( 'Version' ) ),
			\esc_attr( $theme->display( 'ThemeURI' ) )
		);

		if ( \is_child_theme() ) {
			$theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
		}

		return $theme_info;
	}

	/**
	 * Returns a stringified list of all active plugins, separated by a pipe.
	 *
	 * @return string The active plugins.
	 */
	private function get_active_plugins() {
		$updates_available = \get_site_transient( 'update_plugins' );

		$active_plugins = '';
		foreach ( \wp_get_active_and_valid_plugins() as $plugin ) {
			$plugin_data             = \get_plugin_data( $plugin );
			$plugin_file             = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
			$plugin_update_available = '';

			if ( isset( $updates_available->response[ $plugin_file ] ) ) {
				$plugin_update_available = ' [update available]';
			}

			$active_plugins .= \sprintf(
				'%1$s (Version %2$s%3$s, %4$s) | ',
				\esc_html( $plugin_data['Name'] ),
				\esc_html( $plugin_data['Version'] ),
				$plugin_update_available,
				\esc_attr( $plugin_data['PluginURI'] )
			);
		}

		return $active_plugins;
	}

	/**
	 * Returns a CSV list of all must-use and drop-in plugins.
	 *
	 * @return string The active plugins.
	 */
	private function get_mustuse_and_dropins() {
		$dropins         = \get_dropins();
		$mustuse_plugins = \get_mu_plugins();

		if ( ! \is_array( $dropins ) ) {
			$dropins = [];
		}

		if ( ! \is_array( $mustuse_plugins ) ) {
			$mustuse_plugins = [];
		}

		return \sprintf( 'Must-Use plugins: %1$d, Drop-ins: %2$d', \count( $mustuse_plugins ), \count( $dropins ) );
	}

	/**
	 * Return the indexables status details.
	 *
	 * @return string The indexables status in a string.
	 */
	private function get_indexables_status() {
		$indexables_status  = 'Indexing completed: ';
		$indexing_completed = $this->options->get( 'indexables_indexing_completed' );
		$indexing_reason    = $this->options->get( 'indexing_reason' );

		$indexables_status .= ( $indexing_completed ) ? 'yes' : 'no';
		$indexables_status .= ( $indexing_reason ) ? ', latest indexing reason: ' . \esc_html( $indexing_reason ) : '';

		foreach ( [ 'free', 'premium' ] as $migration_name ) {
			$current_status = $this->migration_status->get_error( $migration_name );

			if ( \is_array( $current_status ) && isset( $current_status['message'] ) ) {
				$indexables_status .= ', migration error: ' . \esc_html( $current_status['message'] );
			}
		}

		return $indexables_status;
	}

	/**
	 * Returns language settings for the website and the current user.
	 *
	 * @return string The locale settings of the site and user.
	 */
	private function get_language_settings() {
		$site_locale = \get_locale();
		$user_locale = \get_user_locale();

		$language_settings = \sprintf(
			'Site locale: %1$s, user locale: %2$s',
			( \is_string( $site_locale ) ) ? \esc_html( $site_locale ) : 'unknown',
			( \is_string( $user_locale ) ) ? \esc_html( $user_locale ) : 'unknown'
		);

		return $language_settings;
	}

	/**
	 * Returns the conditionals based on which this integration should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late.
	 */
	protected function filter_settings() {
		/**
		 * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
		 *
		 * @api string - The HelpScout beacon settings.
		 */
		$filterable_helpscout_setting = [
			'products'  => $this->products,
			'pages_ids' => $this->pages_ids,
		];

		$helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );

		$this->products  = $helpscout_settings['products'];
		$this->pages_ids = $helpscout_settings['pages_ids'];
	}
}
admin/indexing-notification-integration.php000066600000017225151130361070015167 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Environment_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Failed_Notification_Presenter;
use Yoast\WP\SEO\Presenters\Admin\Indexing_Notification_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
 * Class Indexing_Notification_Integration.
 *
 * @package Yoast\WP\SEO\Integrations\Admin
 */
class Indexing_Notification_Integration implements Integration_Interface {

	/**
	 * The notification ID.
	 */
	const NOTIFICATION_ID = 'wpseo-reindex';

	/**
	 * Represents the reason that the indexing process failed and should be tried again.
	 *
	 * @deprecated 15.3
	 */
	const REASON_INDEXING_FAILED = Indexing_Reasons::REASON_INDEXING_FAILED;

	/**
	 * Represents the reason that the permalink settings are changed.
	 *
	 * @deprecated 15.3
	 */
	const REASON_PERMALINK_SETTINGS = Indexing_Reasons::REASON_PERMALINK_SETTINGS;

	/**
	 * Represents the reason that the category base is changed.
	 *
	 * @deprecated 15.3
	 */
	const REASON_CATEGORY_BASE_PREFIX = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX;

	/**
	 * Represents the reason that the tag base is changed.
	 *
	 * @deprecated 15.3
	 */
	const REASON_TAG_BASE_PREFIX = Indexing_Reasons::REASON_TAG_BASE_PREFIX;

	/**
	 * Represents the reason that the home url option is changed.
	 *
	 * @deprecated 15.3
	 */
	const REASON_HOME_URL_OPTION = Indexing_Reasons::REASON_HOME_URL_OPTION;

	/**
	 * The Yoast notification center.
	 *
	 * @var Yoast_Notification_Center
	 */
	protected $notification_center;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	protected $product_helper;

	/**
	 * The current page helper.
	 *
	 * @var Current_Page_Helper
	 */
	protected $page_helper;

	/**
	 * The short link helper.
	 *
	 * @var Short_Link_Helper
	 */
	protected $short_link_helper;

	/**
	 * The notification helper.
	 *
	 * @var Notification_Helper
	 */
	protected $notification_helper;

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * The Addon Manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The Environment Helper.
	 *
	 * @var Environment_Helper
	 */
	protected $environment_helper;

	/**
	 * Indexing_Notification_Integration constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 * @param Product_Helper            $product_helper      The product helper.
	 * @param Current_Page_Helper       $page_helper         The current page helper.
	 * @param Short_Link_Helper         $short_link_helper   The short link helper.
	 * @param Notification_Helper       $notification_helper The notification helper.
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param WPSEO_Addon_Manager       $addon_manager       The addon manager.
	 * @param Environment_Helper        $environment_helper  The environment helper.
	 */
	public function __construct(
		Yoast_Notification_Center $notification_center,
		Product_Helper $product_helper,
		Current_Page_Helper $page_helper,
		Short_Link_Helper $short_link_helper,
		Notification_Helper $notification_helper,
		Indexing_Helper $indexing_helper,
		WPSEO_Addon_Manager $addon_manager,
		Environment_Helper $environment_helper
	) {
		$this->notification_center = $notification_center;
		$this->product_helper      = $product_helper;
		$this->page_helper         = $page_helper;
		$this->short_link_helper   = $short_link_helper;
		$this->notification_helper = $notification_helper;
		$this->indexing_helper     = $indexing_helper;
		$this->addon_manager       = $addon_manager;
		$this->environment_helper  = $environment_helper;
	}

	/**
	 * Initializes the integration.
	 *
	 * Adds hooks and jobs to cleanup or add the notification when necessary.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->page_helper->get_current_yoast_seo_page() === 'wpseo_dashboard' ) {
			\add_action( 'admin_init', [ $this, 'maybe_cleanup_notification' ] );
		}

		if ( $this->indexing_helper->has_reason() ) {
			\add_action( 'admin_init', [ $this, 'maybe_create_notification' ] );
		}

		\add_action( self::NOTIFICATION_ID, [ $this, 'maybe_create_notification' ] );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Not_Admin_Ajax_Conditional::class,
			User_Can_Manage_Wpseo_Options_Conditional::class,
		];
	}

	/**
	 * Checks whether the notification should be shown and adds
	 * it to the notification center if this is the case.
	 */
	public function maybe_create_notification() {
		if ( ! $this->should_show_notification() ) {
			return;
		}

		if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) {
			$notification = $this->notification();
			$this->notification_helper->restore_notification( $notification );
			$this->notification_center->add_notification( $notification );
		}
	}

	/**
	 * Checks whether the notification should not be shown anymore and removes
	 * it from the notification center if this is the case.
	 */
	public function maybe_cleanup_notification() {
		$notification = $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID );

		if ( $notification === null ) {
			return;
		}

		if ( $this->should_show_notification() ) {
			return;
		}

		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}

	/**
	 * Checks whether the notification should be shown.
	 *
	 * @return bool If the notification should be shown.
	 */
	protected function should_show_notification() {
		if ( ! $this->environment_helper->is_production_mode() ) {
			return false;
		}
		// Don't show a notification if the indexing has already been started earlier.
		if ( $this->indexing_helper->get_started() > 0 ) {
			return false;
		}

		// Never show a notification when nothing should be indexed.
		return $this->indexing_helper->get_limited_filtered_unindexed_count( 1 ) > 0;
	}

	/**
	 * Returns an instance of the notification.
	 *
	 * @return Yoast_Notification The notification to show.
	 */
	protected function notification() {
		$reason = $this->indexing_helper->get_reason();

		$presenter = $this->get_presenter( $reason );

		return new Yoast_Notification(
			$presenter,
			[
				'type'         => Yoast_Notification::WARNING,
				'id'           => self::NOTIFICATION_ID,
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 0.8,
			]
		);
	}

	/**
	 * Gets the presenter to use to show the notification.
	 *
	 * @param string $reason The reason for the notification.
	 *
	 * @return Indexing_Failed_Notification_Presenter|Indexing_Notification_Presenter
	 */
	protected function get_presenter( $reason ) {
		if ( $reason === Indexing_Reasons::REASON_INDEXING_FAILED ) {
			$presenter = new Indexing_Failed_Notification_Presenter( $this->product_helper, $this->short_link_helper, $this->addon_manager );
		}
		else {
			$total_unindexed = $this->indexing_helper->get_filtered_unindexed_count();
			$presenter       = new Indexing_Notification_Presenter( $this->short_link_helper, $total_unindexed, $reason );
		}

		return $presenter;
	}
}
admin/social-templates-integration.php000066600000024106151130361070014140 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WP_Taxonomy;
use WPSEO_Admin_Editor_Specific_Replace_Vars;
use WPSEO_Admin_Recommended_Replace_Vars;
use WPSEO_Admin_Utils;
use WPSEO_Replacevar_Editor;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast\WP\SEO\Presenters\Admin\Badge_Presenter;
use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter;
use Yoast_Form;

/**
 * Class Social_Templates_Integration.
 *
 * Adds the social fields to the meta tabs for post types, taxonomies and archives.
 */
class Social_Templates_Integration implements Integration_Interface {

	/**
	 * Service that can be used to recommend a set of variables for a WPSEO_Replacevar_Editor.
	 *
	 * @var WPSEO_Admin_Recommended_Replace_Vars
	 */
	private $recommended_replace_vars;

	/**
	 * Service that can be used to recommend an editor specific set of variables for a WPSEO_Replacevar_Editor.
	 *
	 * @var WPSEO_Admin_Editor_Specific_Replace_Vars
	 */
	private $editor_specific_replace_vars;

	/**
	 * Group to which the 'New' badges belong to.
	 *
	 * @var string
	 */
	private $group = 'global-templates';

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class, Open_Graph_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 */
	public function register_hooks() {
		\add_action( 'Yoast\WP\SEO\admin_author_archives_meta_internal', [ $this, 'social_author_archives' ] );
		\add_action( 'Yoast\WP\SEO\admin_date_archives_meta_internal', [ $this, 'social_date_archives' ] );
		\add_action( 'Yoast\WP\SEO\admin_post_types_beforearchive_internal', [ $this, 'social_post_type' ], \PHP_INT_MAX, 2 );
		\add_action( 'Yoast\WP\SEO\admin_post_types_archive_internal', [ $this, 'social_post_types_archive' ], 10, 2 );
		\add_action( 'Yoast\WP\SEO\admin_taxonomies_meta_internal', [ $this, 'social_taxonomies' ], 10, 2 );
	}

	/**
	 * Returns the recommended replacements variables object, creating it if needed.
	 *
	 * @return WPSEO_Admin_Recommended_Replace_Vars
	 */
	protected function get_admin_recommended_replace_vars() {
		if ( \is_null( $this->recommended_replace_vars ) ) {
			$this->recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
		}

		return $this->recommended_replace_vars;
	}

	/**
	 * Returns the editor specific replacements variables object, creating it if needed.
	 *
	 * @return WPSEO_Admin_Editor_Specific_Replace_Vars
	 */
	protected function get_admin_editor_specific_replace_vars() {
		if ( \is_null( $this->editor_specific_replace_vars ) ) {
			$this->editor_specific_replace_vars = new WPSEO_Admin_Editor_Specific_Replace_Vars();
		}

		return $this->editor_specific_replace_vars;
	}

	/**
	 * Build a set of social fields for the author archives in the Search Appearance section.
	 *
	 * @param Yoast_Form $yform The form builder.
	 */
	public function social_author_archives( $yform ) {
		$identifier            = 'author-wpseo';
		$page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( 'author' );
		$page_type_specific    = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( 'author' );

		$this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific );
	}

	/**
	 * Build a set of social fields for the date archives in the Search Appearance section.
	 *
	 * @param Yoast_Form $yform The form builder.
	 */
	public function social_date_archives( $yform ) {
		$identifier            = 'archive-wpseo';
		$page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( 'date' );
		$page_type_specific    = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( 'date' );

		$this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific );
	}

	/**
	 * Build a set of social fields for the post types in the Search Appearance section.
	 *
	 * @param Yoast_Form $yform          The form builder.
	 * @param string     $post_type_name The name of the current post_type that gets the social fields added.
	 */
	public function social_post_type( $yform, $post_type_name ) {
		if ( $post_type_name === 'attachment' ) {
			return;
		}

		$page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_post_type( $post_type_name );
		$page_type_specific    = $this->get_admin_editor_specific_replace_vars()->determine_for_post_type( $post_type_name );

		$this->build_social_fields( $yform, $post_type_name, $page_type_recommended, $page_type_specific );
	}

	/**
	 * Build a set of social fields for the post types archives in the Search Appearance section.
	 *
	 * @param Yoast_Form $yform          The form builder.
	 * @param string     $post_type_name The name of the current post_type that gets the social fields added.
	 */
	public function social_post_types_archive( $yform, $post_type_name ) {
		$identifier            = 'ptarchive-' . $post_type_name;
		$page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( $post_type_name );
		$page_type_specific    = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( $post_type_name );

		$this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific );
	}

	/**
	 * Build a set of social fields for the taxonomies in the Search Appearance section.
	 *
	 * @param Yoast_Form  $yform    The form builder.
	 * @param WP_Taxonomy $taxonomy The taxonomy that gets the social fields added.
	 */
	public function social_taxonomies( $yform, $taxonomy ) {
		$identifier            = 'tax-' . $taxonomy->name;
		$page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_term( $taxonomy->name );
		$page_type_specific    = $this->get_admin_editor_specific_replace_vars()->determine_for_term( $taxonomy->name );

		$this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific );
	}

	/**
	 * Build a set of social fields for the Search Appearance section.
	 *
	 * @param Yoast_Form $yform                 The form builder.
	 * @param string     $identifier            A page-wide unique identifier for data storage and unique DOM elements.
	 * @param string     $page_type_recommended Recommended type of page for a list of replaceable variables.
	 * @param string     $page_type_specific    Editor specific type of page for a list of replaceable variables.
	 */
	protected function build_social_fields( Yoast_Form $yform, $identifier, $page_type_recommended, $page_type_specific ) {
		$image_url_field_id    = 'social-image-url-' . $identifier;
		$image_id_field_id     = 'social-image-id-' . $identifier;
		$is_premium            = \YoastSEO()->helpers->product->is_premium();
		$is_premium_16_5_or_up = \defined( '\WPSEO_PREMIUM_VERSION' ) && \version_compare( \WPSEO_PREMIUM_VERSION, '16.5-RC0', '>=' );
		$is_form_enabled       = $is_premium && $is_premium_16_5_or_up;

		$section_class = 'yoast-settings-section';

		if ( ! $is_form_enabled ) {
			$section_class .= ' yoast-settings-section-disabled';
		}

		\printf( '<div class="%s">', \esc_attr( $section_class ) );

		echo '<div class="social-settings-heading-container">';
		echo '<h3 class="social-settings-heading">' . \esc_html__( 'Social settings', 'wordpress-seo' ) . '</h3>';
		if ( $is_form_enabled ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Is correctly escaped in the Premium_Badge_Presenter.
			echo new Premium_Badge_Presenter( 'global-templates-' . $identifier );
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Is correctly escaped in the Badge_Presenter.
			echo new Badge_Presenter( 'global-templates-' . $identifier, '', $this->group );
		}
		echo '</div>';

		$yform->hidden( $image_url_field_id, $image_url_field_id );
		$yform->hidden( $image_id_field_id, $image_id_field_id );
		\printf(
			'<div
				id="%1$s"
				data-react-image-portal
				data-react-image-portal-target-image="%2$s"
				data-react-image-portal-target-image-id="%3$s"
				data-react-image-portal-is-disabled="%4$s"
				data-react-image-portal-has-image-validation="%5$s"
			></div>',
			\esc_attr( 'yoast-social-' . $identifier . '-image-select' ),
			\esc_attr( $image_url_field_id ),
			\esc_attr( $image_id_field_id ),
			\esc_attr( ! $is_form_enabled ),
			true
		);

		$editor = new WPSEO_Replacevar_Editor(
			$yform,
			[
				'title'                   => 'social-title-' . $identifier,
				'description'             => 'social-description-' . $identifier,
				'page_type_recommended'   => $page_type_recommended,
				'page_type_specific'      => $page_type_specific,
				'paper_style'             => false,
				'label_title'             => \__( 'Social title', 'wordpress-seo' ),
				'label_description'       => \__( 'Social description', 'wordpress-seo' ),
				'description_placeholder' => '',
				'is_disabled'             => ! $is_form_enabled,
			]
		);
		$editor->render();

		if ( $is_premium && ! $is_premium_16_5_or_up ) {
			echo '<div class="yoast-settings-section-upsell">';

			$unlock_alert = \sprintf(
				/* translators: %s expands to 'Yoast SEO Premium'. */
				\esc_html__( 'To unlock this feature please update %s to the latest version.', 'wordpress-seo' ),
				'Yoast SEO Premium'
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above.
			echo new Alert_Presenter( $unlock_alert );

			echo '</div>';
		}

		if ( ! $is_premium ) {
			$wpseo_page = \filter_input( \INPUT_GET, 'page' );

			echo '<div class="yoast-settings-section-upsell">';

			echo '<a class="yoast-button-upsell" href="' . \esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/4e0' ) ) . '" target="_blank">'
			. \esc_html__( 'Unlock with Premium', 'wordpress-seo' )
			// phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly.
			. WPSEO_Admin_Utils::get_new_tab_message()
			. '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'
			. '</a>';
			echo '</div>';
		}

		echo '</div>';
	}
}
admin/first-time-configuration-integration.php000066600000034464151130361070015632 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WP_User;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Option_Tab;
use WPSEO_Shortlinker;
use WPSEO_Utils;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Routes\Indexing_Route;

/**
 * First_Time_Configuration_Integration class
 */
class First_Time_Configuration_Integration implements Integration_Interface {

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	private $addon_manager;

	/**
	 * The shortlinker.
	 *
	 * @var WPSEO_Shortlinker
	 */
	private $shortlinker;

	/**
	 * The options' helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The social profiles helper.
	 *
	 * @var Social_Profiles_Helper
	 */
	private $social_profiles_helper;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	private $product_helper;

	/**
	 * The meta tags context helper.
	 *
	 * @var Meta_Tags_Context
	 */
	private $meta_tags_context;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * First_Time_Configuration_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager    The admin asset manager.
	 * @param WPSEO_Addon_Manager       $addon_manager          The addon manager.
	 * @param WPSEO_Shortlinker         $shortlinker            The shortlinker.
	 * @param Options_Helper            $options_helper         The options helper.
	 * @param Social_Profiles_Helper    $social_profiles_helper The social profile helper.
	 * @param Product_Helper            $product_helper         The product helper.
	 * @param Meta_Tags_Context         $meta_tags_context      The meta tags context helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $admin_asset_manager,
		WPSEO_Addon_Manager $addon_manager,
		WPSEO_Shortlinker $shortlinker,
		Options_Helper $options_helper,
		Social_Profiles_Helper $social_profiles_helper,
		Product_Helper $product_helper,
		Meta_Tags_Context $meta_tags_context
	) {
		$this->admin_asset_manager    = $admin_asset_manager;
		$this->addon_manager          = $addon_manager;
		$this->shortlinker            = $shortlinker;
		$this->options_helper         = $options_helper;
		$this->social_profiles_helper = $social_profiles_helper;
		$this->product_helper         = $product_helper;
		$this->meta_tags_context      = $meta_tags_context;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
		\add_action( 'wpseo_settings_tabs_dashboard', [ $this, 'add_first_time_configuration_tab' ] );
	}

	/**
	 * Adds a dedicated tab in the General sub-page.
	 *
	 * @param WPSEO_Options_Tabs $dashboard_tabs Object representing the tabs of the General sub-page.
	 */
	public function add_first_time_configuration_tab( $dashboard_tabs ) {
		$dashboard_tabs->add_tab(
			new WPSEO_Option_Tab(
				'first-time-configuration',
				\__( 'First-time configuration', 'wordpress-seo' ),
				[ 'save_button' => false ]
			)
		);
	}

	/**
	 * Adds the data for the first-time configuration to the wpseoFirstTimeConfigurationData object.
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_dashboard' || \is_network_admin() ) {
			return;
		}

		$this->admin_asset_manager->enqueue_script( 'indexation' );
		$this->admin_asset_manager->enqueue_script( 'first-time-configuration' );
		$this->admin_asset_manager->enqueue_style( 'admin-css' );
		$this->admin_asset_manager->enqueue_style( 'tailwind' );
		$this->admin_asset_manager->enqueue_style( 'monorepo' );

		$data = [
			'disabled'     => ! \YoastSEO()->helpers->indexable->should_index_indexables(),
			'amount'       => \YoastSEO()->helpers->indexing->get_filtered_unindexed_count(),
			'firstTime'    => ( \YoastSEO()->helpers->indexing->is_initial_indexing() === true ),
			'errorMessage' => '',
			'restApi'      => [
				'root'               => \esc_url_raw( \rest_url() ),
				'indexing_endpoints' => $this->get_endpoints(),
				'nonce'              => \wp_create_nonce( 'wp_rest' ),
			],
		];

		/**
		 * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process.
		 *
		 * @param array $data The indexing data to adapt.
		 */
		$data = \apply_filters( 'wpseo_indexing_data', $data );

		$this->admin_asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data );

		$person_id       = $this->get_person_id();
		$social_profiles = $this->get_social_profiles();

		// This filter is documented in admin/views/tabs/metas/paper-content/general/knowledge-graph.php.
		$knowledge_graph_message = \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' );

		$finished_steps        = $this->get_finished_steps();
		$options               = $this->get_company_or_person_options();
		$selected_option_label = '';
		$filtered_options      = \array_filter(
			$options,
			function ( $item ) {
				return $item['value'] === $this->is_company_or_person();
			}
		);
		$selected_option       = \reset( $filtered_options );
		if ( \is_array( $selected_option ) ) {
			$selected_option_label = $selected_option['label'];
		}

		$this->admin_asset_manager->add_inline_script(
			'first-time-configuration',
			\sprintf(
				'window.wpseoFirstTimeConfigurationData = {
					"canEditUser": %d,
					"companyOrPerson": "%s",
					"companyOrPersonLabel": "%s",
					"companyName": "%s",
					"fallbackCompanyName": "%s",
					"companyLogo": "%s",
					"companyLogoFallback": "%s",
					"companyLogoId": %d,
					"finishedSteps": %s,
					"personId": %d,
					"personName": "%s",
					"personLogo": "%s",
					"personLogoFallback": "%s",
					"personLogoId": %d,
					"siteTagline": "%s",
					"socialProfiles": {
						"facebookUrl": "%s",
						"twitterUsername": "%s",
						"otherSocialUrls": %s,
					},
					"isPremium": %d,
					"tracking": %d,
					"isTrackingAllowedMultisite": %d,
					"isMainSite": %d,
					"companyOrPersonOptions": %s,
					"shouldForceCompany": %d,
					"knowledgeGraphMessage": "%s",
					"shortlinks": {
						"gdpr": "%s",
						"configIndexables": "%s",
						"configIndexablesBenefits": "%s",
					},
				};',
				$this->social_profiles_helper->can_edit_profile( $person_id ),
				$this->is_company_or_person(),
				$selected_option_label,
				$this->get_company_name(),
				$this->get_fallback_company_name( $this->get_company_name() ),
				$this->get_company_logo(),
				$this->get_company_fallback_logo( $this->get_company_logo() ),
				$this->get_company_logo_id(),
				WPSEO_Utils::format_json_encode( $finished_steps ),
				$person_id,
				$this->get_person_name(),
				$this->get_person_logo(),
				$this->get_person_fallback_logo( $this->get_person_logo() ),
				$this->get_person_logo_id(),
				$this->get_site_tagline(),
				$social_profiles['facebook_url'],
				$social_profiles['twitter_username'],
				WPSEO_Utils::format_json_encode( $social_profiles['other_social_urls'] ),
				$this->product_helper->is_premium(),
				$this->has_tracking_enabled(),
				$this->is_tracking_enabled_multisite(),
				$this->is_main_site(),
				WPSEO_Utils::format_json_encode( $options ),
				$this->should_force_company(),
				$knowledge_graph_message,
				$this->shortlinker->build_shortlink( 'https://yoa.st/gdpr-config-workout' ),
				$this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables' ),
				$this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables-benefits' )
			),
			'before'
		);
	}

	/**
	 * Retrieves a list of the endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_endpoints() {
		$endpoints = [
			'prepare'            => Indexing_Route::FULL_PREPARE_ROUTE,
			'terms'              => Indexing_Route::FULL_TERMS_ROUTE,
			'posts'              => Indexing_Route::FULL_POSTS_ROUTE,
			'archives'           => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE,
			'general'            => Indexing_Route::FULL_GENERAL_ROUTE,
			'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE,
			'post_link'          => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE,
			'term_link'          => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE,
		];

		$endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints );

		$endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE;

		return $endpoints;
	}

	// ** Private functions ** //

	/**
	 * Returns the finished steps array.
	 *
	 * @return array An array with the finished steps.
	 */
	private function get_finished_steps() {
		return $this->options_helper->get( 'configuration_finished_steps', [] );
	}

	/**
	 * Returns the entity represented by the site.
	 *
	 * @return string The entity represented by the site.
	 */
	private function is_company_or_person() {
		return $this->options_helper->get( 'company_or_person', '' );
	}

	/**
	 * Gets the company name from the option in the database.
	 *
	 * @return string The company name.
	 */
	private function get_company_name() {
		return $this->options_helper->get( 'company_name', '' );
	}

	/**
	 * Gets the fallback company name from the option in the database if there is no company name.
	 *
	 * @param string $company_name The given company name by the user, default empty string.
	 *
	 * @return string|false The company name.
	 */
	private function get_fallback_company_name( $company_name ) {
		if ( $company_name ) {
			return false;
		}

		return \get_bloginfo( 'name' );
	}

	/**
	 * Gets the company logo from the option in the database.
	 *
	 * @return string The company logo.
	 */
	private function get_company_logo() {
		return $this->options_helper->get( 'company_logo', '' );
	}

	/**
	 * Gets the company logo id from the option in the database.
	 *
	 * @return string The company logo id.
	 */
	private function get_company_logo_id() {
		return $this->options_helper->get( 'company_logo_id', '' );
	}

	/**
	 * Gets the company logo url from the option in the database.
	 *
	 * @param string $company_logo The given company logo by the user, default empty.
	 *
	 * @return string|false The company logo URL.
	 */
	private function get_company_fallback_logo( $company_logo ) {
		if ( $company_logo ) {
			return false;
		}
		$logo_id = $this->meta_tags_context->fallback_to_site_logo();

		return \esc_url( \wp_get_attachment_url( $logo_id ) );
	}

	/**
	 * Gets the person id from the option in the database.
	 *
	 * @return int|null The person id, null if empty.
	 */
	private function get_person_id() {
		return $this->options_helper->get( 'company_or_person_user_id' );
	}

	/**
	 * Gets the person id from the option in the database.
	 *
	 * @return int|null The person id, null if empty.
	 */
	private function get_person_name() {
		$user = \get_userdata( $this->get_person_id() );
		if ( $user instanceof WP_User ) {
			return $user->get( 'display_name' );
		}

		return '';
	}

	/**
	 * Gets the person avatar from the option in the database.
	 *
	 * @return string The person logo.
	 */
	private function get_person_logo() {
		return $this->options_helper->get( 'person_logo', '' );
	}

	/**
	 * Gets the person logo url from the option in the database.
	 *
	 * @param string $person_logo The given person logo by the user, default empty.
	 *
	 * @return string|false The person logo URL.
	 */
	private function get_person_fallback_logo( $person_logo ) {
		if ( $person_logo ) {
			return false;
		}
		$logo_id = $this->meta_tags_context->fallback_to_site_logo();

		return \esc_url( \wp_get_attachment_url( $logo_id ) );
	}

	/**
	 * Gets the person logo id from the option in the database.
	 *
	 * @return string The person logo id.
	 */
	private function get_person_logo_id() {
		return $this->options_helper->get( 'person_logo_id', '' );
	}

	/**
	 * Gets the site tagline.
	 *
	 * @return string The site tagline.
	 */
	private function get_site_tagline() {
		return \get_bloginfo( 'description' );
	}

	/**
	 * Gets the social profiles stored in the database.
	 *
	 * @return string[] The social profiles.
	 */
	private function get_social_profiles() {
		return [
			'facebook_url'      => $this->options_helper->get( 'facebook_site', '' ),
			'twitter_username'  => $this->options_helper->get( 'twitter_site', '' ),
			'other_social_urls' => $this->options_helper->get( 'other_social_urls', [] ),
		];
	}

	/**
	 * Checks whether tracking is enabled.
	 *
	 * @return bool True if tracking is enabled, false otherwise, null if in Free and conf. workout step not finished.
	 */
	private function has_tracking_enabled() {
		$default = false;

		if ( $this->product_helper->is_premium() ) {
			$default = true;
		}

		return $this->options_helper->get( 'tracking', $default );
	}

	/**
	 * Checks whether tracking option is allowed at network level.
	 *
	 * @return bool True if option change is allowed, false otherwise.
	 */
	private function is_tracking_enabled_multisite() {
		$default = true;

		if ( ! \is_multisite() ) {
			return $default;
		}

		return $this->options_helper->get( 'allow_tracking', $default );
	}

	/**
	 * Checks whether we are in a main site.
	 *
	 * @return bool True if it's the main site or a single site, false if it's a subsite.
	 */
	private function is_main_site() {
		return \is_main_site();
	}

	/**
	 * Gets the options for the Company or Person select.
	 * Returns only the company option if it is forced (by Local SEO), otherwise returns company and person option.
	 *
	 * @return array The options for the company-or-person select.
	 */
	private function get_company_or_person_options() {
		$options = [
			[
				'label' => \__( 'Organization', 'wordpress-seo' ),
				'value' => 'company',
				'id'    => 'company',
			],
			[
				'label' => \__( 'Person', 'wordpress-seo' ),
				'value' => 'person',
				'id'    => 'person',
			],
		];
		if ( $this->should_force_company() ) {
			$options = [
				[
					'label' => \__( 'Organization', 'wordpress-seo' ),
					'value' => 'company',
					'id'    => 'company',
				],
			];
		}

		return $options;
	}

	/**
	 * Checks whether we should force "Organization".
	 *
	 * @return bool
	 */
	private function should_force_company() {
		return $this->addon_manager->is_installed( WPSEO_Addon_Manager::LOCAL_SLUG );
	}
}
admin/redirects-integration.php000066600000002646151130361070012663 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Premium_Inactive_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Redirects_Integration class.
 */
class Redirects_Integration implements Integration_Interface {

	/**
	 * Sets up the hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 9 );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case: only when on an admin page and Premium is not active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [
			Admin_Conditional::class,
			Premium_Inactive_Conditional::class,
		];
	}

	/**
	 * Adds the redirects submenu page.
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array The filtered submenu pages.
	 */
	public function add_submenu_page( $submenu_pages ) {
		$submenu_pages[] = [
			'wpseo_dashboard',
			'',
			\__( 'Redirects', 'wordpress-seo' ) . ' <span class="yoast-badge yoast-premium-badge"></span>',
			'edit_others_posts',
			'wpseo_redirects',
			[ $this, 'display' ],
		];

		return $submenu_pages;
	}

	/**
	 * Displays the redirects page.
	 *
	 * @return void
	 */
	public function display() {
		require \WPSEO_PATH . 'admin/pages/redirects.php';
	}
}
admin/fix-news-dependencies-integration.php000066600000003040151130361070015050 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WP_Screen;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\News_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Fix_News_Dependencies_Integration class.
 */
class Fix_News_Dependencies_Integration implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * In this case: when on an admin page.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class, News_Conditional::class ];
	}

	/**
	 * Registers an action to disable script concatenation.
	 */
	public function register_hooks() {
		global $pagenow;

		// Load the editor script when on an edit post or new post page.
		$is_post_edit_page = $pagenow === 'post.php' || $pagenow === 'post-new.php';
		if ( $is_post_edit_page ) {
			\add_action( 'admin_enqueue_scripts', [ $this, 'add_news_script_dependency' ], 11 );
		}
	}

	/**
	 * Fixes the news script dependency.
	 *
	 * @return void
	 */
	public function add_news_script_dependency() {
		$scripts = \wp_scripts();

		if ( ! isset( $scripts->registered['wpseo-news-editor'] ) ) {
			return;
		}

		$is_block_editor  = WP_Screen::get()->is_block_editor();
		$post_edit_handle = 'post-edit';
		if ( ! $is_block_editor ) {
			$post_edit_handle = 'post-edit-classic';
		}

		$scripts->registered['wpseo-news-editor']->deps[] = WPSEO_Admin_Asset_Manager::PREFIX . $post_edit_handle;
	}
}
admin/health-check-integration.php000066600000005317151130361070013215 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Services\Health_Check\Health_Check;

/**
 * Integrates health checks with WordPress' Site Health.
 */
class Health_Check_Integration implements Integration_Interface {

	/**
	 * Contains all the health check implementations.
	 *
	 * @var Health_Check[]
	 */
	private $health_checks = [];

	/**
	 * Uses the dependency injection container to obtain all available implementations of the Health_Check interface.
	 *
	 * @param  Health_Check ...$health_checks The available health checks implementations.
	 * @return void
	 */
	public function __construct( Health_Check ...$health_checks ) {
		$this->health_checks = $health_checks;
	}

	/**
	 * Hooks the health checks into WordPress' site status tests.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'site_status_tests', [ $this, 'add_health_checks' ] );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case: only when on an admin page.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Checks if the input is a WordPress site status tests array, and adds Yoast's health checks if it is.
	 *
	 * @param  string[] $tests Array containing WordPress site status tests.
	 * @return string[] Array containing WordPress site status tests with Yoast's health checks.
	 */
	public function add_health_checks( $tests ) {
		if ( ! $this->is_valid_site_status_tests_array( $tests ) ) {
			return $tests;
		}

		return $this->add_health_checks_to_site_status_tests( $tests );
	}

	/**
	 * Checks if the input array is a WordPress site status tests array.
	 *
	 * @param  mixed $tests Array to check.
	 * @return bool Returns true if the input array is a WordPress site status tests array.
	 */
	private function is_valid_site_status_tests_array( $tests ) {
		if ( ! \is_array( $tests ) ) {
			return false;
		}

		if ( ! \array_key_exists( 'direct', $tests ) ) {
			return false;
		}

		if ( ! \is_array( $tests['direct'] ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Adds the health checks to WordPress' site status tests.
	 *
	 * @param  string[] $tests Array containing WordPress site status tests.
	 * @return string[] Array containing WordPress site status tests with Yoast's health checks.
	 */
	private function add_health_checks_to_site_status_tests( $tests ) {
		foreach ( $this->health_checks as $health_check ) {
			$tests['direct'][ $health_check->get_test_identifier() ] = [
				'test' => [ $health_check, 'run_and_get_result' ],
			];
		}

		return $tests;
	}
}
admin/migration-error-integration.php000066600000002543151130361070014013 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Config\Migration_Status;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Migration_Error_Presenter;

/**
 * Migration_Error_Integration class.
 */
class Migration_Error_Integration implements Integration_Interface {

	/**
	 * The migration status object.
	 *
	 * @var Migration_Status
	 */
	protected $migration_status;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Migration_Error_Integration constructor.
	 *
	 * @param Migration_Status $migration_status The migration status object.
	 */
	public function __construct( Migration_Status $migration_status ) {
		$this->migration_status = $migration_status;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		if ( $this->migration_status->get_error( 'free' ) === false ) {
			return;
		}

		\add_action( 'admin_notices', [ $this, 'render_migration_error' ] );
	}

	/**
	 * Renders the migration error.
	 *
	 * @return void
	 */
	public function render_migration_error() {
		// phpcs:ignore WordPress.Security.EscapeOutput -- The Migration_Error_Presenter already escapes it's output.
		echo new Migration_Error_Presenter( $this->migration_status->get_error( 'free' ) );
	}
}
admin/cron-integration.php000066600000001755151130361070011640 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Date_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Cron_Integration class.
 */
class Cron_Integration implements Integration_Interface {

	/**
	 * The indexing notification integration.
	 *
	 * @var Date_Helper
	 */
	protected $date_helper;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Cron_Integration constructor
	 *
	 * @param Date_Helper $date_helper The date helper.
	 */
	public function __construct(
		Date_Helper $date_helper
	) {
		$this->date_helper = $date_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		if ( ! \wp_next_scheduled( Indexing_Notification_Integration::NOTIFICATION_ID ) ) {
			\wp_schedule_event(
				$this->date_helper->current_time(),
				'daily',
				Indexing_Notification_Integration::NOTIFICATION_ID
			);
		}
	}
}
admin/import-integration.php000066600000021161151130361070012202 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Import_Tool_Selected_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast\WP\SEO\Routes\Importing_Route;
use Yoast\WP\SEO\Services\Importing\Importable_Detector_Service;

/**
 * Loads import script when on the Tool's page.
 */
class Import_Integration implements Integration_Interface {

	/**
	 * Contains the asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	protected $asset_manager;

	/**
	 * The Importable Detector service.
	 *
	 * @var Importable_Detector_Service
	 */
	protected $importable_detector;

	/**
	 * The Importing Route class.
	 *
	 * @var Importing_Route
	 */
	protected $importing_route;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [
			Import_Tool_Selected_Conditional::class,
			Yoast_Tools_Page_Conditional::class,
		];
	}

	/**
	 * Import Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager   $asset_manager       The asset manager.
	 * @param Importable_Detector_Service $importable_detector The importable detector.
	 * @param Importing_Route             $importing_route     The importing route.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Importable_Detector_Service $importable_detector,
		Importing_Route $importing_route
	) {
		$this->asset_manager       = $asset_manager;
		$this->importable_detector = $importable_detector;
		$this->importing_route     = $importing_route;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_import_script' ] );
	}

	/**
	 * Enqueues the Import script.
	 */
	public function enqueue_import_script() {
		\wp_enqueue_style( 'dashicons' );
		$this->asset_manager->enqueue_script( 'import' );

		$data = [
			'restApi' => [
				'root'                => \esc_url_raw( \rest_url() ),
				'cleanup_endpoints'   => $this->get_cleanup_endpoints(),
				'importing_endpoints' => $this->get_importing_endpoints(),
				'nonce'               => \wp_create_nonce( 'wp_rest' ),
			],
			'assets'  => [
				'loading_msg_import'       => \esc_html__( 'The import can take a long time depending on your site\'s size.', 'wordpress-seo' ),
				'loading_msg_cleanup'      => \esc_html__( 'The cleanup can take a long time depending on your site\'s size.', 'wordpress-seo' ),
				'note'                     => \esc_html__( 'Note: ', 'wordpress-seo' ),
				'cleanup_after_import_msg' => \esc_html__( 'After you\'ve imported data from another SEO plugin, please make sure to clean up all the original data from that plugin. (step 5)', 'wordpress-seo' ),
				'select_placeholder'       => \esc_html__( 'Select SEO plugin', 'wordpress-seo' ),
				'no_data_msg'              => \esc_html__( 'No data found from other SEO plugins.', 'wordpress-seo' ),
				'validation_failure'       => $this->get_validation_failure_alert(),
				'import_failure'           => $this->get_import_failure_alert( true ),
				'cleanup_failure'          => $this->get_import_failure_alert( false ),
				'spinner'                  => \admin_url( 'images/loading.gif' ),
				'replacing_texts'          => [
					'cleanup_button'       => \esc_html__( 'Clean up', 'wordpress-seo' ),
					'import_explanation'   => \esc_html__( 'Please select an SEO plugin below to see what data can be imported.', 'wordpress-seo' ),
					'cleanup_explanation'  => \esc_html__( 'Once you\'re certain that your site is working properly with the imported data from another SEO plugin, you can clean up all the original data from that plugin.', 'wordpress-seo' ),
					/* translators: %s: expands to the name of the plugin that is selected to be imported */
					'select_header'        => \esc_html__( 'The import from %s includes:', 'wordpress-seo' ),
					'plugins'              => [
						'aioseo' => [
							[
								'data_name' => \esc_html__( 'Post metadata (SEO titles, descriptions, etc.)', 'wordpress-seo' ),
								'data_note' => \esc_html__( 'Note: This metadata will only be imported if there is no existing Yoast SEO metadata yet.', 'wordpress-seo' ),
							],
							[
								'data_name' => \esc_html__( 'Default settings', 'wordpress-seo' ),
								'data_note' => \esc_html__( 'Note: These settings will overwrite the default settings of Yoast SEO.', 'wordpress-seo' ),
							],
						],
						'other' => [
							[
								'data_name' => \esc_html__( 'Post metadata (SEO titles, descriptions, etc.)', 'wordpress-seo' ),
								'data_note' => \esc_html__( 'Note: This metadata will only be imported if there is no existing Yoast SEO metadata yet.', 'wordpress-seo' ),
							],
						],
					],
				],
			],
		];

		/**
		 * Filter: 'wpseo_importing_data' Filter to adapt the data used in the import process.
		 *
		 * @param array $data The import data to adapt.
		 */
		$data = \apply_filters( 'wpseo_importing_data', $data );

		$this->asset_manager->localize_script( 'import', 'yoastImportData', $data );
	}

	/**
	 * Retrieves a list of the importing endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_importing_endpoints() {
		$available_actions   = $this->importable_detector->detect_importers();
		$importing_endpoints = [];

		$available_sorted_actions = $this->sort_actions( $available_actions );

		foreach ( $available_sorted_actions as $plugin => $types ) {
			foreach ( $types as $type ) {
				$importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type );
			}
		}

		return $importing_endpoints;
	}

	/**
	 * Sorts the array of importing actions, by moving any validating actions to the start for every plugin.
	 *
	 * @param array $available_actions The array of actions that we want to sort.
	 *
	 * @return array The sorted array of actions.
	 */
	protected function sort_actions( $available_actions ) {
		$first_action             = 'validate_data';
		$available_sorted_actions = [];

		foreach ( $available_actions as $plugin => $plugin_available_actions ) {

			$validate_action_position = \array_search( $first_action, $plugin_available_actions, true );

			if ( ! empty( $validate_action_position ) ) {
				unset( $plugin_available_actions[ $validate_action_position ] );
				\array_unshift( $plugin_available_actions, $first_action );
			}

			$available_sorted_actions[ $plugin ] = $plugin_available_actions;
		}

		return $available_sorted_actions;
	}

	/**
	 * Retrieves a list of the importing endpoints to use.
	 *
	 * @return array The endpoints.
	 */
	protected function get_cleanup_endpoints() {
		$available_actions   = $this->importable_detector->detect_cleanups();
		$importing_endpoints = [];

		foreach ( $available_actions as $plugin => $types ) {
			foreach ( $types as $type ) {
				$importing_endpoints[ $plugin ][] = $this->importing_route->get_endpoint( $plugin, $type );
			}
		}

		return $importing_endpoints;
	}

	/**
	 * Gets the validation failure alert using the Alert_Presenter.
	 *
	 * @return string The validation failure alert.
	 */
	protected function get_validation_failure_alert() {
		$content  = \esc_html__( 'The AIOSEO import was cancelled because some AIOSEO data is missing. Please try and take the following steps to fix this:', 'wordpress-seo' );
		$content .= '<br/>';
		$content .= '<ol><li>';
		$content .= \esc_html__( 'If you have never saved any AIOSEO \'Search Appearance\' settings, please do that first and run the import again.', 'wordpress-seo' );
		$content .= '</li>';
		$content .= '<li>';
		$content .= \esc_html__( 'If you already have saved AIOSEO \'Search Appearance\' settings and the issue persists, please contact our support team so we can take a closer look.', 'wordpress-seo' );
		$content .= '</li></ol>';

		$validation_failure_alert = new Alert_Presenter( $content, 'error' );

		return $validation_failure_alert->present();
	}

	/**
	 * Gets the import failure alert using the Alert_Presenter.
	 *
	 * @param bool $is_import Wether it's an import or not.
	 *
	 * @return string The import failure alert.
	 */
	protected function get_import_failure_alert( $is_import ) {
		$content = \esc_html__( 'Cleanup failed with the following error:', 'wordpress-seo' );
		if ( $is_import ) {
			$content = \esc_html__( 'Import failed with the following error:', 'wordpress-seo' );
		}

		$content .= '<br/><br/>';
		$content .= \esc_html( '%s' );

		$import_failure_alert = new Alert_Presenter( $content, 'error' );

		return $import_failure_alert->present();
	}
}
admin/workouts-integration.php000066600000025750151130361070012575 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;

/**
 * WorkoutsIntegration class
 */
class Workouts_Integration implements Integration_Interface {

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	private $addon_manager;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The product helper.
	 *
	 * @var Product_Helper
	 */
	private $product_helper;

	/**
	 * {@inheritDoc}
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Workouts_Integration constructor.
	 *
	 * @param WPSEO_Addon_Manager       $addon_manager       The addon manager.
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
	 * @param Options_Helper            $options_helper      The options helper.
	 * @param Product_Helper            $product_helper      The product helper.
	 */
	public function __construct(
		WPSEO_Addon_Manager $addon_manager,
		WPSEO_Admin_Asset_Manager $admin_asset_manager,
		Options_Helper $options_helper,
		Product_Helper $product_helper
	) {
		$this->addon_manager       = $addon_manager;
		$this->admin_asset_manager = $admin_asset_manager;
		$this->options_helper      = $options_helper;
		$this->product_helper      = $product_helper;
	}

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 8 );
		\add_filter( 'wpseo_submenu_pages', [ $this, 'remove_old_submenu_page' ], 10 );
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
	}

	/**
	 * Adds the workouts submenu page.
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array The filtered submenu pages.
	 */
	public function add_submenu_page( $submenu_pages ) {
		$submenu_pages[] = [
			'wpseo_dashboard',
			'',
			\__( 'Workouts', 'wordpress-seo' ) . ' <span class="yoast-badge yoast-premium-badge"></span>',
			'edit_others_posts',
			'wpseo_workouts',
			[ $this, 'render_target' ],
		];

		return $submenu_pages;
	}

	/**
	 * Removes the workouts submenu page from older Premium versions
	 *
	 * @param array $submenu_pages The Yoast SEO submenu pages.
	 *
	 * @return array The filtered submenu pages.
	 */
	public function remove_old_submenu_page( $submenu_pages ) {
		if ( ! $this->should_update_premium() ) {
			return $submenu_pages;
		}

		// Copy only the Workouts page item that comes first in the array.
		$result_submenu_pages      = [];
		$workouts_page_encountered = false;
		foreach ( $submenu_pages as $item ) {
			if ( $item[4] !== 'wpseo_workouts' || ! $workouts_page_encountered ) {
				$result_submenu_pages[] = $item;
			}
			if ( $item[4] === 'wpseo_workouts' ) {
				$workouts_page_encountered = true;
			}
		}

		return $result_submenu_pages;
	}

	/**
	 * Enqueue the workouts app.
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_workouts' ) {
			return;
		}

		if ( $this->should_update_premium() ) {
			\wp_dequeue_script( 'yoast-seo-premium-workouts' );
		}

		$this->admin_asset_manager->enqueue_style( 'workouts' );

		$workouts_option = $this->get_workouts_option();

		$this->admin_asset_manager->enqueue_script( 'workouts' );
		$this->admin_asset_manager->localize_script(
			'workouts',
			'wpseoWorkoutsData',
			[
				'workouts'                  => $workouts_option,
				'homeUrl'                   => \home_url(),
				'pluginUrl'                 => \esc_url( \plugins_url( '', \WPSEO_FILE ) ),
				'toolsPageUrl'              => \esc_url( \admin_url( 'admin.php?page=wpseo_tools' ) ),
				'usersPageUrl'              => \esc_url( \admin_url( 'users.php' ) ),
				'firstTimeConfigurationUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#top#first-time-configuration' ) ),
				'isPremium'                 => $this->product_helper->is_premium(),
				'shouldUpdatePremium'       => $this->should_update_premium(),
				'upsellText'                => $this->get_upsell_text(),
				'upsellLink'                => $this->get_upsell_link(),
			]
		);
	}

	/**
	 * Renders the target for the React to mount to.
	 */
	public function render_target() {
		if ( $this->should_update_premium() ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in get_update_premium_notice.
			echo $this->get_update_premium_notice();
		}

		echo '<div id="wpseo-workouts-container-free" class="yoast"></div>';
	}

	/**
	 * Gets the workouts option.
	 *
	 * @return mixed|null Returns workouts option if found, null if not.
	 */
	private function get_workouts_option() {
		$workouts_option = $this->options_helper->get( 'workouts_data' );

		// This filter is documented in src/routes/workouts-route.php.
		return \apply_filters( 'Yoast\WP\SEO\workouts_options', $workouts_option );
	}

	/**
	 * Returns the notification to show when Premium needs to be updated.
	 *
	 * @return string The notification to update Premium.
	 */
	private function get_update_premium_notice() {
		$url = $this->get_upsell_link();

		if ( $this->has_premium_subscription_expired() ) {
			/* translators: %s: expands to 'Yoast SEO Premium'. */
			$title = \sprintf( \__( 'Renew your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
			$copy  = \sprintf(
				/* translators: %s: expands to 'Yoast SEO Premium'. */
				\esc_html__(
					'Accessing the latest workouts requires an updated version of %s (at least 17.7), but it looks like your subscription has expired. Please renew your subscription to update and gain access to all the latest features.',
					'wordpress-seo'
				),
				'Yoast SEO Premium'
			);
			$button = '<a class="yoast-button yoast-button-upsell yoast-button--small" href="' . \esc_url( $url ) . '" target="_blank">'
					. \esc_html__( 'Renew your subscription', 'wordpress-seo' )
					. '<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>';
		}
		elseif ( $this->has_premium_subscription_activated() ) {
			/* translators: %s: expands to 'Yoast SEO Premium'. */
			$title = \sprintf( \__( 'Update to the latest version of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
			$copy  = \sprintf(
				/* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */
				\esc_html__( 'It looks like you\'re running an outdated version of %1$s, please %2$supdate to the latest version (at least 17.7)%3$s to gain access to our updated workouts section.', 'wordpress-seo' ),
				'Yoast SEO Premium',
				'<a href="' . \esc_url( $url ) . '">',
				'</a>'
			);
			$button = null;
		}
		else {
			/* translators: %s: expands to 'Yoast SEO Premium'. */
			$title      = \sprintf( \__( 'Activate your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
			$url_button = 'https://yoa.st/workouts-activate-notice-help';
			$copy       = \sprintf(
				/* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */
				\esc_html__( 'It looks like you’re running an outdated and unactivated version of %1$s, please activate your subscription in %2$sMyYoast%3$s and update to the latest version (at least 17.7) to gain access to our updated workouts section.', 'wordpress-seo' ),
				'Yoast SEO Premium',
				'<a href="' . \esc_url( $url ) . '">',
				'</a>'
			);
			$button = '<a class="yoast-button yoast-button--primary yoast-button--small" href="' . \esc_url( $url_button ) . '" target="_blank">'
					. \esc_html__( 'Get help activating your subscription', 'wordpress-seo' )
					. '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
					. '</a>';
		}

		$notice = new Notice_Presenter(
			$title,
			$copy,
			'Assistent_Time_bubble_500x570.png',
			$button
		);

		return $notice->present();
	}

	/**
	 * Check whether Premium should be updated.
	 *
	 * @return bool Returns true when Premium is enabled and the version is below 17.7.
	 */
	private function should_update_premium() {
		$premium_version = $this->product_helper->get_premium_version();
		return $premium_version !== null && \version_compare( $premium_version, '17.7-RC1', '<' );
	}

	/**
	 * Check whether the Premium subscription has expired.
	 *
	 * @return bool Returns true when Premium subscription has expired.
	 */
	private function has_premium_subscription_expired() {
		$subscription = $this->addon_manager->get_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG );

		return ( isset( $subscription->expiry_date ) && ( \strtotime( $subscription->expiry_date ) - \time() ) < 0 );
	}

	/**
	 * Check whether the Premium subscription is activated.
	 *
	 * @return bool Returns true when Premium subscription is activated.
	 */
	private function has_premium_subscription_activated() {
		return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG );
	}

	/**
	 * Returns the upsell/update copy to show in the card buttons.
	 *
	 * @return string Returns a string with the upsell/update copy for the card buttons.
	 */
	private function get_upsell_text() {
		if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) {
			// Use the default defined in the component.
			return '';
		}
		if ( $this->has_premium_subscription_expired() ) {
			return \sprintf(
				/* translators: %s: expands to 'Yoast SEO Premium'. */
				\__( 'Renew %s', 'wordpress-seo' ),
				'Yoast SEO Premium'
			);
		}
		if ( $this->has_premium_subscription_activated() ) {
			return \sprintf(
				/* translators: %s: expands to 'Yoast SEO Premium'. */
				\__( 'Update %s', 'wordpress-seo' ),
				'Yoast SEO Premium'
			);
		}
		return \sprintf(
			/* translators: %s: expands to 'Yoast SEO Premium'. */
			\__( 'Activate %s', 'wordpress-seo' ),
			'Yoast SEO Premium'
		);
	}

	/**
	 * Returns the upsell/update link to show in the card buttons.
	 *
	 * @return string Returns a string with the upsell/update link for the card buttons.
	 */
	private function get_upsell_link() {
		if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) {
			// Use the default defined in the component.
			return '';
		}
		if ( $this->has_premium_subscription_expired() ) {
			return 'https://yoa.st/workout-renew-notice';
		}
		if ( $this->has_premium_subscription_activated() ) {
			return \wp_nonce_url( \self_admin_url( 'update.php?action=upgrade-plugin&plugin=wordpress-seo-premium/wp-seo-premium.php' ), 'upgrade-plugin_wordpress-seo-premium/wp-seo-premium.php' );
		}
		return 'https://yoa.st/workouts-activate-notice-myyoast';
	}
}
front-end-integration.php000066600000033560151130361070011502 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Replace_Vars;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Request_Helper;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter;
use Yoast\WP\SEO\Presenters\Debug\Marker_Open_Presenter;
use Yoast\WP\SEO\Presenters\Title_Presenter;
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class Front_End_Integration.
 */
class Front_End_Integration implements Integration_Interface {

	/**
	 * The memoizer for the meta tags context.
	 *
	 * @var Meta_Tags_Context_Memoizer
	 */
	private $context_memoizer;

	/**
	 * The container.
	 *
	 * @var ContainerInterface
	 */
	protected $container;

	/**
	 * Represents the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * Represents the request helper.
	 *
	 * @var Request_Helper
	 */
	protected $request;

	/**
	 * The helpers surface.
	 *
	 * @var Helpers_Surface
	 */
	protected $helpers;

	/**
	 * The replace vars helper.
	 *
	 * @var WPSEO_Replace_Vars
	 */
	protected $replace_vars;

	/**
	 * The presenters we loop through on each page load.
	 *
	 * @var string[]
	 */
	protected $base_presenters = [
		'Title',
		'Meta_Description',
		'Robots',
	];

	/**
	 * The presenters we loop through on each page load.
	 *
	 * @var string[]
	 */
	protected $indexing_directive_presenters = [
		'Canonical',
		'Rel_Prev',
		'Rel_Next',
	];

	/**
	 * The Open Graph specific presenters.
	 *
	 * @var string[]
	 */
	protected $open_graph_presenters = [
		'Open_Graph\Locale',
		'Open_Graph\Type',
		'Open_Graph\Title',
		'Open_Graph\Description',
		'Open_Graph\Url',
		'Open_Graph\Site_Name',
		'Open_Graph\Article_Publisher',
		'Open_Graph\Article_Author',
		'Open_Graph\Article_Published_Time',
		'Open_Graph\Article_Modified_Time',
		'Open_Graph\Image',
		'Meta_Author',
	];

	/**
	 * The Open Graph specific presenters that should be output on error pages.
	 *
	 * @var array
	 */
	protected $open_graph_error_presenters = [
		'Open_Graph\Locale',
		'Open_Graph\Title',
		'Open_Graph\Site_Name',
	];

	/**
	 * The Twitter card specific presenters.
	 *
	 * @var string[]
	 */
	protected $twitter_card_presenters = [
		'Twitter\Card',
		'Twitter\Title',
		'Twitter\Description',
		'Twitter\Image',
		'Twitter\Creator',
		'Twitter\Site',
	];

	/**
	 * The Slack specific presenters.
	 *
	 * @var string[]
	 */
	protected $slack_presenters = [
		'Slack\Enhanced_Data',
	];

	/**
	 * The Webmaster verification specific presenters.
	 *
	 * @var string[]
	 */
	protected $webmaster_verification_presenters = [
		'Webmaster\Baidu',
		'Webmaster\Bing',
		'Webmaster\Google',
		'Webmaster\Pinterest',
		'Webmaster\Yandex',
	];

	/**
	 * Presenters that are only needed on singular pages.
	 *
	 * @var string[]
	 */
	protected $singular_presenters = [
		'Meta_Author',
		'Open_Graph\Article_Author',
		'Open_Graph\Article_Publisher',
		'Open_Graph\Article_Published_Time',
		'Open_Graph\Article_Modified_Time',
		'Twitter\Creator',
		'Slack\Enhanced_Data',
	];

	/**
	 * The presenters we want to be last in our output.
	 *
	 * @var string[]
	 */
	protected $closing_presenters = [
		'Schema',
	];

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Front_End_Conditional::class ];
	}

	/**
	 * Front_End_Integration constructor.
	 *
	 * @codeCoverageIgnore It sets dependencies.
	 *
	 * @param Meta_Tags_Context_Memoizer $context_memoizer  The meta tags context memoizer.
	 * @param ContainerInterface         $service_container The DI container.
	 * @param Options_Helper             $options           The options helper.
	 * @param Request_Helper             $request           The request helper.
	 * @param Helpers_Surface            $helpers           The helpers surface.
	 * @param WPSEO_Replace_Vars         $replace_vars      The replace vars helper.
	 */
	public function __construct(
		Meta_Tags_Context_Memoizer $context_memoizer,
		ContainerInterface $service_container,
		Options_Helper $options,
		Request_Helper $request,
		Helpers_Surface $helpers,
		WPSEO_Replace_Vars $replace_vars
	) {
		$this->container        = $service_container;
		$this->context_memoizer = $context_memoizer;
		$this->options          = $options;
		$this->request          = $request;
		$this->helpers          = $helpers;
		$this->replace_vars     = $replace_vars;
	}

	/**
	 * Registers the appropriate hooks to show the SEO metadata on the frontend.
	 *
	 * Removes some actions to remove metadata that WordPress shows on the frontend,
	 * to avoid duplicate and/or mismatched metadata.
	 */
	public function register_hooks() {
		\add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 );
		// Filter the title for compatibility with other plugins and themes.
		\add_filter( 'wp_title', [ $this, 'filter_title' ], 15 );
		// Filter the title for compatibility with block-based themes.
		\add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );

		// Removes our robots presenter from the list when wp_robots is handling this.
		\add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] );

		\add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 );

		\remove_action( 'wp_head', 'rel_canonical' );
		\remove_action( 'wp_head', 'index_rel_link' );
		\remove_action( 'wp_head', 'start_post_rel_link' );
		\remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
		\remove_action( 'wp_head', 'noindex', 1 );
		\remove_action( 'wp_head', '_wp_render_title_tag', 1 );
		\remove_action( 'wp_head', '_block_template_render_title_tag', 1 );
		\remove_action( 'wp_head', 'gutenberg_render_title_tag', 1 );
	}

	/**
	 * Filters the title, mainly used for compatibility reasons.
	 *
	 * @return string
	 */
	public function filter_title() {
		$context = $this->context_memoizer->for_current_page();

		$title_presenter = new Title_Presenter();

		/** This filter is documented in src/integrations/front-end-integration.php */
		$title_presenter->presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
		$title_presenter->replace_vars = $this->replace_vars;
		$title_presenter->helpers      = $this->helpers;

		\remove_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
		$title = \esc_html( $title_presenter->get() );
		\add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );

		return $title;
	}

	/**
	 * Filters our robots presenter, but only when wp_robots is attached to the wp_head action.
	 *
	 * @param array $presenters The presenters for current page.
	 *
	 * @return array The filtered presenters.
	 */
	public function filter_robots_presenter( $presenters ) {
		if ( ! \function_exists( 'wp_robots' ) ) {
			return $presenters;
		}

		if ( ! \has_action( 'wp_head', 'wp_robots' ) ) {
			return $presenters;
		}

		if ( $this->request->is_rest_request() ) {
			return $presenters;
		}

		return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] );
	}

	/**
	 * Presents the head in the front-end. Resets wp_query if it's not the main query.
	 *
	 * @codeCoverageIgnore It just calls a WordPress function.
	 */
	public function call_wpseo_head() {
		global $wp_query;

		$old_wp_query = $wp_query;
		// phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query.
		\wp_reset_query();

		\do_action( 'wpseo_head' );

		// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query.
		$GLOBALS['wp_query'] = $old_wp_query;
	}

	/**
	 * Echoes all applicable presenters for a page.
	 */
	public function present_head() {
		$context    = $this->context_memoizer->for_current_page();
		$presenters = $this->get_presenters( $context->page_type, $context );

		/**
		 * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values.
		 *
		 * @api Indexable_Presention The indexable presentation.
		 */
		$presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );

		echo \PHP_EOL;
		foreach ( $presenters as $presenter ) {
			$presenter->presentation = $presentation;
			$presenter->helpers      = $this->helpers;
			$presenter->replace_vars = $this->replace_vars;

			$output = $presenter->present();
			if ( ! empty( $output ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput -- Presenters are responsible for correctly escaping their output.
				echo "\t" . $output . \PHP_EOL;
			}
		}
		echo \PHP_EOL . \PHP_EOL;
	}

	/**
	 * Returns all presenters for this page.
	 *
	 * @param string                 $page_type The page type.
	 * @param Meta_Tags_Context|null $context   The meta tags context for the current page.
	 *
	 * @return Abstract_Indexable_Presenter[] The presenters.
	 */
	public function get_presenters( $page_type, $context = null ) {
		if ( \is_null( $context ) ) {
			$context = $this->context_memoizer->for_current_page();
		}

		$needed_presenters = $this->get_needed_presenters( $page_type );

		$callback   = static function( $presenter ) {
			if ( ! \class_exists( $presenter ) ) {
				return null;
			}
			return new $presenter();
		};
		$presenters = \array_filter( \array_map( $callback, $needed_presenters ) );

		/**
		 * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request.
		 *
		 * @param array             $presenters The presenters.
		 * @param Meta_Tags_Context $context    The meta tags context for the current page.
		 *
		 * @api Abstract_Indexable_Presenter[] List of presenter instances.
		 */
		$presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context );

		if ( ! \is_array( $presenter_instances ) ) {
			$presenter_instances = $presenters;
		}

		$is_presenter_callback = static function ( $presenter_instance ) {
			return $presenter_instance instanceof Abstract_Indexable_Presenter;
		};
		$presenter_instances   = \array_filter( $presenter_instances, $is_presenter_callback );

		return \array_merge(
			[ new Marker_Open_Presenter() ],
			$presenter_instances,
			[ new Marker_Close_Presenter() ]
		);
	}

	/**
	 * Generate the array of presenters we need for the current request.
	 *
	 * @param string $page_type The page type we're retrieving presenters for.
	 *
	 * @return string[] The presenters.
	 */
	private function get_needed_presenters( $page_type ) {
		$presenters = $this->get_presenters_for_page_type( $page_type );

		$presenters = $this->maybe_remove_title_presenter( $presenters );

		$callback   = static function ( $presenter ) {
			return "Yoast\WP\SEO\Presenters\\{$presenter}_Presenter";
		};
		$presenters = \array_map( $callback, $presenters );

		/**
		 * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request.
		 *
		 * @api array List of presenters.
		 */
		$presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters );

		return $presenters;
	}

	/**
	 * Filters the presenters based on the page type.
	 *
	 * @param string $page_type The page type.
	 *
	 * @return string[] The presenters.
	 */
	private function get_presenters_for_page_type( $page_type ) {
		if ( $page_type === 'Error_Page' ) {
			$presenters = $this->base_presenters;
			if ( $this->options->get( 'opengraph' ) === true ) {
				$presenters = \array_merge( $presenters, $this->open_graph_error_presenters );
			}
			return \array_merge( $presenters, $this->closing_presenters );
		}

		$presenters = $this->get_all_presenters();
		if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) {
			$presenters = \array_merge( $presenters, $this->webmaster_verification_presenters );
		}

		// Filter out the presenters only needed for singular pages on non-singular pages.
		if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) {
			$presenters = \array_diff( $presenters, $this->singular_presenters );
		}

		return $presenters;
	}

	/**
	 * Returns a list of all available presenters based on settings.
	 *
	 * @return string[] The presenters.
	 */
	private function get_all_presenters() {
		$presenters = \array_merge( $this->base_presenters, $this->indexing_directive_presenters );
		if ( $this->options->get( 'opengraph' ) === true ) {
			$presenters = \array_merge( $presenters, $this->open_graph_presenters );
		}
		if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) {
			$presenters = \array_merge( $presenters, $this->twitter_card_presenters );
		}
		if ( $this->options->get( 'enable_enhanced_slack_sharing' ) === true && \apply_filters( 'wpseo_output_enhanced_slack_data', true ) !== false ) {
			$presenters = \array_merge( $presenters, $this->slack_presenters );
		}

		return \array_merge( $presenters, $this->closing_presenters );
	}

	/**
	 * Whether the title presenter should be removed.
	 *
	 * @return bool True when the title presenter should be removed, false otherwise.
	 */
	public function should_title_presenter_be_removed() {
		return ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false );
	}

	/**
	 * Checks if the Title presenter needs to be removed.
	 *
	 * @param string[] $presenters The presenters.
	 *
	 * @return string[] The presenters.
	 */
	private function maybe_remove_title_presenter( $presenters ) {
		// Do not remove the title if we're on a REST request.
		if ( $this->request->is_rest_request() ) {
			return $presenters;
		}

		// Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags.
		if ( $this->should_title_presenter_be_removed() ) {
			$presenters = \array_diff( $presenters, [ 'Title' ] );
		}

		return $presenters;
	}
}
cleanup-integration.php000066600000041513151130361070011232 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Closure;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;

/**
 * Adds cleanup hooks.
 */
class Cleanup_Integration implements Integration_Interface {

	/**
	 * Identifier used to determine the current task.
	 */
	const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';

	/**
	 * Identifier for the cron job.
	 */
	const CRON_HOOK = 'wpseo_cleanup_cron';

	/**
	 * Identifier for starting the cleanup.
	 */
	const START_HOOK = 'wpseo_start_cleanup_indexables';

	/**
	 * A helper for taxonomies.
	 *
	 * @var Taxonomy_Helper
	 */
	private $taxonomy;

	/**
	 * A helper for post types.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type;

	/**
	 * A helper for author archives.
	 *
	 * @var Author_Archive_Helper
	 */
	private $author_archive;

	/**
	 * The constructor.
	 *
	 * @param Taxonomy_Helper       $taxonomy       A helper for taxonomies.
	 * @param Post_Type_Helper      $post_type      A helper for post types.
	 * @param Author_Archive_Helper $author_archive A helper for author archives.
	 */
	public function __construct( Taxonomy_Helper $taxonomy, Post_Type_Helper $post_type, Author_Archive_Helper $author_archive ) {
		$this->taxonomy       = $taxonomy;
		$this->post_type      = $post_type;
		$this->author_archive = $author_archive;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( self::START_HOOK, [ $this, 'run_cleanup' ] );
		\add_action( self::CRON_HOOK, [ $this, 'run_cleanup_cron' ] );
		\add_action( 'wpseo_deactivate', [ $this, 'reset_cleanup' ] );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array The array of conditionals.
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * Starts the indexables cleanup.
	 *
	 * @return void
	 */
	public function run_cleanup() {
		$this->reset_cleanup();

		$cleanups = $this->get_cleanup_tasks();
		$limit    = $this->get_limit();

		foreach ( $cleanups as $name => $action ) {
			$items_cleaned = $action( $limit );

			if ( $items_cleaned === false ) {
				return;
			}

			if ( $items_cleaned < $limit ) {
				continue;
			}

			// There are more items to delete for the current cleanup job, start a cronjob at the specified job.
			$this->start_cron_job( $name );

			return;
		}
	}

	/**
	 * Returns an array of cleanup tasks.
	 *
	 * @return Closure[] The cleanup tasks.
	 */
	protected function get_cleanup_tasks() {
		return \array_merge(
			[
				'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function( $limit ) {
					return $this->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit );
				},
				'clean_indexables_by_post_status_auto-draft' => function( $limit ) {
					return $this->clean_indexables_with_post_status( 'auto-draft', $limit );
				},
				'clean_indexables_for_non_publicly_viewable_post' => function ( $limit ) {
					return $this->clean_indexables_for_non_publicly_viewable_post( $limit );
				},
				'clean_indexables_for_non_publicly_viewable_taxonomies' => function ( $limit ) {
					return $this->clean_indexables_for_non_publicly_viewable_taxonomies( $limit );
				},
				'clean_indexables_for_authors_archive_disabled' => function ( $limit ) {
					return $this->clean_indexables_for_authors_archive_disabled( $limit );
				},
				'clean_indexables_for_authors_without_archive' => function ( $limit ) {
					return $this->clean_indexables_for_authors_without_archive( $limit );
				},
			],
			$this->get_additional_tasks(),
			[
				/* These should always be the last ones to be called. */
				'clean_orphaned_content_indexable_hierarchy' => function( $limit ) {
					return $this->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit );
				},
				'clean_orphaned_content_seo_links_indexable_id' => function( $limit ) {
					return $this->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit );
				},
				'clean_orphaned_content_seo_links_target_indexable_id' => function( $limit ) {
					return $this->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit );
				},
			]
		);
	}

	/**
	 * Gets additional tasks from the 'wpseo_cleanup_tasks' filter.
	 *
	 * @return Closure[] Associative array of cleanup functions.
	 */
	private function get_additional_tasks() {

		/**
		 * Filter: Adds the possibility to add addition cleanup functions.
		 *
		 * @api array Associative array with unique keys. Value should be a cleanup function that receives a limit.
		 */
		$additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] );

		if ( ! \is_array( $additional_tasks ) ) {
			return [];
		}

		foreach ( $additional_tasks as $key => $value ) {
			if ( \is_int( $key ) ) {
				return [];
			}
			if ( ( ! \is_object( $value ) ) || ! ( $value instanceof Closure ) ) {
				return [];
			}
		}

		return $additional_tasks;
	}

	/**
	 * Gets the deletion limit for cleanups.
	 *
	 * @return int The limit for the amount of entities to be cleaned.
	 */
	private function get_limit() {
		/**
		 * Filter: Adds the possibility to limit the number of items that are deleted from the database on cleanup.
		 *
		 * @api int $limit Maximum number of indexables to be cleaned up per query.
		 */
		$limit = \apply_filters( 'wpseo_cron_query_limit_size', 1000 );

		if ( ! \is_int( $limit ) ) {
			$limit = 1000;
		}

		return \abs( $limit );
	}

	/**
	 * Resets and stops the cleanup integration.
	 *
	 * @return void
	 */
	public function reset_cleanup() {
		\delete_option( self::CURRENT_TASK_OPTION );
		\wp_unschedule_hook( self::CRON_HOOK );
	}

	/**
	 * Starts the cleanup cron job.
	 *
	 * @param string $task_name The task name of the next cleanup task to run.
	 *
	 * @return void
	 */
	private function start_cron_job( $task_name ) {
		\update_option( self::CURRENT_TASK_OPTION, $task_name );
		\wp_schedule_event(
			( \time() + \HOUR_IN_SECONDS ),
			'hourly',
			self::CRON_HOOK
		);
	}

	/**
	 * The callback that is called for the cleanup cron job.
	 *
	 * @return void
	 */
	public function run_cleanup_cron() {
		$current_task_name = \get_option( self::CURRENT_TASK_OPTION );

		if ( $current_task_name === false ) {
			$this->reset_cleanup();

			return;
		}

		$limit = $this->get_limit();
		$tasks = $this->get_cleanup_tasks();

		// The task may have been added by a filter that has been removed, in that case just start over.
		if ( ! isset( $tasks[ $current_task_name ] ) ) {
			$current_task_name = \key( $tasks );
		}

		$current_task = \current( $tasks );
		while ( $current_task !== false ) {
			// Skip the tasks that have already been done.
			if ( \key( $tasks ) !== $current_task_name ) {
				$current_task = \next( $tasks );
				continue;
			}

			// Call the cleanup callback function that accompanies the current task.
			$items_cleaned = $current_task( $limit );

			if ( $items_cleaned === false ) {
				$this->reset_cleanup();

				return;
			}

			if ( $items_cleaned === 0 ) {
				// Check if we are finished with all tasks.
				if ( \next( $tasks ) === false ) {
					$this->reset_cleanup();

					return;
				}

				// Continue with the next task next time the cron job is run.
				\update_option( self::CURRENT_TASK_OPTION, \key( $tasks ) );

				return;
			}

			// There were items deleted for the current task, continue with the same task next cron call.
			return;
		}
	}

	/**
	 * Deletes rows from the indexable table depending on the object_type and object_sub_type.
	 *
	 * @param string $object_type     The object type to query.
	 * @param string $object_sub_type The object subtype to query.
	 * @param int    $limit           The limit we'll apply to the delete query.
	 *
	 * @return int|bool The number of rows that was deleted or false if the query failed.
	 */
	protected function clean_indexables_with_object_type_and_object_sub_type( $object_type, $object_sub_type, $limit ) {
		global $wpdb;

		$indexable_table = Model::get_table_name( 'Indexable' );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = %s AND object_sub_type = %s ORDER BY id LIMIT %d", $object_type, $object_sub_type, $limit );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->query( $sql );
	}

	/**
	 * Deletes rows from the indexable table depending on the post_status.
	 *
	 * @param string $post_status The post status to query.
	 * @param int    $limit       The limit we'll apply to the delete query.
	 *
	 * @return int|bool The number of rows that was deleted or false if the query failed.
	 */
	protected function clean_indexables_with_post_status( $post_status, $limit ) {
		global $wpdb;

		$indexable_table = Model::get_table_name( 'Indexable' );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND post_status = %s ORDER BY id LIMIT %d", $post_status, $limit );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->query( $sql );
	}

	/**
	 * Cleans up any indexables that belong to post types that are not/no longer publicly viewable.
	 *
	 * @param int $limit The limit we'll apply to the queries.
	 *
	 * @return bool|int The number of deleted rows, false if the query fails.
	 */
	protected function clean_indexables_for_non_publicly_viewable_post( $limit ) {
		global $wpdb;
		$indexable_table = Model::get_table_name( 'Indexable' );

		$included_post_types = $this->post_type->get_indexable_post_types();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
		if ( empty( $included_post_types ) ) {
			$delete_query = $wpdb->prepare(
				"DELETE FROM $indexable_table
				WHERE object_type = 'post'
				AND object_sub_type IS NOT NULL
				LIMIT %d",
				$limit
			);
		}
		else {
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead.
			$delete_query = $wpdb->prepare(
				"DELETE FROM $indexable_table
				WHERE object_type = 'post'
				AND object_sub_type IS NOT NULL
				AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_post_types ), '%s' ) ) . ' )
				LIMIT %d',
				\array_merge( $included_post_types, [ $limit ] )
			);
		}
		// phpcs:enable

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
		return $wpdb->query( $delete_query );
		// phpcs:enable
	}

	/**
	 * Cleans up any indexables that belong to taxonomies that are not/no longer publicly viewable.
	 *
	 * @param int $limit The limit we'll apply to the queries.
	 *
	 * @return bool|int The number of deleted rows, false if the query fails.
	 */
	protected function clean_indexables_for_non_publicly_viewable_taxonomies( $limit ) {
		global $wpdb;
		$indexable_table = Model::get_table_name( 'Indexable' );

		$included_taxonomies = $this->taxonomy->get_indexable_taxonomies();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
		if ( empty( $included_taxonomies ) ) {
			$delete_query = $wpdb->prepare(
				"DELETE FROM $indexable_table
				WHERE object_type = 'term'
				AND object_sub_type IS NOT NULL
				LIMIT %d",
				$limit
			);
		}
		else {
			// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead.
			$delete_query = $wpdb->prepare(
				"DELETE FROM $indexable_table
				WHERE object_type = 'term'
				AND object_sub_type IS NOT NULL
				AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_taxonomies ), '%s' ) ) . ' )
				LIMIT %d',
				\array_merge( $included_taxonomies, [ $limit ] )
			);
		}
		// phpcs:enable

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
		return $wpdb->query( $delete_query );
		// phpcs:enable
	}

	/**
	 * Cleans up any user indexables when the author archives have been disabled.
	 *
	 * @param int $limit The limit we'll apply to the queries.
	 *
	 * @return bool|int The number of deleted rows, false if the query fails.
	 */
	protected function clean_indexables_for_authors_archive_disabled( $limit ) {
		global $wpdb;

		if ( ! $this->author_archive->are_disabled() ) {
			return 0;
		}

		$indexable_table = Model::get_table_name( 'Indexable' );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
		$delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'user' LIMIT %d", $limit );
		// phpcs:enable

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
		return $wpdb->query( $delete_query );
		// phpcs:enable
	}

	/**
	 * Cleans up any indexables that belong to users that have their author archives disabled.
	 *
	 * @param int $limit The limit we'll apply to the queries.
	 *
	 * @return bool|int The number of deleted rows, false if the query fails.
	 */
	protected function clean_indexables_for_authors_without_archive( $limit ) {
		global $wpdb;

		$indexable_table           = Model::get_table_name( 'Indexable' );
		$author_archive_post_types = $this->author_archive->get_author_archive_post_types();
		$viewable_post_stati       = \array_filter( \get_post_stati(), 'is_post_status_viewable' );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
		// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead.
		$delete_query = $wpdb->prepare(
			"DELETE FROM $indexable_table
				WHERE object_type = 'user'
				AND object_id NOT IN (
					SELECT DISTINCT post_author
					FROM $wpdb->posts
					WHERE post_type IN ( " . \implode( ', ', \array_fill( 0, \count( $author_archive_post_types ), '%s' ) ) . ' )
					AND post_status IN ( ' . \implode( ', ', \array_fill( 0, \count( $viewable_post_stati ), '%s' ) ) . ' )
				) LIMIT %d',
			\array_merge( $author_archive_post_types, $viewable_post_stati, [ $limit ] )
		);
		// phpcs:enable

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
		return $wpdb->query( $delete_query );
		// phpcs:enable
	}

	/**
	 * Cleans orphaned rows from a yoast table.
	 *
	 * @param string $table  The table to clean up.
	 * @param string $column The table column the cleanup will rely on.
	 * @param int    $limit  The limit we'll apply to the queries.
	 *
	 * @return int|bool The number of deleted rows, false if the query fails.
	 */
	protected function cleanup_orphaned_from_table( $table, $column, $limit ) {
		global $wpdb;

		$table           = Model::get_table_name( $table );
		$indexable_table = Model::get_table_name( 'Indexable' );

		// Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well.
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		$query = $wpdb->prepare(
			"
			SELECT table_to_clean.{$column}
			FROM {$table} table_to_clean
			LEFT JOIN {$indexable_table} AS indexable_table
			ON table_to_clean.{$column} = indexable_table.id
			WHERE indexable_table.id IS NULL
			AND table_to_clean.{$column} IS NOT NULL
			LIMIT %d",
			$limit
		);
		// phpcs:enable

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		$orphans = $wpdb->get_col( $query );

		if ( empty( $orphans ) ) {
			return 0;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
		return $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' )' );
	}
}
watchers/indexable-category-permalink-watcher.php000066600000003177151130361070016267 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Config\Indexing_Reasons;

/**
 * Watches the stripcategorybase key in wpseo_titles, in order to clear the permalink of the category indexables.
 */
class Indexable_Category_Permalink_Watcher extends Indexable_Permalink_Watcher {

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the stripcategorybase key in wpseo_titles has a change in value, and if so,
	 * clears the permalink for category indexables.
	 *
	 * @param array $old_value The old value of the wpseo_titles option.
	 * @param array $new_value The new value of the wpseo_titles option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		// If this is the first time saving the option, in which case its value would be false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		// If either value is not an array, return.
		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return;
		}

		// If both values aren't set, they haven't changed.
		if ( ! isset( $old_value['stripcategorybase'] ) && ! isset( $new_value['stripcategorybase'] ) ) {
			return;
		}

		// If a new value has been set for 'stripcategorybase', clear the category permalinks.
		if ( $old_value['stripcategorybase'] !== $new_value['stripcategorybase'] ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', 'category', Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX );
		}
	}
}
watchers/indexable-post-meta-watcher.php000066600000005542151130361070014401 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WPSEO_Meta;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * WordPress post meta watcher.
 */
class Indexable_Post_Meta_Watcher implements Integration_Interface {

	/**
	 * The post watcher.
	 *
	 * @var Indexable_Post_Watcher
	 */
	protected $post_watcher;

	/**
	 * An array of post IDs that need to be updated.
	 *
	 * @var array
	 */
	protected $post_ids_to_update = [];

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Postmeta_Watcher constructor.
	 *
	 * @param Indexable_Post_Watcher $post_watcher The post watcher.
	 */
	public function __construct( Indexable_Post_Watcher $post_watcher ) {
		$this->post_watcher = $post_watcher;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		// Register all posts whose meta have changed.
		\add_action( 'added_post_meta', [ $this, 'add_post_id' ], 10, 3 );
		\add_action( 'updated_post_meta', [ $this, 'add_post_id' ], 10, 3 );
		\add_action( 'deleted_post_meta', [ $this, 'add_post_id' ], 10, 3 );

		// Remove posts that get saved as they are handled by the Indexable_Post_Watcher.
		\add_action( 'wp_insert_post', [ $this, 'remove_post_id' ] );
		\add_action( 'delete_post', [ $this, 'remove_post_id' ] );
		\add_action( 'edit_attachment', [ $this, 'remove_post_id' ] );
		\add_action( 'add_attachment', [ $this, 'remove_post_id' ] );
		\add_action( 'delete_attachment', [ $this, 'remove_post_id' ] );

		// Update indexables of all registered posts.
		\register_shutdown_function( [ $this, 'update_indexables' ] );
	}

	/**
	 * Adds a post id to the array of posts to update.
	 *
	 * @param int|string $meta_id  The meta ID.
	 * @param int|string $post_id  The post ID.
	 * @param string     $meta_key The meta key.
	 *
	 * @return void
	 */
	public function add_post_id( $meta_id, $post_id, $meta_key ) {
		// Only register changes to our own meta.
		if ( \strpos( $meta_key, WPSEO_Meta::$meta_prefix ) !== 0 ) {
			return;
		}

		if ( ! \in_array( $post_id, $this->post_ids_to_update, true ) ) {
			$this->post_ids_to_update[] = (int) $post_id;
		}
	}

	/**
	 * Removes a post id from the array of posts to update.
	 *
	 * @param int|string $post_id The post ID.
	 *
	 * @return void
	 */
	public function remove_post_id( $post_id ) {
		$this->post_ids_to_update = \array_diff( $this->post_ids_to_update, [ (int) $post_id ] );
	}

	/**
	 * Updates all indexables changed during the request.
	 *
	 * @return void
	 */
	public function update_indexables() {
		foreach ( $this->post_ids_to_update as $post_id ) {
			$this->post_watcher->build_indexable( $post_id );
		}
	}
}
watchers/indexable-post-type-archive-watcher.php000066600000007141151130361070016050 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Post type archive watcher to save the meta data to an Indexable.
 *
 * Watches the home page options to save the meta information when updated.
 */
class Indexable_Post_Type_Archive_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Post_Type_Archive_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether or not the option has been saved.
	 */
	public function check_option( $old_value, $new_value ) {
		$relevant_keys = [ 'title-ptarchive-', 'metadesc-ptarchive-', 'bctitle-ptarchive-', 'noindex-ptarchive-' ];

		// If this is the first time saving the option, thus when value is false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return false;
		}

		$keys               = \array_unique( \array_merge( \array_keys( $old_value ), \array_keys( $new_value ) ) );
		$post_types_rebuild = [];

		foreach ( $keys as $key ) {
			$post_type = false;
			// Check if it's a key relevant to post type archives.
			foreach ( $relevant_keys as $relevant_key ) {
				if ( \strpos( $key, $relevant_key ) === 0 ) {
					$post_type = \substr( $key, \strlen( $relevant_key ) );
					break;
				}
			}

			// If it's not a relevant key or both values aren't set they haven't changed.
			if ( $post_type === false || ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if (
				! \in_array( $post_type, $post_types_rebuild, true )
				&& (
					! isset( $old_value[ $key ] )
					|| ! isset( $new_value[ $key ] )
					|| $old_value[ $key ] !== $new_value[ $key ]
				)
			) {
				$this->build_indexable( $post_type );
				$post_types_rebuild[] = $post_type;
			}
		}

		return true;
	}

	/**
	 * Saves the post type archive.
	 *
	 * @param string $post_type The post type.
	 *
	 * @return void
	 */
	public function build_indexable( $post_type ) {
		$indexable = $this->repository->find_for_post_type_archive( $post_type, false );
		$indexable = $this->builder->build_for_post_type_archive( $post_type, $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
watchers/indexable-ancestor-watcher.php000066600000020161151130361070014300 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use wpdb;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Permalink_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Ancestor watcher to update the ancestor's children.
 *
 * Updates its children's permalink when the ancestor itself is updated.
 */
class Indexable_Ancestor_Watcher implements Integration_Interface {

	/**
	 * Represents the indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Represents the indexable hierarchy builder.
	 *
	 * @var Indexable_Hierarchy_Builder
	 */
	protected $indexable_hierarchy_builder;

	/**
	 * Represents the indexable hierarchy repository.
	 *
	 * @var Indexable_Hierarchy_Repository
	 */
	protected $indexable_hierarchy_repository;

	/**
	 * Represents the WordPress database object.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Represents the permalink helper.
	 *
	 * @var Permalink_Helper
	 */
	protected $permalink_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * Sets the needed dependencies.
	 *
	 * @param Indexable_Repository           $indexable_repository           The indexable repository.
	 * @param Indexable_Hierarchy_Builder    $indexable_hierarchy_builder    The indexable hierarchy builder.
	 * @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
	 * @param wpdb                           $wpdb                           The wpdb object.
	 * @param Permalink_Helper               $permalink_helper               The permalink helper.
	 * @param Post_Type_Helper               $post_type_helper               The post type helper.
	 */
	public function __construct(
		Indexable_Repository $indexable_repository,
		Indexable_Hierarchy_Builder $indexable_hierarchy_builder,
		Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
		wpdb $wpdb,
		Permalink_Helper $permalink_helper,
		Post_Type_Helper $post_type_helper
	) {
		$this->indexable_repository           = $indexable_repository;
		$this->indexable_hierarchy_builder    = $indexable_hierarchy_builder;
		$this->wpdb                           = $wpdb;
		$this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
		$this->permalink_helper               = $permalink_helper;
		$this->post_type_helper               = $post_type_helper;
	}

	/**
	 * Registers the appropriate hooks.
	 */
	public function register_hooks() {
		\add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink.
	 *
	 * @param Indexable $indexable        The indexable.
	 * @param Indexable $indexable_before The old indexable.
	 *
	 * @return bool True if the children were reset.
	 */
	public function reset_children( $indexable, $indexable_before ) {
		if ( ! \in_array( $indexable->object_type, [ 'post', 'term' ], true ) ) {
			return false;
		}

		// If the permalink was null it means it was reset instead of changed.
		if ( $indexable->permalink === $indexable_before->permalink || \is_null( $indexable_before->permalink ) ) {
			return false;
		}

		$child_indexable_ids = $this->indexable_hierarchy_repository->find_children( $indexable );
		$child_indexables    = $this->indexable_repository->find_by_ids( $child_indexable_ids );

		\array_walk( $child_indexables, [ $this, 'update_hierarchy_and_permalink' ] );

		if ( $indexable->object_type === 'term' ) {
			$child_indexables_for_term = $this->get_children_for_term( $indexable->object_id, $child_indexables );

			\array_walk( $child_indexables_for_term, [ $this, 'update_hierarchy_and_permalink' ] );
		}

		return true;
	}

	/**
	 * Finds all child indexables for the given term.
	 *
	 * @param int         $term_id          Term to fetch the indexable for.
	 * @param Indexable[] $child_indexables The already known child indexables.
	 *
	 * @return array The list of additional child indexables for a given term.
	 */
	public function get_children_for_term( $term_id, array $child_indexables ) {
		// Finds object_ids (posts) for the term.
		$post_object_ids = $this->get_object_ids_for_term( $term_id, $child_indexables );

		// Removes the objects that are already present in the children.
		$existing_post_indexables = \array_filter(
			$child_indexables,
			static function( $indexable ) {
				return $indexable->object_type === 'post';
			}
		);

		$existing_post_object_ids = \wp_list_pluck( $existing_post_indexables, 'object_id' );
		$post_object_ids          = \array_diff( $post_object_ids, $existing_post_object_ids );

		// Finds the indexables for the fetched post_object_ids.
		$post_indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_object_ids, 'post', false );

		// Finds the indexables for the posts that are attached to the term.
		$post_indexable_ids       = \wp_list_pluck( $post_indexables, 'id' );
		$additional_indexable_ids = $this->indexable_hierarchy_repository->find_children_by_ancestor_ids( $post_indexable_ids );

		// Makes sure we only have indexable id's that we haven't fetched before.
		$additional_indexable_ids = \array_diff( $additional_indexable_ids, $post_indexable_ids );

		// Finds the additional indexables.
		$additional_indexables = $this->indexable_repository->find_by_ids( $additional_indexable_ids );

		// Merges all fetched indexables.
		return \array_merge( $post_indexables, $additional_indexables );
	}

	/**
	 * Builds the hierarchy for a post.
	 *
	 * @deprecated 16.4
	 *
	 * @codeCoverageIgnore
	 *
	 * @param int $object_id The post id.
	 * @param int $post_type The post type.
	 */
	public function build_post_hierarchy( $object_id, $post_type ) {
		\_deprecated_function( __METHOD__, '16.4', 'Primary_Category_Quick_Edit_Watcher::build_post_hierarchy' );
	}

	/**
	 * Updates the indexable hierarchy and indexable permalink.
	 *
	 * @param Indexable $indexable The indexable to update the hierarchy and permalink for.
	 */
	protected function update_hierarchy_and_permalink( $indexable ) {
		$this->indexable_hierarchy_builder->build( $indexable );

		$indexable->permalink = $this->permalink_helper->get_permalink_for_indexable( $indexable );
		$indexable->save();
	}

	/**
	 * Retrieves the object id's for a term based on the term-post relationship.
	 *
	 * @param int         $term_id          The term to get the object id's for.
	 * @param Indexable[] $child_indexables The child indexables.
	 *
	 * @return array List with object ids for the term.
	 */
	protected function get_object_ids_for_term( $term_id, $child_indexables ) {
		$filter_terms = static function( $child ) {
			return $child->object_type === 'term';
		};

		$child_terms      = \array_filter( $child_indexables, $filter_terms );
		$child_object_ids = \wp_list_pluck( $child_terms, 'object_id' );

		// Get the term-taxonomy id's for the term and its children.
		$term_taxonomy_ids = $this->wpdb->get_col(
			$this->wpdb->prepare(
				'SELECT term_taxonomy_id
				FROM ' . $this->wpdb->term_taxonomy . '
				WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) + 1 ), '%s' ) ) . ' )',
				$term_id,
				...$child_object_ids
			)
		);

		// In the case of faulty data having been saved the above query can return 0 results.
		if ( empty( $term_taxonomy_ids ) ) {
			return [];
		}

		// Get the (post) object id's that are attached to the term.
		return $this->wpdb->get_col(
			$this->wpdb->prepare(
				'SELECT DISTINCT object_id
				FROM ' . $this->wpdb->term_relationships . '
				WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )',
				...$term_taxonomy_ids
			)
		);
	}
}
watchers/indexable-author-archive-watcher.php000066600000003412151130361070015403 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Watches the `wpseo_titles` option for changes to the author archive settings.
 */
class Indexable_Author_Archive_Watcher implements Integration_Interface {

	/**
	 * Check if the author archives are disabled whenever the `wpseo_titles` option
	 * changes.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action(
			'update_option_wpseo_titles',
			[ $this, 'reschedule_indexable_cleanup_when_author_archives_are_disabled' ],
			10,
			2
		);
	}

	/**
	 * This watcher should only be run when the migrations have been run.
	 * (Otherwise there may not be an indexable table to clean).
	 *
	 * @return string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Reschedule the indexable cleanup routine if the author archives are disabled.
	 * to make sure that all authors are removed from the indexables table.
	 *
	 * When author archives are disabled, they can never be indexed.
	 *
	 * @param array $old_value The old `wpseo_titles` option value.
	 * @param array $new_value The new `wpseo_titles` option value.
	 *
	 * @return void
	 */
	public function reschedule_indexable_cleanup_when_author_archives_are_disabled( $old_value, $new_value ) {
		if ( $old_value['disable-author'] !== true && $new_value['disable-author'] === true ) {
			$cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK );
			if ( $cleanup_not_yet_scheduled ) {
				\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
			}
		}
	}
}
watchers/primary-term-watcher.php000066600000007663151130361070013175 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_Term;
use WPSEO_Meta;
use WPSEO_Primary_Term;
use Yoast\WP\SEO\Builders\Primary_Term_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Primary_Term_Helper;
use Yoast\WP\SEO\Helpers\Site_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Primary Term watcher.
 *
 * Watches Posts to save the primary term when set.
 */
class Primary_Term_Watcher implements Integration_Interface {

	/**
	 * The primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	protected $repository;

	/**
	 * Represents the site helper.
	 *
	 * @var Site_Helper
	 */
	protected $site;

	/**
	 * The primary term helper.
	 *
	 * @var Primary_Term_Helper
	 */
	protected $primary_term;

	/**
	 * The primary term builder.
	 *
	 * @var Primary_Term_Builder
	 */
	protected $primary_term_builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Primary_Term_Watcher constructor.
	 *
	 * @codeCoverageIgnore It sets dependencies.
	 *
	 * @param Primary_Term_Repository $repository           The primary term repository.
	 * @param Site_Helper             $site                 The site helper.
	 * @param Primary_Term_Helper     $primary_term         The primary term helper.
	 * @param Primary_Term_Builder    $primary_term_builder The primary term builder.
	 */
	public function __construct(
		Primary_Term_Repository $repository,
		Site_Helper $site,
		Primary_Term_Helper $primary_term,
		Primary_Term_Builder $primary_term_builder
	) {
		$this->repository           = $repository;
		$this->site                 = $site;
		$this->primary_term         = $primary_term;
		$this->primary_term_builder = $primary_term_builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'save_post', [ $this, 'save_primary_terms' ], \PHP_INT_MAX );
		\add_action( 'delete_post', [ $this, 'delete_primary_terms' ] );
	}

	/**
	 * Saves all selected primary terms.
	 *
	 * @param int $post_id Post ID to save primary terms for.
	 */
	public function save_primary_terms( $post_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->site->is_multisite_and_switched() ) {
			return;
		}

		$taxonomies = $this->primary_term->get_primary_term_taxonomies( $post_id );

		foreach ( $taxonomies as $taxonomy ) {
			$this->save_primary_term( $post_id, $taxonomy );
		}

		$this->primary_term_builder->build( $post_id );
	}

	/**
	 * Saves the primary term for a specific taxonomy.
	 *
	 * @param int     $post_id  Post ID to save primary term for.
	 * @param WP_Term $taxonomy Taxonomy to save primary term for.
	 */
	protected function save_primary_term( $post_id, $taxonomy ) {
		$primary_term = \filter_input( \INPUT_POST, WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term', \FILTER_SANITIZE_NUMBER_INT );

		// We accept an empty string here because we need to save that if no terms are selected.
		if ( $primary_term && \check_admin_referer( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce' ) !== null ) {
			$primary_term_object = new WPSEO_Primary_Term( $taxonomy->name, $post_id );
			$primary_term_object->set_primary_term( $primary_term );
		}
	}

	/**
	 * Deletes primary terms for a post.
	 *
	 * @param int $post_id The post to delete the terms of.
	 *
	 * @return void
	 */
	public function delete_primary_terms( $post_id ) {
		foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) {
			$primary_term = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy->name, false );

			if ( ! $primary_term ) {
				continue;
			}

			$primary_term->delete();
		}
	}
}
watchers/primary-category-quick-edit-watcher.php000066600000012763151130361070016075 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_Post;
use WPSEO_Meta;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Admin\Doing_Post_Quick_Edit_Save_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Class Primary_Category_Quick_Edit_Watcher
 */
class Primary_Category_Quick_Edit_Watcher implements Integration_Interface {

	/**
	 * Holds the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Holds the primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	protected $primary_term_repository;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * The indexable hierarchy builder.
	 *
	 * @var Indexable_Hierarchy_Builder
	 */
	protected $indexable_hierarchy_builder;

	/**
	 * Primary_Category_Quick_Edit_Watcher constructor.
	 *
	 * @param Options_Helper              $options_helper              The options helper.
	 * @param Primary_Term_Repository     $primary_term_repository     The primary term repository.
	 * @param Post_Type_Helper            $post_type_helper            The post type helper.
	 * @param Indexable_Repository        $indexable_repository        The indexable repository.
	 * @param Indexable_Hierarchy_Builder $indexable_hierarchy_builder The indexable hierarchy repository.
	 */
	public function __construct(
		Options_Helper $options_helper,
		Primary_Term_Repository $primary_term_repository,
		Post_Type_Helper $post_type_helper,
		Indexable_Repository $indexable_repository,
		Indexable_Hierarchy_Builder $indexable_hierarchy_builder
	) {
		$this->options_helper              = $options_helper;
		$this->primary_term_repository     = $primary_term_repository;
		$this->post_type_helper            = $post_type_helper;
		$this->indexable_repository        = $indexable_repository;
		$this->indexable_hierarchy_builder = $indexable_hierarchy_builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'set_object_terms', [ $this, 'validate_primary_category' ], 10, 4 );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class, Doing_Post_Quick_Edit_Save_Conditional::class ];
	}

	/**
	 * Validates if the current primary category is still present. If not just remove the post meta for it.
	 *
	 * @param int    $object_id Object ID.
	 * @param array  $terms     Unused. An array of object terms.
	 * @param array  $tt_ids    An array of term taxonomy IDs.
	 * @param string $taxonomy  Taxonomy slug.
	 */
	public function validate_primary_category( $object_id, $terms, $tt_ids, $taxonomy ) {
		$post = \get_post( $object_id );
		if ( $post === null ) {
			return;
		}

		$main_taxonomy = $this->options_helper->get( 'post_types-' . $post->post_type . '-maintax' );
		if ( ! $main_taxonomy || $main_taxonomy === '0' ) {
			return;
		}

		if ( $main_taxonomy !== $taxonomy ) {
			return;
		}

		$primary_category = $this->get_primary_term_id( $post->ID, $main_taxonomy );
		if ( $primary_category === false ) {
			return;
		}

		// The primary category isn't removed.
		if ( \in_array( (string) $primary_category, $tt_ids, true ) ) {
			return;
		}

		$this->remove_primary_term( $post->ID, $main_taxonomy );

		// Rebuild the post hierarchy for this post now the primary term has been changed.
		$this->build_post_hierarchy( $post );
	}

	/**
	 * Returns the primary term id of a post.
	 *
	 * @param int    $post_id       The post ID.
	 * @param string $main_taxonomy The main taxonomy.
	 *
	 * @return int|false The ID of the primary term, or `false` if the post ID is invalid.
	 */
	private function get_primary_term_id( $post_id, $main_taxonomy ) {
		$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );

		if ( $primary_term ) {
			return $primary_term->term_id;
		}

		return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true );
	}

	/**
	 * Removes the primary category.
	 *
	 * @param int    $post_id       The post id to set primary taxonomy for.
	 * @param string $main_taxonomy Name of the taxonomy that is set to be the primary one.
	 */
	private function remove_primary_term( $post_id, $main_taxonomy ) {
		$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );
		if ( $primary_term ) {
			$primary_term->delete();
		}

		// Remove it from the post meta.
		\delete_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy );
	}

	/**
	 * Builds the hierarchy for a post.
	 *
	 * @param WP_Post $post The post.
	 */
	public function build_post_hierarchy( $post ) {
		if ( $this->post_type_helper->is_excluded( $post->post_type ) ) {
			return;
		}

		$indexable = $this->indexable_repository->find_by_id_and_type( $post->ID, 'post' );

		if ( $indexable instanceof Indexable ) {
			$this->indexable_hierarchy_builder->build( $indexable );
		}
	}
}
watchers/indexable-date-archive-watcher.php000066600000005010151130361070015012 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Date archive watcher to save the meta data to an indexable.
 *
 * Watches the date archive options to save the meta information when updated.
 */
class Indexable_Date_Archive_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Date_Archive_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The date archive builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the date archive indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		$relevant_keys = [ 'title-archive-wpseo', 'breadcrumbs-archiveprefix', 'metadesc-archive-wpseo', 'noindex-archive-wpseo' ];

		foreach ( $relevant_keys as $key ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) {
				$this->build_indexable();
				return;
			}
		}
	}

	/**
	 * Saves the date archive.
	 *
	 * @return void
	 */
	public function build_indexable() {
		$indexable = $this->repository->find_for_date_archive( false );
		$this->builder->build_for_date_archive( $indexable );
	}
}
watchers/indexable-post-type-change-watcher.php000066600000013570151130361070015657 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast_Notification;
use Yoast_Notification_Center;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Post type change watcher.
 */
class Indexable_Post_Type_Change_Watcher implements Integration_Interface {

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * Holds the Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Holds the Post_Type_Helper instance.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type_helper;

	/**
	 * The notifications center.
	 *
	 * @var Yoast_Notification_Center
	 */
	private $notification_center;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Not_Admin_Ajax_Conditional::class, Admin_Conditional::class, Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Post_Type_Change_Watcher constructor.
	 *
	 * @param Options_Helper            $options             The options helper.
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param Post_Type_Helper          $post_type_helper    The post_typehelper.
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct(
		Options_Helper $options,
		Indexing_Helper $indexing_helper,
		Post_Type_Helper $post_type_helper,
		Yoast_Notification_Center $notification_center
	) {
		$this->options             = $options;
		$this->indexing_helper     = $indexing_helper;
		$this->post_type_helper    = $post_type_helper;
		$this->notification_center = $notification_center;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'check_post_types_public_availability' ] );
	}

	/**
	 * Checks if one or more post types change visibility.
	 *
	 * @return void
	 */
	public function check_post_types_public_availability() {

		// We have to make sure this is just a plain http request, no ajax/REST.
		if ( \wp_is_json_request() ) {
			return;
		}

		$public_post_types            = \array_keys( $this->post_type_helper->get_public_post_types() );
		$last_known_public_post_types = $this->options->get( 'last_known_public_post_types', [] );

		// Initializing the option on the first run.
		if ( empty( $last_known_public_post_types ) ) {
			$this->options->set( 'last_known_public_post_types', $public_post_types );
			return;
		}

		// We look for new public post types.
		$newly_made_public_post_types = \array_diff( $public_post_types, $last_known_public_post_types );
		// We look for post types that from public have been made private.
		$newly_made_non_public_post_types = \array_diff( $last_known_public_post_types, $public_post_types );

		// Nothing to be done if no changes has been made to post types.
		if ( empty( $newly_made_public_post_types ) && ( empty( $newly_made_non_public_post_types ) ) ) {
			return;
		}

		// Update the list of last known public post types in the database.
		$this->options->set( 'last_known_public_post_types', $public_post_types );

		// There are new post types that have been made public.
		if ( ! empty( $newly_made_public_post_types ) ) {

			// Force a notification requesting to start the SEO data optimization.
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT );

			$this->indexing_helper->set_reason( Indexing_Reasons::REASON_POST_TYPE_MADE_PUBLIC );

			$this->maybe_add_notification();
		}

		// There are post types that have been made private.
		if ( ! empty( $newly_made_non_public_post_types ) ) {
			// Schedule a cron job to remove all the posts whose post type has been made private.
			if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
				if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
					\wp_schedule_single_event( ( time() + 5 ), \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK );
					\wp_schedule_single_event( ( time() + 5 ), Cleanup_Integration::START_HOOK );
				}
			}
		}
	}

	/**
	 * Decides if a notification should be added in the notification center.
	 *
	 * @return void
	 */
	private function maybe_add_notification() {
		$notification = $this->notification_center->get_notification_by_id( 'post-types-made-public' );
		if ( is_null( $notification ) ) {
			$this->add_notification();
		}
	}

	/**
	 * Adds a notification to be shown on the next page request since posts are updated in an ajax request.
	 *
	 * @return void
	 */
	private function add_notification() {
		$message = sprintf(
			/* translators: 1: Opening tag of the link to the Search appearance settings page, 2: Link closing tag. */
			\esc_html__( 'It looks like you\'ve added a new type of content to your website. We recommend that you review your %1$sSearch appearance settings%2$s.', 'wordpress-seo' ),
			'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_titles#top#post-types' ) ) . '">',
			'</a>'
		);

		$notification = new Yoast_Notification(
			$message,
			[
				'type'         => Yoast_Notification::WARNING,
				'id'           => 'post-types-made-public',
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 0.8,
			]
		);

		$this->notification_center->add_notification( $notification );
	}
}
watchers/indexable-home-page-watcher.php000066600000006162151130361070014331 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Home page watcher to save the meta data to an Indexable.
 *
 * Watches the home page options to save the meta information when updated.
 */
class Indexable_Home_Page_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Home_Page_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 15, 3 );
		\add_action( 'update_option_wpseo_social', [ $this, 'check_option' ], 15, 3 );
		\add_action( 'update_option_blog_public', [ $this, 'build_indexable' ] );
		\add_action( 'update_option_blogdescription', [ $this, 'build_indexable' ] );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array  $old_value The old value of the option.
	 * @param array  $new_value The new value of the option.
	 * @param string $option    The name of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value, $option ) {
		$relevant_keys = [
			'wpseo_titles' => [
				'title-home-wpseo',
				'breadcrumbs-home',
				'metadesc-home-wpseo',
				'open_graph_frontpage_title',
				'open_graph_frontpage_desc',
				'open_graph_frontpage_image',
			],
		];

		if ( ! isset( $relevant_keys[ $option ] ) ) {
			return;
		}

		foreach ( $relevant_keys[ $option ] as $key ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) {
				$this->build_indexable();
				return;
			}
		}
	}

	/**
	 * Saves the home page.
	 *
	 * @return void
	 */
	public function build_indexable() {
		$indexable = $this->repository->find_for_home_page( false );
		$indexable = $this->builder->build_for_home_page( $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
watchers/indexable-system-page-watcher.php000066600000005176151130361070014731 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_System_Page_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Search result watcher to save the meta data to an Indexable.
 *
 * Watches the search result options to save the meta information when updated.
 */
class Indexable_System_Page_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_System_Page_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		foreach ( Indexable_System_Page_Builder::OPTION_MAPPING as $type => $options ) {
			foreach ( $options as $option ) {
				// If both values aren't set they haven't changed.
				if ( ! isset( $old_value[ $option ] ) && ! isset( $new_value[ $option ] ) ) {
					return;
				}

				// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
				if (
					! isset( $old_value[ $option ] )
					|| ! isset( $new_value[ $option ] )
					|| $old_value[ $option ] !== $new_value[ $option ]
				) {
					$this->build_indexable( $type );
				}
			}
		}
	}

	/**
	 * Saves the search result.
	 *
	 * @param string $type The type of no index page.
	 *
	 * @return void
	 */
	public function build_indexable( $type ) {
		$indexable = $this->repository->find_for_system_page( $type, false );
		$this->builder->build_for_system_page( $type, $indexable );
	}
}
watchers/indexable-author-watcher.php000066600000004373151130361070013773 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watches an Author to save the meta information to an Indexable when updated.
 */
class Indexable_Author_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Author_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'user_register', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'profile_update', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'deleted_user', [ $this, 'delete_indexable' ] );
	}

	/**
	 * Deletes user meta.
	 *
	 * @param int $user_id User ID to delete the metadata of.
	 *
	 * @return void
	 */
	public function delete_indexable( $user_id ) {
		$indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false );

		if ( ! $indexable ) {
			return;
		}

		$indexable->delete();
	}

	/**
	 * Saves user meta.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return void
	 */
	public function build_indexable( $user_id ) {
		$indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false );
		$indexable = $this->builder->build_for_id_and_type( $user_id, 'user', $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
watchers/option-titles-watcher.php000066600000006174151130361070013353 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\WordPress\Wrapper;

/**
 * Watcher for the titles option.
 *
 * Represents the option titles watcher.
 */
class Option_Titles_Watcher implements Integration_Interface {

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Checks if one of the relevant options has been changed.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether or not the ancestors are removed.
	 */
	public function check_option( $old_value, $new_value ) {
		// If this is the first time saving the option, thus when value is false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return false;
		}

		$relevant_keys = $this->get_relevant_keys();
		if ( empty( $relevant_keys ) ) {
			return false;
		}

		$post_types = [];

		foreach ( $relevant_keys as $post_type => $relevant_option ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $relevant_option ] ) && ! isset( $new_value[ $relevant_option ] ) ) {
				continue;
			}

			if ( $old_value[ $relevant_option ] !== $new_value[ $relevant_option ] ) {
				$post_types[] = $post_type;
			}
		}

		return $this->delete_ancestors( $post_types );
	}

	/**
	 * Retrieves the relevant keys.
	 *
	 * @return array Array with the relevant keys.
	 */
	protected function get_relevant_keys() {
		$post_types = \get_post_types( [ 'public' => true ], 'names' );
		if ( ! \is_array( $post_types ) || $post_types === [] ) {
			return [];
		}

		$relevant_keys = [];
		foreach ( $post_types as $post_type ) {
			$relevant_keys[ $post_type ] = 'post_types-' . $post_type . '-maintax';
		}

		return $relevant_keys;
	}

	/**
	 * Removes the ancestors for given post types.
	 *
	 * @param array $post_types The post types to remove hierarchy for.
	 *
	 * @return bool True when delete query was successful.
	 */
	protected function delete_ancestors( $post_types ) {
		if ( empty( $post_types ) ) {
			return false;
		}

		$wpdb            = Wrapper::get_wpdb();
		$total           = \count( $post_types );
		$hierarchy_table = Model::get_table_name( 'Indexable_Hierarchy' );
		$indexable_table = Model::get_table_name( 'Indexable' );

		$result = $wpdb->query(
			$wpdb->prepare(
				"
				DELETE FROM `$hierarchy_table`
				WHERE indexable_id IN(
					SELECT id
					FROM `$indexable_table`
					WHERE object_type = 'post'
					AND object_sub_type IN( " . \implode( ', ', \array_fill( 0, $total, '%s' ) ) . ' )
				)',
				$post_types
			)
		);

		return $result !== false;
	}
}
watchers/addon-update-watcher.php000066600000016053151130361070013103 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Enables Yoast add-on auto updates when Yoast SEO is enabled and the other way around.
 *
 * Also removes the auto-update toggles from the Yoast SEO add-ons.
 */
class Addon_Update_Watcher implements Integration_Interface {

	/**
	 * ID string used by WordPress to identify the free plugin.
	 *
	 * @var string
	 */
	const WPSEO_FREE_PLUGIN_ID = 'wordpress-seo/wp-seo.php';

	/**
	 * A list of Yoast add-on identifiers.
	 *
	 * @var string[]
	 */
	const ADD_ON_PLUGIN_FILES = [
		'wordpress-seo-premium/wp-seo-premium.php',
		'wpseo-video/video-seo.php',
		'wpseo-local/local-seo.php', // When installing Local through a released zip, the path is different from the path on a dev environment.
		'wpseo-woocommerce/wpseo-woocommerce.php',
		'wpseo-news/wpseo-news.php',
		'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php', // When installing ACF for Yoast through a released zip, the path is different from the path on a dev environment.
	];

	/**
	 * Registers the hooks.
	 */
	public function register_hooks() {
		\add_action( 'add_site_option_auto_update_plugins', [ $this, 'call_toggle_auto_updates_with_empty_array' ], 10, 2 );
		\add_action( 'update_site_option_auto_update_plugins', [ $this, 'toggle_auto_updates_for_add_ons' ], 10, 3 );
		\add_filter( 'plugin_auto_update_setting_html', [ $this, 'replace_auto_update_toggles_of_addons' ], 10, 2 );
		\add_action( 'activated_plugin', [ $this, 'maybe_toggle_auto_updates_for_new_install' ] );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Replaces the auto-update toggle links for the Yoast add-ons
	 * with a text explaining that toggling the Yoast SEO auto-update setting
	 * automatically toggles the one for the setting for the add-ons as well.
	 *
	 * @param string $old_html The old HTML.
	 * @param string $plugin   The plugin.
	 *
	 * @return string The new HTML, with the auto-update toggle link replaced.
	 */
	public function replace_auto_update_toggles_of_addons( $old_html, $plugin ) {
		if ( ! \is_string( $old_html ) ) {
			return $old_html;
		}

		$not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true );

		if ( $not_a_yoast_addon ) {
			return $old_html;
		}

		$auto_updated_plugins = \get_site_option( 'auto_update_plugins' );

		if ( $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $auto_updated_plugins ) ) {
			return \sprintf(
				'<em>%s</em>',
				\sprintf(
				/* Translators: %1$s resolves to Yoast SEO. */
					\esc_html__( 'Auto-updates are enabled based on this setting for %1$s.', 'wordpress-seo' ),
					'Yoast SEO'
				)
			);
		}

		return \sprintf(
			'<em>%s</em>',
			\sprintf(
			/* Translators: %1$s resolves to Yoast SEO. */
				\esc_html__( 'Auto-updates are disabled based on this setting for %1$s.', 'wordpress-seo' ),
				'Yoast SEO'
			)
		);
	}

	/**
	 * Handles the situation where the auto_update_plugins option did not previously exist.
	 *
	 * @param string      $option The name of the option that is being created.
	 * @param array|mixed $value  The new (and first) value of the option that is being created.
	 */
	public function call_toggle_auto_updates_with_empty_array( $option, $value ) {
		if ( $option !== 'auto_update_plugins' ) {
			return;
		}

		$this->toggle_auto_updates_for_add_ons( $option, $value, [] );
	}

	/**
	 * Enables premium auto updates when free are enabled and the other way around.
	 *
	 * @param string $option    The name of the option that has been updated.
	 * @param array  $new_value The new value of the `auto_update_plugins` option.
	 * @param array  $old_value The old value of the `auto_update_plugins` option.
	 *
	 * @return void
	 */
	public function toggle_auto_updates_for_add_ons( $option, $new_value, $old_value ) {
		if ( $option !== 'auto_update_plugins' ) {
			// If future versions of WordPress change this filter's behavior, our behavior should stay consistent.
			return;
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return;
		}

		$auto_updates_are_enabled  = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $new_value );
		$auto_updates_were_enabled = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $old_value );

		if ( $auto_updates_are_enabled === $auto_updates_were_enabled ) {
			// Auto-updates for Yoast SEO have stayed the same, so have neither been enabled or disabled.
			return;
		}

		$auto_updates_have_been_enabled = $auto_updates_are_enabled && ! $auto_updates_were_enabled;

		if ( $auto_updates_have_been_enabled ) {
			$this->enable_auto_updates_for_addons( $new_value );
			return;
		}
		else {
			$this->disable_auto_updates_for_addons( $new_value );
			return;
		}

		if ( ! $auto_updates_are_enabled ) {
			return;
		}

		$auto_updates_have_been_removed = false;
		foreach ( self::ADD_ON_PLUGIN_FILES as $addon ) {
			if ( ! $this->are_auto_updates_enabled( $addon, $new_value ) ) {
				$auto_updates_have_been_removed = true;
				break;
			}
		}

		if ( $auto_updates_have_been_removed ) {
			$this->enable_auto_updates_for_addons( $new_value );
		}
	}

	/**
	 * Trigger a change in the auto update detection whenever a new Yoast addon is activated.
	 *
	 * @param string $plugin The plugin that is activated.
	 *
	 * @return void
	 */
	public function maybe_toggle_auto_updates_for_new_install( $plugin ) {
		$not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true );

		if ( $not_a_yoast_addon ) {
			return;
		}

		$enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
		$this->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
	}

	/**
	 * Enables auto-updates for all addons.
	 *
	 * @param string[] $auto_updated_plugins The current list of auto-updated plugins.
	 */
	protected function enable_auto_updates_for_addons( $auto_updated_plugins ) {
		$plugins = \array_unique( \array_merge( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) );
		\update_site_option( 'auto_update_plugins', $plugins );
	}

	/**
	 * Disables auto-updates for all addons.
	 *
	 * @param string[] $auto_updated_plugins The current list of auto-updated plugins.
	 */
	protected function disable_auto_updates_for_addons( $auto_updated_plugins ) {
		$plugins = \array_values( \array_diff( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) );
		\update_site_option( 'auto_update_plugins', $plugins );
	}

	/**
	 * Checks whether auto updates for a plugin are enabled.
	 *
	 * @param string $plugin_id            The plugin ID.
	 * @param array  $auto_updated_plugins The array of auto updated plugins.
	 *
	 * @return bool Whether auto updates for a plugin are enabled.
	 */
	protected function are_auto_updates_enabled( $plugin_id, $auto_updated_plugins ) {
		if ( $auto_updated_plugins === false || ! \is_array( $auto_updated_plugins ) ) {
			return false;
		}

		return \in_array( $plugin_id, $auto_updated_plugins, true );
	}
}
watchers/indexable-static-home-page-watcher.php000066600000004216151130361070015614 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watcher that checks for changes in the page used as homepage.
 *
 * Watches the static homepage option and updates the permalinks accordingly.
 */
class Indexable_Static_Home_Page_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Indexable_Static_Home_Page_Watcher constructor.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 */
	public function __construct( Indexable_Repository $repository ) {
		$this->repository = $repository;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_page_on_front', [ $this, 'update_static_homepage_permalink' ], 10, 2 );
	}

	/**
	 * Updates the new and previous homepage's permalink when the static home page is updated.
	 *
	 * @param string $old_value The previous homepage's ID.
	 * @param int    $value     The new homepage's ID.
	 */
	public function update_static_homepage_permalink( $old_value, $value ) {
		if ( \is_string( $old_value ) ) {
			$old_value = (int) $old_value;
		}

		if ( $old_value === $value ) {
			return;
		}

		$this->update_permalink_for_page( $old_value );
		$this->update_permalink_for_page( $value );
	}

	/**
	 * Updates the permalink based on the selected homepage settings.
	 *
	 * @param int $page_id The page's id.
	 */
	private function update_permalink_for_page( $page_id ) {
		if ( $page_id === 0 ) {
			return;
		}

		$indexable = $this->repository->find_by_id_and_type( $page_id, 'post', false );

		if ( $indexable === false ) {
			return;
		}

		$indexable->permalink = \get_permalink( $page_id );

		$indexable->save();
	}
}
watchers/search-engines-discouraged-watcher.php000066600000015225151130361070015720 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Search_Engines_Discouraged_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
 * Shows a notification for users who have access for robots disabled.
 *
 * @class Search_Engines_Discouraged_Watcher
 */
class Search_Engines_Discouraged_Watcher implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The notification ID.
	 */
	const NOTIFICATION_ID = 'wpseo-search-engines-discouraged';

	/**
	 * The Yoast notification center.
	 *
	 * @var Yoast_Notification_Center
	 */
	protected $notification_center;

	/**
	 * The notification helper.
	 *
	 * @var Notification_Helper
	 */
	protected $notification_helper;

	/**
	 * The search engines discouraged presenter.
	 *
	 * @var Search_Engines_Discouraged_Presenter
	 */
	protected $presenter;

	/**
	 * The current page helper.
	 *
	 * @var Current_Page_Helper
	 */
	protected $current_page_helper;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The capability helper.
	 *
	 * @var Capability_Helper
	 */
	protected $capability_helper;

	/**
	 * Search_Engines_Discouraged_Watcher constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 * @param Notification_Helper       $notification_helper The notification helper.
	 * @param Current_Page_Helper       $current_page_helper The current page helper.
	 * @param Options_Helper            $options_helper      The options helper.
	 * @param Capability_Helper         $capability_helper   The capability helper.
	 */
	public function __construct(
		Yoast_Notification_Center $notification_center,
		Notification_Helper $notification_helper,
		Current_Page_Helper $current_page_helper,
		Options_Helper $options_helper,
		Capability_Helper $capability_helper
	) {
		$this->notification_center = $notification_center;
		$this->notification_helper = $notification_helper;
		$this->current_page_helper = $current_page_helper;
		$this->options_helper      = $options_helper;
		$this->capability_helper   = $capability_helper;
		$this->presenter           = new Search_Engines_Discouraged_Presenter();
	}

	/**
	 * Initializes the integration.
	 *
	 * On admin_init, it is checked whether the notification about search engines being discouraged should be shown.
	 * On admin_notices, the notice about the search engines being discouraged will be shown when necessary.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'manage_search_engines_discouraged_notification' ] );

		/*
		 * The `admin_notices` hook fires on single site admin pages vs.
		 * `network_admin_notices` which fires on multisite admin pages and
		 * `user_admin_notices` which fires on multisite user admin pages.
		 */
		\add_action( 'admin_notices', [ $this, 'maybe_show_search_engines_discouraged_notice' ] );
	}

	/**
	 * Manage the search engines discouraged notification.
	 *
	 * Shows the notification if needed and deletes it if needed.
	 *
	 * @return void
	 */
	public function manage_search_engines_discouraged_notification() {
		if ( ! $this->should_show_search_engines_discouraged_notification() ) {
			$this->remove_search_engines_discouraged_notification_if_exists();
		}
		else {
			$this->maybe_add_search_engines_discouraged_notification();
		}
	}

	/**
	 * Show the search engine discouraged notice when needed.
	 *
	 * @return void
	 */
	public function maybe_show_search_engines_discouraged_notice() {
		if ( ! $this->should_show_search_engines_discouraged_notice() ) {
			return;
		}
		$this->show_search_engines_discouraged_notice();
	}

	/**
	 * Whether the search engines discouraged notification should be shown.
	 *
	 * @return bool
	 */
	protected function should_show_search_engines_discouraged_notification() {
		return $this->search_engines_are_discouraged() && $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false;
	}

	/**
	 * Remove the search engines discouraged notification if it exists.
	 *
	 * @return void
	 */
	protected function remove_search_engines_discouraged_notification_if_exists() {
		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}

	/**
	 * Add the search engines discouraged notification if it does not exist yet.
	 *
	 * @return void
	 */
	protected function maybe_add_search_engines_discouraged_notification() {
		if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) {
			$notification = $this->notification();
			$this->notification_helper->restore_notification( $notification );
			$this->notification_center->add_notification( $notification );
		}
	}

	/**
	 * Checks whether search engines are discouraged from indexing the site.
	 *
	 * @return bool Whether search engines are discouraged from indexing the site.
	 */
	protected function search_engines_are_discouraged() {
		return (string) \get_option( 'blog_public' ) === '0';
	}

	/**
	 * Whether the search engines notice should be shown.
	 *
	 * @return bool
	 */
	protected function should_show_search_engines_discouraged_notice() {
		$pages_to_show_notice = [
			'index.php',
			'plugins.php',
			'update-core.php',
		];

		return (
			$this->search_engines_are_discouraged()
			&& $this->capability_helper->current_user_can( 'manage_options' )
			&& $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false
			&& (
				$this->current_page_helper->is_yoast_seo_page()
				|| \in_array( $this->current_page_helper->get_current_admin_page(), $pages_to_show_notice, true )
			)
			&& $this->current_page_helper->get_current_yoast_seo_page() !== 'wpseo_dashboard'
		);
	}

	/**
	 * Show the search engines discouraged notice.
	 *
	 * @return void
	 */
	protected function show_search_engines_discouraged_notice() {
		\printf(
			'<div id="robotsmessage" class="notice notice-error">%1$s</div>',
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe.
			$this->presenter->present()
		);
	}

	/**
	 * Returns an instance of the notification.
	 *
	 * @return Yoast_Notification The notification to show.
	 */
	protected function notification() {
		return new Yoast_Notification(
			$this->presenter->present(),
			[
				'type'         => Yoast_Notification::ERROR,
				'id'           => self::NOTIFICATION_ID,
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 1,
			]
		);
	}
}
watchers/indexable-taxonomy-change-watcher.php000066600000013705151130361070015571 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast_Notification;
use Yoast_Notification_Center;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Taxonomy watcher.
 *
 * Responds to changes in taxonomies public availability.
 */
class Indexable_Taxonomy_Change_Watcher implements Integration_Interface {

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * Holds the Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	private $options;

	/**
	 * Holds the Taxonomy_Helper instance.
	 *
	 * @var Taxonomy_Helper
	 */
	private $taxonomy_helper;

	/**
	 * The notifications center.
	 *
	 * @var Yoast_Notification_Center
	 */
	private $notification_center;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Not_Admin_Ajax_Conditional::class, Admin_Conditional::class, Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Taxonomy_Change_Watcher constructor.
	 *
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param Options_Helper            $options             The options helper.
	 * @param Taxonomy_Helper           $taxonomy_helper     The taxonomy helper.
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct(
		Indexing_Helper $indexing_helper,
		Options_Helper $options,
		Taxonomy_Helper $taxonomy_helper,
		Yoast_Notification_Center $notification_center
	) {
		$this->indexing_helper     = $indexing_helper;
		$this->options             = $options;
		$this->taxonomy_helper     = $taxonomy_helper;
		$this->notification_center = $notification_center;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'check_taxonomy_public_availability' ] );
	}

	/**
	 * Checks if one or more taxonomies change visibility.
	 *
	 * @return void
	 */
	public function check_taxonomy_public_availability() {

		// We have to make sure this is just a plain http request, no ajax/REST.
		if ( \wp_is_json_request() ) {
			return;
		}

		$public_taxonomies            = \array_keys( $this->taxonomy_helper->get_public_taxonomies() );
		$last_known_public_taxonomies = $this->options->get( 'last_known_public_taxonomies', [] );

		// Initializing the option on the first run.
		if ( empty( $last_known_public_taxonomies ) ) {
			$this->options->set( 'last_known_public_taxonomies', $public_taxonomies );
			return;
		}

		// We look for new public taxonomies.
		$newly_made_public_taxonomies = \array_diff( $public_taxonomies, $last_known_public_taxonomies );
		// We look fortaxonomies that from public have been made private.
		$newly_made_non_public_taxonomies = \array_diff( $last_known_public_taxonomies, $public_taxonomies );

		// Nothing to be done if no changes has been made to taxonomies.
		if ( empty( $newly_made_public_taxonomies ) && ( empty( $newly_made_non_public_taxonomies ) ) ) {
			return;
		}

		// Update the list of last known public taxonomies in the database.
		$this->options->set( 'last_known_public_taxonomies', $public_taxonomies );

		// There are new taxonomies that have been made public.
		if ( ! empty( $newly_made_public_taxonomies ) ) {

			// Force a notification requesting to start the SEO data optimization.
			\delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT );

			$this->indexing_helper->set_reason( Indexing_Reasons::REASON_TAXONOMY_MADE_PUBLIC );

			$this->maybe_add_notification();
		}

		// There are taxonomies that have been made private.
		if ( ! empty( $newly_made_non_public_taxonomies ) ) {
			// Schedule a cron job to remove all the terms whose taxonomy has been made private.
			if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
				if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
					\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK );
					\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
				}
			}
		}
	}

	/**
	 * Decides if a notification should be added in the notification center.
	 *
	 * @return void
	 */
	private function maybe_add_notification() {
		$notification = $this->notification_center->get_notification_by_id( 'taxonomies-made-public' );
		if ( is_null( $notification ) ) {
			$this->add_notification();
		}
	}

	/**
	 * Adds a notification to be shown on the next page request since posts are updated in an ajax request.
	 *
	 * @return void
	 */
	private function add_notification() {
		$message = sprintf(
			/* translators: 1: Opening tag of the link to the Search appearance settings page, 2: Link closing tag. */
			\esc_html__( 'It looks like you\'ve added a new taxonomy to your website. We recommend that you review your %1$sSearch appearance settings%2$s.', 'wordpress-seo' ),
			'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_titles#top#taxonomies' ) ) . '">',
			'</a>'
		);

		$notification = new Yoast_Notification(
			$message,
			[
				'type'         => Yoast_Notification::WARNING,
				'id'           => 'taxonomies-made-public',
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 0.8,
			]
		);

		$this->notification_center->add_notification( $notification );
	}
}
watchers/indexable-permalink-watcher.php000066600000017305151130361070014452 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * WordPress Permalink structure watcher.
 *
 * Handles updates to the permalink_structure for the Indexables table.
 */
class Indexable_Permalink_Watcher implements Integration_Interface {

	/**
	 * Represents the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The taxonomy helper.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Permalink_Watcher constructor.
	 *
	 * @param Post_Type_Helper $post_type       The post type helper.
	 * @param Options_Helper   $options         The options helper.
	 * @param Indexable_Helper $indexable       The indexable helper.
	 * @param Taxonomy_Helper  $taxonomy_helper The taxonomy helper.
	 */
	public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable, Taxonomy_Helper $taxonomy_helper ) {
		$this->post_type        = $post_type;
		$this->options_helper   = $options;
		$this->indexable_helper = $indexable;
		$this->taxonomy_helper  = $taxonomy_helper;

		$this->schedule_cron();
	}

	/**
	 * Registers the hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_permalink_structure', [ $this, 'reset_permalinks' ] );
		\add_action( 'update_option_category_base', [ $this, 'reset_permalinks_term' ], 10, 3 );
		\add_action( 'update_option_tag_base', [ $this, 'reset_permalinks_term' ], 10, 3 );
		\add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] );
		\add_action( 'wpseo_deactivate', [ $this, 'unschedule_cron' ] );
	}

	/**
	 * Resets the permalinks for everything that is related to the permalink structure.
	 */
	public function reset_permalinks() {

		$post_types = $this->get_post_types();
		foreach ( $post_types as $post_type ) {
			$this->reset_permalinks_post_type( $post_type );
		}

		$taxonomies = $this->get_taxonomies_for_post_types( $post_types );
		foreach ( $taxonomies as $taxonomy ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy );
		}

		$this->indexable_helper->reset_permalink_indexables( 'user' );
		$this->indexable_helper->reset_permalink_indexables( 'date-archive' );
		$this->indexable_helper->reset_permalink_indexables( 'system-page' );

		// Always update `permalink_structure` in the wpseo option.
		$this->options_helper->set( 'permalink_structure', \get_option( 'permalink_structure' ) );
	}

	/**
	 * Resets the permalink for the given post type.
	 *
	 * @param string $post_type The post type to reset.
	 */
	public function reset_permalinks_post_type( $post_type ) {
		$this->indexable_helper->reset_permalink_indexables( 'post', $post_type );
		$this->indexable_helper->reset_permalink_indexables( 'post-type-archive', $post_type );
	}

	/**
	 * Resets the term indexables when the base has been changed.
	 *
	 * @param string $old_value Unused. The old option value.
	 * @param string $new_value Unused. The new option value.
	 * @param string $type      The option name.
	 */
	public function reset_permalinks_term( $old_value, $new_value, $type ) {
		$subtype = $type;

		$reason = Indexing_Reasons::REASON_PERMALINK_SETTINGS;

		// When the subtype contains _base, just strip it.
		if ( \strstr( $subtype, '_base' ) ) {
			$subtype = \substr( $type, 0, -5 );
		}

		if ( $subtype === 'tag' ) {
			$subtype = 'post_tag';
			$reason  = Indexing_Reasons::REASON_TAG_BASE_PREFIX;
		}

		if ( $subtype === 'category' ) {
			$reason = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX;
		}

		$this->indexable_helper->reset_permalink_indexables( 'term', $subtype, $reason );
	}

	/**
	 * Resets the permalink indexables automatically, if necessary.
	 *
	 * @return bool Whether the reset request ran.
	 */
	public function force_reset_permalinks() {
		if ( \get_option( 'tag_base' ) !== $this->options_helper->get( 'tag_base_url' ) ) {
			$this->reset_permalinks_term( null, null, 'tag_base' );
			$this->options_helper->set( 'tag_base_url', \get_option( 'tag_base' ) );
		}
		if ( \get_option( 'category_base' ) !== $this->options_helper->get( 'category_base_url' ) ) {
			$this->reset_permalinks_term( null, null, 'category_base' );
			$this->options_helper->set( 'category_base_url', \get_option( 'category_base' ) );
		}

		if ( $this->should_reset_permalinks() ) {
			$this->reset_permalinks();

			return true;
		}

		$this->reset_altered_custom_taxonomies();

		return true;
	}

	/**
	 * Checks whether the permalinks should be reset after `permalink_structure` has changed.
	 *
	 * @return bool Whether the permalinks should be reset.
	 */
	public function should_reset_permalinks() {
		return \get_option( 'permalink_structure' ) !== $this->options_helper->get( 'permalink_structure' );
	}

	/**
	 * Resets custom taxonomies if their slugs have changed.
	 *
	 * @return void
	 */
	public function reset_altered_custom_taxonomies() {
		$taxonomies            = $this->taxonomy_helper->get_custom_taxonomies();
		$custom_taxonomy_bases = $this->options_helper->get( 'custom_taxonomy_slugs', [] );
		$new_taxonomy_bases    = [];

		foreach ( $taxonomies as $taxonomy ) {
			$taxonomy_slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy );

			$new_taxonomy_bases[ $taxonomy ] = $taxonomy_slug;

			if ( ! \array_key_exists( $taxonomy, $custom_taxonomy_bases ) ) {
				continue;
			}

			if ( $taxonomy_slug !== $custom_taxonomy_bases[ $taxonomy ] ) {
				$this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy );
			}
		}

		$this->options_helper->set( 'custom_taxonomy_slugs', $new_taxonomy_bases );
	}

	/**
	 * Retrieves a list with the public post types.
	 *
	 * @return array The post types.
	 */
	protected function get_post_types() {
		/**
		 * Filter: Gives the possibility to filter out post types.
		 *
		 * @param array $post_types The post type names.
		 *
		 * @return array The post types.
		 */
		$post_types = \apply_filters( 'wpseo_post_types_reset_permalinks', $this->post_type->get_public_post_types() );

		return $post_types;
	}

	/**
	 * Retrieves the taxonomies that belongs to the public post types.
	 *
	 * @param array $post_types The post types to get taxonomies for.
	 *
	 * @return array The retrieved taxonomies.
	 */
	protected function get_taxonomies_for_post_types( $post_types ) {
		$taxonomies = [];
		foreach ( $post_types as $post_type ) {
			$taxonomies[] = \get_object_taxonomies( $post_type, 'names' );
		}

		$taxonomies = \array_merge( [], ...$taxonomies );
		$taxonomies = \array_unique( $taxonomies );

		return $taxonomies;
	}

	/**
	 * Schedules the WP-Cron job to check the permalink_structure status.
	 *
	 * @return void
	 */
	protected function schedule_cron() {
		if ( \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) {
			return;
		}

		\wp_schedule_event( \time(), 'daily', 'wpseo_permalink_structure_check' );
	}

	/**
	 * Unschedules the WP-Cron job to check the permalink_structure status.
	 *
	 * @return void
	 */
	public function unschedule_cron() {
		if ( ! \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) {
			return;
		}

		\wp_clear_scheduled_hook( 'wpseo_permalink_structure_check' );
	}
}
watchers/auto-update-watcher.php000066600000002763151130361070012771 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;

/**
 * Shows a notification for users who have WordPress auto updates enabled but not Yoast SEO auto updates.
 */
class Auto_Update_Watcher implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The notification ID.
	 */
	const NOTIFICATION_ID = 'wpseo-auto-update';

	/**
	 * The Yoast notification center.
	 *
	 * @var Yoast_Notification_Center
	 */
	protected $notification_center;

	/**
	 * Auto_Update constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct( Yoast_Notification_Center $notification_center ) {
		$this->notification_center = $notification_center;
	}

	/**
	 * Initializes the integration.
	 *
	 * On admin_init, it is checked whether the notification to auto-update Yoast SEO needs to be shown or removed.
	 * This is also done when major WP core updates are being enabled or disabled,
	 * and when automatic updates for Yoast SEO are being enabled or disabled.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'remove_notification' ] );
	}

	/**
	 * Removes the notification from the notification center, if it exists.
	 *
	 * @return void
	 */
	public function remove_notification() {
		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}
}
watchers/indexable-post-watcher.php000066600000022254151130361070013454 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Exception;
use WP_Post;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Loggers\Logger;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use YoastSEO_Vendor\Psr\Log\LogLevel;

/**
 * WordPress Post watcher.
 *
 * Fills the Indexable according to Post data.
 */
class Indexable_Post_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * The indexable hierarchy repository.
	 *
	 * @var Indexable_Hierarchy_Repository
	 */
	private $hierarchy_repository;

	/**
	 * The link builder.
	 *
	 * @var Indexable_Link_Builder
	 */
	protected $link_builder;

	/**
	 * The author archive helper.
	 *
	 * @var Author_Archive_Helper
	 */
	private $author_archive;

	/**
	 * Holds the Post_Helper instance.
	 *
	 * @var Post_Helper
	 */
	private $post;

	/**
	 * Holds the logger.
	 *
	 * @var Logger
	 */
	protected $logger;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Post_Watcher constructor.
	 *
	 * @param Indexable_Repository           $repository           The repository to use.
	 * @param Indexable_Builder              $builder              The post builder to use.
	 * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository to use.
	 * @param Indexable_Link_Builder         $link_builder         The link builder.
	 * @param Author_Archive_Helper          $author_archive       The author archive helper.
	 * @param Post_Helper                    $post                 The post helper.
	 * @param Logger                         $logger               The logger.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Indexable_Hierarchy_Repository $hierarchy_repository,
		Indexable_Link_Builder $link_builder,
		Author_Archive_Helper $author_archive,
		Post_Helper $post,
		Logger $logger
	) {
		$this->repository           = $repository;
		$this->builder              = $builder;
		$this->hierarchy_repository = $hierarchy_repository;
		$this->link_builder         = $link_builder;
		$this->author_archive       = $author_archive;
		$this->post                 = $post;
		$this->logger               = $logger;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'wp_insert_post', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'delete_post', [ $this, 'delete_indexable' ] );

		\add_action( 'edit_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'add_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'delete_attachment', [ $this, 'delete_indexable' ] );
	}

	/**
	 * Deletes the meta when a post is deleted.
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function delete_indexable( $post_id ) {
		$indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );

		// Only interested in post indexables.
		if ( ! $indexable || $indexable->object_type !== 'post' ) {
			return;
		}

		$this->update_relations( $this->post->get_post( $post_id ) );

		$this->update_has_public_posts( $indexable );

		$this->hierarchy_repository->clear_ancestors( $indexable->id );
		$this->link_builder->delete( $indexable );
		$indexable->delete();
	}

	/**
	 * Updates the relations when the post indexable is built.
	 *
	 * @param Indexable $indexable The indexable.
	 * @param WP_Post   $post      The post.
	 */
	public function updated_indexable( $indexable, $post ) {
		// Only interested in post indexables.
		if ( $indexable->object_type !== 'post' ) {
			return;
		}

		if ( \is_a( $post, Indexable::class ) ) {
			\_deprecated_argument( __FUNCTION__, '17.7', 'The $old_indexable argument has been deprecated.' );
			$post = $this->post->get_post( $indexable->object_id );
		}

		$this->update_relations( $post );

		$indexable->save();
	}

	/**
	 * Saves post meta.
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function build_indexable( $post_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->is_multisite_and_switched() ) {
			return;
		}

		try {
			$indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );
			$indexable = $this->builder->build_for_id_and_type( $post_id, 'post', $indexable );

			$post = $this->post->get_post( $post_id );

			/*
			 * Update whether an author has public posts.
			 * For example this post could be set to Draft or Private,
			 * which can influence if its author has any public posts at all.
			 */
			if ( $indexable ) {
				$this->update_has_public_posts( $indexable );
			}

			// Build links for this post.
			if ( $post && $indexable && \in_array( $post->post_status, $this->post->get_public_post_statuses(), true ) ) {
				$this->link_builder->build( $indexable, $post->post_content );
				// Save indexable to persist the updated link count.
				$indexable->save();
				$this->updated_indexable( $indexable, $post );
			}
		} catch ( Exception $exception ) {
			$this->logger->log( LogLevel::ERROR, $exception->getMessage() );
		}
	}

	/**
	 * Updates the has_public_posts when the post indexable is built.
	 *
	 * @param Indexable $indexable The indexable to check.
	 */
	protected function update_has_public_posts( $indexable ) {
		// Update the author indexable's has public posts value.
		try {
			$author_indexable = $this->repository->find_by_id_and_type( $indexable->author_id, 'user' );
			if ( $author_indexable ) {
				$author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id );
				$author_indexable->save();

				$this->reschedule_cleanup_if_author_has_no_posts( $author_indexable );
			}
		} catch ( Exception $exception ) {
			$this->logger->log( LogLevel::ERROR, $exception->getMessage() );
		}

		// Update possible attachment's has public posts value.
		$this->post->update_has_public_posts_on_attachments( $indexable->object_id, $indexable->is_public );
	}

	/**
	 * Reschedule indexable cleanup if the author does not have any public posts.
	 * This should remove the author from the indexable table, since we do not
	 * want to store authors without public facing posts in the table.
	 *
	 * @param Indexable $author_indexable The author indexable.
	 *
	 * @return void
	 */
	protected function reschedule_cleanup_if_author_has_no_posts( $author_indexable ) {
		if ( $author_indexable->has_public_posts === false ) {
			$cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK );
			if ( $cleanup_not_yet_scheduled ) {
				\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
			}
		}
	}

	/**
	 * Updates the relations on post save or post status change.
	 *
	 * @param WP_Post $post The post that has been updated.
	 */
	protected function update_relations( $post ) {
		$related_indexables = $this->get_related_indexables( $post );

		foreach ( $related_indexables as $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, $post->post_modified_gmt );
			$indexable->save();
		}
	}

	/**
	 * Retrieves the related indexables for given post.
	 *
	 * @param WP_Post $post The post to get the indexables for.
	 *
	 * @return Indexable[] The indexables.
	 */
	protected function get_related_indexables( $post ) {
		/**
		 * The related indexables.
		 *
		 * @var Indexable[] $related_indexables .
		 */
		$related_indexables   = [];
		$related_indexables[] = $this->repository->find_by_id_and_type( $post->post_author, 'user', false );
		$related_indexables[] = $this->repository->find_for_post_type_archive( $post->post_type, false );
		$related_indexables[] = $this->repository->find_for_home_page( false );

		$taxonomies = \get_post_taxonomies( $post->ID );
		$taxonomies = \array_filter( $taxonomies, 'is_taxonomy_viewable' );
		$term_ids   = [];
		foreach ( $taxonomies as $taxonomy ) {
			$terms = \get_the_terms( $post->ID, $taxonomy );

			if ( empty( $terms ) || \is_wp_error( $terms ) ) {
				continue;
			}

			$term_ids = \array_merge( $term_ids, \wp_list_pluck( $terms, 'term_id' ) );
		}
		$related_indexables = \array_merge(
			$related_indexables,
			$this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false )
		);

		return \array_filter( $related_indexables );
	}

	/**
	 * Tests if the site is multisite and switched.
	 *
	 * @return bool True when the site is multisite and switched
	 */
	protected function is_multisite_and_switched() {
		return \is_multisite() && \ms_is_switched();
	}
}
watchers/indexable-homeurl-watcher.php000066600000005410151130361070014135 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_CLI;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Home url option watcher.
 *
 * Handles updates to the home URL option for the Indexables table.
 */
class Indexable_HomeUrl_Watcher implements Integration_Interface {

	/**
	 * Represents the options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_HomeUrl_Watcher constructor.
	 *
	 * @param Post_Type_Helper $post_type The post type helper.
	 * @param Options_Helper   $options   The options helper.
	 * @param Indexable_Helper $indexable The indexable helper.
	 */
	public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable ) {
		$this->post_type        = $post_type;
		$this->options_helper   = $options;
		$this->indexable_helper = $indexable;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_home', [ $this, 'reset_permalinks' ] );
		\add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] );
	}

	/**
	 * Resets the permalinks for everything that is related to the permalink structure.
	 *
	 * @return void
	 */
	public function reset_permalinks() {
		$this->indexable_helper->reset_permalink_indexables( null, null, Indexing_Reasons::REASON_HOME_URL_OPTION );

		// Reset the home_url option.
		$this->options_helper->set( 'home_url', \get_home_url() );
	}

	/**
	 * Resets the permalink indexables automatically, if necessary.
	 *
	 * @return bool Whether the request ran.
	 */
	public function force_reset_permalinks() {
		if ( $this->should_reset_permalinks() ) {
			$this->reset_permalinks();

			if ( \defined( 'WP_CLI' ) && \WP_CLI ) {
				WP_CLI::success( \__( 'All permalinks were successfully reset', 'wordpress-seo' ) );
			}

			return true;
		}

		return false;
	}

	/**
	 * Checks whether permalinks should be reset.
	 *
	 * @return bool Whether the permalinks should be reset.
	 */
	public function should_reset_permalinks() {
		return \get_home_url() !== $this->options_helper->get( 'home_url' );
	}
}
watchers/option-wpseo-watcher.php000066600000007560151130361070013204 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Wordproof_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Watcher for the wpseo option.
 *
 * Represents the option wpseo watcher.
 */
class Option_Wpseo_Watcher implements Integration_Interface {

	use No_Conditionals;

	/**
	 * Holds the WordProof helper instance.
	 *
	 * @var Wordproof_Helper
	 */
	protected $wordproof;

	/**
	 * The constructor for a watcher of WPSEO options.
	 *
	 * @param Wordproof_Helper $wordproof The WordProof helper instance.
	 */
	public function __construct( Wordproof_Helper $wordproof ) {
		$this->wordproof = $wordproof;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo', [ $this, 'check_semrush_option_disabled' ], 10, 2 );
		\add_action( 'update_option_wpseo', [ $this, 'check_wincher_option_disabled' ], 10, 2 );
		\add_action( 'update_option_wpseo', [ $this, 'check_wordproof_option_disabled' ], 10, 2 );
	}

	/**
	 * Checks if the SEMrush integration is disabled; if so, deletes the tokens.
	 *
	 * We delete the tokens if the SEMrush integration is disabled, no matter if
	 * the value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the SEMrush tokens have been deleted or not.
	 */
	public function check_semrush_option_disabled( $old_value, $new_value ) {
		return $this->check_token_option_disabled( 'semrush_integration_active', 'semrush_tokens', $new_value );
	}

	/**
	 * Checks if the Wincher integration is disabled; if so, deletes the tokens
	 * and website id.
	 *
	 * We delete them if the Wincher integration is disabled, no matter if the
	 * value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the Wincher tokens have been deleted or not.
	 */
	public function check_wincher_option_disabled( $old_value, $new_value ) {
		$disabled = $this->check_token_option_disabled( 'wincher_integration_active', 'wincher_tokens', $new_value );
		if ( $disabled ) {
			\YoastSEO()->helpers->options->set( 'wincher_website_id', '' );
		}
		return $disabled;
	}

	/**
	 * Checks if the WordProof integration is disabled; if so, deletes the tokens
	 *
	 * We delete them if the WordProof integration is disabled, no matter if the
	 * value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the WordProof tokens have been deleted or not.
	 */
	public function check_wordproof_option_disabled( $old_value, $new_value ) {
		$disabled = $this->check_token_option_disabled( 'wordproof_integration_active', 'wordproof_tokens', $new_value );
		if ( $disabled ) {
			$this->wordproof->remove_site_options();
		}
		return $disabled;
	}

	/**
	 * Checks if the passed integration is disabled; if so, deletes the tokens.
	 *
	 * We delete the tokens if the integration is disabled, no matter if
	 * the value has actually changed or not.
	 *
	 * @param string $integration_option The intergration option name.
	 * @param string $target_option      The target option to remove the tokens from.
	 * @param array  $new_value          The new value of the option.
	 *
	 * @return bool Whether the tokens have been deleted or not.
	 */
	protected function check_token_option_disabled( $integration_option, $target_option, $new_value ) {
		if ( \array_key_exists( $integration_option, $new_value ) && $new_value[ $integration_option ] === false ) {
			\YoastSEO()->helpers->options->set( $target_option, [] );
			return true;
		}
		return false;
	}
}
watchers/indexable-term-watcher.php000066600000006413151130361070013435 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Site_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watches Terms/Taxonomies to fill the related Indexable.
 */
class Indexable_Term_Watcher implements Integration_Interface {

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * The link builder.
	 *
	 * @var Indexable_Link_Builder
	 */
	protected $link_builder;

	/**
	 * Represents the site helper.
	 *
	 * @var Site_Helper
	 */
	protected $site;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Indexable_Term_Watcher constructor.
	 *
	 * @param Indexable_Repository   $repository   The repository to use.
	 * @param Indexable_Builder      $builder      The post builder to use.
	 * @param Indexable_Link_Builder $link_builder The lint builder to use.
	 * @param Site_Helper            $site         The site helper.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Indexable_Link_Builder $link_builder,
		Site_Helper $site
	) {
		$this->repository   = $repository;
		$this->builder      = $builder;
		$this->link_builder = $link_builder;
		$this->site         = $site;
	}

	/**
	 * Registers the hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'created_term', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'edited_term', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'delete_term', [ $this, 'delete_indexable' ], \PHP_INT_MAX );
	}

	/**
	 * Deletes a term from the index.
	 *
	 * @param int $term_id The Term ID to delete.
	 *
	 * @return void
	 */
	public function delete_indexable( $term_id ) {
		$indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false );

		if ( ! $indexable ) {
			return;
		}

		$indexable->delete();
	}

	/**
	 * Update the taxonomy meta data on save.
	 *
	 * @param int $term_id ID of the term to save data for.
	 *
	 * @return void
	 */
	public function build_indexable( $term_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->site->is_multisite_and_switched() ) {
			return;
		}

		$term = \get_term( $term_id );

		if ( $term === null || \is_wp_error( $term ) ) {
			return;
		}

		if ( ! \is_taxonomy_viewable( $term->taxonomy ) ) {
			return;
		}

		$indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false );

		// If we haven't found an existing indexable, create it. Otherwise update it.
		$indexable = $this->builder->build_for_id_and_type( $term_id, 'term', $indexable );

		if ( ! $indexable ) {
			return;
		}

		// Update links.
		$this->link_builder->build( $indexable, $term->description );

		$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
		$indexable->save();
	}
}
xmlrpc.php000066600000002344151130361070006566 0ustar00<?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\XMLRPC_Conditional;

/**
 * Noindexes the xmlrpc.php file and all ways to request it.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Known false positive with acronyms. Fix expected in YoastCS 3.x.
 */
class XMLRPC implements Integration_Interface {

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case when the current request is an XML-RPC request.
	 *
	 * @return array The conditionals based on which this class should be loaded.
	 */
	public static function get_conditionals() {
		return [ XMLRPC_Conditional::class ];
	}

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'xmlrpc_methods', [ $this, 'robots_header' ] );
	}

	/**
	 * Sets a noindex, follow x-robots-tag header on all XMLRPC requests.
	 *
	 * @codeCoverageIgnore Basically impossible to test from the command line.
	 *
	 * @param array $methods The methods.
	 *
	 * @return array The methods.
	 */
	public function robots_header( $methods ) {
		if ( \headers_sent() === false ) {
			\header( 'X-Robots-Tag: noindex, follow', true );
		}

		return $methods;
	}
}
admin/link-count-notification-integration.php000066600000004044151136633520015450 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;

/**
 * Link_Count_Notification_Integration class.
 *
 * @deprecated 15.1
 * @codeCoverageIgnore
 */
class Link_Count_Notification_Integration implements Integration_Interface {

	/**
	 * The ID of the link indexing notification.
	 *
	 * @var string
	 */
	const NOTIFICATION_ID = 'wpseo-reindex-links';

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );

		return [ Admin_Conditional::class ];
	}

	/**
	 * Link_Count_Notification_Integration constructor.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @param Yoast_Notification_Center $notification_center       The Yoast notification center.
	 * @param Post_Link_Indexing_Action $post_link_indexing_action The post link indexing action.
	 */
	public function __construct(
		Yoast_Notification_Center $notification_center,
		Post_Link_Indexing_Action $post_link_indexing_action
	) {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Initializes the integration.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 */
	public function register_hooks() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Removes the notification when it is set and the amount of unindexed items is lower than the threshold.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 */
	public function cleanup_notification() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Adds the notification when it isn't set already and the amount of unindexed items is greater than the set
	 * threshold.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 */
	public function manage_notification() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}
}
admin/link-count-tools-integration.php000066600000005133151136633520014122 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action;
use Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Link_Count_Tools_Integration class.
 *
 * @deprecated 15.1
 * @codeCoverageIgnore
 */
class Link_Count_Tools_Integration implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );

		return [
			Admin_Conditional::class,
			Yoast_Admin_And_Dashboard_Conditional::class,
			Migrations_Conditional::class,
		];
	}

	/**
	 * Constructor.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @param Post_Link_Indexing_Action $post_link_indexing_action The post link indexing action.
	 * @param Term_Link_Indexing_Action $term_link_indexing_action The term link indexing action.
	 * @param WPSEO_Admin_Asset_Manager $asset_manager             The asset manager.
	 */
	public function __construct(
		Post_Link_Indexing_Action $post_link_indexing_action,
		Term_Link_Indexing_Action $term_link_indexing_action,
		WPSEO_Admin_Asset_Manager $asset_manager
	) {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Initializes the integration.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 */
	public function register_hooks() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Enqueues all required assets.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the tools list item.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_tools_overview_item() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the link count indexing modal.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_modal() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return int
	 */
	protected function get_total_unindexed() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );

		return 0;
	}
}
admin/indexation-integration.php000066600000011471151136633520013045 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Indexation_Integration class.
 *
 * @deprecated 15.1
 * @codeCoverageIgnore
 */
class Indexation_Integration implements Integration_Interface {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );

		return [
			Admin_Conditional::class,
			Yoast_Admin_And_Dashboard_Conditional::class,
			Migrations_Conditional::class,
		];
	}

	/**
	 * Indexation_Integration constructor.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @param Indexable_Post_Indexation_Action              $post_indexation              The post indexation action.
	 * @param Indexable_Term_Indexation_Action              $term_indexation              The term indexation action.
	 * @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation The archive indexation action.
	 * @param Indexable_General_Indexation_Action           $general_indexation           The general indexation action.
	 * @param Indexable_Indexing_Complete_Action            $complete_indexation_action   The complete indexation action.
	 * @param Options_Helper                                $options_helper               The options helper.
	 * @param WPSEO_Admin_Asset_Manager                     $asset_manager                The admin asset manager.
	 * @param Yoast_Tools_Page_Conditional                  $yoast_tools_page_conditional The Yoast tools page conditional.
	 * @param Indexable_Helper                              $indexable_helper             The indexable helper.
	 */
	public function __construct(
		Indexable_Post_Indexation_Action $post_indexation,
		Indexable_Term_Indexation_Action $term_indexation,
		Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation,
		Indexable_General_Indexation_Action $general_indexation,
		Indexable_Indexing_Complete_Action $complete_indexation_action,
		Options_Helper $options_helper,
		WPSEO_Admin_Asset_Manager $asset_manager,
		Yoast_Tools_Page_Conditional $yoast_tools_page_conditional,
		Indexable_Helper $indexable_helper
	) {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Initializes the integration.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 */
	public function register_hooks() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Enqueues the required scripts.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function enqueue_scripts() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the indexation warning.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_indexation_warning() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the indexation modal.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_indexation_modal() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the indexation list item.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_indexation_list_item() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Renders the indexation permalink warning.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function render_indexation_permalink_warning() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Runs a single indexation pass of each indexation action. Intended for use as a shutdown function.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function shutdown_indexation() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );
	}

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @deprecated 15.1
	 * @codeCoverageIgnore
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_total_unindexed() {
		\_deprecated_function( __METHOD__, 'WPSEO 15.1' );

		return 0;
	}
}
admin/ryte-integration.php000066600000006522151136633520011667 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Admin;

use wfConfig;
use WP_Error;
use WPSEO_Ryte_Option;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Handles the request for getting the Ryte status.
 *
 * @deprecated 19.6
 * @codeCoverageIgnore
 */
class Ryte_Integration implements Integration_Interface {

	/**
	 * Constructor.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @param Options_Helper $options_helper The options helper object used to determine if Ryte is active or not.
	 */
	public function __construct( Options_Helper $options_helper ) {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );
	}

	/**
	 * Sets up the hooks.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function register_hooks() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * In this case: only when on an admin page.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return array The conditionals.
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class ];
	}

	/**
	 * Determines if we can use the functionality.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return bool True if this functionality can be used.
	 */
	public function is_active() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return false;
	}

	/**
	 * Hooks to run on plugin activation.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 */
	public function activate_hooks() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );
	}

	/**
	 * Determines whether to add a custom cron weekly schedule.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function maybe_add_weekly_schedule() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );
	}

	/**
	 * Adds a custom weekly cron schedule.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @param array $schedules The existing custom cron schedules.
	 *
	 * @return array Enriched list of custom cron schedules.
	 */
	public function add_weekly_schedule( $schedules ) {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return [];
	}

	/**
	 * Fetches the data from Ryte.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return bool Whether the request ran.
	 */
	public function fetch_from_ryte() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return true;
	}

	/**
	 * Retrieves the option to use.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return WPSEO_Ryte_Option The option.
	 */
	public function get_option() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return new WPSEO_Ryte_Option();
	}

	/**
	 * Sends a request to Ryte to get the indexability status.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return int The indexability status value.
	 */
	protected function request_indexability() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return -1;
	}

	/**
	 * Retrieves the Ryte API response property.
	 *
	 * @deprecated 19.6
	 * @codeCoverageIgnore
	 *
	 * @return array|WP_Error The response or WP_Error on failure.
	 */
	public function get_response() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 19.6' );

		return [];
	}
}
wincher.php000066600000000405151137605300006720 0ustar00<?php
/**
 * WPSEO plugin file.
 *
 * @package WPSEO\Admin\Views\Integrations
 *
 * @uses    Yoast_Form $yform Form object.
 */

$yform = Yoast_Form::get_instance();

// Ensure we don't accidentally reset the website ID.
$yform->hidden( 'wincher_website_id' );
.htaccess000066600000000424151144114550006347 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>admin/addon-installation/.htaccess000066600000000424151150330550013220 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>admin/.htaccess000066600000000424151150330550007434 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>watchers/.htaccess000066600000000424151150330560010165 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>alerts/.htaccess000066600000000424151150330560007637 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>third-party/.htaccess000066600000000424151150330560010614 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>front-end/.htaccess000066600000000424151150330560010241 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>blocks/.htaccess000066600000000424151150330560007622 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>