Your IP : 216.73.216.162


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

class-slug-change-watcher.php000066600000015241151130632540012217 0ustar00<?php
/**
 * WPSEO plugin file.
 *
 * @package WPSEO\Admin\Watchers
 */

/**
 * Class WPSEO_Slug_Change_Watcher.
 */
class WPSEO_Slug_Change_Watcher implements WPSEO_WordPress_Integration {

	/**
	 * Registers all hooks to WordPress.
	 *
	 * @return void
	 */
	public function register_hooks() {
		// If the current plugin is Yoast SEO Premium, stop registering.
		if ( YoastSEO()->helpers->product->is_premium() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );

		// Detect a post trash.
		add_action( 'wp_trash_post', [ $this, 'detect_post_trash' ] );

		// Detect a post delete.
		add_action( 'before_delete_post', [ $this, 'detect_post_delete' ] );

		// Detects deletion of a term.
		add_action( 'delete_term_taxonomy', [ $this, 'detect_term_delete' ] );
	}

	/**
	 * Enqueues the quick edit handler.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		global $pagenow;

		if ( ! in_array( $pagenow, [ 'edit.php', 'edit-tags.php' ], true ) ) {
			return;
		}

		$asset_manager = new WPSEO_Admin_Asset_Manager();
		$asset_manager->enqueue_script( 'quick-edit-handler' );
	}

	/**
	 * Shows an message when a post is about to get trashed.
	 *
	 * @param int $post_id The current post ID.
	 *
	 * @return void
	 */
	public function detect_post_trash( $post_id ) {
		if ( ! $this->is_post_viewable( $post_id ) ) {
			return;
		}

		/* translators: %1$s expands to the translated name of the post type. */
		$first_sentence = sprintf( __( 'You just trashed a %1$s.', 'wordpress-seo' ), $this->get_post_type_label( get_post_type( $post_id ) ) );
		$message        = $this->get_message( $first_sentence );

		$this->add_notification( $message );
	}

	/**
	 * Shows an message when a post is about to get trashed.
	 *
	 * @param int $post_id The current post ID.
	 *
	 * @return void
	 */
	public function detect_post_delete( $post_id ) {
		if ( ! $this->is_post_viewable( $post_id ) ) {
			return;
		}

		/* translators: %1$s expands to the translated name of the post type. */
		$first_sentence = sprintf( __( 'You just deleted a %1$s.', 'wordpress-seo' ), $this->get_post_type_label( get_post_type( $post_id ) ) );
		$message        = $this->get_message( $first_sentence );

		$this->add_notification( $message );
	}

	/**
	 * Shows a message when a term is about to get deleted.
	 *
	 * @param int $term_id The term ID that will be deleted.
	 *
	 * @return void
	 */
	public function detect_term_delete( $term_id ) {
		if ( ! $this->is_term_viewable( $term_id ) ) {
			return;
		}

		$first_sentence = sprintf(
			/* translators: 1: term label */
			__( 'You just deleted a %1$s.', 'wordpress-seo' ),
			$this->get_taxonomy_label_for_term( $term_id )
		);

		$message = $this->get_message( $first_sentence );

		$this->add_notification( $message );
	}

	/**
	 * Checks if the post is viewable.
	 *
	 * @param string $post_id The post id to check.
	 *
	 * @return bool Whether the post is viewable or not.
	 */
	protected function is_post_viewable( $post_id ) {
		$post_type = get_post_type( $post_id );
		if ( ! WPSEO_Post_Type::is_post_type_accessible( $post_type ) ) {
			return false;
		}

		$post_status = get_post_status( $post_id );
		if ( ! $this->check_visible_post_status( $post_status ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks if the term is viewable.
	 *
	 * @param string $term_id The term ID to check.
	 *
	 * @return bool Whether the term is viewable or not.
	 */
	protected function is_term_viewable( $term_id ) {
		$term = get_term( $term_id );

		if ( ! $term || is_wp_error( $term ) ) {
			return false;
		}

		$taxonomy = get_taxonomy( $term->taxonomy );
		if ( ! $taxonomy ) {
			return false;
		}

		return $taxonomy->publicly_queryable || $taxonomy->public;
	}

	/**
	 * Gets the taxonomy label to use for a term.
	 *
	 * @param int $term_id The term ID.
	 *
	 * @return string The taxonomy's singular label.
	 */
	protected function get_taxonomy_label_for_term( $term_id ) {
		$term     = get_term( $term_id );
		$taxonomy = get_taxonomy( $term->taxonomy );

		return $taxonomy->labels->singular_name;
	}

	/**
	 * Retrieves the singular post type label.
	 *
	 * @param string $post_type Post type to retrieve label from.
	 *
	 * @return string The singular post type name.
	 */
	protected function get_post_type_label( $post_type ) {
		$post_type_object = get_post_type_object( $post_type );

		// If the post type of this post wasn't registered default back to post.
		if ( $post_type_object === null ) {
			$post_type_object = get_post_type_object( 'post' );
		}

		return $post_type_object->labels->singular_name;
	}

	/**
	 * Checks whether the given post status is visible or not.
	 *
	 * @param string $post_status The post status to check.
	 *
	 * @return bool Whether or not the post is visible.
	 */
	protected function check_visible_post_status( $post_status ) {
		$visible_post_statuses = [
			'publish',
			'static',
			'private',
		];

		return in_array( $post_status, $visible_post_statuses, true );
	}

	/**
	 * Returns the message around changed URLs.
	 *
	 * @param string $first_sentence The first sentence of the notification.
	 *
	 * @return string The full notification.
	 */
	protected function get_message( $first_sentence ) {
		return '<h2>' . __( 'Make sure you don\'t miss out on traffic!', 'wordpress-seo' ) . '</h2>'
			. '<p>'
			. $first_sentence
			. ' ' . __( 'Search engines and other websites can still send traffic to your deleted post.', 'wordpress-seo' )
			. ' ' . __( 'You should create a redirect to ensure your visitors do not get a 404 error when they click on the no longer working URL.', 'wordpress-seo' )
			/* translators: %s expands to Yoast SEO Premium */
			. ' ' . sprintf( __( 'With %s, you can easily create such redirects.', 'wordpress-seo' ), 'Yoast SEO Premium' )
			. '</p>'
			. '<p><a class="yoast-button-upsell" href="' . WPSEO_Shortlinker::get( 'https://yoa.st/1d0' ) . '" target="_blank">'
			/* translators: %s expands to Yoast SEO Premium */
			. sprintf( __( 'Get %s', 'wordpress-seo' ), 'Yoast SEO Premium' )
			. '<span class="screen-reader-text">' . __( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
			. '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'
			. '</a></p>';
	}

	/**
	 * Adds a notification to be shown on the next page request since posts are updated in an ajax request.
	 *
	 * @param string $message The message to add to the notification.
	 *
	 * @return void
	 */
	protected function add_notification( $message ) {
		$notification = new Yoast_Notification(
			$message,
			[
				'type'           => 'notice-warning is-dismissible',
				'yoast_branding' => true,
			]
		);

		$notification_center = Yoast_Notification_Center::get();
		$notification_center->add_notification( $notification );
	}
}
primary-term-watcher.php000066600000007663151131531760011363 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_Term;
use WPSEO_Meta;
use WPSEO_Primary_Term;
use Yoast\WP\SEO\Builders\Primary_Term_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Primary_Term_Helper;
use Yoast\WP\SEO\Helpers\Site_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Primary Term watcher.
 *
 * Watches Posts to save the primary term when set.
 */
class Primary_Term_Watcher implements Integration_Interface {

	/**
	 * The primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	protected $repository;

	/**
	 * Represents the site helper.
	 *
	 * @var Site_Helper
	 */
	protected $site;

	/**
	 * The primary term helper.
	 *
	 * @var Primary_Term_Helper
	 */
	protected $primary_term;

	/**
	 * The primary term builder.
	 *
	 * @var Primary_Term_Builder
	 */
	protected $primary_term_builder;

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

	/**
	 * Primary_Term_Watcher constructor.
	 *
	 * @codeCoverageIgnore It sets dependencies.
	 *
	 * @param Primary_Term_Repository $repository           The primary term repository.
	 * @param Site_Helper             $site                 The site helper.
	 * @param Primary_Term_Helper     $primary_term         The primary term helper.
	 * @param Primary_Term_Builder    $primary_term_builder The primary term builder.
	 */
	public function __construct(
		Primary_Term_Repository $repository,
		Site_Helper $site,
		Primary_Term_Helper $primary_term,
		Primary_Term_Builder $primary_term_builder
	) {
		$this->repository           = $repository;
		$this->site                 = $site;
		$this->primary_term         = $primary_term;
		$this->primary_term_builder = $primary_term_builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'save_post', [ $this, 'save_primary_terms' ], \PHP_INT_MAX );
		\add_action( 'delete_post', [ $this, 'delete_primary_terms' ] );
	}

	/**
	 * Saves all selected primary terms.
	 *
	 * @param int $post_id Post ID to save primary terms for.
	 */
	public function save_primary_terms( $post_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->site->is_multisite_and_switched() ) {
			return;
		}

		$taxonomies = $this->primary_term->get_primary_term_taxonomies( $post_id );

		foreach ( $taxonomies as $taxonomy ) {
			$this->save_primary_term( $post_id, $taxonomy );
		}

		$this->primary_term_builder->build( $post_id );
	}

	/**
	 * Saves the primary term for a specific taxonomy.
	 *
	 * @param int     $post_id  Post ID to save primary term for.
	 * @param WP_Term $taxonomy Taxonomy to save primary term for.
	 */
	protected function save_primary_term( $post_id, $taxonomy ) {
		$primary_term = \filter_input( \INPUT_POST, WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term', \FILTER_SANITIZE_NUMBER_INT );

		// We accept an empty string here because we need to save that if no terms are selected.
		if ( $primary_term && \check_admin_referer( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce' ) !== null ) {
			$primary_term_object = new WPSEO_Primary_Term( $taxonomy->name, $post_id );
			$primary_term_object->set_primary_term( $primary_term );
		}
	}

	/**
	 * Deletes primary terms for a post.
	 *
	 * @param int $post_id The post to delete the terms of.
	 *
	 * @return void
	 */
	public function delete_primary_terms( $post_id ) {
		foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) {
			$primary_term = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy->name, false );

			if ( ! $primary_term ) {
				continue;
			}

			$primary_term->delete();
		}
	}
}
indexable-post-watcher.php000066600000022254151131531760011642 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Exception;
use WP_Post;
use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Loggers\Logger;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use YoastSEO_Vendor\Psr\Log\LogLevel;

/**
 * WordPress Post watcher.
 *
 * Fills the Indexable according to Post data.
 */
class Indexable_Post_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * The indexable hierarchy repository.
	 *
	 * @var Indexable_Hierarchy_Repository
	 */
	private $hierarchy_repository;

	/**
	 * The link builder.
	 *
	 * @var Indexable_Link_Builder
	 */
	protected $link_builder;

	/**
	 * The author archive helper.
	 *
	 * @var Author_Archive_Helper
	 */
	private $author_archive;

	/**
	 * Holds the Post_Helper instance.
	 *
	 * @var Post_Helper
	 */
	private $post;

	/**
	 * Holds the logger.
	 *
	 * @var Logger
	 */
	protected $logger;

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

	/**
	 * Indexable_Post_Watcher constructor.
	 *
	 * @param Indexable_Repository           $repository           The repository to use.
	 * @param Indexable_Builder              $builder              The post builder to use.
	 * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository to use.
	 * @param Indexable_Link_Builder         $link_builder         The link builder.
	 * @param Author_Archive_Helper          $author_archive       The author archive helper.
	 * @param Post_Helper                    $post                 The post helper.
	 * @param Logger                         $logger               The logger.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Indexable_Hierarchy_Repository $hierarchy_repository,
		Indexable_Link_Builder $link_builder,
		Author_Archive_Helper $author_archive,
		Post_Helper $post,
		Logger $logger
	) {
		$this->repository           = $repository;
		$this->builder              = $builder;
		$this->hierarchy_repository = $hierarchy_repository;
		$this->link_builder         = $link_builder;
		$this->author_archive       = $author_archive;
		$this->post                 = $post;
		$this->logger               = $logger;
	}

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

		\add_action( 'edit_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'add_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'delete_attachment', [ $this, 'delete_indexable' ] );
	}

	/**
	 * Deletes the meta when a post is deleted.
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function delete_indexable( $post_id ) {
		$indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );

		// Only interested in post indexables.
		if ( ! $indexable || $indexable->object_type !== 'post' ) {
			return;
		}

		$this->update_relations( $this->post->get_post( $post_id ) );

		$this->update_has_public_posts( $indexable );

		$this->hierarchy_repository->clear_ancestors( $indexable->id );
		$this->link_builder->delete( $indexable );
		$indexable->delete();
	}

	/**
	 * Updates the relations when the post indexable is built.
	 *
	 * @param Indexable $indexable The indexable.
	 * @param WP_Post   $post      The post.
	 */
	public function updated_indexable( $indexable, $post ) {
		// Only interested in post indexables.
		if ( $indexable->object_type !== 'post' ) {
			return;
		}

		if ( \is_a( $post, Indexable::class ) ) {
			\_deprecated_argument( __FUNCTION__, '17.7', 'The $old_indexable argument has been deprecated.' );
			$post = $this->post->get_post( $indexable->object_id );
		}

		$this->update_relations( $post );

		$indexable->save();
	}

	/**
	 * Saves post meta.
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return void
	 */
	public function build_indexable( $post_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->is_multisite_and_switched() ) {
			return;
		}

		try {
			$indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );
			$indexable = $this->builder->build_for_id_and_type( $post_id, 'post', $indexable );

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

			/*
			 * Update whether an author has public posts.
			 * For example this post could be set to Draft or Private,
			 * which can influence if its author has any public posts at all.
			 */
			if ( $indexable ) {
				$this->update_has_public_posts( $indexable );
			}

			// Build links for this post.
			if ( $post && $indexable && \in_array( $post->post_status, $this->post->get_public_post_statuses(), true ) ) {
				$this->link_builder->build( $indexable, $post->post_content );
				// Save indexable to persist the updated link count.
				$indexable->save();
				$this->updated_indexable( $indexable, $post );
			}
		} catch ( Exception $exception ) {
			$this->logger->log( LogLevel::ERROR, $exception->getMessage() );
		}
	}

	/**
	 * Updates the has_public_posts when the post indexable is built.
	 *
	 * @param Indexable $indexable The indexable to check.
	 */
	protected function update_has_public_posts( $indexable ) {
		// Update the author indexable's has public posts value.
		try {
			$author_indexable = $this->repository->find_by_id_and_type( $indexable->author_id, 'user' );
			if ( $author_indexable ) {
				$author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id );
				$author_indexable->save();

				$this->reschedule_cleanup_if_author_has_no_posts( $author_indexable );
			}
		} catch ( Exception $exception ) {
			$this->logger->log( LogLevel::ERROR, $exception->getMessage() );
		}

		// Update possible attachment's has public posts value.
		$this->post->update_has_public_posts_on_attachments( $indexable->object_id, $indexable->is_public );
	}

	/**
	 * Reschedule indexable cleanup if the author does not have any public posts.
	 * This should remove the author from the indexable table, since we do not
	 * want to store authors without public facing posts in the table.
	 *
	 * @param Indexable $author_indexable The author indexable.
	 *
	 * @return void
	 */
	protected function reschedule_cleanup_if_author_has_no_posts( $author_indexable ) {
		if ( $author_indexable->has_public_posts === false ) {
			$cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK );
			if ( $cleanup_not_yet_scheduled ) {
				\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
			}
		}
	}

	/**
	 * Updates the relations on post save or post status change.
	 *
	 * @param WP_Post $post The post that has been updated.
	 */
	protected function update_relations( $post ) {
		$related_indexables = $this->get_related_indexables( $post );

		foreach ( $related_indexables as $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, $post->post_modified_gmt );
			$indexable->save();
		}
	}

	/**
	 * Retrieves the related indexables for given post.
	 *
	 * @param WP_Post $post The post to get the indexables for.
	 *
	 * @return Indexable[] The indexables.
	 */
	protected function get_related_indexables( $post ) {
		/**
		 * The related indexables.
		 *
		 * @var Indexable[] $related_indexables .
		 */
		$related_indexables   = [];
		$related_indexables[] = $this->repository->find_by_id_and_type( $post->post_author, 'user', false );
		$related_indexables[] = $this->repository->find_for_post_type_archive( $post->post_type, false );
		$related_indexables[] = $this->repository->find_for_home_page( false );

		$taxonomies = \get_post_taxonomies( $post->ID );
		$taxonomies = \array_filter( $taxonomies, 'is_taxonomy_viewable' );
		$term_ids   = [];
		foreach ( $taxonomies as $taxonomy ) {
			$terms = \get_the_terms( $post->ID, $taxonomy );

			if ( empty( $terms ) || \is_wp_error( $terms ) ) {
				continue;
			}

			$term_ids = \array_merge( $term_ids, \wp_list_pluck( $terms, 'term_id' ) );
		}
		$related_indexables = \array_merge(
			$related_indexables,
			$this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false )
		);

		return \array_filter( $related_indexables );
	}

	/**
	 * Tests if the site is multisite and switched.
	 *
	 * @return bool True when the site is multisite and switched
	 */
	protected function is_multisite_and_switched() {
		return \is_multisite() && \ms_is_switched();
	}
}
indexable-post-type-archive-watcher.php000066600000007141151131531760014236 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Post type archive watcher to save the meta data to an Indexable.
 *
 * Watches the home page options to save the meta information when updated.
 */
class Indexable_Post_Type_Archive_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

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

	/**
	 * Indexable_Post_Type_Archive_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether or not the option has been saved.
	 */
	public function check_option( $old_value, $new_value ) {
		$relevant_keys = [ 'title-ptarchive-', 'metadesc-ptarchive-', 'bctitle-ptarchive-', 'noindex-ptarchive-' ];

		// If this is the first time saving the option, thus when value is false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return false;
		}

		$keys               = \array_unique( \array_merge( \array_keys( $old_value ), \array_keys( $new_value ) ) );
		$post_types_rebuild = [];

		foreach ( $keys as $key ) {
			$post_type = false;
			// Check if it's a key relevant to post type archives.
			foreach ( $relevant_keys as $relevant_key ) {
				if ( \strpos( $key, $relevant_key ) === 0 ) {
					$post_type = \substr( $key, \strlen( $relevant_key ) );
					break;
				}
			}

			// If it's not a relevant key or both values aren't set they haven't changed.
			if ( $post_type === false || ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if (
				! \in_array( $post_type, $post_types_rebuild, true )
				&& (
					! isset( $old_value[ $key ] )
					|| ! isset( $new_value[ $key ] )
					|| $old_value[ $key ] !== $new_value[ $key ]
				)
			) {
				$this->build_indexable( $post_type );
				$post_types_rebuild[] = $post_type;
			}
		}

		return true;
	}

	/**
	 * Saves the post type archive.
	 *
	 * @param string $post_type The post type.
	 *
	 * @return void
	 */
	public function build_indexable( $post_type ) {
		$indexable = $this->repository->find_for_post_type_archive( $post_type, false );
		$indexable = $this->builder->build_for_post_type_archive( $post_type, $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
indexable-post-meta-watcher.php000066600000005542151131531760012567 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WPSEO_Meta;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * WordPress post meta watcher.
 */
class Indexable_Post_Meta_Watcher implements Integration_Interface {

	/**
	 * The post watcher.
	 *
	 * @var Indexable_Post_Watcher
	 */
	protected $post_watcher;

	/**
	 * An array of post IDs that need to be updated.
	 *
	 * @var array
	 */
	protected $post_ids_to_update = [];

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

	/**
	 * Indexable_Postmeta_Watcher constructor.
	 *
	 * @param Indexable_Post_Watcher $post_watcher The post watcher.
	 */
	public function __construct( Indexable_Post_Watcher $post_watcher ) {
		$this->post_watcher = $post_watcher;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		// Register all posts whose meta have changed.
		\add_action( 'added_post_meta', [ $this, 'add_post_id' ], 10, 3 );
		\add_action( 'updated_post_meta', [ $this, 'add_post_id' ], 10, 3 );
		\add_action( 'deleted_post_meta', [ $this, 'add_post_id' ], 10, 3 );

		// Remove posts that get saved as they are handled by the Indexable_Post_Watcher.
		\add_action( 'wp_insert_post', [ $this, 'remove_post_id' ] );
		\add_action( 'delete_post', [ $this, 'remove_post_id' ] );
		\add_action( 'edit_attachment', [ $this, 'remove_post_id' ] );
		\add_action( 'add_attachment', [ $this, 'remove_post_id' ] );
		\add_action( 'delete_attachment', [ $this, 'remove_post_id' ] );

		// Update indexables of all registered posts.
		\register_shutdown_function( [ $this, 'update_indexables' ] );
	}

	/**
	 * Adds a post id to the array of posts to update.
	 *
	 * @param int|string $meta_id  The meta ID.
	 * @param int|string $post_id  The post ID.
	 * @param string     $meta_key The meta key.
	 *
	 * @return void
	 */
	public function add_post_id( $meta_id, $post_id, $meta_key ) {
		// Only register changes to our own meta.
		if ( \strpos( $meta_key, WPSEO_Meta::$meta_prefix ) !== 0 ) {
			return;
		}

		if ( ! \in_array( $post_id, $this->post_ids_to_update, true ) ) {
			$this->post_ids_to_update[] = (int) $post_id;
		}
	}

	/**
	 * Removes a post id from the array of posts to update.
	 *
	 * @param int|string $post_id The post ID.
	 *
	 * @return void
	 */
	public function remove_post_id( $post_id ) {
		$this->post_ids_to_update = \array_diff( $this->post_ids_to_update, [ (int) $post_id ] );
	}

	/**
	 * Updates all indexables changed during the request.
	 *
	 * @return void
	 */
	public function update_indexables() {
		foreach ( $this->post_ids_to_update as $post_id ) {
			$this->post_watcher->build_indexable( $post_id );
		}
	}
}
indexable-term-watcher.php000066600000006413151131531760011623 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Site_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watches Terms/Taxonomies to fill the related Indexable.
 */
class Indexable_Term_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * The link builder.
	 *
	 * @var Indexable_Link_Builder
	 */
	protected $link_builder;

	/**
	 * Represents the site helper.
	 *
	 * @var Site_Helper
	 */
	protected $site;

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

	/**
	 * Indexable_Term_Watcher constructor.
	 *
	 * @param Indexable_Repository   $repository   The repository to use.
	 * @param Indexable_Builder      $builder      The post builder to use.
	 * @param Indexable_Link_Builder $link_builder The lint builder to use.
	 * @param Site_Helper            $site         The site helper.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Indexable_Link_Builder $link_builder,
		Site_Helper $site
	) {
		$this->repository   = $repository;
		$this->builder      = $builder;
		$this->link_builder = $link_builder;
		$this->site         = $site;
	}

	/**
	 * Registers the hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'created_term', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'edited_term', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'delete_term', [ $this, 'delete_indexable' ], \PHP_INT_MAX );
	}

	/**
	 * Deletes a term from the index.
	 *
	 * @param int $term_id The Term ID to delete.
	 *
	 * @return void
	 */
	public function delete_indexable( $term_id ) {
		$indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false );

		if ( ! $indexable ) {
			return;
		}

		$indexable->delete();
	}

	/**
	 * Update the taxonomy meta data on save.
	 *
	 * @param int $term_id ID of the term to save data for.
	 *
	 * @return void
	 */
	public function build_indexable( $term_id ) {
		// Bail if this is a multisite installation and the site has been switched.
		if ( $this->site->is_multisite_and_switched() ) {
			return;
		}

		$term = \get_term( $term_id );

		if ( $term === null || \is_wp_error( $term ) ) {
			return;
		}

		if ( ! \is_taxonomy_viewable( $term->taxonomy ) ) {
			return;
		}

		$indexable = $this->repository->find_by_id_and_type( $term_id, 'term', false );

		// If we haven't found an existing indexable, create it. Otherwise update it.
		$indexable = $this->builder->build_for_id_and_type( $term_id, 'term', $indexable );

		if ( ! $indexable ) {
			return;
		}

		// Update links.
		$this->link_builder->build( $indexable, $term->description );

		$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
		$indexable->save();
	}
}
primary-category-quick-edit-watcher.php000066600000012763151131531760014263 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_Post;
use WPSEO_Meta;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Admin\Doing_Post_Quick_Edit_Save_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Class Primary_Category_Quick_Edit_Watcher
 */
class Primary_Category_Quick_Edit_Watcher implements Integration_Interface {

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

	/**
	 * Holds the primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	protected $primary_term_repository;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

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

	/**
	 * The indexable hierarchy builder.
	 *
	 * @var Indexable_Hierarchy_Builder
	 */
	protected $indexable_hierarchy_builder;

	/**
	 * Primary_Category_Quick_Edit_Watcher constructor.
	 *
	 * @param Options_Helper              $options_helper              The options helper.
	 * @param Primary_Term_Repository     $primary_term_repository     The primary term repository.
	 * @param Post_Type_Helper            $post_type_helper            The post type helper.
	 * @param Indexable_Repository        $indexable_repository        The indexable repository.
	 * @param Indexable_Hierarchy_Builder $indexable_hierarchy_builder The indexable hierarchy repository.
	 */
	public function __construct(
		Options_Helper $options_helper,
		Primary_Term_Repository $primary_term_repository,
		Post_Type_Helper $post_type_helper,
		Indexable_Repository $indexable_repository,
		Indexable_Hierarchy_Builder $indexable_hierarchy_builder
	) {
		$this->options_helper              = $options_helper;
		$this->primary_term_repository     = $primary_term_repository;
		$this->post_type_helper            = $post_type_helper;
		$this->indexable_repository        = $indexable_repository;
		$this->indexable_hierarchy_builder = $indexable_hierarchy_builder;
	}

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

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

	/**
	 * Validates if the current primary category is still present. If not just remove the post meta for it.
	 *
	 * @param int    $object_id Object ID.
	 * @param array  $terms     Unused. An array of object terms.
	 * @param array  $tt_ids    An array of term taxonomy IDs.
	 * @param string $taxonomy  Taxonomy slug.
	 */
	public function validate_primary_category( $object_id, $terms, $tt_ids, $taxonomy ) {
		$post = \get_post( $object_id );
		if ( $post === null ) {
			return;
		}

		$main_taxonomy = $this->options_helper->get( 'post_types-' . $post->post_type . '-maintax' );
		if ( ! $main_taxonomy || $main_taxonomy === '0' ) {
			return;
		}

		if ( $main_taxonomy !== $taxonomy ) {
			return;
		}

		$primary_category = $this->get_primary_term_id( $post->ID, $main_taxonomy );
		if ( $primary_category === false ) {
			return;
		}

		// The primary category isn't removed.
		if ( \in_array( (string) $primary_category, $tt_ids, true ) ) {
			return;
		}

		$this->remove_primary_term( $post->ID, $main_taxonomy );

		// Rebuild the post hierarchy for this post now the primary term has been changed.
		$this->build_post_hierarchy( $post );
	}

	/**
	 * Returns the primary term id of a post.
	 *
	 * @param int    $post_id       The post ID.
	 * @param string $main_taxonomy The main taxonomy.
	 *
	 * @return int|false The ID of the primary term, or `false` if the post ID is invalid.
	 */
	private function get_primary_term_id( $post_id, $main_taxonomy ) {
		$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );

		if ( $primary_term ) {
			return $primary_term->term_id;
		}

		return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true );
	}

	/**
	 * Removes the primary category.
	 *
	 * @param int    $post_id       The post id to set primary taxonomy for.
	 * @param string $main_taxonomy Name of the taxonomy that is set to be the primary one.
	 */
	private function remove_primary_term( $post_id, $main_taxonomy ) {
		$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );
		if ( $primary_term ) {
			$primary_term->delete();
		}

		// Remove it from the post meta.
		\delete_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy );
	}

	/**
	 * Builds the hierarchy for a post.
	 *
	 * @param WP_Post $post The post.
	 */
	public function build_post_hierarchy( $post ) {
		if ( $this->post_type_helper->is_excluded( $post->post_type ) ) {
			return;
		}

		$indexable = $this->indexable_repository->find_by_id_and_type( $post->ID, 'post' );

		if ( $indexable instanceof Indexable ) {
			$this->indexable_hierarchy_builder->build( $indexable );
		}
	}
}
indexable-taxonomy-change-watcher.php000066600000013705151131531760013757 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast_Notification;
use Yoast_Notification_Center;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Taxonomy watcher.
 *
 * Responds to changes in taxonomies public availability.
 */
class Indexable_Taxonomy_Change_Watcher implements Integration_Interface {

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

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

	/**
	 * Holds the Taxonomy_Helper instance.
	 *
	 * @var Taxonomy_Helper
	 */
	private $taxonomy_helper;

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

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

	/**
	 * Indexable_Taxonomy_Change_Watcher constructor.
	 *
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param Options_Helper            $options             The options helper.
	 * @param Taxonomy_Helper           $taxonomy_helper     The taxonomy helper.
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct(
		Indexing_Helper $indexing_helper,
		Options_Helper $options,
		Taxonomy_Helper $taxonomy_helper,
		Yoast_Notification_Center $notification_center
	) {
		$this->indexing_helper     = $indexing_helper;
		$this->options             = $options;
		$this->taxonomy_helper     = $taxonomy_helper;
		$this->notification_center = $notification_center;
	}

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

	/**
	 * Checks if one or more taxonomies change visibility.
	 *
	 * @return void
	 */
	public function check_taxonomy_public_availability() {

		// We have to make sure this is just a plain http request, no ajax/REST.
		if ( \wp_is_json_request() ) {
			return;
		}

		$public_taxonomies            = \array_keys( $this->taxonomy_helper->get_public_taxonomies() );
		$last_known_public_taxonomies = $this->options->get( 'last_known_public_taxonomies', [] );

		// Initializing the option on the first run.
		if ( empty( $last_known_public_taxonomies ) ) {
			$this->options->set( 'last_known_public_taxonomies', $public_taxonomies );
			return;
		}

		// We look for new public taxonomies.
		$newly_made_public_taxonomies = \array_diff( $public_taxonomies, $last_known_public_taxonomies );
		// We look fortaxonomies that from public have been made private.
		$newly_made_non_public_taxonomies = \array_diff( $last_known_public_taxonomies, $public_taxonomies );

		// Nothing to be done if no changes has been made to taxonomies.
		if ( empty( $newly_made_public_taxonomies ) && ( empty( $newly_made_non_public_taxonomies ) ) ) {
			return;
		}

		// Update the list of last known public taxonomies in the database.
		$this->options->set( 'last_known_public_taxonomies', $public_taxonomies );

		// There are new taxonomies that have been made public.
		if ( ! empty( $newly_made_public_taxonomies ) ) {

			// Force a notification requesting to start the SEO data optimization.
			\delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( Indexable_Term_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT );

			$this->indexing_helper->set_reason( Indexing_Reasons::REASON_TAXONOMY_MADE_PUBLIC );

			$this->maybe_add_notification();
		}

		// There are taxonomies that have been made private.
		if ( ! empty( $newly_made_non_public_taxonomies ) ) {
			// Schedule a cron job to remove all the terms whose taxonomy has been made private.
			if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
				if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
					\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK );
					\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
				}
			}
		}
	}

	/**
	 * Decides if a notification should be added in the notification center.
	 *
	 * @return void
	 */
	private function maybe_add_notification() {
		$notification = $this->notification_center->get_notification_by_id( 'taxonomies-made-public' );
		if ( is_null( $notification ) ) {
			$this->add_notification();
		}
	}

	/**
	 * Adds a notification to be shown on the next page request since posts are updated in an ajax request.
	 *
	 * @return void
	 */
	private function add_notification() {
		$message = sprintf(
			/* translators: 1: Opening tag of the link to the Search appearance settings page, 2: Link closing tag. */
			\esc_html__( 'It looks like you\'ve added a new taxonomy to your website. We recommend that you review your %1$sSearch appearance settings%2$s.', 'wordpress-seo' ),
			'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_titles#top#taxonomies' ) ) . '">',
			'</a>'
		);

		$notification = new Yoast_Notification(
			$message,
			[
				'type'         => Yoast_Notification::WARNING,
				'id'           => 'taxonomies-made-public',
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 0.8,
			]
		);

		$this->notification_center->add_notification( $notification );
	}
}
search-engines-discouraged-watcher.php000066600000015225151131531760014106 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Notification_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Search_Engines_Discouraged_Presenter;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
 * Shows a notification for users who have access for robots disabled.
 *
 * @class Search_Engines_Discouraged_Watcher
 */
class Search_Engines_Discouraged_Watcher implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The notification ID.
	 */
	const NOTIFICATION_ID = 'wpseo-search-engines-discouraged';

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

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

	/**
	 * The search engines discouraged presenter.
	 *
	 * @var Search_Engines_Discouraged_Presenter
	 */
	protected $presenter;

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

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

	/**
	 * The capability helper.
	 *
	 * @var Capability_Helper
	 */
	protected $capability_helper;

	/**
	 * Search_Engines_Discouraged_Watcher constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 * @param Notification_Helper       $notification_helper The notification helper.
	 * @param Current_Page_Helper       $current_page_helper The current page helper.
	 * @param Options_Helper            $options_helper      The options helper.
	 * @param Capability_Helper         $capability_helper   The capability helper.
	 */
	public function __construct(
		Yoast_Notification_Center $notification_center,
		Notification_Helper $notification_helper,
		Current_Page_Helper $current_page_helper,
		Options_Helper $options_helper,
		Capability_Helper $capability_helper
	) {
		$this->notification_center = $notification_center;
		$this->notification_helper = $notification_helper;
		$this->current_page_helper = $current_page_helper;
		$this->options_helper      = $options_helper;
		$this->capability_helper   = $capability_helper;
		$this->presenter           = new Search_Engines_Discouraged_Presenter();
	}

	/**
	 * Initializes the integration.
	 *
	 * On admin_init, it is checked whether the notification about search engines being discouraged should be shown.
	 * On admin_notices, the notice about the search engines being discouraged will be shown when necessary.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'manage_search_engines_discouraged_notification' ] );

		/*
		 * The `admin_notices` hook fires on single site admin pages vs.
		 * `network_admin_notices` which fires on multisite admin pages and
		 * `user_admin_notices` which fires on multisite user admin pages.
		 */
		\add_action( 'admin_notices', [ $this, 'maybe_show_search_engines_discouraged_notice' ] );
	}

	/**
	 * Manage the search engines discouraged notification.
	 *
	 * Shows the notification if needed and deletes it if needed.
	 *
	 * @return void
	 */
	public function manage_search_engines_discouraged_notification() {
		if ( ! $this->should_show_search_engines_discouraged_notification() ) {
			$this->remove_search_engines_discouraged_notification_if_exists();
		}
		else {
			$this->maybe_add_search_engines_discouraged_notification();
		}
	}

	/**
	 * Show the search engine discouraged notice when needed.
	 *
	 * @return void
	 */
	public function maybe_show_search_engines_discouraged_notice() {
		if ( ! $this->should_show_search_engines_discouraged_notice() ) {
			return;
		}
		$this->show_search_engines_discouraged_notice();
	}

	/**
	 * Whether the search engines discouraged notification should be shown.
	 *
	 * @return bool
	 */
	protected function should_show_search_engines_discouraged_notification() {
		return $this->search_engines_are_discouraged() && $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false;
	}

	/**
	 * Remove the search engines discouraged notification if it exists.
	 *
	 * @return void
	 */
	protected function remove_search_engines_discouraged_notification_if_exists() {
		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}

	/**
	 * Add the search engines discouraged notification if it does not exist yet.
	 *
	 * @return void
	 */
	protected function maybe_add_search_engines_discouraged_notification() {
		if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) {
			$notification = $this->notification();
			$this->notification_helper->restore_notification( $notification );
			$this->notification_center->add_notification( $notification );
		}
	}

	/**
	 * Checks whether search engines are discouraged from indexing the site.
	 *
	 * @return bool Whether search engines are discouraged from indexing the site.
	 */
	protected function search_engines_are_discouraged() {
		return (string) \get_option( 'blog_public' ) === '0';
	}

	/**
	 * Whether the search engines notice should be shown.
	 *
	 * @return bool
	 */
	protected function should_show_search_engines_discouraged_notice() {
		$pages_to_show_notice = [
			'index.php',
			'plugins.php',
			'update-core.php',
		];

		return (
			$this->search_engines_are_discouraged()
			&& $this->capability_helper->current_user_can( 'manage_options' )
			&& $this->options_helper->get( 'ignore_search_engines_discouraged_notice', false ) === false
			&& (
				$this->current_page_helper->is_yoast_seo_page()
				|| \in_array( $this->current_page_helper->get_current_admin_page(), $pages_to_show_notice, true )
			)
			&& $this->current_page_helper->get_current_yoast_seo_page() !== 'wpseo_dashboard'
		);
	}

	/**
	 * Show the search engines discouraged notice.
	 *
	 * @return void
	 */
	protected function show_search_engines_discouraged_notice() {
		\printf(
			'<div id="robotsmessage" class="notice notice-error">%1$s</div>',
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe.
			$this->presenter->present()
		);
	}

	/**
	 * Returns an instance of the notification.
	 *
	 * @return Yoast_Notification The notification to show.
	 */
	protected function notification() {
		return new Yoast_Notification(
			$this->presenter->present(),
			[
				'type'         => Yoast_Notification::ERROR,
				'id'           => self::NOTIFICATION_ID,
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 1,
			]
		);
	}
}
indexable-author-watcher.php000066600000004373151131531760012161 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watches an Author to save the meta information to an Indexable when updated.
 */
class Indexable_Author_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

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

	/**
	 * Indexable_Author_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'user_register', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'profile_update', [ $this, 'build_indexable' ], \PHP_INT_MAX );
		\add_action( 'deleted_user', [ $this, 'delete_indexable' ] );
	}

	/**
	 * Deletes user meta.
	 *
	 * @param int $user_id User ID to delete the metadata of.
	 *
	 * @return void
	 */
	public function delete_indexable( $user_id ) {
		$indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false );

		if ( ! $indexable ) {
			return;
		}

		$indexable->delete();
	}

	/**
	 * Saves user meta.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return void
	 */
	public function build_indexable( $user_id ) {
		$indexable = $this->repository->find_by_id_and_type( $user_id, 'user', false );
		$indexable = $this->builder->build_for_id_and_type( $user_id, 'user', $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
indexable-static-home-page-watcher.php000066600000004216151131531760014002 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Watcher that checks for changes in the page used as homepage.
 *
 * Watches the static homepage option and updates the permalinks accordingly.
 */
class Indexable_Static_Home_Page_Watcher implements Integration_Interface {

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

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

	/**
	 * Indexable_Static_Home_Page_Watcher constructor.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 */
	public function __construct( Indexable_Repository $repository ) {
		$this->repository = $repository;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_page_on_front', [ $this, 'update_static_homepage_permalink' ], 10, 2 );
	}

	/**
	 * Updates the new and previous homepage's permalink when the static home page is updated.
	 *
	 * @param string $old_value The previous homepage's ID.
	 * @param int    $value     The new homepage's ID.
	 */
	public function update_static_homepage_permalink( $old_value, $value ) {
		if ( \is_string( $old_value ) ) {
			$old_value = (int) $old_value;
		}

		if ( $old_value === $value ) {
			return;
		}

		$this->update_permalink_for_page( $old_value );
		$this->update_permalink_for_page( $value );
	}

	/**
	 * Updates the permalink based on the selected homepage settings.
	 *
	 * @param int $page_id The page's id.
	 */
	private function update_permalink_for_page( $page_id ) {
		if ( $page_id === 0 ) {
			return;
		}

		$indexable = $this->repository->find_by_id_and_type( $page_id, 'post', false );

		if ( $indexable === false ) {
			return;
		}

		$indexable->permalink = \get_permalink( $page_id );

		$indexable->save();
	}
}
option-titles-watcher.php000066600000006174151131531760011541 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\WordPress\Wrapper;

/**
 * Watcher for the titles option.
 *
 * Represents the option titles watcher.
 */
class Option_Titles_Watcher implements Integration_Interface {

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

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

	/**
	 * Checks if one of the relevant options has been changed.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether or not the ancestors are removed.
	 */
	public function check_option( $old_value, $new_value ) {
		// If this is the first time saving the option, thus when value is false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return false;
		}

		$relevant_keys = $this->get_relevant_keys();
		if ( empty( $relevant_keys ) ) {
			return false;
		}

		$post_types = [];

		foreach ( $relevant_keys as $post_type => $relevant_option ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $relevant_option ] ) && ! isset( $new_value[ $relevant_option ] ) ) {
				continue;
			}

			if ( $old_value[ $relevant_option ] !== $new_value[ $relevant_option ] ) {
				$post_types[] = $post_type;
			}
		}

		return $this->delete_ancestors( $post_types );
	}

	/**
	 * Retrieves the relevant keys.
	 *
	 * @return array Array with the relevant keys.
	 */
	protected function get_relevant_keys() {
		$post_types = \get_post_types( [ 'public' => true ], 'names' );
		if ( ! \is_array( $post_types ) || $post_types === [] ) {
			return [];
		}

		$relevant_keys = [];
		foreach ( $post_types as $post_type ) {
			$relevant_keys[ $post_type ] = 'post_types-' . $post_type . '-maintax';
		}

		return $relevant_keys;
	}

	/**
	 * Removes the ancestors for given post types.
	 *
	 * @param array $post_types The post types to remove hierarchy for.
	 *
	 * @return bool True when delete query was successful.
	 */
	protected function delete_ancestors( $post_types ) {
		if ( empty( $post_types ) ) {
			return false;
		}

		$wpdb            = Wrapper::get_wpdb();
		$total           = \count( $post_types );
		$hierarchy_table = Model::get_table_name( 'Indexable_Hierarchy' );
		$indexable_table = Model::get_table_name( 'Indexable' );

		$result = $wpdb->query(
			$wpdb->prepare(
				"
				DELETE FROM `$hierarchy_table`
				WHERE indexable_id IN(
					SELECT id
					FROM `$indexable_table`
					WHERE object_type = 'post'
					AND object_sub_type IN( " . \implode( ', ', \array_fill( 0, $total, '%s' ) ) . ' )
				)',
				$post_types
			)
		);

		return $result !== false;
	}
}
indexable-date-archive-watcher.php000066600000005010151131531760013200 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Date archive watcher to save the meta data to an indexable.
 *
 * Watches the date archive options to save the meta information when updated.
 */
class Indexable_Date_Archive_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

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

	/**
	 * Indexable_Date_Archive_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The date archive builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the date archive indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		$relevant_keys = [ 'title-archive-wpseo', 'breadcrumbs-archiveprefix', 'metadesc-archive-wpseo', 'noindex-archive-wpseo' ];

		foreach ( $relevant_keys as $key ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) {
				$this->build_indexable();
				return;
			}
		}
	}

	/**
	 * Saves the date archive.
	 *
	 * @return void
	 */
	public function build_indexable() {
		$indexable = $this->repository->find_for_date_archive( false );
		$this->builder->build_for_date_archive( $indexable );
	}
}
indexable-ancestor-watcher.php000066600000020161151131531760012466 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use wpdb;
use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Permalink_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Ancestor watcher to update the ancestor's children.
 *
 * Updates its children's permalink when the ancestor itself is updated.
 */
class Indexable_Ancestor_Watcher implements Integration_Interface {

	/**
	 * Represents the indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Represents the indexable hierarchy builder.
	 *
	 * @var Indexable_Hierarchy_Builder
	 */
	protected $indexable_hierarchy_builder;

	/**
	 * Represents the indexable hierarchy repository.
	 *
	 * @var Indexable_Hierarchy_Repository
	 */
	protected $indexable_hierarchy_repository;

	/**
	 * Represents the WordPress database object.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Represents the permalink helper.
	 *
	 * @var Permalink_Helper
	 */
	protected $permalink_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * Sets the needed dependencies.
	 *
	 * @param Indexable_Repository           $indexable_repository           The indexable repository.
	 * @param Indexable_Hierarchy_Builder    $indexable_hierarchy_builder    The indexable hierarchy builder.
	 * @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
	 * @param wpdb                           $wpdb                           The wpdb object.
	 * @param Permalink_Helper               $permalink_helper               The permalink helper.
	 * @param Post_Type_Helper               $post_type_helper               The post type helper.
	 */
	public function __construct(
		Indexable_Repository $indexable_repository,
		Indexable_Hierarchy_Builder $indexable_hierarchy_builder,
		Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
		wpdb $wpdb,
		Permalink_Helper $permalink_helper,
		Post_Type_Helper $post_type_helper
	) {
		$this->indexable_repository           = $indexable_repository;
		$this->indexable_hierarchy_builder    = $indexable_hierarchy_builder;
		$this->wpdb                           = $wpdb;
		$this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
		$this->permalink_helper               = $permalink_helper;
		$this->post_type_helper               = $post_type_helper;
	}

	/**
	 * Registers the appropriate hooks.
	 */
	public function register_hooks() {
		\add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 );
	}

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

	/**
	 * If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink.
	 *
	 * @param Indexable $indexable        The indexable.
	 * @param Indexable $indexable_before The old indexable.
	 *
	 * @return bool True if the children were reset.
	 */
	public function reset_children( $indexable, $indexable_before ) {
		if ( ! \in_array( $indexable->object_type, [ 'post', 'term' ], true ) ) {
			return false;
		}

		// If the permalink was null it means it was reset instead of changed.
		if ( $indexable->permalink === $indexable_before->permalink || \is_null( $indexable_before->permalink ) ) {
			return false;
		}

		$child_indexable_ids = $this->indexable_hierarchy_repository->find_children( $indexable );
		$child_indexables    = $this->indexable_repository->find_by_ids( $child_indexable_ids );

		\array_walk( $child_indexables, [ $this, 'update_hierarchy_and_permalink' ] );

		if ( $indexable->object_type === 'term' ) {
			$child_indexables_for_term = $this->get_children_for_term( $indexable->object_id, $child_indexables );

			\array_walk( $child_indexables_for_term, [ $this, 'update_hierarchy_and_permalink' ] );
		}

		return true;
	}

	/**
	 * Finds all child indexables for the given term.
	 *
	 * @param int         $term_id          Term to fetch the indexable for.
	 * @param Indexable[] $child_indexables The already known child indexables.
	 *
	 * @return array The list of additional child indexables for a given term.
	 */
	public function get_children_for_term( $term_id, array $child_indexables ) {
		// Finds object_ids (posts) for the term.
		$post_object_ids = $this->get_object_ids_for_term( $term_id, $child_indexables );

		// Removes the objects that are already present in the children.
		$existing_post_indexables = \array_filter(
			$child_indexables,
			static function( $indexable ) {
				return $indexable->object_type === 'post';
			}
		);

		$existing_post_object_ids = \wp_list_pluck( $existing_post_indexables, 'object_id' );
		$post_object_ids          = \array_diff( $post_object_ids, $existing_post_object_ids );

		// Finds the indexables for the fetched post_object_ids.
		$post_indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_object_ids, 'post', false );

		// Finds the indexables for the posts that are attached to the term.
		$post_indexable_ids       = \wp_list_pluck( $post_indexables, 'id' );
		$additional_indexable_ids = $this->indexable_hierarchy_repository->find_children_by_ancestor_ids( $post_indexable_ids );

		// Makes sure we only have indexable id's that we haven't fetched before.
		$additional_indexable_ids = \array_diff( $additional_indexable_ids, $post_indexable_ids );

		// Finds the additional indexables.
		$additional_indexables = $this->indexable_repository->find_by_ids( $additional_indexable_ids );

		// Merges all fetched indexables.
		return \array_merge( $post_indexables, $additional_indexables );
	}

	/**
	 * Builds the hierarchy for a post.
	 *
	 * @deprecated 16.4
	 *
	 * @codeCoverageIgnore
	 *
	 * @param int $object_id The post id.
	 * @param int $post_type The post type.
	 */
	public function build_post_hierarchy( $object_id, $post_type ) {
		\_deprecated_function( __METHOD__, '16.4', 'Primary_Category_Quick_Edit_Watcher::build_post_hierarchy' );
	}

	/**
	 * Updates the indexable hierarchy and indexable permalink.
	 *
	 * @param Indexable $indexable The indexable to update the hierarchy and permalink for.
	 */
	protected function update_hierarchy_and_permalink( $indexable ) {
		$this->indexable_hierarchy_builder->build( $indexable );

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

	/**
	 * Retrieves the object id's for a term based on the term-post relationship.
	 *
	 * @param int         $term_id          The term to get the object id's for.
	 * @param Indexable[] $child_indexables The child indexables.
	 *
	 * @return array List with object ids for the term.
	 */
	protected function get_object_ids_for_term( $term_id, $child_indexables ) {
		$filter_terms = static function( $child ) {
			return $child->object_type === 'term';
		};

		$child_terms      = \array_filter( $child_indexables, $filter_terms );
		$child_object_ids = \wp_list_pluck( $child_terms, 'object_id' );

		// Get the term-taxonomy id's for the term and its children.
		$term_taxonomy_ids = $this->wpdb->get_col(
			$this->wpdb->prepare(
				'SELECT term_taxonomy_id
				FROM ' . $this->wpdb->term_taxonomy . '
				WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) + 1 ), '%s' ) ) . ' )',
				$term_id,
				...$child_object_ids
			)
		);

		// In the case of faulty data having been saved the above query can return 0 results.
		if ( empty( $term_taxonomy_ids ) ) {
			return [];
		}

		// Get the (post) object id's that are attached to the term.
		return $this->wpdb->get_col(
			$this->wpdb->prepare(
				'SELECT DISTINCT object_id
				FROM ' . $this->wpdb->term_relationships . '
				WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )',
				...$term_taxonomy_ids
			)
		);
	}
}
indexable-homeurl-watcher.php000066600000005410151131531760012323 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use WP_CLI;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Home url option watcher.
 *
 * Handles updates to the home URL option for the Indexables table.
 */
class Indexable_HomeUrl_Watcher implements Integration_Interface {

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

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type;

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

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

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

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

	/**
	 * Resets the permalinks for everything that is related to the permalink structure.
	 *
	 * @return void
	 */
	public function reset_permalinks() {
		$this->indexable_helper->reset_permalink_indexables( null, null, Indexing_Reasons::REASON_HOME_URL_OPTION );

		// Reset the home_url option.
		$this->options_helper->set( 'home_url', \get_home_url() );
	}

	/**
	 * Resets the permalink indexables automatically, if necessary.
	 *
	 * @return bool Whether the request ran.
	 */
	public function force_reset_permalinks() {
		if ( $this->should_reset_permalinks() ) {
			$this->reset_permalinks();

			if ( \defined( 'WP_CLI' ) && \WP_CLI ) {
				WP_CLI::success( \__( 'All permalinks were successfully reset', 'wordpress-seo' ) );
			}

			return true;
		}

		return false;
	}

	/**
	 * Checks whether permalinks should be reset.
	 *
	 * @return bool Whether the permalinks should be reset.
	 */
	public function should_reset_permalinks() {
		return \get_home_url() !== $this->options_helper->get( 'home_url' );
	}
}
indexable-author-archive-watcher.php000066600000003412151131531760013571 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * Watches the `wpseo_titles` option for changes to the author archive settings.
 */
class Indexable_Author_Archive_Watcher implements Integration_Interface {

	/**
	 * Check if the author archives are disabled whenever the `wpseo_titles` option
	 * changes.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action(
			'update_option_wpseo_titles',
			[ $this, 'reschedule_indexable_cleanup_when_author_archives_are_disabled' ],
			10,
			2
		);
	}

	/**
	 * This watcher should only be run when the migrations have been run.
	 * (Otherwise there may not be an indexable table to clean).
	 *
	 * @return string[] The conditionals.
	 */
	public static function get_conditionals() {
		return [ Migrations_Conditional::class ];
	}

	/**
	 * Reschedule the indexable cleanup routine if the author archives are disabled.
	 * to make sure that all authors are removed from the indexables table.
	 *
	 * When author archives are disabled, they can never be indexed.
	 *
	 * @param array $old_value The old `wpseo_titles` option value.
	 * @param array $new_value The new `wpseo_titles` option value.
	 *
	 * @return void
	 */
	public function reschedule_indexable_cleanup_when_author_archives_are_disabled( $old_value, $new_value ) {
		if ( $old_value['disable-author'] !== true && $new_value['disable-author'] === true ) {
			$cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK );
			if ( $cleanup_not_yet_scheduled ) {
				\wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
			}
		}
	}
}
indexable-category-permalink-watcher.php000066600000003177151131531760014455 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Config\Indexing_Reasons;

/**
 * Watches the stripcategorybase key in wpseo_titles, in order to clear the permalink of the category indexables.
 */
class Indexable_Category_Permalink_Watcher extends Indexable_Permalink_Watcher {

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

	/**
	 * Checks if the stripcategorybase key in wpseo_titles has a change in value, and if so,
	 * clears the permalink for category indexables.
	 *
	 * @param array $old_value The old value of the wpseo_titles option.
	 * @param array $new_value The new value of the wpseo_titles option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		// If this is the first time saving the option, in which case its value would be false.
		if ( $old_value === false ) {
			$old_value = [];
		}

		// If either value is not an array, return.
		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return;
		}

		// If both values aren't set, they haven't changed.
		if ( ! isset( $old_value['stripcategorybase'] ) && ! isset( $new_value['stripcategorybase'] ) ) {
			return;
		}

		// If a new value has been set for 'stripcategorybase', clear the category permalinks.
		if ( $old_value['stripcategorybase'] !== $new_value['stripcategorybase'] ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', 'category', Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX );
		}
	}
}
indexable-post-type-change-watcher.php000066600000013570151131531760014045 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast_Notification;
use Yoast_Notification_Center;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Not_Admin_Ajax_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexing_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Cleanup_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;

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

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

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

	/**
	 * Holds the Post_Type_Helper instance.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type_helper;

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

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

	/**
	 * Indexable_Post_Type_Change_Watcher constructor.
	 *
	 * @param Options_Helper            $options             The options helper.
	 * @param Indexing_Helper           $indexing_helper     The indexing helper.
	 * @param Post_Type_Helper          $post_type_helper    The post_typehelper.
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct(
		Options_Helper $options,
		Indexing_Helper $indexing_helper,
		Post_Type_Helper $post_type_helper,
		Yoast_Notification_Center $notification_center
	) {
		$this->options             = $options;
		$this->indexing_helper     = $indexing_helper;
		$this->post_type_helper    = $post_type_helper;
		$this->notification_center = $notification_center;
	}

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

	/**
	 * Checks if one or more post types change visibility.
	 *
	 * @return void
	 */
	public function check_post_types_public_availability() {

		// We have to make sure this is just a plain http request, no ajax/REST.
		if ( \wp_is_json_request() ) {
			return;
		}

		$public_post_types            = \array_keys( $this->post_type_helper->get_public_post_types() );
		$last_known_public_post_types = $this->options->get( 'last_known_public_post_types', [] );

		// Initializing the option on the first run.
		if ( empty( $last_known_public_post_types ) ) {
			$this->options->set( 'last_known_public_post_types', $public_post_types );
			return;
		}

		// We look for new public post types.
		$newly_made_public_post_types = \array_diff( $public_post_types, $last_known_public_post_types );
		// We look for post types that from public have been made private.
		$newly_made_non_public_post_types = \array_diff( $last_known_public_post_types, $public_post_types );

		// Nothing to be done if no changes has been made to post types.
		if ( empty( $newly_made_public_post_types ) && ( empty( $newly_made_non_public_post_types ) ) ) {
			return;
		}

		// Update the list of last known public post types in the database.
		$this->options->set( 'last_known_public_post_types', $public_post_types );

		// There are new post types that have been made public.
		if ( ! empty( $newly_made_public_post_types ) ) {

			// Force a notification requesting to start the SEO data optimization.
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( Indexable_Post_Indexation_Action::UNINDEXED_LIMITED_COUNT_TRANSIENT );

			$this->indexing_helper->set_reason( Indexing_Reasons::REASON_POST_TYPE_MADE_PUBLIC );

			$this->maybe_add_notification();
		}

		// There are post types that have been made private.
		if ( ! empty( $newly_made_non_public_post_types ) ) {
			// Schedule a cron job to remove all the posts whose post type has been made private.
			if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
				if ( ! \wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
					\wp_schedule_single_event( ( time() + 5 ), \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK );
					\wp_schedule_single_event( ( time() + 5 ), Cleanup_Integration::START_HOOK );
				}
			}
		}
	}

	/**
	 * Decides if a notification should be added in the notification center.
	 *
	 * @return void
	 */
	private function maybe_add_notification() {
		$notification = $this->notification_center->get_notification_by_id( 'post-types-made-public' );
		if ( is_null( $notification ) ) {
			$this->add_notification();
		}
	}

	/**
	 * Adds a notification to be shown on the next page request since posts are updated in an ajax request.
	 *
	 * @return void
	 */
	private function add_notification() {
		$message = sprintf(
			/* translators: 1: Opening tag of the link to the Search appearance settings page, 2: Link closing tag. */
			\esc_html__( 'It looks like you\'ve added a new type of content to your website. We recommend that you review your %1$sSearch appearance settings%2$s.', 'wordpress-seo' ),
			'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_titles#top#post-types' ) ) . '">',
			'</a>'
		);

		$notification = new Yoast_Notification(
			$message,
			[
				'type'         => Yoast_Notification::WARNING,
				'id'           => 'post-types-made-public',
				'capabilities' => 'wpseo_manage_options',
				'priority'     => 0.8,
			]
		);

		$this->notification_center->add_notification( $notification );
	}
}
indexable-system-page-watcher.php000066600000005176151131531760013117 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Builders\Indexable_System_Page_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Search result watcher to save the meta data to an Indexable.
 *
 * Watches the search result options to save the meta information when updated.
 */
class Indexable_System_Page_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

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

	/**
	 * Indexable_System_Page_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value ) {
		foreach ( Indexable_System_Page_Builder::OPTION_MAPPING as $type => $options ) {
			foreach ( $options as $option ) {
				// If both values aren't set they haven't changed.
				if ( ! isset( $old_value[ $option ] ) && ! isset( $new_value[ $option ] ) ) {
					return;
				}

				// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
				if (
					! isset( $old_value[ $option ] )
					|| ! isset( $new_value[ $option ] )
					|| $old_value[ $option ] !== $new_value[ $option ]
				) {
					$this->build_indexable( $type );
				}
			}
		}
	}

	/**
	 * Saves the search result.
	 *
	 * @param string $type The type of no index page.
	 *
	 * @return void
	 */
	public function build_indexable( $type ) {
		$indexable = $this->repository->find_for_system_page( $type, false );
		$this->builder->build_for_system_page( $type, $indexable );
	}
}
auto-update-watcher.php000066600000002763151131531760011157 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification_Center;

/**
 * Shows a notification for users who have WordPress auto updates enabled but not Yoast SEO auto updates.
 */
class Auto_Update_Watcher implements Integration_Interface {

	use No_Conditionals;

	/**
	 * The notification ID.
	 */
	const NOTIFICATION_ID = 'wpseo-auto-update';

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

	/**
	 * Auto_Update constructor.
	 *
	 * @param Yoast_Notification_Center $notification_center The notification center.
	 */
	public function __construct( Yoast_Notification_Center $notification_center ) {
		$this->notification_center = $notification_center;
	}

	/**
	 * Initializes the integration.
	 *
	 * On admin_init, it is checked whether the notification to auto-update Yoast SEO needs to be shown or removed.
	 * This is also done when major WP core updates are being enabled or disabled,
	 * and when automatic updates for Yoast SEO are being enabled or disabled.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'admin_init', [ $this, 'remove_notification' ] );
	}

	/**
	 * Removes the notification from the notification center, if it exists.
	 *
	 * @return void
	 */
	public function remove_notification() {
		$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
	}
}
option-wpseo-watcher.php000066600000007560151131531760011372 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

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

/**
 * Watcher for the wpseo option.
 *
 * Represents the option wpseo watcher.
 */
class Option_Wpseo_Watcher implements Integration_Interface {

	use No_Conditionals;

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

	/**
	 * The constructor for a watcher of WPSEO options.
	 *
	 * @param Wordproof_Helper $wordproof The WordProof helper instance.
	 */
	public function __construct( Wordproof_Helper $wordproof ) {
		$this->wordproof = $wordproof;
	}

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

	/**
	 * Checks if the SEMrush integration is disabled; if so, deletes the tokens.
	 *
	 * We delete the tokens if the SEMrush integration is disabled, no matter if
	 * the value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the SEMrush tokens have been deleted or not.
	 */
	public function check_semrush_option_disabled( $old_value, $new_value ) {
		return $this->check_token_option_disabled( 'semrush_integration_active', 'semrush_tokens', $new_value );
	}

	/**
	 * Checks if the Wincher integration is disabled; if so, deletes the tokens
	 * and website id.
	 *
	 * We delete them if the Wincher integration is disabled, no matter if the
	 * value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the Wincher tokens have been deleted or not.
	 */
	public function check_wincher_option_disabled( $old_value, $new_value ) {
		$disabled = $this->check_token_option_disabled( 'wincher_integration_active', 'wincher_tokens', $new_value );
		if ( $disabled ) {
			\YoastSEO()->helpers->options->set( 'wincher_website_id', '' );
		}
		return $disabled;
	}

	/**
	 * Checks if the WordProof integration is disabled; if so, deletes the tokens
	 *
	 * We delete them if the WordProof integration is disabled, no matter if the
	 * value has actually changed or not.
	 *
	 * @param array $old_value The old value of the option.
	 * @param array $new_value The new value of the option.
	 *
	 * @return bool Whether the WordProof tokens have been deleted or not.
	 */
	public function check_wordproof_option_disabled( $old_value, $new_value ) {
		$disabled = $this->check_token_option_disabled( 'wordproof_integration_active', 'wordproof_tokens', $new_value );
		if ( $disabled ) {
			$this->wordproof->remove_site_options();
		}
		return $disabled;
	}

	/**
	 * Checks if the passed integration is disabled; if so, deletes the tokens.
	 *
	 * We delete the tokens if the integration is disabled, no matter if
	 * the value has actually changed or not.
	 *
	 * @param string $integration_option The intergration option name.
	 * @param string $target_option      The target option to remove the tokens from.
	 * @param array  $new_value          The new value of the option.
	 *
	 * @return bool Whether the tokens have been deleted or not.
	 */
	protected function check_token_option_disabled( $integration_option, $target_option, $new_value ) {
		if ( \array_key_exists( $integration_option, $new_value ) && $new_value[ $integration_option ] === false ) {
			\YoastSEO()->helpers->options->set( $target_option, [] );
			return true;
		}
		return false;
	}
}
indexable-permalink-watcher.php000066600000017305151131531760012640 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Config\Indexing_Reasons;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;

/**
 * WordPress Permalink structure watcher.
 *
 * Handles updates to the permalink_structure for the Indexables table.
 */
class Indexable_Permalink_Watcher implements Integration_Interface {

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

	/**
	 * The taxonomy helper.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type;

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

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

	/**
	 * Indexable_Permalink_Watcher constructor.
	 *
	 * @param Post_Type_Helper $post_type       The post type helper.
	 * @param Options_Helper   $options         The options helper.
	 * @param Indexable_Helper $indexable       The indexable helper.
	 * @param Taxonomy_Helper  $taxonomy_helper The taxonomy helper.
	 */
	public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable, Taxonomy_Helper $taxonomy_helper ) {
		$this->post_type        = $post_type;
		$this->options_helper   = $options;
		$this->indexable_helper = $indexable;
		$this->taxonomy_helper  = $taxonomy_helper;

		$this->schedule_cron();
	}

	/**
	 * Registers the hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		\add_action( 'update_option_permalink_structure', [ $this, 'reset_permalinks' ] );
		\add_action( 'update_option_category_base', [ $this, 'reset_permalinks_term' ], 10, 3 );
		\add_action( 'update_option_tag_base', [ $this, 'reset_permalinks_term' ], 10, 3 );
		\add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] );
		\add_action( 'wpseo_deactivate', [ $this, 'unschedule_cron' ] );
	}

	/**
	 * Resets the permalinks for everything that is related to the permalink structure.
	 */
	public function reset_permalinks() {

		$post_types = $this->get_post_types();
		foreach ( $post_types as $post_type ) {
			$this->reset_permalinks_post_type( $post_type );
		}

		$taxonomies = $this->get_taxonomies_for_post_types( $post_types );
		foreach ( $taxonomies as $taxonomy ) {
			$this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy );
		}

		$this->indexable_helper->reset_permalink_indexables( 'user' );
		$this->indexable_helper->reset_permalink_indexables( 'date-archive' );
		$this->indexable_helper->reset_permalink_indexables( 'system-page' );

		// Always update `permalink_structure` in the wpseo option.
		$this->options_helper->set( 'permalink_structure', \get_option( 'permalink_structure' ) );
	}

	/**
	 * Resets the permalink for the given post type.
	 *
	 * @param string $post_type The post type to reset.
	 */
	public function reset_permalinks_post_type( $post_type ) {
		$this->indexable_helper->reset_permalink_indexables( 'post', $post_type );
		$this->indexable_helper->reset_permalink_indexables( 'post-type-archive', $post_type );
	}

	/**
	 * Resets the term indexables when the base has been changed.
	 *
	 * @param string $old_value Unused. The old option value.
	 * @param string $new_value Unused. The new option value.
	 * @param string $type      The option name.
	 */
	public function reset_permalinks_term( $old_value, $new_value, $type ) {
		$subtype = $type;

		$reason = Indexing_Reasons::REASON_PERMALINK_SETTINGS;

		// When the subtype contains _base, just strip it.
		if ( \strstr( $subtype, '_base' ) ) {
			$subtype = \substr( $type, 0, -5 );
		}

		if ( $subtype === 'tag' ) {
			$subtype = 'post_tag';
			$reason  = Indexing_Reasons::REASON_TAG_BASE_PREFIX;
		}

		if ( $subtype === 'category' ) {
			$reason = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX;
		}

		$this->indexable_helper->reset_permalink_indexables( 'term', $subtype, $reason );
	}

	/**
	 * Resets the permalink indexables automatically, if necessary.
	 *
	 * @return bool Whether the reset request ran.
	 */
	public function force_reset_permalinks() {
		if ( \get_option( 'tag_base' ) !== $this->options_helper->get( 'tag_base_url' ) ) {
			$this->reset_permalinks_term( null, null, 'tag_base' );
			$this->options_helper->set( 'tag_base_url', \get_option( 'tag_base' ) );
		}
		if ( \get_option( 'category_base' ) !== $this->options_helper->get( 'category_base_url' ) ) {
			$this->reset_permalinks_term( null, null, 'category_base' );
			$this->options_helper->set( 'category_base_url', \get_option( 'category_base' ) );
		}

		if ( $this->should_reset_permalinks() ) {
			$this->reset_permalinks();

			return true;
		}

		$this->reset_altered_custom_taxonomies();

		return true;
	}

	/**
	 * Checks whether the permalinks should be reset after `permalink_structure` has changed.
	 *
	 * @return bool Whether the permalinks should be reset.
	 */
	public function should_reset_permalinks() {
		return \get_option( 'permalink_structure' ) !== $this->options_helper->get( 'permalink_structure' );
	}

	/**
	 * Resets custom taxonomies if their slugs have changed.
	 *
	 * @return void
	 */
	public function reset_altered_custom_taxonomies() {
		$taxonomies            = $this->taxonomy_helper->get_custom_taxonomies();
		$custom_taxonomy_bases = $this->options_helper->get( 'custom_taxonomy_slugs', [] );
		$new_taxonomy_bases    = [];

		foreach ( $taxonomies as $taxonomy ) {
			$taxonomy_slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy );

			$new_taxonomy_bases[ $taxonomy ] = $taxonomy_slug;

			if ( ! \array_key_exists( $taxonomy, $custom_taxonomy_bases ) ) {
				continue;
			}

			if ( $taxonomy_slug !== $custom_taxonomy_bases[ $taxonomy ] ) {
				$this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy );
			}
		}

		$this->options_helper->set( 'custom_taxonomy_slugs', $new_taxonomy_bases );
	}

	/**
	 * Retrieves a list with the public post types.
	 *
	 * @return array The post types.
	 */
	protected function get_post_types() {
		/**
		 * Filter: Gives the possibility to filter out post types.
		 *
		 * @param array $post_types The post type names.
		 *
		 * @return array The post types.
		 */
		$post_types = \apply_filters( 'wpseo_post_types_reset_permalinks', $this->post_type->get_public_post_types() );

		return $post_types;
	}

	/**
	 * Retrieves the taxonomies that belongs to the public post types.
	 *
	 * @param array $post_types The post types to get taxonomies for.
	 *
	 * @return array The retrieved taxonomies.
	 */
	protected function get_taxonomies_for_post_types( $post_types ) {
		$taxonomies = [];
		foreach ( $post_types as $post_type ) {
			$taxonomies[] = \get_object_taxonomies( $post_type, 'names' );
		}

		$taxonomies = \array_merge( [], ...$taxonomies );
		$taxonomies = \array_unique( $taxonomies );

		return $taxonomies;
	}

	/**
	 * Schedules the WP-Cron job to check the permalink_structure status.
	 *
	 * @return void
	 */
	protected function schedule_cron() {
		if ( \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) {
			return;
		}

		\wp_schedule_event( \time(), 'daily', 'wpseo_permalink_structure_check' );
	}

	/**
	 * Unschedules the WP-Cron job to check the permalink_structure status.
	 *
	 * @return void
	 */
	public function unschedule_cron() {
		if ( ! \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) {
			return;
		}

		\wp_clear_scheduled_hook( 'wpseo_permalink_structure_check' );
	}
}
addon-update-watcher.php000066600000016053151131531760011271 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

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

/**
 * Enables Yoast add-on auto updates when Yoast SEO is enabled and the other way around.
 *
 * Also removes the auto-update toggles from the Yoast SEO add-ons.
 */
class Addon_Update_Watcher implements Integration_Interface {

	/**
	 * ID string used by WordPress to identify the free plugin.
	 *
	 * @var string
	 */
	const WPSEO_FREE_PLUGIN_ID = 'wordpress-seo/wp-seo.php';

	/**
	 * A list of Yoast add-on identifiers.
	 *
	 * @var string[]
	 */
	const ADD_ON_PLUGIN_FILES = [
		'wordpress-seo-premium/wp-seo-premium.php',
		'wpseo-video/video-seo.php',
		'wpseo-local/local-seo.php', // When installing Local through a released zip, the path is different from the path on a dev environment.
		'wpseo-woocommerce/wpseo-woocommerce.php',
		'wpseo-news/wpseo-news.php',
		'acf-content-analysis-for-yoast-seo/yoast-acf-analysis.php', // When installing ACF for Yoast through a released zip, the path is different from the path on a dev environment.
	];

	/**
	 * Registers the hooks.
	 */
	public function register_hooks() {
		\add_action( 'add_site_option_auto_update_plugins', [ $this, 'call_toggle_auto_updates_with_empty_array' ], 10, 2 );
		\add_action( 'update_site_option_auto_update_plugins', [ $this, 'toggle_auto_updates_for_add_ons' ], 10, 3 );
		\add_filter( 'plugin_auto_update_setting_html', [ $this, 'replace_auto_update_toggles_of_addons' ], 10, 2 );
		\add_action( 'activated_plugin', [ $this, 'maybe_toggle_auto_updates_for_new_install' ] );
	}

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

	/**
	 * Replaces the auto-update toggle links for the Yoast add-ons
	 * with a text explaining that toggling the Yoast SEO auto-update setting
	 * automatically toggles the one for the setting for the add-ons as well.
	 *
	 * @param string $old_html The old HTML.
	 * @param string $plugin   The plugin.
	 *
	 * @return string The new HTML, with the auto-update toggle link replaced.
	 */
	public function replace_auto_update_toggles_of_addons( $old_html, $plugin ) {
		if ( ! \is_string( $old_html ) ) {
			return $old_html;
		}

		$not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true );

		if ( $not_a_yoast_addon ) {
			return $old_html;
		}

		$auto_updated_plugins = \get_site_option( 'auto_update_plugins' );

		if ( $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $auto_updated_plugins ) ) {
			return \sprintf(
				'<em>%s</em>',
				\sprintf(
				/* Translators: %1$s resolves to Yoast SEO. */
					\esc_html__( 'Auto-updates are enabled based on this setting for %1$s.', 'wordpress-seo' ),
					'Yoast SEO'
				)
			);
		}

		return \sprintf(
			'<em>%s</em>',
			\sprintf(
			/* Translators: %1$s resolves to Yoast SEO. */
				\esc_html__( 'Auto-updates are disabled based on this setting for %1$s.', 'wordpress-seo' ),
				'Yoast SEO'
			)
		);
	}

	/**
	 * Handles the situation where the auto_update_plugins option did not previously exist.
	 *
	 * @param string      $option The name of the option that is being created.
	 * @param array|mixed $value  The new (and first) value of the option that is being created.
	 */
	public function call_toggle_auto_updates_with_empty_array( $option, $value ) {
		if ( $option !== 'auto_update_plugins' ) {
			return;
		}

		$this->toggle_auto_updates_for_add_ons( $option, $value, [] );
	}

	/**
	 * Enables premium auto updates when free are enabled and the other way around.
	 *
	 * @param string $option    The name of the option that has been updated.
	 * @param array  $new_value The new value of the `auto_update_plugins` option.
	 * @param array  $old_value The old value of the `auto_update_plugins` option.
	 *
	 * @return void
	 */
	public function toggle_auto_updates_for_add_ons( $option, $new_value, $old_value ) {
		if ( $option !== 'auto_update_plugins' ) {
			// If future versions of WordPress change this filter's behavior, our behavior should stay consistent.
			return;
		}

		if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) {
			return;
		}

		$auto_updates_are_enabled  = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $new_value );
		$auto_updates_were_enabled = $this->are_auto_updates_enabled( self::WPSEO_FREE_PLUGIN_ID, $old_value );

		if ( $auto_updates_are_enabled === $auto_updates_were_enabled ) {
			// Auto-updates for Yoast SEO have stayed the same, so have neither been enabled or disabled.
			return;
		}

		$auto_updates_have_been_enabled = $auto_updates_are_enabled && ! $auto_updates_were_enabled;

		if ( $auto_updates_have_been_enabled ) {
			$this->enable_auto_updates_for_addons( $new_value );
			return;
		}
		else {
			$this->disable_auto_updates_for_addons( $new_value );
			return;
		}

		if ( ! $auto_updates_are_enabled ) {
			return;
		}

		$auto_updates_have_been_removed = false;
		foreach ( self::ADD_ON_PLUGIN_FILES as $addon ) {
			if ( ! $this->are_auto_updates_enabled( $addon, $new_value ) ) {
				$auto_updates_have_been_removed = true;
				break;
			}
		}

		if ( $auto_updates_have_been_removed ) {
			$this->enable_auto_updates_for_addons( $new_value );
		}
	}

	/**
	 * Trigger a change in the auto update detection whenever a new Yoast addon is activated.
	 *
	 * @param string $plugin The plugin that is activated.
	 *
	 * @return void
	 */
	public function maybe_toggle_auto_updates_for_new_install( $plugin ) {
		$not_a_yoast_addon = ! \in_array( $plugin, self::ADD_ON_PLUGIN_FILES, true );

		if ( $not_a_yoast_addon ) {
			return;
		}

		$enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
		$this->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
	}

	/**
	 * Enables auto-updates for all addons.
	 *
	 * @param string[] $auto_updated_plugins The current list of auto-updated plugins.
	 */
	protected function enable_auto_updates_for_addons( $auto_updated_plugins ) {
		$plugins = \array_unique( \array_merge( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) );
		\update_site_option( 'auto_update_plugins', $plugins );
	}

	/**
	 * Disables auto-updates for all addons.
	 *
	 * @param string[] $auto_updated_plugins The current list of auto-updated plugins.
	 */
	protected function disable_auto_updates_for_addons( $auto_updated_plugins ) {
		$plugins = \array_values( \array_diff( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) );
		\update_site_option( 'auto_update_plugins', $plugins );
	}

	/**
	 * Checks whether auto updates for a plugin are enabled.
	 *
	 * @param string $plugin_id            The plugin ID.
	 * @param array  $auto_updated_plugins The array of auto updated plugins.
	 *
	 * @return bool Whether auto updates for a plugin are enabled.
	 */
	protected function are_auto_updates_enabled( $plugin_id, $auto_updated_plugins ) {
		if ( $auto_updated_plugins === false || ! \is_array( $auto_updated_plugins ) ) {
			return false;
		}

		return \in_array( $plugin_id, $auto_updated_plugins, true );
	}
}
indexable-home-page-watcher.php000066600000006162151131531760012517 0ustar00<?php

namespace Yoast\WP\SEO\Integrations\Watchers;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Home page watcher to save the meta data to an Indexable.
 *
 * Watches the home page options to save the meta information when updated.
 */
class Indexable_Home_Page_Watcher implements Integration_Interface {

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

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

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

	/**
	 * Indexable_Home_Page_Watcher constructor.
	 *
	 * @param Indexable_Repository $repository The repository to use.
	 * @param Indexable_Builder    $builder    The post builder to use.
	 */
	public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) {
		$this->repository = $repository;
		$this->builder    = $builder;
	}

	/**
	 * Initializes the integration.
	 *
	 * This is the place to register hooks and filters.
	 */
	public function register_hooks() {
		\add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 15, 3 );
		\add_action( 'update_option_wpseo_social', [ $this, 'check_option' ], 15, 3 );
		\add_action( 'update_option_blog_public', [ $this, 'build_indexable' ] );
		\add_action( 'update_option_blogdescription', [ $this, 'build_indexable' ] );
	}

	/**
	 * Checks if the home page indexable needs to be rebuild based on option values.
	 *
	 * @param array  $old_value The old value of the option.
	 * @param array  $new_value The new value of the option.
	 * @param string $option    The name of the option.
	 *
	 * @return void
	 */
	public function check_option( $old_value, $new_value, $option ) {
		$relevant_keys = [
			'wpseo_titles' => [
				'title-home-wpseo',
				'breadcrumbs-home',
				'metadesc-home-wpseo',
				'open_graph_frontpage_title',
				'open_graph_frontpage_desc',
				'open_graph_frontpage_image',
			],
		];

		if ( ! isset( $relevant_keys[ $option ] ) ) {
			return;
		}

		foreach ( $relevant_keys[ $option ] as $key ) {
			// If both values aren't set they haven't changed.
			if ( ! isset( $old_value[ $key ] ) && ! isset( $new_value[ $key ] ) ) {
				continue;
			}

			// If the value was set but now isn't, is set but wasn't or is not the same it has changed.
			if ( ! isset( $old_value[ $key ] ) || ! isset( $new_value[ $key ] ) || $old_value[ $key ] !== $new_value[ $key ] ) {
				$this->build_indexable();
				return;
			}
		}
	}

	/**
	 * Saves the home page.
	 *
	 * @return void
	 */
	public function build_indexable() {
		$indexable = $this->repository->find_for_home_page( false );
		$indexable = $this->builder->build_for_home_page( $indexable );

		if ( $indexable ) {
			$indexable->object_last_modified = \max( $indexable->object_last_modified, \current_time( 'mysql' ) );
			$indexable->save();
		}
	}
}
.htaccess000066600000000424151145143630006351 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>