Файловый менеджер - Редактировать - /home/kunzqhe/photostocker/2/integrations.tar
Ðазад
third-party/the-events-calendar.php 0000644 00000002447 15154134502 0013364 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional; use Yoast\WP\SEO\Conditionals\The_Events_Calendar_Conditional; use Yoast\WP\SEO\Generators\Schema\Third_Party\Events_Calendar_Schema; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class The_Events_Calendar */ class The_Events_Calendar implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class, The_Events_Calendar_Conditional::class, Open_Graph_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_schema_graph_pieces', [ $this, 'add_graph_pieces' ], 11, 2 ); } /** * Adds the events graph pieces to the schema collector. * * @param array $pieces The current graph pieces. * @param string $context The current context. * * @return array Extended graph pieces. */ public function add_graph_pieces( $pieces, $context ) { $pieces[] = new Events_Calendar_Schema( $context ); return $pieces; } } third-party/woocommerce-permalinks.php 0000644 00000005576 15154134502 0014223 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * The permalink watcher. */ class Woocommerce_Permalinks implements Integration_Interface { /** * Represents the indexable helper. * * @var Indexable_Helper */ protected $indexable_helper; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ WooCommerce_Conditional::class, Migrations_Conditional::class ]; } /** * Constructor. * * @param Indexable_Helper $indexable_helper Indexable Helper. */ public function __construct( Indexable_Helper $indexable_helper ) { $this->indexable_helper = $indexable_helper; } /** * Registers the hooks. * * @codeCoverageIgnore */ public function register_hooks() { \add_filter( 'wpseo_post_types_reset_permalinks', [ $this, 'filter_product_from_post_types' ] ); \add_action( 'update_option_woocommerce_permalinks', [ $this, 'reset_woocommerce_permalinks' ], 10, 2 ); } /** * Filters the product post type from the post type. * * @param array $post_types The post types to filter. * * @return array The filtered post types. */ public function filter_product_from_post_types( $post_types ) { unset( $post_types['product'] ); return $post_types; } /** * Resets the indexables for WooCommerce based on the changed permalink fields. * * @param array $old The old value. * @param array $new The new value. */ public function reset_woocommerce_permalinks( $old, $new ) { $changed_options = \array_diff( $old, $new ); if ( \array_key_exists( 'product_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'post', 'product' ); } if ( \array_key_exists( 'attribute_base', $changed_options ) ) { $attribute_taxonomies = $this->get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $attribute_name ) { $this->indexable_helper->reset_permalink_indexables( 'term', $attribute_name ); } } if ( \array_key_exists( 'category_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'term', 'product_cat' ); } if ( \array_key_exists( 'tag_base', $changed_options ) ) { $this->indexable_helper->reset_permalink_indexables( 'term', 'product_tag' ); } } /** * Retrieves the taxonomies based on the attributes. * * @return array The taxonomies. */ protected function get_attribute_taxonomies() { $taxonomies = []; foreach ( \wc_get_attribute_taxonomies() as $attribute_taxonomy ) { $taxonomies[] = \wc_attribute_taxonomy_name( $attribute_taxonomy->attribute_name ); } $taxonomies = \array_filter( $taxonomies ); return $taxonomies; } } third-party/web-stories.php 0000644 00000006363 15154134502 0011777 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Web_Stories_Conditional; use Yoast\WP\SEO\Integrations\Front_End_Integration; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Presentations\Indexable_Presentation; /** * Web Stories integration. */ class Web_Stories implements Integration_Interface { /** * The front end integration. * * @var Front_End_Integration */ protected $front_end; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Web_Stories_Conditional::class ]; } /** * Constructs the Web Stories integration * * @param Front_End_Integration $front_end The front end integration. */ public function __construct( Front_End_Integration $front_end ) { $this->front_end = $front_end; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'web_stories_enable_metadata', '__return_false' ); \add_action( 'web_stories_enable_schemaorg_metadata', '__return_false' ); \add_action( 'web_stories_enable_open_graph_metadata', '__return_false' ); \add_action( 'web_stories_enable_twitter_metadata', '__return_false' ); \add_action( 'web_stories_story_head', [ $this, 'web_stories_story_head' ], 1 ); \add_filter( 'wpseo_schema_article_post_types', [ $this, 'filter_schema_article_post_types' ] ); \add_filter( 'wpseo_schema_article_type', [ $this, 'filter_schema_article_type' ], 10, 2 ); \add_filter( 'wpseo_metadesc', [ $this, 'filter_meta_description' ], 10, 2 ); } /** * Hooks into web story <head> generation to modify output. * * @return void */ public function web_stories_story_head() { \remove_action( 'web_stories_story_head', 'rel_canonical' ); \add_action( 'web_stories_story_head', [ $this->front_end, 'call_wpseo_head' ], 9 ); } /** * Adds web story post type to list of which post types to output Article schema for. * * @param string[] $post_types Array of post types. * @return string[] Array of post types. */ public function filter_schema_article_post_types( $post_types ) { $post_types[] = 'web-story'; return $post_types; } /** * Filters the meta description for stories. * * @param string $description The description sentence. * @param Indexable_Presentation $presentation The presentation of an indexable. * @return string The description sentence. */ public function filter_meta_description( $description, $presentation ) { if ( $description || $presentation->model->object_sub_type !== 'web-story' ) { return $description; } return \get_the_excerpt( $presentation->model->object_id ); } /** * Filters Article type for Web Stories. * * @param string|string[] $type The Article type. * @param Indexable $indexable The indexable. * @return string|string[] Article type. */ public function filter_schema_article_type( $type, $indexable ) { if ( $indexable->object_sub_type !== 'web-story' ) { return $type; } if ( \is_string( $type ) && $type === 'None' ) { return 'Article'; } return $type; } } third-party/bbpress.php 0000644 00000002651 15154134502 0011170 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * BbPress integration. */ class BbPress implements Integration_Interface { /** * The options helper. * * @var Options_Helper */ private $options; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * BbPress constructor. * * @codeCoverageIgnore It only sets dependencies. * * @param Options_Helper $options The options helper. */ public function __construct( Options_Helper $options ) { $this->options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options->get( 'breadcrumbs-enable' ) !== true ) { return; } /** * If breadcrumbs are active (which they supposedly are if the users has enabled this settings, * there's no reason to have bbPress breadcrumbs as well. * * {@internal The class itself is only loaded when the template tag is encountered * via the template tag function in the wpseo-functions.php file.}} */ \add_filter( 'bbp_get_breadcrumb', '__return_false' ); } } third-party/exclude-elementor-post-types.php 0000644 00000001761 15154134502 0015277 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Activated_Conditional; use Yoast\WP\SEO\Integrations\Abstract_Exclude_Post_Type; /** * Excludes certain Elementor-specific post types from the indexable table. * * Posts with these post types will not be saved to the indexable table. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded */ class Exclude_Elementor_Post_Types extends Abstract_Exclude_Post_Type { /** * This integration is only active when the Elementor plugin * is installed and activated. * * @return array|string[] The conditionals. */ public static function get_conditionals() { return [ Elementor_Activated_Conditional::class ]; } /** * Returns the names of the post types to be excluded. * To be used in the wpseo_indexable_excluded_post_types filter. * * @return array The names of the post types. */ public function get_post_type() { return [ 'elementor_library' ]; } } third-party/exclude-woocommerce-post-types.php 0000644 00000001724 15154134502 0015623 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Integrations\Abstract_Exclude_Post_Type; /** * Excludes certain WooCommerce-specific post types from the indexable table. * * Posts with these post types will not be saved to the indexable table. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded */ class Exclude_WooCommerce_Post_Types extends Abstract_Exclude_Post_Type { /** * This integration is only active when the WooCommerce plugin * is installed and activated. * * @return array|string[] The conditionals. */ public static function get_conditionals() { return [ WooCommerce_Conditional::class ]; } /** * Returns the names of the post types to be excluded. * To be used in the wpseo_indexable_excluded_post_types filter. * * @return array The names of the post types. */ public function get_post_type() { return [ 'shop_order' ]; } } third-party/w3-total-cache.php 0000644 00000001474 15154134502 0012245 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Third_Party\W3_Total_Cache_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * W3 Total Cache integration. */ class W3_Total_Cache implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ W3_Total_Cache_Conditional::class ]; } /** * Initializes the integration. * * On successful update/add of the taxonomy meta option, flush the W3TC cache. * * @return void */ public function register_hooks() { \add_action( 'add_option_wpseo_taxonomy_meta', 'w3tc_objectcache_flush' ); \add_action( 'update_option_wpseo_taxonomy_meta', 'w3tc_objectcache_flush' ); } } third-party/wpml.php 0000644 00000003526 15154134502 0010511 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * WPML integration. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Known false positive with acronyms. Fix expected in YoastCS 3.x. */ class WPML implements Integration_Interface { /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wpseo_home_url', [ $this, 'filter_home_url_before' ] ); \add_filter( 'home_url', [ $this, 'filter_home_url_after' ], 100 ); } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ WPML_Conditional::class ]; } /** * Adds a filter to WPML's wpml_get_home_url filter to ensure we get the unmanipulated home URL. */ public function filter_home_url_before() { \add_filter( 'wpml_get_home_url', [ $this, 'wpml_get_home_url' ], 10, 2 ); } /** * Removes the wpml_get_home_url filter to return the WPML, language-enriched home URL. * * @param string $home_url The filtered home URL. * * @return string The unfiltered home URL. */ public function filter_home_url_after( $home_url ) { \remove_filter( 'wpml_get_home_url', [ $this, 'wpml_get_home_url' ], 10 ); return $home_url; } /** * Returns the original URL instead of the language-enriched URL. * This method gets automatically triggered by the wpml_get_home_url filter. * * @param string $home_url The url altered by WPML. Unused. * @param string $url The url that isn't altered by WPML. * * @return string The original url. */ public function wpml_get_home_url( $home_url, $url ) { return $url; } } third-party/amp.php 0000644 00000002723 15154134502 0010305 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Integrations\Front_End_Integration; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * AMP integration. */ class AMP implements Integration_Interface { /** * The front end integration. * * @var Front_End_Integration */ protected $front_end; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Constructs the AMP integration * * @param Front_End_Integration $front_end The front end integration. */ public function __construct( Front_End_Integration $front_end ) { $this->front_end = $front_end; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'amp_post_template_head', [ $this, 'remove_amp_meta_output' ], 0 ); \add_action( 'amp_post_template_head', [ $this->front_end, 'call_wpseo_head' ], 9 ); } /** * Removes amp meta output. * * @return void */ public function remove_amp_meta_output() { \remove_action( 'amp_post_template_head', 'amp_post_template_add_title' ); \remove_action( 'amp_post_template_head', 'amp_post_template_add_canonical' ); \remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' ); } } third-party/jetpack.php 0000644 00000001504 15154134502 0011145 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Conditionals\Jetpack_Conditional; use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Jetpack integration. */ class Jetpack implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class, Jetpack_Conditional::class, Open_Graph_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'jetpack_enable_open_graph', '__return_false' ); } } third-party/elementor.php 0000644 00000055055 15154134502 0011530 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Elementor\Controls_Manager; use Elementor\Core\DocumentTypes\PageBase; use WP_Post; use WP_Screen; use WPSEO_Admin_Asset_Manager; use WPSEO_Admin_Asset_Yoast_Components_L10n; use WPSEO_Admin_Recommended_Replace_Vars; use WPSEO_Language_Utils; use WPSEO_Meta; use WPSEO_Metabox_Analysis_Readability; use WPSEO_Metabox_Analysis_SEO; use WPSEO_Metabox_Formatter; use WPSEO_Post_Metabox_Formatter; use WPSEO_Replace_Vars; use WPSEO_Utils; use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional; use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional; use Yoast\WP\SEO\Helpers\Capability_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Meta_Fields_Presenter; /** * Integrates the Yoast SEO metabox in the Elementor editor. */ class Elementor implements Integration_Interface { /** * The identifier for the elementor tab. */ const YOAST_TAB = 'yoast-tab'; /** * Represents the post. * * @var WP_Post|null */ protected $post; /** * Represents the admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Represents the options helper. * * @var Options_Helper */ protected $options; /** * Represents the capability helper. * * @var Capability_Helper */ protected $capability; /** * Holds whether the socials are enabled. * * @var bool */ protected $social_is_enabled; /** * Holds whether the advanced settings are enabled. * * @var bool */ protected $is_advanced_metadata_enabled; /** * Helper to determine whether or not the SEO analysis is enabled. * * @var WPSEO_Metabox_Analysis_SEO */ protected $seo_analysis; /** * Helper to determine whether or not the readability analysis is enabled. * * @var WPSEO_Metabox_Analysis_Readability */ protected $readability_analysis; /** * Represents the estimated_reading_time_conditional. * * @var Estimated_Reading_Time_Conditional */ protected $estimated_reading_time_conditional; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Elementor_Edit_Conditional::class ]; } /** * Constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. * @param Options_Helper $options The options helper. * @param Capability_Helper $capability The capability helper. * @param Estimated_Reading_Time_Conditional $estimated_reading_time_conditional The Estimated Reading Time * conditional. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Options_Helper $options, Capability_Helper $capability, Estimated_Reading_Time_Conditional $estimated_reading_time_conditional ) { $this->asset_manager = $asset_manager; $this->options = $options; $this->capability = $capability; $this->seo_analysis = new WPSEO_Metabox_Analysis_SEO(); $this->readability_analysis = new WPSEO_Metabox_Analysis_Readability(); $this->social_is_enabled = $this->options->get( 'opengraph', false ) || $this->options->get( 'twitter', false ); $this->is_advanced_metadata_enabled = $this->capability->current_user_can( 'wpseo_edit_advanced_metadata' ) || $this->options->get( 'disableadvanced_meta' ) === false; $this->estimated_reading_time_conditional = $estimated_reading_time_conditional; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp_ajax_wpseo_elementor_save', [ $this, 'save_postdata' ] ); // We need to delay the post type lookup to give other plugins a chance to register custom post types. \add_action( 'init', [ $this, 'register_elementor_hooks' ], \PHP_INT_MAX ); } /** * Registers our Elementor hooks. */ public function register_elementor_hooks() { if ( ! $this->display_metabox( $this->get_metabox_post()->post_type ) ) { return; } \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'init' ] ); // We are too late for elementor/init. We should see if we can be on time, or else this workaround works (we do always get the "else" though). if ( ! \did_action( 'elementor/init' ) ) { \add_action( 'elementor/init', [ $this, 'add_yoast_panel_tab' ] ); } else { $this->add_yoast_panel_tab(); } \add_action( 'elementor/documents/register_controls', [ $this, 'register_document_controls' ] ); } /** * Initializes the integration. * * @return void */ public function init() { $this->asset_manager->register_assets(); $this->enqueue(); $this->render_hidden_fields(); } /** * Register a panel tab slug, in order to allow adding controls to this tab. */ public function add_yoast_panel_tab() { Controls_Manager::add_tab( $this::YOAST_TAB, \__( 'Yoast SEO', 'wordpress-seo' ) ); } /** * Register additional document controls. * * @param PageBase $document The PageBase document. */ public function register_document_controls( $document ) { // PageBase is the base class for documents like `post` `page` and etc. if ( ! $document instanceof PageBase || ! $document::get_property( 'has_elements' ) ) { return; } // This is needed to get the tab to appear, but will be overwritten in the JavaScript. $document->start_controls_section( 'yoast_temporary_section', [ 'label' => \__( 'Yoast SEO', 'wordpress-seo' ), 'tab' => self::YOAST_TAB, ] ); $document->end_controls_section(); } // Below is mostly copied from `class-metabox.php`. That constructor has side-effects we do not need. /** * Determines whether the metabox should be shown for the passed identifier. * * By default the check is done for post types, but can also be used for taxonomies. * * @param string|null $identifier The identifier to check. * @param string $type The type of object to check. Defaults to post_type. * * @return bool Whether or not the metabox should be displayed. */ public function display_metabox( $identifier = null, $type = 'post_type' ) { return WPSEO_Utils::is_metabox_active( $identifier, $type ); } /** * Saves the WP SEO metadata for posts. * * Outputs JSON via wp_send_json then stops code execution. * * {@internal $_POST parameters are validated via sanitize_post_meta().}} * * @return void */ public function save_postdata() { global $post; $post_id = \filter_input( \INPUT_POST, 'post_id', \FILTER_SANITIZE_NUMBER_INT ); if ( ! \current_user_can( 'manage_options' ) ) { \wp_send_json_error( 'Unauthorized', 401 ); } \check_ajax_referer( 'wpseo_elementor_save', '_wpseo_elementor_nonce' ); // Bail if this is a multisite installation and the site has been switched. if ( \is_multisite() && \ms_is_switched() ) { \wp_send_json_error( 'Switched multisite', 409 ); } \clean_post_cache( $post_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly. $post = \get_post( $post_id ); if ( ! \is_object( $post ) ) { // Non-existent post. \wp_send_json_error( 'Post not found', 400 ); } \do_action( 'wpseo_save_compare_data', $post ); // Initialize meta, amongst other things it registers sanitization. WPSEO_Meta::init(); $social_fields = []; if ( $this->social_is_enabled ) { $social_fields = WPSEO_Meta::get_meta_field_defs( 'social', $post->post_type ); } // The below methods use the global post so make sure it is setup. \setup_postdata( $post ); $meta_boxes = \apply_filters( 'wpseo_save_metaboxes', [] ); $meta_boxes = \array_merge( $meta_boxes, WPSEO_Meta::get_meta_field_defs( 'general', $post->post_type ), WPSEO_Meta::get_meta_field_defs( 'advanced', $post->post_type ), $social_fields, WPSEO_Meta::get_meta_field_defs( 'schema', $post->post_type ) ); foreach ( $meta_boxes as $key => $meta_box ) { // If analysis is disabled remove that analysis score value from the DB. if ( $this->is_meta_value_disabled( $key ) ) { WPSEO_Meta::delete( $key, $post_id ); continue; } $data = null; $field_name = WPSEO_Meta::$form_prefix . $key; if ( $meta_box['type'] === 'checkbox' ) { $data = isset( $_POST[ $field_name ] ) ? 'on' : 'off'; } else { if ( isset( $_POST[ $field_name ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Sanitized through sanitize_post_meta. $data = \wp_unslash( $_POST[ $field_name ] ); // For multi-select. if ( \is_array( $data ) ) { $data = \array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $data ); } if ( \is_string( $data ) ) { $data = ( $key !== 'canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data ); } } // Reset options when no entry is present with multiselect - only applies to `meta-robots-adv` currently. if ( ! isset( $_POST[ $field_name ] ) && ( $meta_box['type'] === 'multiselect' ) ) { $data = []; } } if ( $data !== null ) { WPSEO_Meta::set_value( $key, $data, $post_id ); } } // Saving the WP post to save the slug. $slug = \filter_input( \INPUT_POST, WPSEO_Meta::$form_prefix . 'slug', \FILTER_SANITIZE_STRING ); if ( $post->post_name !== $slug ) { $post_array = $post->to_array(); $post_array['post_name'] = $slug; $save_successful = \wp_insert_post( $post_array ); if ( \is_wp_error( $save_successful ) ) { \wp_send_json_error( 'Slug not saved', 400 ); } // Update the post object to ensure we have the actual slug. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Updating the post is needed to get the current slug. $post = \get_post( $post_id ); if ( ! \is_object( $post ) ) { \wp_send_json_error( 'Updated slug not found', 400 ); } } \do_action( 'wpseo_saved_postdata' ); // Output the slug, because it is processed by WP and we need the actual slug again. \wp_send_json_success( [ 'slug' => $post->post_name ] ); } /** * Determines if the given meta value key is disabled. * * @param string $key The key of the meta value. * * @return bool Whether the given meta value key is disabled. */ public function is_meta_value_disabled( $key ) { if ( $key === 'linkdex' && ! $this->seo_analysis->is_enabled() ) { return true; } if ( $key === 'content_score' && ! $this->readability_analysis->is_enabled() ) { return true; } return false; } /** * Enqueues all the needed JS and CSS. * * @return void */ public function enqueue() { $post_id = \get_queried_object_id(); if ( empty( $post_id ) ) { $post_id = \sanitize_text_field( \filter_input( \INPUT_GET, 'post' ) ); } if ( $post_id !== 0 ) { // Enqueue files needed for upload functionality. \wp_enqueue_media( [ 'post' => $post_id ] ); } $this->asset_manager->enqueue_style( 'admin-global' ); $this->asset_manager->enqueue_style( 'metabox-css' ); $this->asset_manager->enqueue_style( 'scoring' ); $this->asset_manager->enqueue_style( 'select2' ); $this->asset_manager->enqueue_style( 'monorepo' ); $this->asset_manager->enqueue_style( 'admin-css' ); $this->asset_manager->enqueue_style( 'elementor' ); $this->asset_manager->enqueue_script( 'elementor' ); $yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n(); $yoast_components_l10n->localize_script( 'elementor' ); $this->asset_manager->localize_script( 'elementor', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() ); $this->asset_manager->localize_script( 'elementor', 'wpseoFeaturesL10n', WPSEO_Utils::retrieve_enabled_features() ); $plugins_script_data = [ 'replaceVars' => [ 'no_parent_text' => \__( '(no parent)', 'wordpress-seo' ), 'replace_vars' => $this->get_replace_vars(), 'recommended_replace_vars' => $this->get_recommended_replace_vars(), 'hidden_replace_vars' => $this->get_hidden_replace_vars(), 'scope' => $this->determine_scope(), 'has_taxonomies' => $this->current_post_type_has_taxonomies(), ], 'shortcodes' => [ 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ), 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(), ], ]; $worker_script_data = [ 'url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ), 'dependencies' => \YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ), 'keywords_assessment_url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ), 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(), // We need to make the feature flags separately available inside of the analysis web worker. 'enabled_features' => WPSEO_Utils::retrieve_enabled_features(), ]; $alert_dismissal_action = \YoastSEO()->classes->get( Alert_Dismissal_Action::class ); $dismissed_alerts = $alert_dismissal_action->all_dismissed(); $script_data = [ 'media' => [ 'choose_image' => \__( 'Use Image', 'wordpress-seo' ) ], 'metabox' => $this->get_metabox_script_data(), 'userLanguageCode' => WPSEO_Language_Utils::get_language( \get_user_locale() ), 'isPost' => true, 'isBlockEditor' => WP_Screen::get()->is_block_editor(), 'isElementorEditor' => true, 'analysis' => [ 'plugins' => $plugins_script_data, 'worker' => $worker_script_data, 'estimatedReadingTimeEnabled' => $this->estimated_reading_time_conditional->is_met(), ], 'dismissedAlerts' => $dismissed_alerts, ]; if ( \post_type_supports( $this->get_metabox_post()->post_type, 'thumbnail' ) ) { $this->asset_manager->enqueue_style( 'featured-image' ); $script_data['featuredImage'] = [ 'featured_image_notice' => \__( 'SEO issue: The featured image should be at least 200 by 200 pixels to be picked up by Facebook and other social media sites.', 'wordpress-seo' ), ]; } $this->asset_manager->localize_script( 'elementor', 'wpseoScriptData', $script_data ); $this->asset_manager->enqueue_user_language_script(); } /** * Renders the metabox hidden fields. * * @return void */ protected function render_hidden_fields() { // Wrap in a form with an action and post_id for the submit. \printf( '<form id="yoast-form" method="post" action="%1$s"><input type="hidden" name="action" value="wpseo_elementor_save" /><input type="hidden" id="post_ID" name="post_id" value="%2$s" />', \esc_url( \admin_url( 'admin-ajax.php' ) ), \esc_attr( $this->get_metabox_post()->ID ) ); \wp_nonce_field( 'wpseo_elementor_save', '_wpseo_elementor_nonce' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'general' ); if ( $this->is_advanced_metadata_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'advanced' ); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'schema', $this->get_metabox_post()->post_type ); if ( $this->social_is_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe. echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' ); } \printf( '<input type="hidden" id="%1$s" name="%1$s" value="%2$s" />', \esc_attr( WPSEO_Meta::$form_prefix . 'slug' ), \esc_attr( $this->get_metabox_post()->post_name ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be escaped in the filter. echo \apply_filters( 'wpseo_elementor_hidden_fields', '' ); echo '</form>'; } /** * Returns post in metabox context. * * @return WP_Post|null */ protected function get_metabox_post() { if ( $this->post !== null ) { return $this->post; } $post = \filter_input( \INPUT_GET, 'post' ); if ( ! empty( $post ) ) { $post_id = (int) WPSEO_Utils::validate_int( $post ); $this->post = \get_post( $post_id ); return $this->post; } if ( isset( $GLOBALS['post'] ) ) { $this->post = $GLOBALS['post']; return $this->post; } return null; } /** * Passes variables to js for use with the post-scraper. * * @return array */ protected function get_metabox_script_data() { $permalink = ''; if ( \is_object( $this->get_metabox_post() ) ) { $permalink = \get_sample_permalink( $this->get_metabox_post()->ID ); $permalink = $permalink[0]; } $post_formatter = new WPSEO_Metabox_Formatter( new WPSEO_Post_Metabox_Formatter( $this->get_metabox_post(), [], $permalink ) ); $values = $post_formatter->get_values(); /** This filter is documented in admin/filters/class-cornerstone-filter.php. */ $post_types = \apply_filters( 'wpseo_cornerstone_post_types', \YoastSEO()->helpers->post_type->get_accessible_post_types() ); if ( $values['cornerstoneActive'] && ! \in_array( $this->get_metabox_post()->post_type, $post_types, true ) ) { $values['cornerstoneActive'] = false; } return $values; } /** * Prepares the replace vars for localization. * * @return array Replace vars. */ protected function get_replace_vars() { $cached_replacement_vars = []; $vars_to_cache = [ 'date', 'id', 'sitename', 'sitedesc', 'sep', 'page', 'currentyear', 'currentdate', 'currentmonth', 'currentday', 'tag', 'category', 'category_title', 'primary_category', 'pt_single', 'pt_plural', 'modified', 'name', 'user_description', 'pagetotal', 'pagenumber', 'post_year', 'post_month', 'post_day', 'author_first_name', 'author_last_name', 'permalink', 'post_content', ]; foreach ( $vars_to_cache as $var ) { $cached_replacement_vars[ $var ] = \wpseo_replace_vars( '%%' . $var . '%%', $this->get_metabox_post() ); } // Merge custom replace variables with the WordPress ones. return \array_merge( $cached_replacement_vars, $this->get_custom_replace_vars( $this->get_metabox_post() ) ); } /** * Prepares the recommended replace vars for localization. * * @return array Recommended replacement variables. */ protected function get_recommended_replace_vars() { $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); // What is recommended depends on the current context. $post_type = $recommended_replace_vars->determine_for_post( $this->get_metabox_post() ); return $recommended_replace_vars->get_recommended_replacevars_for( $post_type ); } /** * Returns the list of replace vars that should be hidden inside the editor. * * @return string[] The hidden replace vars. */ protected function get_hidden_replace_vars() { return ( new WPSEO_Replace_Vars() )->get_hidden_replace_vars(); } /** * Gets the custom replace variables for custom taxonomies and fields. * * @param WP_Post $post The post to check for custom taxonomies and fields. * * @return array Array containing all the replacement variables. */ protected function get_custom_replace_vars( $post ) { return [ 'custom_fields' => $this->get_custom_fields_replace_vars( $post ), 'custom_taxonomies' => $this->get_custom_taxonomies_replace_vars( $post ), ]; } /** * Gets the custom replace variables for custom taxonomies. * * @param WP_Post $post The post to check for custom taxonomies. * * @return array Array containing all the replacement variables. */ protected function get_custom_taxonomies_replace_vars( $post ) { $taxonomies = \get_object_taxonomies( $post, 'objects' ); $custom_replace_vars = []; foreach ( $taxonomies as $taxonomy_name => $taxonomy ) { if ( \is_string( $taxonomy ) ) { // If attachment, see https://core.trac.wordpress.org/ticket/37368 . $taxonomy_name = $taxonomy; $taxonomy = \get_taxonomy( $taxonomy_name ); } if ( $taxonomy->_builtin && $taxonomy->public ) { continue; } $custom_replace_vars[ $taxonomy_name ] = [ 'name' => $taxonomy->name, 'description' => $taxonomy->description, ]; } return $custom_replace_vars; } /** * Gets the custom replace variables for custom fields. * * @param WP_Post $post The post to check for custom fields. * * @return array Array containing all the replacement variables. */ protected function get_custom_fields_replace_vars( $post ) { $custom_replace_vars = []; // If no post object is passed, return the empty custom_replace_vars array. if ( ! \is_object( $post ) ) { return $custom_replace_vars; } $custom_fields = \get_post_custom( $post->ID ); foreach ( $custom_fields as $custom_field_name => $custom_field ) { // Skip private custom fields. if ( \substr( $custom_field_name, 0, 1 ) === '_' ) { continue; } // Skip custom field values that are serialized. if ( \is_serialized( $custom_field[0] ) ) { continue; } $custom_replace_vars[ $custom_field_name ] = $custom_field[0]; } return $custom_replace_vars; } /** * Determines the scope based on the post type. * This can be used by the replacevar plugin to determine if a replacement needs to be executed. * * @return string String describing the current scope. */ protected function determine_scope() { if ( $this->get_metabox_post()->post_type === 'page' ) { return 'page'; } return 'post'; } /** * Determines whether or not the current post type has registered taxonomies. * * @return bool Whether the current post type has taxonomies. */ protected function current_post_type_has_taxonomies() { $post_taxonomies = \get_object_taxonomies( $this->get_metabox_post()->post_type ); return ! empty( $post_taxonomies ); } /** * Returns an array with shortcode tags for all registered shortcodes. * * @return array */ protected function get_valid_shortcode_tags() { $shortcode_tags = []; foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) { $shortcode_tags[] = $tag; } return $shortcode_tags; } } third-party/woocommerce.php 0000644 00000022511 15154134502 0012044 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use WPSEO_Replace_Vars; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Pagination_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Presentations\Indexable_Presentation; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * WooCommerce integration. */ class WooCommerce implements Integration_Interface { /** * The options helper. * * @var Options_Helper */ private $options; /** * The WPSEO Replace Vars object. * * @var WPSEO_Replace_Vars */ private $replace_vars; /** * The memoizer for the meta tags context. * * @var Meta_Tags_Context_Memoizer */ protected $context_memoizer; /** * The indexable repository. * * @var Indexable_Repository */ private $repository; /** * The pagination helper. * * @var Pagination_Helper */ protected $pagination_helper; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ WooCommerce_Conditional::class, Front_End_Conditional::class ]; } /** * WooCommerce constructor. * * @param Options_Helper $options The options helper. * @param WPSEO_Replace_Vars $replace_vars The replace vars helper. * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. * @param Indexable_Repository $repository The indexable repository. * @param Pagination_Helper $pagination_helper The paginataion helper. */ public function __construct( Options_Helper $options, WPSEO_Replace_Vars $replace_vars, Meta_Tags_Context_Memoizer $context_memoizer, Indexable_Repository $repository, Pagination_Helper $pagination_helper ) { $this->options = $options; $this->replace_vars = $replace_vars; $this->context_memoizer = $context_memoizer; $this->repository = $repository; $this->pagination_helper = $pagination_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_frontend_page_type_simple_page_id', [ $this, 'get_page_id' ] ); \add_filter( 'wpseo_breadcrumb_indexables', [ $this, 'add_shop_to_breadcrumbs' ] ); \add_filter( 'wpseo_title', [ $this, 'title' ], 10, 2 ); \add_filter( 'wpseo_metadesc', [ $this, 'description' ], 10, 2 ); \add_filter( 'wpseo_canonical', [ $this, 'canonical' ], 10, 2 ); \add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 10, 3 ); } /** * Returns the correct canonical when WooCommerce is enabled. * * @param string $canonical The current canonical. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The correct canonical. */ public function canonical( $canonical, $presentation = null ) { if ( ! $this->is_shop_page() ) { return $canonical; } $url = $this->get_shop_paginated_link( 'curr', $presentation ); if ( $url ) { return $url; } return $canonical; } /** * Returns correct adjacent pages when WooCommerce is enabled. * * @param string $link The current link. * @param string $rel Link relationship, prev or next. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The correct link. */ public function adjacent_rel_url( $link, $rel, $presentation = null ) { if ( ! $this->is_shop_page() ) { return $link; } if ( $rel !== 'next' && $rel !== 'prev' ) { return $link; } $url = $this->get_shop_paginated_link( $rel, $presentation ); if ( $url ) { return $url; } return $link; } /** * Adds a breadcrumb for the shop page. * * @param Indexable[] $indexables The array with indexables. * * @return Indexable[] The indexables to be shown in the breadcrumbs, with the shop page added. */ public function add_shop_to_breadcrumbs( $indexables ) { $shop_page_id = $this->get_shop_page_id(); if ( $shop_page_id < 1 ) { return $indexables; } foreach ( $indexables as $index => $indexable ) { if ( $indexable->object_type === 'post-type-archive' && $indexable->object_sub_type === 'product' ) { $indexables[ $index ] = $this->repository->find_by_id_and_type( $shop_page_id, 'post' ); } } return $indexables; } /** * Returns the ID of the WooCommerce shop page when the currently opened page is the shop page. * * @param int $page_id The page id. * * @return int The Page ID of the shop. */ public function get_page_id( $page_id ) { if ( ! $this->is_shop_page() ) { return $page_id; } return $this->get_shop_page_id(); } /** * Handles the title. * * @param string $title The title. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The title to use. */ public function title( $title, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); if ( $presentation->model->title ) { return $title; } if ( ! $this->is_shop_page() ) { return $title; } if ( ! \is_archive() ) { return $title; } $shop_page_id = $this->get_shop_page_id(); if ( $shop_page_id < 1 ) { return $title; } $product_template_title = $this->get_product_template( 'title-product', $shop_page_id ); if ( $product_template_title ) { return $product_template_title; } return $title; } /** * Handles the meta description. * * @param string $description The title. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string The description to use. */ public function description( $description, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); if ( $presentation->model->description ) { return $description; } if ( ! $this->is_shop_page() ) { return $description; } if ( ! \is_archive() ) { return $description; } $shop_page_id = $this->get_shop_page_id(); if ( $shop_page_id < 1 ) { return $description; } $product_template_description = $this->get_product_template( 'metadesc-product', $shop_page_id ); if ( $product_template_description ) { return $product_template_description; } return $description; } /** * Checks if the current page is a WooCommerce shop page. * * @return bool True when the page is a shop page. */ protected function is_shop_page() { if ( ! \is_shop() ) { return false; } if ( \is_search() ) { return false; } return true; } /** * Uses template for the given option name and replace the replacement variables on it. * * @param string $option_name The option name to get the template for. * @param string $shop_page_id The page id to retrieve template for. * * @return string The rendered value. */ protected function get_product_template( $option_name, $shop_page_id ) { $template = $this->options->get( $option_name ); $page = \get_post( $shop_page_id ); return $this->replace_vars->replace( $template, $page ); } /** * Returns the id of the set WooCommerce shop page. * * @return int The ID of the set page. */ protected function get_shop_page_id() { if ( ! \function_exists( 'wc_get_page_id' ) ) { return -1; } return \wc_get_page_id( 'shop' ); } /** * Get paginated link for shop page. * * @param string $rel Link relationship, prev or next or curr. * @param Indexable_Presentation|null $presentation The indexable presentation. * * @return string|null The link. */ protected function get_shop_paginated_link( $rel, $presentation = null ) { $presentation = $this->ensure_presentation( $presentation ); $permalink = $presentation->get_permalink(); if ( ! $permalink ) { return null; } $current_page = \max( 1, $this->pagination_helper->get_current_archive_page_number() ); if ( $rel === 'curr' && $current_page === 1 ) { return $permalink; } if ( $rel === 'curr' && $current_page > 1 ) { return $this->pagination_helper->get_paginated_url( $permalink, $current_page ); } if ( $rel === 'prev' && $current_page === 2 ) { return $permalink; } if ( $rel === 'prev' && $current_page > 2 ) { return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page - 1 ) ); } if ( $rel === 'next' && $current_page < $this->pagination_helper->get_number_of_archive_pages() ) { return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page + 1 ) ); } return null; } /** * Ensures a presentation is available. * * @param Indexable_Presentation $presentation The indexable presentation. * * @return Indexable_Presentation The presentation, taken from the current page if the input was invalid. */ protected function ensure_presentation( $presentation ) { if ( \is_a( $presentation, Indexable_Presentation::class ) ) { return $presentation; } $context = $this->context_memoizer->for_current_page(); return $context->presentation; } } third-party/wpml-wpseo-notification.php 0000644 00000006576 15154134502 0014340 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Third_Party; use Yoast\WP\SEO\Conditionals\Third_Party\WPML_Conditional; use Yoast\WP\SEO\Conditionals\Third_Party\WPML_WPSEO_Conditional; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast_Notification; use Yoast_Notification_Center; /** * Adds a notification to the dashboard if the WPML plugin is installed, * but the Yoast SEO Multilingual plugin (a glue plugin to make Yoast SEO and WPML work nicely together) * is not. */ class WPML_WPSEO_Notification implements Integration_Interface { /** * The notification ID. * * @internal */ const NOTIFICATION_ID = 'wpml-wpseo-not-installed'; /** * The short link helper. * * @var Short_Link_Helper */ protected $short_link_helper; /** * The notification center. * * @var Yoast_Notification_Center */ protected $notification_center; /** * The WPML WPSEO conditional. * * @var WPML_WPSEO_Conditional */ protected $wpml_wpseo_conditional; /** * WPML WPSEO notification constructor. * * @param Short_Link_Helper $short_link_helper The short link helper. * @param Yoast_Notification_Center $notification_center The notification center. * @param WPML_WPSEO_Conditional $wpml_wpseo_conditional The WPML WPSEO conditional. */ public function __construct( Short_Link_Helper $short_link_helper, Yoast_Notification_Center $notification_center, WPML_WPSEO_Conditional $wpml_wpseo_conditional ) { $this->short_link_helper = $short_link_helper; $this->notification_center = $notification_center; $this->wpml_wpseo_conditional = $wpml_wpseo_conditional; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_notices', [ $this, 'notify_not_installed' ] ); } /** * Returns the conditionals based in which this loadable should be active. * * This integration should only be active when WPML is installed and activated. * * @return array The conditionals. */ public static function get_conditionals() { return [ WPML_Conditional::class ]; } /** * Notify the user that the Yoast SEO Multilingual plugin is not installed * (when the WPML plugin is installed). * * Remove the notification again when it is installed. * * @return void */ public function notify_not_installed() { if ( ! $this->wpml_wpseo_conditional->is_met() ) { $this->notification_center->add_notification( $this->get_notification() ); return; } $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Generates the notification to show to the user when WPML is installed, * but the Yoast SEO Multilingual plugin is not. * * @return Yoast_Notification The notification. */ protected function get_notification() { return new Yoast_Notification( \sprintf( /* translators: %1$s expands to an opening anchor tag, %2$s expands to an closing anchor tag. */ \__( 'We notice that you have installed WPML. To make sure your canonical URLs are set correctly, %1$sinstall and activate the Yoast SEO Multilingual add-on%2$s as well!', 'wordpress-seo' ), '<a href="' . \esc_url( $this->short_link_helper->get( 'https://yoa.st/wpml-yoast-seo' ) ) . '" target="_blank">', '</a>' ), [ 'id' => self::NOTIFICATION_ID, 'type' => Yoast_Notification::WARNING, ] ); } } breadcrumbs-integration.php 0000644 00000004000 15154134502 0012061 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use WPSEO_Replace_Vars; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter; use Yoast\WP\SEO\Surfaces\Helpers_Surface; /** * Adds customizations to the front end for breadcrumbs. */ class Breadcrumbs_Integration implements Integration_Interface { /** * The breadcrumbs presenter. * * @var Breadcrumbs_Presenter */ private $presenter; /** * The meta tags context memoizer. * * @var Meta_Tags_Context_Memoizer */ private $context_memoizer; /** * Breadcrumbs integration constructor. * * @param Helpers_Surface $helpers The helpers. * @param WPSEO_Replace_Vars $replace_vars The replace vars. * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. */ public function __construct( Helpers_Surface $helpers, WPSEO_Replace_Vars $replace_vars, Meta_Tags_Context_Memoizer $context_memoizer ) { $this->context_memoizer = $context_memoizer; $this->presenter = new Breadcrumbs_Presenter(); $this->presenter->helpers = $helpers; $this->presenter->replace_vars = $replace_vars; } /** * Returns the conditionals based in which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return []; } /** * Registers the `wpseo_breadcrumb` shortcode. * * @codeCoverageIgnore */ public function register_hooks() { \add_shortcode( 'wpseo_breadcrumb', [ $this, 'render' ] ); } /** * Renders the breadcrumbs. * * @return string The rendered breadcrumbs. */ public function render() { $context = $this->context_memoizer->for_current_page(); /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $this->presenter->presentation = $presentation; return $this->presenter->present(); } } front-end/rss-footer-embed.php 0000644 00000012334 15154134502 0012331 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class RSS_Footer_Embed. */ class RSS_Footer_Embed implements Integration_Interface { /** * The options helper. * * @var Options_Helper */ protected $options; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Sets the required helpers. * * @codeCoverageIgnore It only handles dependencies. * * @param Options_Helper $options The options helper. */ public function __construct( Options_Helper $options ) { $this->options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'the_content_feed', [ $this, 'embed_rssfooter' ] ); \add_filter( 'the_excerpt_rss', [ $this, 'embed_rssfooter_excerpt' ] ); } /** * Adds the RSS footer (or header) to the full RSS feed item. * * @param string $content Feed item content. * * @return string */ public function embed_rssfooter( $content ) { if ( ! $this->include_rss_footer( 'full' ) ) { return $content; } return $this->embed_rss( $content ); } /** * Adds the RSS footer (or header) to the excerpt RSS feed item. * * @param string $content Feed item excerpt. * * @return string */ public function embed_rssfooter_excerpt( $content ) { if ( ! $this->include_rss_footer( 'excerpt' ) ) { return $content; } return $this->embed_rss( \wpautop( $content ) ); } /** * Checks if the RSS footer should included. * * @param string $context The context of the RSS content. * * @return bool Whether or not the RSS footer should included. */ protected function include_rss_footer( $context ) { if ( ! \is_feed() ) { return false; } /** * Filter: 'wpseo_include_rss_footer' - Allow the RSS footer to be dynamically shown/hidden. * * @api boolean $show_embed Indicates if the RSS footer should be shown or not. * * @param string $context The context of the RSS content - 'full' or 'excerpt'. */ if ( ! \apply_filters( 'wpseo_include_rss_footer', true, $context ) ) { return false; } return $this->is_configured(); } /** * Checks if the RSS feed fields are configured. * * @return bool True when one of the fields has a value. */ protected function is_configured() { return ( $this->options->get( 'rssbefore', '' ) !== '' || $this->options->get( 'rssafter', '' ) ); } /** * Adds the RSS footer and/or header to an RSS feed item. * * @param string $content Feed item content. * * @return string The content to add. */ protected function embed_rss( $content ) { $before = $this->rss_replace_vars( $this->options->get( 'rssbefore', '' ) ); $after = $this->rss_replace_vars( $this->options->get( 'rssafter', '' ) ); $content = $before . $content . $after; return $content; } /** * Replaces the possible RSS variables with their actual values. * * @param string $content The RSS content that should have the variables replaced. * * @return string */ protected function rss_replace_vars( $content ) { if ( $content === '' ) { return $content; } $replace_vars = $this->get_replace_vars( $this->get_link_template(), \get_post() ); $content = \stripslashes( \trim( $content ) ); $content = \str_ireplace( \array_keys( $replace_vars ), \array_values( $replace_vars ), $content ); return \wpautop( $content ); } /** * Retrieves the replacement variables. * * @codeCoverageIgnore It just contains too much WordPress functions. * * @param string $link_template The link template. * @param mixed $post The post to use. * * @return array The replacement variables. */ protected function get_replace_vars( $link_template, $post ) { $author_link = ''; if ( \is_object( $post ) ) { $author_link = \sprintf( $link_template, \esc_url( \get_author_posts_url( $post->post_author ) ), \esc_html( \get_the_author() ) ); } return [ '%%AUTHORLINK%%' => $author_link, '%%POSTLINK%%' => \sprintf( $link_template, \esc_url( \get_permalink() ), \esc_html( \get_the_title() ) ), '%%BLOGLINK%%' => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) ), '%%BLOGDESCLINK%%' => \sprintf( $link_template, \esc_url( \get_bloginfo( 'url' ) ), \esc_html( \get_bloginfo( 'name' ) ) . ' - ' . \esc_html( \get_bloginfo( 'description' ) ) ), ]; } /** * Retrieves the link template. * * @return string The link template. */ protected function get_link_template() { /** * Filter: 'nofollow_rss_links' - Allow the developer to determine whether or not to follow the links in * the bits Yoast SEO adds to the RSS feed, defaults to true. * * @api bool $unsigned Whether or not to follow the links in RSS feed, defaults to true. * * @since 1.4.20 */ if ( \apply_filters( 'nofollow_rss_links', true ) ) { return '<a rel="nofollow" href="%1$s">%2$s</a>'; } return '<a href="%1$s">%2$s</a>'; } } front-end/category-term-description.php 0000644 00000002434 15154134502 0014257 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Adds support for shortcodes to category and term descriptions. */ class Category_Term_Description implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'category_description', [ $this, 'add_shortcode_support' ] ); \add_filter( 'term_description', [ $this, 'add_shortcode_support' ] ); } /** * Adds shortcode support to category and term descriptions. * * This methods wrap in output buffering to prevent shortcodes that echo stuff * instead of return from breaking things. * * @param string $description String to add shortcodes in. * * @return string Content with shortcodes filtered out. */ public function add_shortcode_support( $description ) { \ob_start(); $description = \do_shortcode( $description ); \ob_end_clean(); return $description; } } front-end/wp-robots-integration.php 0000644 00000013010 15154134502 0013421 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Conditionals\WP_Robots_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presenters\Robots_Presenter; /** * Class WP_Robots_Integration * * @package Yoast\WP\SEO\Integrations\Front_End */ class WP_Robots_Integration implements Integration_Interface { /** * The meta tags context memoizer. * * @var Meta_Tags_Context_Memoizer */ protected $context_memoizer; /** * Sets the dependencies for this integration. * * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. */ public function __construct( Meta_Tags_Context_Memoizer $context_memoizer ) { $this->context_memoizer = $context_memoizer; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { /** * Allow control of the `wp_robots` filter by prioritizing our hook 10 less than max. * Use the `wpseo_robots` filter to filter the Yoast robots output, instead of WordPress core. */ \add_filter( 'wp_robots', [ $this, 'add_robots' ], ( \PHP_INT_MAX - 10 ) ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class, WP_Robots_Conditional::class, ]; } /** * Adds our robots tag value to the WordPress robots tag output. * * @param array $robots The current robots data. * * @return array The robots data. */ public function add_robots( $robots ) { if ( ! \is_array( $robots ) ) { return $this->get_robots_value(); } $merged_robots = \array_merge( $robots, $this->get_robots_value() ); $filtered_robots = $this->enforce_robots_congruence( $merged_robots ); $sorted_robots = $this->sort_robots( $filtered_robots ); // Filter all falsy-null robot values. return \array_filter( $sorted_robots ); } /** * Retrieves the robots key-value pairs. * * @return array The robots key-value pairs. */ protected function get_robots_value() { $context = $this->context_memoizer->for_current_page(); $robots_presenter = new Robots_Presenter(); $robots_presenter->presentation = $context->presentation; return $this->format_robots( $robots_presenter->get() ); } /** * Formats our robots fields, to match the pattern WordPress is using. * * Our format: `[ 'index' => 'noindex', 'max-image-preview' => 'max-image-preview:large', ... ]` * WordPress format: `[ 'noindex' => true, 'max-image-preview' => 'large', ... ]` * * @param array $robots Our robots value. * * @return array The formatted robots. */ protected function format_robots( $robots ) { foreach ( $robots as $key => $value ) { // When the entry represents for example: max-image-preview:large. $colon_position = \strpos( $value, ':' ); if ( $colon_position !== false ) { $robots[ $key ] = \substr( $value, ( $colon_position + 1 ) ); continue; } // When index => noindex, we want a separate noindex as entry in array. if ( \strpos( $value, 'no' ) === 0 ) { $robots[ $key ] = false; $robots[ $value ] = true; continue; } // When the key is equal to the value, just make its value a boolean. if ( $key === $value ) { $robots[ $key ] = true; } } return $robots; } /** * Ensures all other possible robots values are congruent with nofollow and or noindex. * * WordPress might add some robot values again. * When the page is set to noindex we want to filter out these values. * * @param array $robots The robots. * * @return array The filtered robots. */ protected function enforce_robots_congruence( $robots ) { if ( ! empty( $robots['nofollow'] ) ) { $robots['follow'] = null; } if ( ! empty( $robots['noarchive'] ) ) { $robots['archive'] = null; } if ( ! empty( $robots['noimageindex'] ) ) { $robots['imageindex'] = null; // `max-image-preview` should set be to `none` when `noimageindex` is present. // Using `isset` rather than `! empty` here so that in the rare case of `max-image-preview` // being equal to an empty string due to filtering, its value would still be set to `none`. if ( isset( $robots['max-image-preview'] ) ) { $robots['max-image-preview'] = 'none'; } } if ( ! empty( $robots['nosnippet'] ) ) { $robots['snippet'] = null; } if ( ! empty( $robots['noindex'] ) ) { $robots['index'] = null; $robots['imageindex'] = null; $robots['noimageindex'] = null; $robots['archive'] = null; $robots['noarchive'] = null; $robots['snippet'] = null; $robots['nosnippet'] = null; $robots['max-snippet'] = null; $robots['max-image-preview'] = null; $robots['max-video-preview'] = null; } return $robots; } /** * Sorts the robots array. * * @param array $robots The robots array. * * @return array The sorted robots array. */ protected function sort_robots( $robots ) { \uksort( $robots, static function ( $a, $b ) { $order = [ 'index' => 0, 'noindex' => 1, 'follow' => 2, 'nofollow' => 3, ]; $ai = isset( $order[ $a ] ) ? $order[ $a ] : 4; $bi = isset( $order[ $b ] ) ? $order[ $b ] : 4; return ( $ai - $bi ); } ); return $robots; } } front-end/backwards-compatibility.php 0000644 00000003301 15154134502 0013756 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Adds actions that were previously called and are now deprecated. */ class Backwards_Compatibility implements Integration_Interface { /** * Represents the options helper. * * @var Options_Helper */ protected $options; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Backwards_Compatibility constructor * * @param Options_Helper $options The options helper. */ public function __construct( Options_Helper $options ) { $this->options = $options; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->options->get( 'opengraph' ) === true ) { \add_action( 'wpseo_head', [ $this, 'call_wpseo_opengraph' ], 30 ); } if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) { \add_action( 'wpseo_head', [ $this, 'call_wpseo_twitter' ], 40 ); } } /** * Calls the old wpseo_opengraph action. * * @return void */ public function call_wpseo_opengraph() { \do_action_deprecated( 'wpseo_opengraph', [], '14.0', 'wpseo_frontend_presenters' ); } /** * Calls the old wpseo_twitter action. * * @return void */ public function call_wpseo_twitter() { \do_action_deprecated( 'wpseo_twitter', [], '14.0', 'wpseo_frontend_presenters' ); } } front-end/redirects.php 0000644 00000010313 15154134502 0011133 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Meta_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Redirect_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class Redirects. */ class Redirects implements Integration_Interface { /** * The options helper. * * @var Options_Helper */ protected $options; /** * The meta helper. * * @var Meta_Helper */ protected $meta; /** * The current page helper. * * @var Current_Page_Helper */ protected $current_page; /** * The redirect helper. * * @var Redirect_Helper */ private $redirect; /** * Sets the helpers. * * @codeCoverageIgnore * * @param Options_Helper $options Options helper. * @param Meta_Helper $meta Meta helper. * @param Current_Page_Helper $current_page The current page helper. * @param Redirect_Helper $redirect The redirect helper. */ public function __construct( Options_Helper $options, Meta_Helper $meta, Current_Page_Helper $current_page, Redirect_Helper $redirect ) { $this->options = $options; $this->meta = $meta; $this->current_page = $current_page; $this->redirect = $redirect; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'wp', [ $this, 'archive_redirect' ] ); \add_action( 'wp', [ $this, 'page_redirect' ], 99 ); \add_action( 'template_redirect', [ $this, 'attachment_redirect' ], 1 ); } /** * When certain archives are disabled, this redirects those to the homepage. */ public function archive_redirect() { if ( $this->need_archive_redirect() ) { $this->redirect->do_safe_redirect( \get_bloginfo( 'url' ), 301 ); } } /** * Based on the redirect meta value, this function determines whether it should redirect the current post / page. */ public function page_redirect() { if ( ! $this->current_page->is_simple_page() ) { return; } $post = \get_post(); if ( ! \is_object( $post ) ) { return; } $redirect = $this->meta->get_value( 'redirect', $post->ID ); if ( $redirect === '' ) { return; } $this->redirect->do_safe_redirect( $redirect, 301 ); } /** * If the option to disable attachment URLs is checked, this performs the redirect to the attachment. */ public function attachment_redirect() { if ( ! $this->current_page->is_attachment() ) { return; } if ( $this->options->get( 'disable-attachment', false ) === false ) { return; } $url = $this->get_attachment_url(); if ( empty( $url ) ) { return; } $this->redirect->do_unsafe_redirect( $url, 301 ); } /** * Checks if certain archive pages are disabled to determine if a archive redirect is needed. * * @codeCoverageIgnore * * @return bool Whether or not to redirect an archive page. */ protected function need_archive_redirect() { if ( $this->options->get( 'disable-date', false ) && $this->current_page->is_date_archive() ) { return true; } if ( $this->options->get( 'disable-author', false ) && $this->current_page->is_author_archive() ) { return true; } if ( $this->options->get( 'disable-post_format', false ) && $this->current_page->is_post_format_archive() ) { return true; } return false; } /** * Retrieves the attachment url for the current page. * * @codeCoverageIgnore It wraps WordPress functions. * * @return string The attachment url. */ protected function get_attachment_url() { /** * Allows the developer to change the target redirection URL for attachments. * * @api string $attachment_url The attachment URL for the queried object. * @api object $queried_object The queried object. * * @since 7.5.3 */ return \apply_filters( 'wpseo_attachment_redirect_url', \wp_get_attachment_url( \get_queried_object_id() ), \get_queried_object() ); } } front-end/open-graph-oembed.php 0000644 00000006115 15154134502 0012445 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use WP_Post; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Surfaces\Meta_Surface; /** * Class Open_Graph_OEmbed. */ class Open_Graph_OEmbed implements Integration_Interface { /** * The meta surface. * * @var Meta_Surface */ private $meta; /** * The oEmbed data. * * @var array */ private $data; /** * The post ID for the current post. * * @var int */ private $post_id; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class, Open_Graph_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'oembed_response_data', [ $this, 'set_oembed_data' ], 10, 2 ); } /** * Open_Graph_OEmbed constructor. * * @param Meta_Surface $meta The meta surface. */ public function __construct( Meta_Surface $meta ) { $this->meta = $meta; } /** * Callback function to pass to the oEmbed's response data that will enable * support for using the image and title set by the WordPress SEO plugin's fields. This * address the concern where some social channels/subscribed use oEmebed data over Open Graph data * if both are present. * * @link https://developer.wordpress.org/reference/hooks/oembed_response_data/ for hook info. * * @param array $data The oEmbed data. * @param WP_Post $post The current Post object. * * @return array An array of oEmbed data with modified values where appropriate. */ public function set_oembed_data( $data, $post ) { // Data to be returned. $this->data = $data; $this->post_id = $post->ID; $this->set_title(); $this->set_description(); $this->set_image(); return $this->data; } /** * Sets the OpenGraph title if configured. */ protected function set_title() { $opengraph_title = $this->meta->for_post( $this->post_id )->open_graph_title; if ( ! empty( $opengraph_title ) ) { $this->data['title'] = $opengraph_title; } } /** * Sets the OpenGraph description if configured. */ protected function set_description() { $opengraph_description = $this->meta->for_post( $this->post_id )->open_graph_description; if ( ! empty( $opengraph_description ) ) { $this->data['description'] = $opengraph_description; } } /** * Sets the image if it has been configured. */ protected function set_image() { $images = $this->meta->for_post( $this->post_id )->open_graph_images; $image = \reset( $images ); if ( empty( $image ) ) { return; } if ( ! isset( $image['url'] ) ) { return; } $this->data['thumbnail_url'] = $image['url']; if ( isset( $image['width'] ) ) { $this->data['thumbnail_width'] = $image['width']; } if ( isset( $image['height'] ) ) { $this->data['thumbnail_height'] = $image['height']; } } } front-end/handle-404.php 0000644 00000005127 15154134502 0010716 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper; /** * Handles intercepting requests. */ class Handle_404 implements Integration_Interface { /** * The WP Query wrapper. * * @var WP_Query_Wrapper */ private $query_wrapper; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'pre_handle_404', [ $this, 'handle_404' ] ); } /** * Handle_404 constructor. * * @codeCoverageIgnore Handles dependencies. * * @param WP_Query_Wrapper $query_wrapper The query wrapper. */ public function __construct( WP_Query_Wrapper $query_wrapper ) { $this->query_wrapper = $query_wrapper; } /** * Handles the 404 status code. * * @param bool $handled Whether we've handled the request. * * @return bool True if it's 404. */ public function handle_404( $handled ) { if ( ! $this->is_feed_404() ) { return $handled; } $this->set_404(); $this->set_headers(); \add_filter( 'old_slug_redirect_url', '__return_false' ); \add_filter( 'redirect_canonical', '__return_false' ); return true; } /** * If there are no posts in a feed, make it 404 instead of sending an empty RSS feed. * * @return bool True if it's 404. */ protected function is_feed_404() { if ( ! \is_feed() ) { return false; } $wp_query = $this->query_wrapper->get_query(); // Don't 404 if the query contains post(s) or an object. if ( $wp_query->posts || $wp_query->get_queried_object() ) { return false; } // Don't 404 if it isn't archive or singular. if ( ! $wp_query->is_archive() && ! $wp_query->is_singular() ) { return false; } return true; } /** * Sets the 404 status code. */ protected function set_404() { $wp_query = $this->query_wrapper->get_query(); $wp_query->is_feed = false; $wp_query->set_404(); $this->query_wrapper->set_query( $wp_query ); } /** * Sets the headers for http. * * @codeCoverageIgnore */ protected function set_headers() { // Overwrite Content-Type header. if ( ! \headers_sent() ) { \header( 'Content-Type: ' . \get_option( 'html_type' ) . '; charset=' . \get_option( 'blog_charset' ) ); } \status_header( 404 ); \nocache_headers(); } } front-end/schema-accessibility-feature.php 0000644 00000004462 15154134502 0014675 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Context\Meta_Tags_Context; use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece; use Yoast\WP\SEO\Generators\Schema\Article; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Adds the table of contents accessibility feature to the article piece with a fallback to the webpage piece. */ class Schema_Accessibility_Feature implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_schema_webpage', [ $this, 'maybe_add_accessibility_feature' ], 10, 4 ); \add_filter( 'wpseo_schema_article', [ $this, 'add_accessibility_feature' ], 10, 2 ); } /** * Adds the accessibility feature to the webpage if there is no article. * * @param array $piece The graph piece. * @param Meta_Tags_Context $context The context. * @param Abstract_Schema_Piece $the_generator The current schema generator. * @param Abstract_Schema_Piece[] $generators The schema generators. * * @return array The graph piece. */ public function maybe_add_accessibility_feature( $piece, $context, $the_generator, $generators ) { foreach ( $generators as $generator ) { if ( \is_a( $generator, Article::class ) && $generator->is_needed() ) { return $piece; } } return $this->add_accessibility_feature( $piece, $context ); } /** * Adds the accessibility feature to a schema graph piece. * * @param array $piece The schema piece. * @param Meta_Tags_Context $context The context. * * @return array The graph piece. */ public function add_accessibility_feature( $piece, $context ) { if ( empty( $context->blocks['yoast-seo/table-of-contents'] ) ) { return $piece; } if ( isset( $piece['accessibilityFeature'] ) ) { $piece['accessibilityFeature'][] = 'tableOfContents'; } else { $piece['accessibilityFeature'] = [ 'tableOfContents', ]; } return $piece; } } front-end/force-rewrite-title.php 0000644 00000010630 15154134502 0013045 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Wrappers\WP_Query_Wrapper; /** * Class Force_Rewrite_Title. */ class Force_Rewrite_Title implements Integration_Interface { /** * The options helper. * * @var Options_Helper */ private $options; /** * Toggle indicating whether output buffering has been started. * * @var bool */ private $ob_started = false; /** * The WP Query wrapper. * * @var WP_Query_Wrapper */ private $wp_query; /** * Sets the helpers. * * @codeCoverageIgnore It just handles dependencies. * * @param Options_Helper $options Options helper. * @param WP_Query_Wrapper $wp_query WP query wrapper. */ public function __construct( Options_Helper $options, WP_Query_Wrapper $wp_query ) { $this->options = $options; $this->wp_query = $wp_query; } /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { // When the option is disabled. if ( ! $this->options->get( 'forcerewritetitle', false ) ) { return; } // For WordPress versions below 4.4. if ( \current_theme_supports( 'title-tag' ) ) { return; } \add_action( 'template_redirect', [ $this, 'force_rewrite_output_buffer' ], 99999 ); \add_action( 'wp_footer', [ $this, 'flush_cache' ], -1 ); } /** * Used in the force rewrite functionality this retrieves the output, replaces the title with the proper SEO * title and then flushes the output. * * @return bool */ public function flush_cache() { if ( $this->ob_started !== true ) { return false; } $content = $this->get_buffered_output(); $old_wp_query = $this->wp_query->get_query(); \wp_reset_query(); // When the file has the debug mark. if ( \preg_match( '/(?\'before\'.*)<!-- This site is optimized with the Yoast SEO.*<!-- \/ Yoast SEO( Premium)? plugin. -->(?\'after\'.*)/is', $content, $matches ) ) { $content = $this->replace_titles_from_content( $content, $matches ); unset( $matches ); } // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- The query gets reset here with the original query. $GLOBALS['wp_query'] = $old_wp_query; // phpcs:ignore WordPress.Security.EscapeOutput -- The output should already have been escaped, we are only filtering it. echo $content; return true; } /** * Starts the output buffer so it can later be fixed by flush_cache(). */ public function force_rewrite_output_buffer() { $this->ob_started = true; $this->start_output_buffering(); } /** * Replaces the titles from the parts that contains a title. * * @param string $content The content to remove the titles from. * @param array $parts_with_title The parts containing a title. * * @return string The modified content. */ protected function replace_titles_from_content( $content, $parts_with_title ) { if ( isset( $parts_with_title['before'] ) && \is_string( $parts_with_title['before'] ) ) { $content = $this->replace_title( $parts_with_title['before'], $content ); } if ( isset( $parts_with_title['after'] ) ) { $content = $this->replace_title( $parts_with_title['after'], $content ); } return $content; } /** * Removes the title from the part that contains the title and put this modified part back * into the content. * * @param string $part_with_title The part with the title that needs to be replaced. * @param string $content The entire content. * * @return string The altered content. */ protected function replace_title( $part_with_title, $content ) { $part_without_title = \preg_replace( '/<title.*?\/title>/i', '', $part_with_title ); return \str_replace( $part_with_title, $part_without_title, $content ); } /** * Starts the output buffering. * * @codeCoverageIgnore */ protected function start_output_buffering() { \ob_start(); } /** * Retrieves the buffered output. * * @codeCoverageIgnore * * @return false|string The buffered output. */ protected function get_buffered_output() { return \ob_get_clean(); } } front-end/theme-titles.php 0000644 00000002272 15154134502 0011560 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Notify the user by giving a deprecated notice. */ class Theme_Titles implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'thematic_doctitle', [ $this, 'title' ], 15 ); \add_filter( 'woo_title', [ $this, 'title' ], 99 ); } /** * Filters the title for woo_title and the thematic_doctitle. * * @deprecated 14.0 * * @codeCoverageIgnore * * @param string $title The title. * * @return string The title. */ public function title( $title ) { \_deprecated_function( __METHOD__, 'WPSEO 14.0', \esc_html__( 'a theme that has proper title-tag theme support, or adapt your theme to have that support', 'wordpress-seo' ) ); return $title; } } front-end/indexing-controls.php 0000644 00000005253 15154134502 0012624 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Robots_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class Indexing_Controls. */ class Indexing_Controls implements Integration_Interface { /** * The robots helper. * * @var Robots_Helper */ protected $robots; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * The constructor. * * @codeCoverageIgnore Sets the dependencies. * * @param Robots_Helper $robots The robots helper. */ public function __construct( Robots_Helper $robots ) { $this->robots = $robots; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @codeCoverageIgnore * * @return void */ public function register_hooks() { // The option `blog_public` is set in Settings > Reading > Search Engine Visibility. if ( (string) \get_option( 'blog_public' ) === '0' ) { \add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] ); } \add_action( 'template_redirect', [ $this, 'noindex_robots' ] ); \add_filter( 'loginout', [ $this, 'nofollow_link' ] ); \add_filter( 'register', [ $this, 'nofollow_link' ] ); // Remove actions that we will handle through our wpseo_head call, and probably change the output of. \remove_action( 'wp_head', 'rel_canonical' ); \remove_action( 'wp_head', 'index_rel_link' ); \remove_action( 'wp_head', 'start_post_rel_link' ); \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' ); \remove_action( 'wp_head', 'noindex', 1 ); } /** * Sends a Robots HTTP header preventing URL from being indexed in the search results while allowing search engines * to follow the links in the object at the URL. * * @return bool Boolean indicating whether the noindex header was sent. */ public function noindex_robots() { if ( ! \is_robots() ) { return false; } return $this->set_robots_header(); } /** * Adds rel="nofollow" to a link, only used for login / registration links. * * @param string $input The link element as a string. * * @return string */ public function nofollow_link( $input ) { return \str_replace( '<a ', '<a rel="nofollow" ', $input ); } /** * Sets the x-robots-tag to noindex follow. * * @codeCoverageIgnore Too difficult to test. * * @return bool */ protected function set_robots_header() { if ( \headers_sent() === false ) { \header( 'X-Robots-Tag: noindex, follow', true ); return true; } return false; } } front-end/comment-link-fixer.php 0000644 00000007123 15154134502 0012664 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Front_End; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Helpers\Redirect_Helper; use Yoast\WP\SEO\Helpers\Robots_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class Comment_Link_Fixer. */ class Comment_Link_Fixer implements Integration_Interface { /** * The redirects helper. * * @var Redirect_Helper */ protected $redirect; /** * The robots helper. * * @var Robots_Helper */ protected $robots; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Comment_Link_Fixer constructor. * * @codeCoverageIgnore It only sets depedencies. * * @param Redirect_Helper $redirect The redirect helper. * @param Robots_Helper $robots The robots helper. */ public function __construct( Redirect_Helper $redirect, Robots_Helper $robots ) { $this->redirect = $redirect; $this->robots = $robots; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { if ( $this->clean_reply_to_com() ) { \add_filter( 'comment_reply_link', [ $this, 'remove_reply_to_com' ] ); \add_action( 'template_redirect', [ $this, 'replytocom_redirect' ], 1 ); } // When users view a reply to a comment, this URL parameter is set. These should never be indexed separately. if ( $this->has_replytocom_parameter() ) { \add_filter( 'wpseo_robots_array', [ $this->robots, 'set_robots_no_index' ] ); } } /** * Checks if the url contains the ?replytocom query parameter. * * @codeCoverageIgnore Wraps the filter input. * * @return string The value of replytocom. */ protected function has_replytocom_parameter() { return \filter_input( \INPUT_GET, 'replytocom' ); } /** * Removes the ?replytocom variable from the link, replacing it with a #comment-<number> anchor. * * @todo Should this function also allow for relative urls ? * * @param string $link The comment link as a string. * * @return string The modified link. */ public function remove_reply_to_com( $link ) { return \preg_replace( '`href=(["\'])(?:.*(?:\?|&|&)replytocom=(\d+)#respond)`', 'href=$1#comment-$2', $link ); } /** * Redirects out the ?replytocom variables. * * @return bool True when redirect has been done. */ public function replytocom_redirect() { if ( isset( $_GET['replytocom'] ) && \is_singular() ) { $url = \get_permalink( $GLOBALS['post']->ID ); $hash = \sanitize_text_field( \wp_unslash( $_GET['replytocom'] ) ); $query_string = ''; if ( isset( $_SERVER['QUERY_STRING'] ) ) { $query_string = \remove_query_arg( 'replytocom', \sanitize_text_field( \wp_unslash( $_SERVER['QUERY_STRING'] ) ) ); } if ( ! empty( $query_string ) ) { $url .= '?' . $query_string; } $url .= '#comment-' . $hash; $this->redirect->do_safe_redirect( $url, 301 ); return true; } return false; } /** * Checks whether we can allow the feature that removes ?replytocom query parameters. * * @codeCoverageIgnore It just wraps a call to a filter. * * @return bool True to remove, false not to remove. */ private function clean_reply_to_com() { /** * Filter: 'wpseo_remove_reply_to_com' - Allow disabling the feature that removes ?replytocom query parameters. * * @param bool $return True to remove, false not to remove. */ return (bool) \apply_filters( 'wpseo_remove_reply_to_com', true ); } } front-end-integration.php 0000644 00000031420 15154134502 0011472 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use WPSEO_Replace_Vars; use Yoast\WP\SEO\Conditionals\Front_End_Conditional; use Yoast\WP\SEO\Context\Meta_Tags_Context; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter; use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter; use Yoast\WP\SEO\Presenters\Debug\Marker_Open_Presenter; use Yoast\WP\SEO\Presenters\Title_Presenter; use Yoast\WP\SEO\Surfaces\Helpers_Surface; use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface; /** * Class Front_End_Integration. */ class Front_End_Integration implements Integration_Interface { /** * The memoizer for the meta tags context. * * @var Meta_Tags_Context_Memoizer */ private $context_memoizer; /** * The container. * * @var ContainerInterface */ protected $container; /** * Represents the options helper. * * @var Options_Helper */ protected $options; /** * Represents the request helper. * * @var Request_Helper */ protected $request; /** * The helpers surface. * * @var Helpers_Surface */ protected $helpers; /** * The replace vars helper. * * @var WPSEO_Replace_Vars */ protected $replace_vars; /** * The presenters we loop through on each page load. * * @var string[] */ protected $base_presenters = [ 'Title', 'Meta_Description', 'Robots', ]; /** * The presenters we loop through on each page load. * * @var string[] */ protected $indexing_directive_presenters = [ 'Canonical', 'Rel_Prev', 'Rel_Next', ]; /** * The Open Graph specific presenters. * * @var string[] */ protected $open_graph_presenters = [ 'Open_Graph\Locale', 'Open_Graph\Type', 'Open_Graph\Title', 'Open_Graph\Description', 'Open_Graph\Url', 'Open_Graph\Site_Name', 'Open_Graph\Article_Publisher', 'Open_Graph\Article_Author', 'Open_Graph\Article_Published_Time', 'Open_Graph\Article_Modified_Time', 'Open_Graph\Image', ]; /** * The Open Graph specific presenters that should be output on error pages. * * @var array */ protected $open_graph_error_presenters = [ 'Open_Graph\Locale', 'Open_Graph\Title', 'Open_Graph\Site_Name', ]; /** * The Twitter card specific presenters. * * @var string[] */ protected $twitter_card_presenters = [ 'Twitter\Card', 'Twitter\Title', 'Twitter\Description', 'Twitter\Image', 'Twitter\Creator', 'Twitter\Site', ]; /** * The Slack specific presenters. * * @var string[] */ protected $slack_presenters = [ 'Slack\Enhanced_Data', ]; /** * The Webmaster verification specific presenters. * * @var string[] */ protected $webmaster_verification_presenters = [ 'Webmaster\Baidu', 'Webmaster\Bing', 'Webmaster\Google', 'Webmaster\Pinterest', 'Webmaster\Yandex', ]; /** * Presenters that are only needed on singular pages. * * @var string[] */ protected $singular_presenters = [ 'Open_Graph\Article_Author', 'Open_Graph\Article_Publisher', 'Open_Graph\Article_Published_Time', 'Open_Graph\Article_Modified_Time', 'Twitter\Creator', 'Slack\Enhanced_Data', ]; /** * The presenters we want to be last in our output. * * @var string[] */ protected $closing_presenters = [ 'Schema', ]; /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Front_End_Conditional::class ]; } /** * Front_End_Integration constructor. * * @codeCoverageIgnore It sets dependencies. * * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer. * @param ContainerInterface $service_container The DI container. * @param Options_Helper $options The options helper. * @param Request_Helper $request The request helper. * @param Helpers_Surface $helpers The helpers surface. * @param WPSEO_Replace_Vars $replace_vars The replace vars helper. */ public function __construct( Meta_Tags_Context_Memoizer $context_memoizer, ContainerInterface $service_container, Options_Helper $options, Request_Helper $request, Helpers_Surface $helpers, WPSEO_Replace_Vars $replace_vars ) { $this->container = $service_container; $this->context_memoizer = $context_memoizer; $this->options = $options; $this->request = $request; $this->helpers = $helpers; $this->replace_vars = $replace_vars; } /** * Registers the appropriate hooks to show the SEO metadata on the frontend. * * Removes some actions to remove metadata that WordPress shows on the frontend, * to avoid duplicate and/or mismatched metadata. */ public function register_hooks() { \add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 ); // Filter the title for compatibility with other plugins and themes. \add_filter( 'wp_title', [ $this, 'filter_title' ], 15 ); // Removes our robots presenter from the list when wp_robots is handling this. \add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] ); \add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 ); \remove_action( 'wp_head', 'rel_canonical' ); \remove_action( 'wp_head', 'index_rel_link' ); \remove_action( 'wp_head', 'start_post_rel_link' ); \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' ); \remove_action( 'wp_head', 'noindex', 1 ); \remove_action( 'wp_head', '_wp_render_title_tag', 1 ); \remove_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); } /** * Filters the title, mainly used for compatibility reasons. * * @return string */ public function filter_title() { $context = $this->context_memoizer->for_current_page(); $title_presenter = new Title_Presenter(); /** This filter is documented in src/integrations/front-end-integration.php */ $title_presenter->presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $title_presenter->replace_vars = $this->replace_vars; $title_presenter->helpers = $this->helpers; return \esc_html( $title_presenter->get() ); } /** * Filters our robots presenter, but only when wp_robots is attached to the wp_head action. * * @param array $presenters The presenters for current page. * * @return array The filtered presenters. */ public function filter_robots_presenter( $presenters ) { if ( ! \function_exists( 'wp_robots' ) ) { return $presenters; } if ( ! \has_action( 'wp_head', 'wp_robots' ) ) { return $presenters; } if ( $this->request->is_rest_request() ) { return $presenters; } return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] ); } /** * Presents the head in the front-end. Resets wp_query if it's not the main query. * * @codeCoverageIgnore It just calls a WordPress function. */ public function call_wpseo_head() { global $wp_query; $old_wp_query = $wp_query; // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query. \wp_reset_query(); \do_action( 'wpseo_head' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query. $GLOBALS['wp_query'] = $old_wp_query; } /** * Echoes all applicable presenters for a page. */ public function present_head() { $context = $this->context_memoizer->for_current_page(); $presenters = $this->get_presenters( $context->page_type, $context ); /** * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values. * * @api Indexable_Presention The indexable presentation. */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); echo \PHP_EOL; foreach ( $presenters as $presenter ) { $presenter->presentation = $presentation; $presenter->helpers = $this->helpers; $presenter->replace_vars = $this->replace_vars; $output = $presenter->present(); if ( ! empty( $output ) ) { // phpcs:ignore WordPress.Security.EscapeOutput -- Presenters are responsible for correctly escaping their output. echo "\t" . $output . \PHP_EOL; } } echo \PHP_EOL . \PHP_EOL; } /** * Returns all presenters for this page. * * @param string $page_type The page type. * @param Meta_Tags_Context|null $context The meta tags context for the current page. * * @return Abstract_Indexable_Presenter[] The presenters. */ public function get_presenters( $page_type, $context = null ) { if ( \is_null( $context ) ) { $context = $this->context_memoizer->for_current_page(); } $needed_presenters = $this->get_needed_presenters( $page_type ); $callback = static function( $presenter ) { if ( ! \class_exists( $presenter ) ) { return null; } return new $presenter(); }; $presenters = \array_filter( \array_map( $callback, $needed_presenters ) ); /** * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request. * * @param array $presenters The presenters. * @param Meta_Tags_Context $context The meta tags context for the current page. * * @api Abstract_Indexable_Presenter[] List of presenter instances. */ $presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context ); if ( ! \is_array( $presenter_instances ) ) { $presenter_instances = $presenters; } $is_presenter_callback = static function ( $presenter_instance ) { return $presenter_instance instanceof Abstract_Indexable_Presenter; }; $presenter_instances = \array_filter( $presenter_instances, $is_presenter_callback ); return \array_merge( [ new Marker_Open_Presenter() ], $presenter_instances, [ new Marker_Close_Presenter() ] ); } /** * Generate the array of presenters we need for the current request. * * @param string $page_type The page type we're retrieving presenters for. * * @return string[] The presenters. */ private function get_needed_presenters( $page_type ) { $presenters = $this->get_presenters_for_page_type( $page_type ); if ( ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false ) ) { // Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags. $presenters = \array_diff( $presenters, [ 'Title' ] ); } $callback = static function ( $presenter ) { return "Yoast\WP\SEO\Presenters\\{$presenter}_Presenter"; }; $presenters = \array_map( $callback, $presenters ); /** * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request. * * @api array List of presenters. */ $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters ); return $presenters; } /** * Filters the presenters based on the page type. * * @param string $page_type The page type. * * @return string[] The presenters. */ private function get_presenters_for_page_type( $page_type ) { if ( $page_type === 'Error_Page' ) { $presenters = $this->base_presenters; if ( $this->options->get( 'opengraph' ) === true ) { $presenters = \array_merge( $presenters, $this->open_graph_error_presenters ); } return \array_merge( $presenters, $this->closing_presenters ); } $presenters = $this->get_all_presenters(); if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) { $presenters = \array_merge( $presenters, $this->webmaster_verification_presenters ); } // Filter out the presenters only needed for singular pages on non-singular pages. if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) { $presenters = \array_diff( $presenters, $this->singular_presenters ); } return $presenters; } /** * Returns a list of all available presenters based on settings. * * @return string[] The presenters. */ private function get_all_presenters() { $presenters = \array_merge( $this->base_presenters, $this->indexing_directive_presenters ); if ( $this->options->get( 'opengraph' ) === true ) { $presenters = \array_merge( $presenters, $this->open_graph_presenters ); } if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) { $presenters = \array_merge( $presenters, $this->twitter_card_presenters ); } if ( $this->options->get( 'enable_enhanced_slack_sharing' ) === true && \apply_filters( 'wpseo_output_enhanced_slack_data', true ) !== false ) { $presenters = \array_merge( $presenters, $this->slack_presenters ); } return \array_merge( $presenters, $this->closing_presenters ); } } admin/helpscout-beacon.php 0000644 00000022441 15154134502 0011603 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WPSEO_Addon_Manager; use WPSEO_Admin_Asset_Manager; use WPSEO_Tracking_Server_Data; use WPSEO_Utils; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class WPSEO_HelpScout */ class HelpScout_Beacon implements Integration_Interface { /** * The id for the beacon. * * @var string */ protected $beacon_id = '2496aba6-0292-489c-8f5d-1c0fba417c2f'; /** * The id for the beacon for users that have tracking on. * * @var string */ protected $beacon_id_tracking_users = '6b8e74c5-aa81-4295-b97b-c2a62a13ea7f'; /** * The products the beacon is loaded for. * * @var array */ protected $products = []; /** * Whether to asks the user's consent before loading in HelpScout. * * @var bool */ protected $ask_consent = true; /** * The options helper. * * @var Options_Helper */ protected $options; /** * The array of pages we need to show the beacon on with their respective beacon IDs. * * @var array */ protected $pages_ids; /** * The array of pages we need to show the beacon on. * * @var array */ protected $base_pages = [ 'wpseo_dashboard', 'wpseo_titles', 'wpseo_search_console', 'wpseo_social', 'wpseo_tools', 'wpseo_licenses', 'wpseo_workouts', ]; /** * The current admin page * * @var string */ protected $page; /** * The asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Headless_Rest_Endpoints_Enabled_Conditional constructor. * * @param Options_Helper $options The options helper. * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. */ public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager ) { $this->options = $options; $this->asset_manager = $asset_manager; $this->ask_consent = ! $this->options->get( 'tracking' ); $this->page = \filter_input( \INPUT_GET, 'page', \FILTER_SANITIZE_STRING ); foreach ( $this->base_pages as $page ) { if ( $this->ask_consent ) { // We want to be able to show surveys to people who have tracking on, so we give them a different beacon. $this->pages_ids[ $page ] = $this->beacon_id_tracking_users; } else { $this->pages_ids[ $page ] = $this->beacon_id; } } } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] ); \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] ); } /** * Enqueues the HelpScout script. */ public function enqueue_help_scout_script() { // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon. $this->filter_settings(); if ( ! $this->is_beacon_page() ) { return; } $this->asset_manager->enqueue_script( 'help-scout-beacon' ); } /** * Outputs a small piece of javascript for the beacon. */ public function output_beacon_js() { if ( ! $this->is_beacon_page() ) { return; } \printf( '<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>', ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon', \esc_html( $this->pages_ids[ $this->page ] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode. WPSEO_Utils::format_json_encode( (array) $this->get_session_data() ) ); } /** * Checks if the current page is a page containing the beacon. * * @return bool */ private function is_beacon_page() { $return = false; if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) { $return = true; } /** * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon. * * @api bool - Whether we show the beacon or not. */ return \apply_filters( 'wpseo_helpscout_show_beacon', $return ); } /** * Retrieves the identifying data. * * @return string The data to pass as identifying data. */ protected function get_session_data() { $current_user = \wp_get_current_user(); // Do not make these strings translatable! They are for our support agents, the user won't see them! $data = [ 'name' => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ), 'email' => $current_user->user_email, 'WordPress Version' => $this->get_wordpress_version(), 'Server' => $this->get_server_info(), '<a href="' . \admin_url( 'themes.php' ) . '">Theme</a>' => $this->get_theme_info(), '<a href="' . \admin_url( 'plugins.php' ) . '">Plugins</a>' => $this->get_active_plugins(), ]; if ( ! empty( $this->products ) ) { $addon_manager = new WPSEO_Addon_Manager(); foreach ( $this->products as $product ) { $subscription = $addon_manager->get_subscription( $product ); if ( ! $subscription ) { continue; } $data[ $subscription->product->name ] = $this->get_product_info( $subscription ); } } return WPSEO_Utils::format_json_encode( $data ); } /** * Returns basic info about the server software. * * @return string */ private function get_server_info() { $server_tracking_data = new WPSEO_Tracking_Server_Data(); $server_data = $server_tracking_data->get(); $server_data = $server_data['server']; $fields_to_use = [ 'IP' => 'ip', 'Hostname' => 'Hostname', 'OS' => 'os', 'PHP' => 'PhpVersion', 'CURL' => 'CurlVersion', ]; $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . '(SSL Support' . $server_data['CurlVersion']['sslSupport'] . ')'; $server_info = '<table>'; foreach ( $fields_to_use as $label => $field_to_use ) { if ( isset( $server_data[ $field_to_use ] ) ) { $server_info .= \sprintf( '<tr><td>%1$s</td><td>%2$s</td></tr>', \esc_html( $label ), \esc_html( $server_data[ $field_to_use ] ) ); } } $server_info .= '</table>'; return $server_info; } /** * Returns info about the Yoast SEO plugin version and license. * * @param object $plugin The plugin. * * @return string The product info. */ private function get_product_info( $plugin ) { if ( empty( $plugin ) ) { return ''; } $product_info = '<table>'; $product_info .= '<tr><td>Version</td><td>' . $plugin->product->version . '</td></tr>'; $product_info .= '<tr><td>Expiration date</td><td>' . $plugin->expiry_date . '</td></tr>'; $product_info .= '</table>'; return $product_info; } /** * Returns the WordPress version + a suffix if current WP is multi site. * * @return string The WordPress version string. */ private function get_wordpress_version() { global $wp_version; $wordpress_version = $wp_version; if ( \is_multisite() ) { $wordpress_version .= ' MULTI-SITE'; } return $wordpress_version; } /** * Returns a formatted HTML string for the current theme. * * @return string The theme info as string. */ private function get_theme_info() { $theme = \wp_get_theme(); $theme_info = \sprintf( '<a href="%1$s">%2$s</a> v%3$s by %4$s', \esc_attr( $theme->display( 'ThemeURI' ) ), \esc_html( $theme->display( 'Name' ) ), \esc_html( $theme->display( 'Version' ) ), \esc_html( $theme->display( 'Author' ) ) ); if ( \is_child_theme() ) { $theme_info .= \sprintf( '<br />Child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) ); } return $theme_info; } /** * Returns a formatted HTML list of all active plugins. * * @return string The active plugins. */ private function get_active_plugins() { $updates_available = \get_site_transient( 'update_plugins' ); $active_plugins = ''; foreach ( \wp_get_active_and_valid_plugins() as $plugin ) { $plugin_data = \get_plugin_data( $plugin ); $plugin_file = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin ); if ( isset( $updates_available->response[ $plugin_file ] ) ) { $active_plugins .= '<i class="icon-close1"></i> '; } $active_plugins .= \sprintf( '<a href="%1$s">%2$s</a> v%3$s', \esc_attr( $plugin_data['PluginURI'] ), \esc_html( $plugin_data['Name'] ), \esc_html( $plugin_data['Version'] ) ); } return $active_plugins; } /** * Returns the conditionals based on which this integration should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late. */ protected function filter_settings() { /** * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings. * * @api string - The HelpScout beacon settings. */ $filterable_helpscout_setting = [ 'products' => $this->products, 'pages_ids' => $this->pages_ids, ]; $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting ); $this->products = $helpscout_settings['products']; $this->pages_ids = $helpscout_settings['pages_ids']; } } admin/migration-error-integration.php 0000644 00000002543 15154134502 0014012 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Config\Migration_Status; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Migration_Error_Presenter; /** * Migration_Error_Integration class. */ class Migration_Error_Integration implements Integration_Interface { /** * The migration status object. * * @var Migration_Status */ protected $migration_status; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Migration_Error_Integration constructor. * * @param Migration_Status $migration_status The migration status object. */ public function __construct( Migration_Status $migration_status ) { $this->migration_status = $migration_status; } /** * {@inheritDoc} */ public function register_hooks() { if ( $this->migration_status->get_error( 'free' ) === false ) { return; } \add_action( 'admin_notices', [ $this, 'render_migration_error' ] ); } /** * Renders the migration error. * * @return void */ public function render_migration_error() { // phpcs:ignore WordPress.Security.EscapeOutput -- The Migration_Error_Presenter already escapes it's output. echo new Migration_Error_Presenter( $this->migration_status->get_error( 'free' ) ); } } admin/addon-installation/installation-integration.php 0000644 00000013272 15154134502 0017160 0 ustar 00 <?php // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on. namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation; use WPSEO_Addon_Manager; use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Activate_Action; use Yoast\WP\SEO\Actions\Addon_Installation\Addon_Install_Action; use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional; use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Activation_Error_Exception; use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Already_Installed_Exception; use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Installation_Error_Exception; use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Activate_Plugins_Exception; use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Install_Plugins_Exception; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Represents the Addon installation feature. */ class Installation_Integration implements Integration_Interface { /** * The installation action. * * @var Addon_Install_Action */ protected $addon_install_action; /** * The activation action. * * @var Addon_Activate_Action */ protected $addon_activate_action; /** * The addon manager. * * @var WPSEO_Addon_Manager */ protected $addon_manager; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class, Licenses_Page_Conditional::class, Addon_Installation_Conditional::class, ]; } /** * Addon_Installation constructor. * * @param WPSEO_Addon_Manager $addon_manager The addon manager. * @param Addon_Activate_Action $addon_activate_action The addon activate action. * @param Addon_Install_Action $addon_install_action The addon install action. */ public function __construct( WPSEO_Addon_Manager $addon_manager, Addon_Activate_Action $addon_activate_action, Addon_Install_Action $addon_install_action ) { $this->addon_manager = $addon_manager; $this->addon_activate_action = $addon_activate_action; $this->addon_install_action = $addon_install_action; } /** * Registers all hooks to WordPress. */ public function register_hooks() { \add_action( 'wpseo_install_and_activate_addons', [ $this, 'install_and_activate_addons' ] ); } /** * Installs and activates missing addons. * * @return void */ public function install_and_activate_addons() { if ( \filter_input( \INPUT_GET, 'action' ) !== 'install' ) { return; } \check_admin_referer( 'wpseo_addon_installation', 'nonce' ); echo '<div class="wrap yoast wpseo_table_page">'; \printf( '<h1 id="wpseo-title" class="yoast-h1">%s</h1>', \esc_html__( 'Installing and activating addons', 'wordpress-seo' ) ); $licensed_addons = $this->addon_manager->get_myyoast_site_information()->subscriptions; foreach ( $licensed_addons as $addon ) { \printf( '<p><strong>%s</strong></p>', \esc_html( $addon->product->name ) ); list( $installed, $output ) = $this->install_addon( $addon->product->slug, $addon->product->download ); if ( $installed ) { $activation_output = $this->activate_addon( $addon->product->slug ); $output = \array_merge( $output, $activation_output ); } echo '<p>'; echo \implode( '<br />', \array_map( 'esc_html', $output ) ); echo '</p>'; } \printf( /* translators: %1$s expands to an anchor tag to the admin premium page, %2$s expands to Yoast SEO Premium, %3$s expands to a closing anchor tag */ \esc_html__( '%1$s Continue to %2$s%3$s', 'wordpress-seo' ), '<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_licenses' ) ) . '">', 'Yoast SEO Premium', '</a>' ); echo '</div>'; exit; } /** * Activates an addon. * * @param string $addon_slug The addon to activate. * * @return array The output of the activation. */ public function activate_addon( $addon_slug ) { $output = []; try { $this->addon_activate_action->activate_addon( $addon_slug ); /* Translators: %s expands to the name of the addon. */ $output[] = \__( 'Addon activated.', 'wordpress-seo' ); } catch ( User_Cannot_Activate_Plugins_Exception $exception ) { $output[] = \__( 'You are not allowed to activate plugins.', 'wordpress-seo' ); } catch ( Addon_Activation_Error_Exception $exception ) { $output[] = \sprintf( /* Translators:%s expands to the error message. */ \__( 'Addon activation failed because of an error: %s.', 'wordpress-seo' ), $exception->getMessage() ); } return $output; } /** * Installs an addon. * * @param string $addon_slug The slug of the addon to install. * @param string $addon_download The download URL of the addon. * * @return array The installation success state and the output of the installation. */ public function install_addon( $addon_slug, $addon_download ) { $installed = false; $output = []; try { $installed = $this->addon_install_action->install_addon( $addon_slug, $addon_download ); } catch ( Addon_Already_Installed_Exception $exception ) { /* Translators: %s expands to the name of the addon. */ $output[] = \__( 'Addon installed.', 'wordpress-seo' ); $installed = true; } catch ( User_Cannot_Install_Plugins_Exception $exception ) { $output[] = \__( 'You are not allowed to install plugins.', 'wordpress-seo' ); } catch ( Addon_Installation_Error_Exception $exception ) { $output[] = \sprintf( /* Translators: %s expands to the error message. */ \__( 'Addon installation failed because of an error: %s.', 'wordpress-seo' ), $exception->getMessage() ); } return [ $installed, $output ]; } } admin/addon-installation/dialog-integration.php 0000644 00000006634 15154134502 0015722 0 ustar 00 <?php // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Discussed in Tech Council, a better solution is being worked on. namespace Yoast\WP\SEO\Integrations\Admin\Addon_Installation; use WPSEO_Addon_Manager; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Addon_Installation_Conditional; use Yoast\WP\SEO\Conditionals\Admin\Licenses_Page_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Represents the Addon installation feature. */ class Dialog_Integration implements Integration_Interface { /** * The addon manager. * * @var WPSEO_Addon_Manager */ protected $addon_manager; /** * The addons. * * @var array */ protected $owned_addons; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class, Licenses_Page_Conditional::class, Addon_Installation_Conditional::class, ]; } /** * Addon_Installation constructor. * * @param WPSEO_Addon_Manager $addon_manager The addon manager. */ public function __construct( WPSEO_Addon_Manager $addon_manager ) { $this->addon_manager = $addon_manager; } /** * Registers all hooks to WordPress. */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'start_addon_installation' ] ); } /** * Starts the addon installation flow. * * @return void */ public function start_addon_installation() { // Only show the dialog when we explicitly want to see it. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: This is not a form. if ( ! isset( $_GET['install'] ) || $_GET['install'] !== 'true' ) { return; } $this->bust_myyoast_addon_information_cache(); $this->owned_addons = $this->get_owned_addons(); if ( \count( $this->owned_addons ) > 0 ) { \add_action( 'admin_enqueue_scripts', [ $this, 'show_modal' ] ); } else { \add_action( 'admin_notices', [ $this, 'throw_no_owned_addons_warning' ] ); } } /** * Throws a no owned addons warning. * * @return void */ public function throw_no_owned_addons_warning() { echo '<div class="notice notice-warning"><p>' . \sprintf( /* translators: %1$s expands to Yoast SEO */ \esc_html__( 'No %1$s plugins have been installed. You don\'t seem to own any active subscriptions.', 'wordpress-seo' ), 'Yoast SEO' ) . '</p></div>'; } /** * Shows the modal. * * @return void */ public function show_modal() { \wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'addon-installation', 'wpseoAddonInstallationL10n', [ 'addons' => $this->owned_addons, 'nonce' => \wp_create_nonce( 'wpseo_addon_installation' ), ] ); $asset_manager = new WPSEO_Admin_Asset_Manager(); $asset_manager->enqueue_script( 'addon-installation' ); } /** * Retrieves a list of owned addons for the site in MyYoast. * * @return array List of owned addons with slug as key and name as value. */ protected function get_owned_addons() { $owned_addons = []; foreach ( $this->addon_manager->get_myyoast_site_information()->subscriptions as $addon ) { $owned_addons[] = $addon->product->name; } return $owned_addons; } /** * Bust the site information transients to have fresh data. * * @return void */ protected function bust_myyoast_addon_information_cache() { $this->addon_manager->remove_site_information_transients(); } } admin/background-indexing-integration.php 0000644 00000013671 15154134502 0014620 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action; use Yoast\WP\SEO\Actions\Indexing\Indexable_Indexing_Complete_Action; use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action; use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action; use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action; use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action; use Yoast\WP\SEO\Actions\Indexing\Term_Link_Indexing_Action; use Yoast\WP\SEO\Conditionals\Get_Request_Conditional; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional; use Yoast\WP\SEO\Helpers\Indexing_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class Background_Indexing_Integration. * * @package Yoast\WP\SEO\Integrations\Admin */ class Background_Indexing_Integration implements Integration_Interface { /** * The post indexing action. * * @var Indexable_Post_Indexation_Action */ protected $post_indexation; /** * The term indexing action. * * @var Indexable_Term_Indexation_Action */ protected $term_indexation; /** * The post type archive indexing action. * * @var Indexable_Post_Type_Archive_Indexation_Action */ protected $post_type_archive_indexation; /** * Represents the general indexing. * * @var Indexable_General_Indexation_Action */ protected $general_indexation; /** * Represents the indexing completed action. * * @var Indexable_Indexing_Complete_Action */ protected $complete_indexation_action; /** * The post link indexing action. * * @var Post_Link_Indexing_Action */ protected $post_link_indexing_action; /** * The term link indexing action. * * @var Term_Link_Indexing_Action */ protected $term_link_indexing_action; /** * Represents the indexing helper. * * @var Indexing_Helper */ protected $indexing_helper; /** * Returns the conditionals based on which this integration should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Yoast_Admin_And_Dashboard_Conditional::class, Migrations_Conditional::class, Get_Request_Conditional::class, ]; } /** * Shutdown_Indexing_Integration constructor. * * @param Indexable_Post_Indexation_Action $post_indexation The post indexing action. * @param Indexable_Term_Indexation_Action $term_indexation The term indexing action. * @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation The post type archive indexing action. * @param Indexable_General_Indexation_Action $general_indexation The general indexing action. * @param Indexable_Indexing_Complete_Action $complete_indexation_action The complete indexing action. * @param Post_Link_Indexing_Action $post_link_indexing_action The post indexing action. * @param Term_Link_Indexing_Action $term_link_indexing_action The term indexing action. * @param Indexing_Helper $indexing_helper The indexing helper. */ public function __construct( Indexable_Post_Indexation_Action $post_indexation, Indexable_Term_Indexation_Action $term_indexation, Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation, Indexable_General_Indexation_Action $general_indexation, Indexable_Indexing_Complete_Action $complete_indexation_action, Post_Link_Indexing_Action $post_link_indexing_action, Term_Link_Indexing_Action $term_link_indexing_action, Indexing_Helper $indexing_helper ) { $this->post_indexation = $post_indexation; $this->term_indexation = $term_indexation; $this->post_type_archive_indexation = $post_type_archive_indexation; $this->general_indexation = $general_indexation; $this->complete_indexation_action = $complete_indexation_action; $this->post_link_indexing_action = $post_link_indexing_action; $this->term_link_indexing_action = $term_link_indexing_action; $this->indexing_helper = $indexing_helper; } /** * Register hooks. */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'register_shutdown_indexing' ], 10 ); } /** * Enqueues the required scripts. * * @return void */ public function register_shutdown_indexing() { if ( $this->should_index_on_shutdown( $this->get_shutdown_limit() ) ) { \register_shutdown_function( [ $this, 'index' ] ); } } /** * Run a single indexing pass of each indexing action. Intended for use as a shutdown function. * * @return void */ public function index() { $this->post_indexation->index(); $this->term_indexation->index(); $this->general_indexation->index(); $this->post_type_archive_indexation->index(); $this->post_link_indexing_action->index(); $this->term_link_indexing_action->index(); $this->complete_indexation_action->complete(); } /** * Retrieves the shutdown limit. This limit is the amount of indexables that is generated in the background. * * @return int The shutdown limit. */ protected function get_shutdown_limit() { /** * Filter 'wpseo_shutdown_indexation_limit' - Allow filtering the number of objects that can be indexed during shutdown. * * @api int The maximum number of objects indexed. */ return \apply_filters( 'wpseo_shutdown_indexation_limit', 25 ); } /** * Determine whether background indexation should be performed. * * @param int $shutdown_limit The shutdown limit used to determine whether indexation should be run. * * @return bool Should background indexation be performed. */ public function should_index_on_shutdown( $shutdown_limit ) { $total = $this->indexing_helper->get_limited_filtered_unindexed_count( $shutdown_limit ); return ( $total > 0 && $total < $shutdown_limit ); } } admin/social-templates-integration.php 0000644 00000024151 15154134502 0014137 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WP_Taxonomy; use WPSEO_Admin_Editor_Specific_Replace_Vars; use WPSEO_Admin_Recommended_Replace_Vars; use WPSEO_Admin_Utils; use WPSEO_Replacevar_Editor; use WPSEO_Shortlinker; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter; use Yoast\WP\SEO\Presenters\Admin\Badge_Presenter; use Yoast\WP\SEO\Presenters\Admin\Premium_Badge_Presenter; use Yoast_Form; /** * Class Social_Templates_Integration. * * Adds the social fields to the meta tabs for post types, taxonomies and archives. */ class Social_Templates_Integration implements Integration_Interface { /** * Service that can be used to recommend a set of variables for a WPSEO_Replacevar_Editor. * * @var WPSEO_Admin_Recommended_Replace_Vars */ private $recommended_replace_vars; /** * Service that can be used to recommend an editor specific set of variables for a WPSEO_Replacevar_Editor. * * @var WPSEO_Admin_Editor_Specific_Replace_Vars */ private $editor_specific_replace_vars; /** * Group to which the 'New' badges belong to. * * @var string */ private $group = 'global-templates'; /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class, Open_Graph_Conditional::class ]; } /** * Initializes the integration. */ public function register_hooks() { \add_action( 'Yoast\WP\SEO\admin_author_archives_meta', [ $this, 'social_author_archives' ] ); \add_action( 'Yoast\WP\SEO\admin_date_archives_meta', [ $this, 'social_date_archives' ] ); \add_action( 'Yoast\WP\SEO\admin_post_types_beforearchive', [ $this, 'social_post_type' ], \PHP_INT_MAX, 2 ); \add_action( 'Yoast\WP\SEO\admin_post_types_archive', [ $this, 'social_post_types_archive' ], 10, 2 ); \add_action( 'Yoast\WP\SEO\admin_taxonomies_meta', [ $this, 'social_taxonomies' ], 10, 2 ); } /** * Returns the recommended replacements variables object, creating it if needed. * * @return WPSEO_Admin_Recommended_Replace_Vars */ protected function get_admin_recommended_replace_vars() { if ( \is_null( $this->recommended_replace_vars ) ) { $this->recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars(); } return $this->recommended_replace_vars; } /** * Returns the editor specific replacements variables object, creating it if needed. * * @return WPSEO_Admin_Editor_Specific_Replace_Vars */ protected function get_admin_editor_specific_replace_vars() { if ( \is_null( $this->editor_specific_replace_vars ) ) { $this->editor_specific_replace_vars = new WPSEO_Admin_Editor_Specific_Replace_Vars(); } return $this->editor_specific_replace_vars; } /** * Build a set of social fields for the author archives in the Search Appearance section. * * @param Yoast_Form $yform The form builder. */ public function social_author_archives( $yform ) { $identifier = 'author-wpseo'; $page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( 'author' ); $page_type_specific = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( 'author' ); $this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific ); } /** * Build a set of social fields for the date archives in the Search Appearance section. * * @param Yoast_Form $yform The form builder. */ public function social_date_archives( $yform ) { $identifier = 'archive-wpseo'; $page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( 'date' ); $page_type_specific = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( 'date' ); $this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific ); } /** * Build a set of social fields for the post types in the Search Appearance section. * * @param Yoast_Form $yform The form builder. * @param string $post_type_name The name of the current post_type that gets the social fields added. */ public function social_post_type( $yform, $post_type_name ) { if ( $post_type_name === 'attachment' ) { return; } $page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_post_type( $post_type_name ); $page_type_specific = $this->get_admin_editor_specific_replace_vars()->determine_for_post_type( $post_type_name ); $this->build_social_fields( $yform, $post_type_name, $page_type_recommended, $page_type_specific ); } /** * Build a set of social fields for the post types archives in the Search Appearance section. * * @param Yoast_Form $yform The form builder. * @param string $post_type_name The name of the current post_type that gets the social fields added. */ public function social_post_types_archive( $yform, $post_type_name ) { $identifier = 'ptarchive-' . $post_type_name; $page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_archive( $post_type_name ); $page_type_specific = $this->get_admin_editor_specific_replace_vars()->determine_for_archive( $post_type_name ); $this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific ); } /** * Build a set of social fields for the taxonomies in the Search Appearance section. * * @param Yoast_Form $yform The form builder. * @param WP_Taxonomy $taxonomy The taxonomy that gets the social fields added. */ public function social_taxonomies( $yform, $taxonomy ) { $identifier = 'tax-' . $taxonomy->name; $page_type_recommended = $this->get_admin_recommended_replace_vars()->determine_for_term( $taxonomy->name ); $page_type_specific = $this->get_admin_editor_specific_replace_vars()->determine_for_term( $taxonomy->name ); $this->build_social_fields( $yform, $identifier, $page_type_recommended, $page_type_specific ); } /** * Build a set of social fields for the Search Appearance section. * * @param Yoast_Form $yform The form builder. * @param string $identifier A page-wide unique identifier for data storage and unique DOM elements. * @param string $page_type_recommended Recommended type of page for a list of replaceable variables. * @param string $page_type_specific Editor specific type of page for a list of replaceable variables. */ protected function build_social_fields( Yoast_Form $yform, $identifier, $page_type_recommended, $page_type_specific ) { $image_url_field_id = 'social-image-url-' . $identifier; $image_id_field_id = 'social-image-id-' . $identifier; $is_premium = \YoastSEO()->helpers->product->is_premium(); $is_premium_16_5_or_up = \defined( '\WPSEO_PREMIUM_VERSION' ) && \version_compare( \WPSEO_PREMIUM_VERSION, '16.5-RC0', '>=' ); $is_form_enabled = $is_premium && $is_premium_16_5_or_up; $section_class = 'yoast-settings-section'; if ( ! $is_form_enabled ) { $section_class .= ' yoast-settings-section-disabled'; } \printf( '<div class="%s">', \esc_attr( $section_class ) ); echo '<div class="social-settings-heading-container">'; echo '<h3 class="social-settings-heading">' . \esc_html__( 'Social settings', 'wordpress-seo' ) . '</h3>'; if ( $is_form_enabled ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Is correctly escaped in the Premium_Badge_Presenter. echo new Premium_Badge_Presenter( 'global-templates-' . $identifier ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Is correctly escaped in the Badge_Presenter. echo new Badge_Presenter( 'global-templates-' . $identifier, '', $this->group ); } echo '</div>'; $yform->hidden( $image_url_field_id, $image_url_field_id ); $yform->hidden( $image_id_field_id, $image_id_field_id ); \printf( '<div id="%1$s" data-react-image-portal data-react-image-portal-target-image="%2$s" data-react-image-portal-target-image-id="%3$s" data-react-image-portal-is-disabled="%4$s" data-react-image-portal-has-image-validation="%5$s" ></div>', \esc_attr( 'yoast-social-' . $identifier . '-image-select' ), \esc_attr( $image_url_field_id ), \esc_attr( $image_id_field_id ), \esc_attr( ! $is_form_enabled ), true ); $editor = new WPSEO_Replacevar_Editor( $yform, [ 'title' => 'social-title-' . $identifier, 'description' => 'social-description-' . $identifier, 'page_type_recommended' => $page_type_recommended, 'page_type_specific' => $page_type_specific, 'paper_style' => false, 'label_title' => \__( 'Social title', 'wordpress-seo' ), 'label_description' => \__( 'Social description', 'wordpress-seo' ), 'description_placeholder' => \__( 'Modify your social description by editing it right here.', 'wordpress-seo' ), 'is_disabled' => ! $is_form_enabled, ] ); $editor->render(); if ( $is_premium && ! $is_premium_16_5_or_up ) { echo '<div class="yoast-settings-section-upsell">'; $unlock_alert = \sprintf( /* translators: %s expands to 'Yoast SEO Premium'. */ \esc_html__( 'To unlock this feature please update %s to the latest version.', 'wordpress-seo' ), 'Yoast SEO Premium' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped above. echo new Alert_Presenter( $unlock_alert ); echo '</div>'; } if ( ! $is_premium ) { $wpseo_page = \filter_input( \INPUT_GET, 'page' ); echo '<div class="yoast-settings-section-upsell">'; echo '<a class="yoast-button-upsell" href="' . \esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/4e0' ) ) . '" target="_blank">' . \esc_html__( 'Unlock with Premium', 'wordpress-seo' ) // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. . WPSEO_Admin_Utils::get_new_tab_message() . '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>' . '</a>'; echo '</div>'; } echo '</div>'; } } admin/workouts-integration.php 0000644 00000005035 15154134502 0012566 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WPSEO_Admin_Asset_Manager; use WPSEO_Shortlinker; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\Premium_Inactive_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * WorkoutsIntegration class */ class Workouts_Integration implements Integration_Interface { /** * The admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ private $admin_asset_manager; /** * The shortlinker. * * @var WPSEO_Shortlinker */ private $shortlinker; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class, Premium_Inactive_Conditional::class ]; } /** * Workouts_Integration constructor. * * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. * @param WPSEO_Shortlinker $shortlinker The shortlinker. */ public function __construct( WPSEO_Admin_Asset_Manager $admin_asset_manager, WPSEO_Shortlinker $shortlinker ) { $this->admin_asset_manager = $admin_asset_manager; $this->shortlinker = $shortlinker; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 8 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Adds the workouts submenu page. * * @param array $submenu_pages The Yoast SEO submenu pages. * * @return array The filtered submenu pages. */ public function add_submenu_page( $submenu_pages ) { // This inserts the workouts menu page at the correct place in the array without overriding that position. $submenu_pages[] = [ 'wpseo_dashboard', '', \__( 'Workouts', 'wordpress-seo' ) . ' <span class="yoast-badge yoast-premium-badge"></span>', 'edit_others_posts', 'wpseo_workouts', [ $this, 'render' ], ]; return $submenu_pages; } /** * Enqueue the workouts app. */ public function enqueue_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved. if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_workouts' ) { return; } $this->admin_asset_manager->enqueue_style( 'workouts' ); } /** * Renders the target for the React to mount to. */ public function render() { $cornerstone_guide = $this->shortlinker->build_shortlink( 'https://yoa.st/4f1' ); $cornerstone_upsell = $this->shortlinker->build_shortlink( 'https://yoa.st/4f2' ); require_once \WPSEO_PATH . 'admin/views/workouts.php'; } } admin/cron-integration.php 0000644 00000001755 15154134502 0011637 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Helpers\Date_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Cron_Integration class. */ class Cron_Integration implements Integration_Interface { /** * The indexing notification integration. * * @var Date_Helper */ protected $date_helper; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Cron_Integration constructor * * @param Date_Helper $date_helper The date helper. */ public function __construct( Date_Helper $date_helper ) { $this->date_helper = $date_helper; } /** * {@inheritDoc} */ public function register_hooks() { if ( ! \wp_next_scheduled( Indexing_Notification_Integration::NOTIFICATION_ID ) ) { \wp_schedule_event( $this->date_helper->current_time(), 'daily', Indexing_Notification_Integration::NOTIFICATION_ID ); } } } admin/menu-badge-integration.php 0000644 00000001624 15154134502 0012675 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Menu_Badge_Integration class. */ class Menu_Badge_Integration implements Integration_Interface { /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'add_inline_styles' ] ); } /** * Renders the migration error. * * @return void */ public function add_inline_styles() { $custom_css = 'ul.wp-submenu span.yoast-premium-badge::after, #wpadminbar span.yoast-premium-badge::after { content:"' . \__( 'Premium', 'wordpress-seo' ) . '"}'; \wp_add_inline_style( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global', $custom_css ); } } admin/link-count-columns-integration.php 0000644 00000016760 15154134502 0014441 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WP_Query; use wpdb; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Actions\Indexing\Post_Link_Indexing_Action; use Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\Should_Index_Links_Conditional; use Yoast\WP\SEO\Helpers\Post_Type_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Link_Count_Columns_Integration class. */ class Link_Count_Columns_Integration implements Integration_Interface { /** * Partial column name. * * @var string */ const COLUMN_LINKED = 'linked'; /** * Partial column name. * * @var string */ const COLUMN_LINKS = 'links'; /** * The post type helper. * * @var Post_Type_Helper */ protected $post_type_helper; /** * The database object. * * @var wpdb */ protected $wpdb; /** * The post link builder. * * @var Post_Link_Indexing_Action */ protected $post_link_indexing_action; /** * The admin columns cache. * * @var Admin_Columns_Cache_Integration */ protected $admin_columns_cache; /** * {@inheritDoc} */ public static function get_conditionals() { return [ Admin_Conditional::class, Posts_Overview_Or_Ajax_Conditional::class, Should_Index_Links_Conditional::class, ]; } /** * Link_Count_Columns_Integration constructor * * @param Post_Type_Helper $post_type_helper The post type helper. * @param wpdb $wpdb The wpdb object. * @param Post_Link_Indexing_Action $post_link_indexing_action The post link indexing action. * @param Admin_Columns_Cache_Integration $admin_columns_cache The admin columns cache. */ public function __construct( Post_Type_Helper $post_type_helper, wpdb $wpdb, Post_Link_Indexing_Action $post_link_indexing_action, Admin_Columns_Cache_Integration $admin_columns_cache ) { $this->post_type_helper = $post_type_helper; $this->wpdb = $wpdb; $this->post_link_indexing_action = $post_link_indexing_action; $this->admin_columns_cache = $admin_columns_cache; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'posts_clauses', [ $this, 'order_by_links' ], 1, 2 ); \add_filter( 'posts_clauses', [ $this, 'order_by_linked' ], 1, 2 ); \add_filter( 'admin_init', [ $this, 'register_init_hooks' ] ); // Adds a filter to exclude the attachments from the link count. \add_filter( 'wpseo_link_count_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] ); } /** * Register hooks that need to be registered after `init` due to all post types not yet being registered. */ public function register_init_hooks() { $public_post_types = \apply_filters( 'wpseo_link_count_post_types', $this->post_type_helper->get_accessible_post_types() ); if ( ! \is_array( $public_post_types ) || empty( $public_post_types ) ) { return; } foreach ( $public_post_types as $post_type ) { \add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_post_columns' ] ); \add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 ); \add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] ); } } /** * Adds the columns for the post overview. * * @param array $columns Array with columns. * * @return array The extended array with columns. */ public function add_post_columns( $columns ) { if ( ! \is_array( $columns ) ) { return $columns; } $columns[ 'wpseo-' . self::COLUMN_LINKS ] = \sprintf( '<span class="yoast-linked-to yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>', \esc_attr__( 'Number of outgoing internal links in this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ), \esc_html__( 'Outgoing internal links', 'wordpress-seo' ) ); if ( $this->post_link_indexing_action->get_total_unindexed() === 0 ) { $columns[ 'wpseo-' . self::COLUMN_LINKED ] = \sprintf( '<span class="yoast-linked-from yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>', \esc_attr__( 'Number of internal links linking to this post. See "Yoast Columns" text in the help tab for more info.', 'wordpress-seo' ), \esc_html__( 'Received internal links', 'wordpress-seo' ) ); } return $columns; } /** * Modifies the query pieces to allow ordering column by links to post. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * * @return array */ public function order_by_linked( $pieces, $query ) { if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKED ) { return $pieces; } return $this->build_sort_query_pieces( $pieces, $query, 'incoming_link_count' ); } /** * Modifies the query pieces to allow ordering column by links to post. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * * @return array */ public function order_by_links( $pieces, $query ) { if ( $query->get( 'orderby' ) !== 'wpseo-' . self::COLUMN_LINKS ) { return $pieces; } return $this->build_sort_query_pieces( $pieces, $query, 'link_count' ); } /** * Builds the pieces for a sorting query. * * @param array $pieces Array of Query pieces. * @param WP_Query $query The Query on which to apply. * @param string $field The field in the table to JOIN on. * * @return array Modified Query pieces. */ protected function build_sort_query_pieces( $pieces, $query, $field ) { // We only want our code to run in the main WP query. if ( ! $query->is_main_query() ) { return $pieces; } // Get the order query variable - ASC or DESC. $order = \strtoupper( $query->get( 'order' ) ); // Make sure the order setting qualifies. If not, set default as ASC. if ( ! \in_array( $order, [ 'ASC', 'DESC' ], true ) ) { $order = 'ASC'; } $table = Model::get_table_name( 'Indexable' ); $pieces['join'] .= " LEFT JOIN $table AS yoast_indexable ON yoast_indexable.object_id = {$this->wpdb->posts}.ID AND yoast_indexable.object_type = 'post' "; $pieces['orderby'] = "yoast_indexable.$field $order, FIELD( {$this->wpdb->posts}.post_status, 'publish' ) $order, {$pieces['orderby']}"; return $pieces; } /** * Displays the column content for the given column. * * @param string $column_name Column to display the content for. * @param int $post_id Post to display the column content for. */ public function column_content( $column_name, $post_id ) { $indexable = $this->admin_columns_cache->get_indexable( $post_id ); // Nothing to output if we don't have the value. if ( $indexable === false ) { return; } switch ( $column_name ) { case 'wpseo-' . self::COLUMN_LINKS: echo (int) $indexable->link_count; return; case 'wpseo-' . self::COLUMN_LINKED: if ( \get_post_status( $post_id ) === 'publish' ) { echo (int) $indexable->incoming_link_count; } } } /** * Sets the sortable columns. * * @param array $columns Array with sortable columns. * * @return array The extended array with sortable columns. */ public function column_sort( array $columns ) { $columns[ 'wpseo-' . self::COLUMN_LINKS ] = 'wpseo-' . self::COLUMN_LINKS; $columns[ 'wpseo-' . self::COLUMN_LINKED ] = 'wpseo-' . self::COLUMN_LINKED; return $columns; } } admin/fix-news-dependencies-integration.php 0000644 00000003040 15154134502 0015047 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WP_Screen; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\News_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Fix_News_Dependencies_Integration class. */ class Fix_News_Dependencies_Integration implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * In this case: when on an admin page. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, News_Conditional::class ]; } /** * Registers an action to disable script concatenation. */ public function register_hooks() { global $pagenow; // Load the editor script when on an edit post or new post page. $is_post_edit_page = $pagenow === 'post.php' || $pagenow === 'post-new.php'; if ( $is_post_edit_page ) { \add_action( 'admin_enqueue_scripts', [ $this, 'add_news_script_dependency' ], 11 ); } } /** * Fixes the news script dependency. * * @return void */ public function add_news_script_dependency() { $scripts = \wp_scripts(); if ( ! isset( $scripts->registered['wpseo-news-editor'] ) ) { return; } $is_block_editor = WP_Screen::get()->is_block_editor(); $post_edit_handle = 'post-edit'; if ( ! $is_block_editor ) { $post_edit_handle = 'post-edit-classic'; } $scripts->registered['wpseo-news-editor']->deps[] = WPSEO_Admin_Asset_Manager::PREFIX . $post_edit_handle; } } admin/indexing-tool-integration.php 0000644 00000015555 15154134502 0013461 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WPSEO_Addon_Manager; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Conditionals\No_Tool_Selected_Conditional; use Yoast\WP\SEO\Conditionals\Yoast_Tools_Page_Conditional; use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Helpers\Indexing_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Indexing_Error_Presenter; use Yoast\WP\SEO\Presenters\Admin\Indexing_List_Item_Presenter; use Yoast\WP\SEO\Routes\Indexing_Route; /** * Class Indexing_Tool_Integration. Bridge to the Javascript indexing tool on Yoast SEO Tools page. * * @package Yoast\WP\SEO\Integrations\Admin */ class Indexing_Tool_Integration implements Integration_Interface { /** * Represents the admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Represents the indexables helper. * * @var Indexable_Helper */ protected $indexable_helper; /** * The short link helper. * * @var Short_Link_Helper */ protected $short_link_helper; /** * Represents the indexing helper. * * @var Indexing_Helper */ protected $indexing_helper; /** * The addon manager. * * @var WPSEO_Addon_Manager */ protected $addon_manager; /** * The product helper. * * @var Product_Helper */ protected $product_helper; /** * Returns the conditionals based on which this integration should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class, No_Tool_Selected_Conditional::class, Yoast_Tools_Page_Conditional::class, ]; } /** * Indexing_Integration constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Short_Link_Helper $short_link_helper The short link helper. * @param Indexing_Helper $indexing_helper The indexing helper. * @param WPSEO_Addon_Manager $addon_manager The addon manager. * @param Product_Helper $product_helper The product helper. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Indexable_Helper $indexable_helper, Short_Link_Helper $short_link_helper, Indexing_Helper $indexing_helper, WPSEO_Addon_Manager $addon_manager, Product_Helper $product_helper ) { $this->asset_manager = $asset_manager; $this->indexable_helper = $indexable_helper; $this->short_link_helper = $short_link_helper; $this->indexing_helper = $indexing_helper; $this->addon_manager = $addon_manager; $this->product_helper = $product_helper; } /** * Register hooks. */ public function register_hooks() { \add_action( 'wpseo_tools_overview_list_items', [ $this, 'render_indexing_list_item' ], 10 ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ], 10 ); } /** * Enqueues the required scripts. * * @return void */ public function enqueue_scripts() { $this->asset_manager->enqueue_script( 'indexation' ); $this->asset_manager->enqueue_style( 'admin-css' ); $this->asset_manager->enqueue_style( 'monorepo' ); $data = [ 'disabled' => ! $this->indexable_helper->should_index_indexables(), 'amount' => $this->indexing_helper->get_filtered_unindexed_count(), 'firstTime' => ( $this->indexing_helper->is_initial_indexing() === true ), 'errorMessage' => $this->render_indexing_error(), 'restApi' => [ 'root' => \esc_url_raw( \rest_url() ), 'endpoints' => $this->get_endpoints(), 'nonce' => \wp_create_nonce( 'wp_rest' ), ], ]; /** * Filter: 'wpseo_indexing_data' Filter to adapt the data used in the indexing process. * * @param array $data The indexing data to adapt. */ $data = \apply_filters( 'wpseo_indexing_data', $data ); $this->asset_manager->localize_script( 'indexation', 'yoastIndexingData', $data ); } /** * The error to show if optimization failed. * * @return string The error to show if optimization failed. */ protected function render_indexing_error() { $presenter = new Indexing_Error_Presenter( $this->short_link_helper, $this->product_helper, $this->addon_manager ); return $presenter->present(); } /** * Determines if the site has a valid Premium subscription. * * @return bool If the site has a valid Premium subscription. */ protected function has_valid_premium_subscription() { return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ); } /** * Renders the indexing list item. * * @return void */ public function render_indexing_list_item() { if ( \current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The output is correctly escaped in the presenter. echo new Indexing_List_Item_Presenter( $this->short_link_helper ); } } /** * Retrieves a list of the endpoints to use. * * @return array The endpoints. */ protected function get_endpoints() { $endpoints = [ 'prepare' => Indexing_Route::FULL_PREPARE_ROUTE, 'terms' => Indexing_Route::FULL_TERMS_ROUTE, 'posts' => Indexing_Route::FULL_POSTS_ROUTE, 'archives' => Indexing_Route::FULL_POST_TYPE_ARCHIVES_ROUTE, 'general' => Indexing_Route::FULL_GENERAL_ROUTE, 'indexablesComplete' => Indexing_Route::FULL_INDEXABLES_COMPLETE_ROUTE, 'post_link' => Indexing_Route::FULL_POST_LINKS_INDEXING_ROUTE, 'term_link' => Indexing_Route::FULL_TERM_LINKS_INDEXING_ROUTE, ]; $endpoints = \apply_filters( 'wpseo_indexing_endpoints', $endpoints ); $endpoints['complete'] = Indexing_Route::FULL_COMPLETE_ROUTE; return $endpoints; } /** * Returns the total number of unindexed objects. * * @deprecated 15.3 * @codeCoverageIgnore * * @param int $unindexed_count The total number of unindexed indexables. * * @return int The total number of unindexed objects. */ public function get_unindexed_indexables_count( $unindexed_count = 0 ) { \_deprecated_function( __METHOD__, 'WPSEO 15.3' ); return $this->indexing_helper->get_unindexed_count(); } /** * Returns the total number of unindexed objects and applies a filter for third party integrations. * * @deprecated 15.3 * @codeCoverageIgnore * * @param int $unindexed_count The total number of unindexed objects. * * @return int The total number of unindexed objects. */ public function get_unindexed_count( $unindexed_count = 0 ) { \_deprecated_function( __METHOD__, 'WPSEO 15.3' ); return $this->indexing_helper->get_filtered_unindexed_count(); } } admin/admin-columns-cache-integration.php 0000644 00000016734 15154134502 0014510 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WP_Post; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Admin_Columns_Cache_Integration class. */ class Admin_Columns_Cache_Integration implements Integration_Interface { /** * Cache of indexables. * * @var Indexable[] */ protected $indexable_cache = []; /** * The indexable repository. * * @var Indexable_Repository */ protected $indexable_repository; /** * Returns the conditionals based on which this loadable should be active. * * In this case: only when on an admin page. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Admin_Columns_Cache_Integration constructor. * * @param Indexable_Repository $indexable_repository The indexable repository. */ public function __construct( Indexable_Repository $indexable_repository ) { $this->indexable_repository = $indexable_repository; } /** * Registers the appropriate actions and filters to fill the cache with * indexables on admin pages. * * This cache is used in showing the Yoast SEO columns on the posts overview * page (e.g. keyword score, incoming link count, etc.) */ public function register_hooks() { // Hook into tablenav to calculate links and linked. \add_action( 'manage_posts_extra_tablenav', [ $this, 'maybe_fill_cache' ] ); } /** * Makes sure we calculate all values in one query by filling our cache beforehand. * * @param string $target Extra table navigation location which is triggered. */ public function maybe_fill_cache( $target ) { if ( $target === 'top' ) { $this->fill_cache(); } } /** * Fills the cache of indexables for all known post IDs. */ public function fill_cache() { global $wp_query; // No need to continue building a cache if the main query did not return anything to cache. if ( empty( $wp_query->posts ) ) { return; } $posts = $wp_query->posts; $post_ids = []; // Post lists return a list of objects. if ( isset( $posts[0] ) && \is_a( $posts[0], 'WP_Post' ) ) { $post_ids = \wp_list_pluck( $posts, 'ID' ); } elseif ( isset( $posts[0] ) && \is_object( $posts[0] ) ) { $post_ids = $this->get_current_page_page_ids( $posts ); } elseif ( ! empty( $posts ) ) { // Page list returns an array of post IDs. $post_ids = \array_keys( $posts ); } if ( empty( $post_ids ) ) { return; } if ( isset( $posts[0] ) && ! \is_a( $posts[0], WP_Post::class ) ) { // Prime the post caches as core would to avoid duplicate queries. // This needs to be done as this executes before core does. \_prime_post_caches( $post_ids ); } $indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_ids, 'post', false ); foreach ( $indexables as $indexable ) { $this->indexable_cache[ $indexable->object_id ] = $indexable; } } /** * Returns the indexable for a given post ID. * * @param int $post_id The post ID. * * @return Indexable|false The indexable. False if none could be found. */ public function get_indexable( $post_id ) { if ( ! \array_key_exists( $post_id, $this->indexable_cache ) ) { $this->indexable_cache[ $post_id ] = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); } return $this->indexable_cache[ $post_id ]; } /** * Gets all the page IDs set to be shown on the current page. * This is copied over with some changes from WP_Posts_List_Table::_display_rows_hierarchical. * * @param array $pages The pages, each containing an ID and post_parent. * * @return array The IDs of all pages shown on the current page. */ private function get_current_page_page_ids( $pages ) { global $per_page; $pagenum = isset( $_REQUEST['paged'] ) ? \absint( $_REQUEST['paged'] ) : 0; $pagenum = \max( 1, $pagenum ); /* * Arrange pages into two parts: top level pages and children_pages * children_pages is two dimensional array, eg. * children_pages[10][] contains all sub-pages whose parent is 10. * It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations * If searching, ignore hierarchy and treat everything as top level */ if ( empty( $_REQUEST['s'] ) ) { $top_level_pages = []; $children_pages = []; $pages_map = []; foreach ( $pages as $page ) { // Catch and repair bad pages. if ( $page->post_parent === $page->ID ) { $page->post_parent = 0; } if ( $page->post_parent === 0 ) { $top_level_pages[] = $page; } else { $children_pages[ $page->post_parent ][] = $page; } $pages_map[ $page->ID ] = $page; } $pages = &$top_level_pages; } $count = 0; $start = ( ( $pagenum - 1 ) * $per_page ); $end = ( $start + $per_page ); $to_display = []; foreach ( $pages as $page ) { if ( $count >= $end ) { break; } if ( $count >= $start ) { $to_display[] = $page->ID; } ++$count; $this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map ); } // If it is the last pagenum and there are orphaned pages, display them with paging as well. if ( isset( $children_pages ) && $count < $end ) { foreach ( $children_pages as $orphans ) { foreach ( $orphans as $op ) { if ( $count >= $end ) { break; } if ( $count >= $start ) { $to_display[] = $op->ID; } ++$count; } } } return $to_display; } /** * Adds all child pages due to be shown on the current page to the $to_display array. * Copied over with some changes from WP_Posts_List_Table::_page_rows. * * @param array $children_pages The full map of child pages. * @param int $count The number of pages already processed. * @param int $parent The parent that's currently being processed. * @param int $start The number at which the current overview starts. * @param int $end The number at which the current overview ends. * @param int $to_display The page IDs to be shown. * @param int $pages_map A map of page ID to an object with ID and post_parent. * * @return void */ private function get_child_page_ids( &$children_pages, &$count, $parent, $start, $end, &$to_display, &$pages_map ) { if ( ! isset( $children_pages[ $parent ] ) ) { return; } foreach ( $children_pages[ $parent ] as $page ) { if ( $count >= $end ) { break; } // If the page starts in a subtree, print the parents. if ( $count === $start && $page->post_parent > 0 ) { $my_parents = []; $my_parent = $page->post_parent; while ( $my_parent ) { // Get the ID from the list or the attribute if my_parent is an object. $parent_id = $my_parent; if ( \is_object( $my_parent ) ) { $parent_id = $my_parent->ID; } $my_parent = $pages_map[ $parent_id ]; $my_parents[] = $my_parent; if ( ! $my_parent->post_parent ) { break; } $my_parent = $my_parent->post_parent; } while ( $my_parent = \array_pop( $my_parents ) ) { $to_display[] = $my_parent->ID; } } if ( $count >= $start ) { $to_display[] = $page->ID; } ++$count; $this->get_child_page_ids( $children_pages, $count, $page->ID, $start, $end, $to_display, $pages_map ); } unset( $children_pages[ $parent ] ); // Required in order to keep track of orphans. } } admin/indexing-notification-integration.php 0000644 00000016667 15154134502 0015177 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use WPSEO_Addon_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Config\Indexing_Reasons; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Environment_Helper; use Yoast\WP\SEO\Helpers\Indexing_Helper; use Yoast\WP\SEO\Helpers\Notification_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; use Yoast\WP\SEO\Helpers\Short_Link_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Indexing_Failed_Notification_Presenter; use Yoast\WP\SEO\Presenters\Admin\Indexing_Notification_Presenter; use Yoast_Notification; use Yoast_Notification_Center; /** * Class Indexing_Notification_Integration. * * @package Yoast\WP\SEO\Integrations\Admin */ class Indexing_Notification_Integration implements Integration_Interface { /** * The notification ID. */ const NOTIFICATION_ID = 'wpseo-reindex'; /** * Represents the reason that the indexing process failed and should be tried again. * * @deprecated 15.3 */ const REASON_INDEXING_FAILED = Indexing_Reasons::REASON_INDEXING_FAILED; /** * Represents the reason that the permalink settings are changed. * * @deprecated 15.3 */ const REASON_PERMALINK_SETTINGS = Indexing_Reasons::REASON_PERMALINK_SETTINGS; /** * Represents the reason that the category base is changed. * * @deprecated 15.3 */ const REASON_CATEGORY_BASE_PREFIX = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX; /** * Represents the reason that the tag base is changed. * * @deprecated 15.3 */ const REASON_TAG_BASE_PREFIX = Indexing_Reasons::REASON_TAG_BASE_PREFIX; /** * Represents the reason that the home url option is changed. * * @deprecated 15.3 */ const REASON_HOME_URL_OPTION = Indexing_Reasons::REASON_HOME_URL_OPTION; /** * The Yoast notification center. * * @var Yoast_Notification_Center */ protected $notification_center; /** * The product helper. * * @var Product_Helper */ protected $product_helper; /** * The current page helper. * * @var Current_Page_Helper */ protected $page_helper; /** * The short link helper. * * @var Short_Link_Helper */ protected $short_link_helper; /** * The notification helper. * * @var Notification_Helper */ protected $notification_helper; /** * The indexing helper. * * @var Indexing_Helper */ protected $indexing_helper; /** * The Addon Manager. * * @var WPSEO_Addon_Manager */ protected $addon_manager; /** * The Environment Helper. * * @var Environment_Helper */ protected $environment_helper; /** * Indexing_Notification_Integration constructor. * * @param Yoast_Notification_Center $notification_center The notification center. * @param Product_Helper $product_helper The product helper. * @param Current_Page_Helper $page_helper The current page helper. * @param Short_Link_Helper $short_link_helper The short link helper. * @param Notification_Helper $notification_helper The notification helper. * @param Indexing_Helper $indexing_helper The indexing helper. * @param WPSEO_Addon_Manager $addon_manager The addon manager. * @param Environment_Helper $environment_helper The environment helper. */ public function __construct( Yoast_Notification_Center $notification_center, Product_Helper $product_helper, Current_Page_Helper $page_helper, Short_Link_Helper $short_link_helper, Notification_Helper $notification_helper, Indexing_Helper $indexing_helper, WPSEO_Addon_Manager $addon_manager, Environment_Helper $environment_helper ) { $this->notification_center = $notification_center; $this->product_helper = $product_helper; $this->page_helper = $page_helper; $this->short_link_helper = $short_link_helper; $this->notification_helper = $notification_helper; $this->indexing_helper = $indexing_helper; $this->addon_manager = $addon_manager; $this->environment_helper = $environment_helper; } /** * Initializes the integration. * * Adds hooks and jobs to cleanup or add the notification when necessary. * * @return void */ public function register_hooks() { if ( $this->page_helper->get_current_yoast_seo_page() === 'wpseo_dashboard' ) { \add_action( 'admin_init', [ $this, 'maybe_cleanup_notification' ] ); } if ( $this->indexing_helper->has_reason() ) { \add_action( 'admin_init', [ $this, 'maybe_create_notification' ] ); } \add_action( self::NOTIFICATION_ID, [ $this, 'maybe_create_notification' ] ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class, ]; } /** * Checks whether the notification should be shown and adds * it to the notification center if this is the case. */ public function maybe_create_notification() { if ( ! $this->should_show_notification() ) { return; } if ( ! $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ) ) { $notification = $this->notification(); $this->notification_helper->restore_notification( $notification ); $this->notification_center->add_notification( $notification ); } } /** * Checks whether the notification should not be shown anymore and removes * it from the notification center if this is the case. */ public function maybe_cleanup_notification() { $notification = $this->notification_center->get_notification_by_id( self::NOTIFICATION_ID ); if ( $notification === null ) { return; } if ( $this->should_show_notification() ) { return; } $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Checks whether the notification should be shown. * * @return bool If the notification should be shown. */ protected function should_show_notification() { if ( ! $this->environment_helper->is_production_mode() ) { return false; } // Don't show a notification if the indexing has already been started earlier. if ( $this->indexing_helper->get_started() > 0 ) { return false; } // Never show a notification when nothing should be indexed. return $this->indexing_helper->get_limited_filtered_unindexed_count( 1 ) > 0; } /** * Returns an instance of the notification. * * @return Yoast_Notification The notification to show. */ protected function notification() { $reason = $this->indexing_helper->get_reason(); $presenter = $this->get_presenter( $reason ); return new Yoast_Notification( $presenter, [ 'type' => Yoast_Notification::WARNING, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ] ); } /** * Gets the presenter to use to show the notification. * * @param string $reason The reason for the notification. * * @return Indexing_Failed_Notification_Presenter|Indexing_Notification_Presenter */ protected function get_presenter( $reason ) { if ( $reason === Indexing_Reasons::REASON_INDEXING_FAILED ) { $presenter = new Indexing_Failed_Notification_Presenter( $this->product_helper, $this->short_link_helper, $this->addon_manager ); } else { $total_unindexed = $this->indexing_helper->get_filtered_unindexed_count(); $presenter = new Indexing_Notification_Presenter( $this->short_link_helper, $total_unindexed, $reason ); } return $presenter; } } admin/disable-concatenate-scripts-integration.php 0000644 00000001756 15154134502 0016251 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Admin; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Disable_Concatenate_Scripts_Integration class. */ class Disable_Concatenate_Scripts_Integration implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * In this case: when on an admin page. * * @return array The conditionals. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Registers an action to disable script concatenation. */ public function register_hooks() { \add_action( 'wp_print_scripts', [ $this, 'disable_concatenate_scripts' ] ); } /** * Due to bugs in the 5.5 core release concatenate scripts is causing errors. * * Because of this we disable it. * * @return void */ public function disable_concatenate_scripts() { global $concatenate_scripts; $concatenate_scripts = false; } } duplicate-post-integration.php 0000644 00000001534 15154134502 0012536 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Yoast\WP\SEO\Conditionals\No_Conditionals; /** * Class to manage the integration with Yoast Duplicate Post. */ class Duplicate_Post_Integration implements Integration_Interface { use No_Conditionals; /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'duplicate_post_excludelist_filter', [ $this, 'exclude_zapier_meta' ] ); } /** * Filters out the Zapier meta when you copy a post with Yoast Duplicate Post. * * @param array $meta_excludelist The current excludelist of meta fields. * * @return array The updated excludelist. */ public function exclude_zapier_meta( $meta_excludelist ) { $meta_excludelist[] = 'zapier_trigger_sent'; return $meta_excludelist; } } cleanup-integration.php 0000644 00000022747 15154134502 0011241 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Closure; use Yoast\WP\Lib\Model; /** * Adds cleanup hooks. */ class Cleanup_Integration implements Integration_Interface { /** * Identifier used to determine the current task. */ const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task'; /** * Identifier for the cron job. */ const CRON_HOOK = 'wpseo_cleanup_cron'; /** * Identifier for starting the cleanup. */ const START_HOOK = 'wpseo_start_cleanup_indexables'; /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( self::START_HOOK, [ $this, 'run_cleanup' ] ); \add_action( self::CRON_HOOK, [ $this, 'run_cleanup_cron' ] ); \add_action( 'wpseo_deactivate', [ $this, 'reset_cleanup' ] ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array The array of conditionals. */ public static function get_conditionals() { return []; } /** * Starts the indexables cleanup. * * @return void */ public function run_cleanup() { $this->reset_cleanup(); $cleanups = $this->get_cleanup_tasks(); $limit = $this->get_limit(); foreach ( $cleanups as $name => $action ) { $items_cleaned = $action( $limit ); if ( $items_cleaned === false ) { return; } if ( $items_cleaned < $limit ) { continue; } // There are more items to delete for the current cleanup job, start a cronjob at the specified job. $this->start_cron_job( $name ); return; } } /** * Returns an array of cleanup tasks. * * @return Closure[] The cleanup tasks. */ protected function get_cleanup_tasks() { return \array_merge( [ 'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function( $limit ) { return $this->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit ); }, 'clean_indexables_by_post_status_auto-draft' => function( $limit ) { return $this->clean_indexables_with_post_status( 'auto-draft', $limit ); }, ], $this->get_additional_tasks(), [ /* These should always be the last ones to be called. */ 'clean_orphaned_content_indexable_hierarchy' => function( $limit ) { return $this->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit ); }, 'clean_orphaned_content_seo_links_indexable_id' => function( $limit ) { return $this->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit ); }, 'clean_orphaned_content_seo_links_target_indexable_id' => function( $limit ) { return $this->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit ); }, ] ); } /** * Gets additional tasks from the 'wpseo_cleanup_tasks' filter. * * @return Closure[] Associative array of cleanup functions. */ private function get_additional_tasks() { /** * Filter: Adds the possibility to add addition cleanup functions. * * @api array Associative array with unique keys. Value should be a cleanup function that receives a limit. */ $additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] ); if ( ! \is_array( $additional_tasks ) ) { return []; } foreach ( $additional_tasks as $key => $value ) { if ( \is_int( $key ) ) { return []; } if ( ( ! \is_object( $value ) ) || ! ( $value instanceof Closure ) ) { return []; } } return $additional_tasks; } /** * Gets the deletion limit for cleanups. * * @return int The limit for the amount of entities to be cleaned. */ private function get_limit() { /** * Filter: Adds the possibility to limit the number of items that are deleted from the database on cleanup. * * @api int $limit Maximum number of indexables to be cleaned up per query. */ $limit = \apply_filters( 'wpseo_cron_query_limit_size', 1000 ); if ( ! \is_int( $limit ) ) { $limit = 1000; } return \abs( $limit ); } /** * Resets and stops the cleanup integration. * * @return void */ public function reset_cleanup() { \delete_option( self::CURRENT_TASK_OPTION ); \wp_unschedule_hook( self::CRON_HOOK ); } /** * Starts the cleanup cron job. * * @param string $task_name The task name of the next cleanup task to run. * * @return void */ private function start_cron_job( $task_name ) { \update_option( self::CURRENT_TASK_OPTION, $task_name ); \wp_schedule_event( ( \time() + \HOUR_IN_SECONDS ), 'hourly', self::CRON_HOOK ); } /** * The callback that is called for the cleanup cron job. * * @return void */ public function run_cleanup_cron() { $current_task_name = \get_option( self::CURRENT_TASK_OPTION ); if ( $current_task_name === false ) { $this->reset_cleanup(); return; } $limit = $this->get_limit(); $tasks = $this->get_cleanup_tasks(); // The task may have been added by a filter that has been removed, in that case just start over. if ( ! isset( $tasks[ $current_task_name ] ) ) { $current_task_name = \key( $tasks ); } $current_task = \current( $tasks ); while ( $current_task !== false ) { // Skip the tasks that have already been done. if ( \key( $tasks ) !== $current_task_name ) { $current_task = \next( $tasks ); continue; } // Call the cleanup callback function that accompanies the current task. $items_cleaned = $current_task( $limit ); if ( $items_cleaned === false ) { $this->reset_cleanup(); return; } if ( $items_cleaned === 0 ) { // Check if we are finished with all tasks. if ( \next( $tasks ) === false ) { $this->reset_cleanup(); return; } // Continue with the next task next time the cron job is run. \update_option( self::CURRENT_TASK_OPTION, \key( $tasks ) ); return; } // There were items deleted for the current task, continue with the same task next cron call. return; } } /** * Deletes rows from the indexable table depending on the object_type and object_sub_type. * * @param string $object_type The object type to query. * @param string $object_sub_type The object subtype to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ protected function clean_indexables_with_object_type_and_object_sub_type( $object_type, $object_sub_type, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = %s AND object_sub_type = %s ORDER BY id LIMIT %d", $object_type, $object_sub_type, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Deletes rows from the indexable table depending on the post_status. * * @param string $post_status The post status to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ protected function clean_indexables_with_post_status( $post_status, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND post_status = %s ORDER BY id LIMIT %d", $post_status, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Cleans orphaned rows from a yoast table. * * @param string $table The table to clean up. * @param string $column The table column the cleanup will rely on. * @param int $limit The limit we'll apply to the queries. * * @return int|bool The number of deleted rows, false if the query fails. */ protected function cleanup_orphaned_from_table( $table, $column, $limit ) { global $wpdb; $table = Model::get_table_name( $table ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT table_to_clean.{$column} FROM {$table} table_to_clean LEFT JOIN {$indexable_table} AS indexable_table ON table_to_clean.{$column} = indexable_table.id WHERE indexable_table.id IS NULL AND table_to_clean.{$column} IS NOT NULL LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' )' ); } } integration-interface.php 0000644 00000000652 15154134502 0011541 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Yoast\WP\SEO\Loadable_Interface; /** * An interface for registering integrations with WordPress. * * @codeCoverageIgnore It represents an interface. */ interface Integration_Interface extends Loadable_Interface { /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks(); } abstract-exclude-post-type.php 0000644 00000002310 15154134502 0012445 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; /** * Abstract class for excluding certain post types from being indexed. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded */ abstract class Abstract_Exclude_Post_Type implements Integration_Interface { /** * Initializes the integration. */ public function register_hooks() { \add_filter( 'wpseo_indexable_excluded_post_types', [ $this, 'exclude_post_types' ] ); } /** * Exclude the post type from the indexable table. * * @param array $excluded_post_types The excluded post types. * * @return array The excluded post types, including the specific post type. */ public function exclude_post_types( $excluded_post_types ) { return \array_merge( $excluded_post_types, $this->get_post_type() ); } /** * This integration is only active when the child class's conditionals are met. * * @return array|string[] The conditionals. */ public static function get_conditionals() { return []; } /** * Returns the names of the post types to be excluded. * To be used in the wpseo_indexable_excluded_post_types filter. * * @return array The names of the post types. */ abstract public function get_post_type(); } alerts/abstract-dismissable-alert.php 0000644 00000001645 15154134502 0013762 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Alerts; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Dismissable_Alert class. */ abstract class Abstract_Dismissable_Alert implements Integration_Interface { /** * Holds the alert identifier. * * @var string */ protected $alert_identifier; /** * {@inheritDoc} */ public static function get_conditionals() { return []; } /** * {@inheritDoc} */ public function register_hooks() { \add_filter( 'wpseo_allowed_dismissable_alerts', [ $this, 'register_dismissable_alert' ] ); } /** * Registers the dismissable alert. * * @param string[] $allowed_dismissable_alerts The allowed dismissable alerts. * * @return string[] The allowed dismissable alerts. */ public function register_dismissable_alert( $allowed_dismissable_alerts ) { $allowed_dismissable_alerts[] = $this->alert_identifier; return $allowed_dismissable_alerts; } } xmlrpc.php 0000644 00000002344 15154134502 0006565 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Yoast\WP\SEO\Conditionals\XMLRPC_Conditional; /** * Noindexes the xmlrpc.php file and all ways to request it. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Known false positive with acronyms. Fix expected in YoastCS 3.x. */ class XMLRPC implements Integration_Interface { /** * Returns the conditionals based on which this loadable should be active. * * In this case when the current request is an XML-RPC request. * * @return array The conditionals based on which this class should be loaded. */ public static function get_conditionals() { return [ XMLRPC_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_filter( 'xmlrpc_methods', [ $this, 'robots_header' ] ); } /** * Sets a noindex, follow x-robots-tag header on all XMLRPC requests. * * @codeCoverageIgnore Basically impossible to test from the command line. * * @param array $methods The methods. * * @return array The methods. */ public function robots_header( $methods ) { if ( \headers_sent() === false ) { \header( 'X-Robots-Tag: noindex, follow', true ); } return $methods; } } exclude-oembed-cache-post-type.php 0000644 00000001577 15154134502 0013154 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; /** * Excludes certain oEmbed Cache-specific post types from the indexable table. * * Posts with these post types will not be saved to the indexable table. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded */ class Exclude_Oembed_Cache_Post_Type extends Abstract_Exclude_Post_Type { /** * This integration is only active when the database migrations have been run. * * @return array|string[] The conditionals. */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Returns the names of the post types to be excluded. * To be used in the wpseo_indexable_excluded_post_types filter. * * @return array The names of the post types. */ public function get_post_type() { return [ 'oembed_cache' ]; } } feature-flag-integration.php 0000644 00000005672 15154134502 0012152 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Conditionals\Feature_Flag_Conditional; /** * Gathers all feature flags and surfaces them to the JavaScript side of the plugin. */ class Feature_Flag_Integration implements Integration_Interface { /** * The admin asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * All of the feature flag conditionals. * * @var Feature_Flag_Conditional[] */ protected $feature_flags; /** * Feature_Flag_Integration constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager. * @param Feature_Flag_Conditional ...$feature_flags All of the known feature flag conditionals. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Feature_Flag_Conditional ...$feature_flags ) { $this->asset_manager = $asset_manager; $this->feature_flags = $feature_flags; } /** * Returns the conditionals based on which this loadable should be active. * * @return string[] The conditionals based on which this loadable should be active. */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Initializes the integration. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_feature_flags' ] ); } /** * Gathers all the feature flags and injects them into the JavaScript. * * @return void */ public function add_feature_flags() { $enabled_features = $this->get_enabled_features(); // Localize under both names for BC. $this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeatureFlags', $enabled_features ); $this->asset_manager->localize_script( 'feature-flag-package', 'wpseoFeaturesL10n', $enabled_features ); } /** * Returns an array of all enabled feature flags. * * @return string[] The array of enabled features. */ public function get_enabled_features() { $enabled_features = []; foreach ( $this->feature_flags as $feature_flag ) { if ( $feature_flag->is_met() ) { $enabled_features[] = $feature_flag->get_feature_name(); } } return $this->filter_enabled_features( $enabled_features ); } /** * Runs the list of enabled feature flags through a filter. * * @param string[] $enabled_features The list of currently enabled feature flags. * * @return string[] The (possibly adapted) list of enabled features. */ protected function filter_enabled_features( $enabled_features ) { /** * Filters the list of currently enabled feature flags. * * @param string[] $enabled_features The current list of enabled feature flags. */ $filtered_enabled_features = \apply_filters( 'wpseo_enable_feature', $enabled_features ); if ( ! \is_array( $filtered_enabled_features ) ) { $filtered_enabled_features = $enabled_features; } return $filtered_enabled_features; } } schema-blocks.php 0000644 00000010271 15154134502 0007771 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use WPSEO_Admin_Asset_Manager; use WPSEO_Admin_Asset_Yoast_Components_L10n; use Yoast\WP\SEO\Conditionals\Schema_Blocks_Conditional; use Yoast\WP\SEO\Helpers\Short_Link_Helper; /** * Loads schema block templates into Gutenberg. */ class Schema_Blocks implements Integration_Interface { /** * The registered templates. * * @var string[] */ protected $templates = []; /** * Contains the asset manager. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * Represents the schema blocks conditional. * * @var Schema_Blocks_Conditional */ protected $blocks_conditional; /** * Represents the short link helper. * * @var Short_Link_Helper */ protected $short_link_helper; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Schema_Blocks_Conditional::class, ]; } /** * Schema_Blocks constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. * @param Schema_Blocks_Conditional $blocks_conditional The schema blocks conditional. * @param Short_Link_Helper $short_link_helper The short link helper. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Schema_Blocks_Conditional $blocks_conditional, Short_Link_Helper $short_link_helper ) { $this->asset_manager = $asset_manager; $this->blocks_conditional = $blocks_conditional; $this->short_link_helper = $short_link_helper; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'enqueue_block_editor_assets', [ $this, 'load' ] ); \add_action( 'enqueue_block_editor_assets', [ $this, 'load_translations' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'output' ] ); } /** * Registers a schema template. * * @param string $template The template to be registered. * If starting with a / is assumed to be an absolute path. * If not starting with a / is assumed to be relative to WPSEO_PATH. * * @return void */ public function register_template( $template ) { if ( \substr( $template, 0, 1 ) !== '/' ) { $template = \WPSEO_PATH . '/' . $template; } $this->templates[] = $template; } /** * Loads all schema block templates and the required JS library for them. * * @return void */ public function load() { $this->asset_manager->enqueue_script( 'schema-blocks' ); $this->asset_manager->enqueue_style( 'schema-blocks' ); $this->asset_manager->localize_script( 'schema-blocks', 'yoastSchemaBlocks', [ 'requiredLink' => $this->short_link_helper->build( 'https://yoa.st/required-fields' ), 'recommendedLink' => $this->short_link_helper->build( 'https://yoa.st/recommended-fields' ), ] ); } /** * Outputs the set templates. */ public function output() { if ( ! $this->asset_manager->is_script_enqueued( 'schema-blocks' ) ) { return; } $templates = []; // When the schema blocks feature flag is enabled, use the registered templates. if ( $this->blocks_conditional->is_met() ) { $templates = $this->templates; } /** * Filter: 'wpseo_load_schema_templates' - Allow adding additional schema templates. * * @param array $templates The templates to filter. */ $templates = \apply_filters( 'wpseo_load_schema_templates', $templates ); if ( ! \is_array( $templates ) || empty( $templates ) ) { return; } foreach ( $templates as $template ) { if ( ! \file_exists( $template ) ) { continue; } // `.schema` and other suffixes become Schema (root) templates. $type = ( \substr( $template, -10 ) === '.block.php' ) ? 'block' : 'schema'; echo '<script type="text/' . \esc_html( $type ) . '-template">'; include $template; echo '</script>'; } } /** * Loads the translations and localizes the schema-blocks script file. */ public function load_translations() { $yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n(); $yoast_components_l10n->localize_script( 'schema-blocks' ); } } estimated-reading-time.php 0000644 00000002127 15154134502 0011601 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use Yoast\WP\SEO\Conditionals\Admin\Estimated_Reading_Time_Conditional; /** * Estimated reading time class. */ class Estimated_Reading_Time implements Integration_Interface { /** * Returns the conditionals based in which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Estimated_Reading_Time_Conditional::class ]; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_filter( 'wpseo_metabox_entries_general', [ $this, 'add_estimated_reading_time_hidden_fields' ] ); } /** * Adds an estimated-reading-time hidden field. * * @param array $field_defs The $fields_defs. * * @return array */ public function add_estimated_reading_time_hidden_fields( $field_defs ) { if ( \is_array( $field_defs ) ) { $field_defs['estimated-reading-time-minutes'] = [ 'type' => 'hidden', 'title' => 'estimated-reading-time-minutes', ]; } return $field_defs; } } blocks/breadcrumbs-block.php 0000644 00000007176 15154134502 0012126 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Blocks; use WPSEO_Replace_Vars; use Yoast\WP\SEO\Helpers\Request_Helper; use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer; use Yoast\WP\SEO\Presenters\Breadcrumbs_Presenter; use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Surfaces\Helpers_Surface; /** * Siblings block class */ class Breadcrumbs_Block extends Dynamic_Block { /** * The name of the block. * * @var string */ protected $block_name = 'breadcrumbs'; /** * The editor script for the block. * * @var string */ protected $script = 'yoast-seo-dynamic-blocks'; /** * The Meta_Tags_Context_Memoizer. * * @var Meta_Tags_Context_Memoizer */ private $context_memoizer; /** * The Replace vars helper. * * @var WPSEO_Replace_Vars */ private $replace_vars; /** * The helpers surface. * * @var Helpers_Surface */ private $helpers; /** * The indexable repository. * * @var Indexable_Repository */ private $indexable_repository; /** * The request helper. * * @var Request_Helper */ private $request_helper; /** * Siblings_Block constructor. * * @param Meta_Tags_Context_Memoizer $context_memoizer The context. * @param WPSEO_Replace_Vars $replace_vars The replace variable helper. * @param Helpers_Surface $helpers The Helpers surface. * @param Indexable_Repository $indexable_repository The indexable repository. * @param Request_Helper $request_helper The request helper. */ public function __construct( Meta_Tags_Context_Memoizer $context_memoizer, WPSEO_Replace_Vars $replace_vars, Helpers_Surface $helpers, Indexable_Repository $indexable_repository, Request_Helper $request_helper ) { $this->context_memoizer = $context_memoizer; $this->replace_vars = $replace_vars; $this->helpers = $helpers; $this->indexable_repository = $indexable_repository; $this->request_helper = $request_helper; } /** * Presents the breadcrumbs output for the current page or the available post_id. * * @param array $attributes The block attributes. * * @return string The block output. */ public function present( $attributes ) { $presenter = new Breadcrumbs_Presenter(); // $this->context_memoizer->for_current_page only works on the frontend. To render the right breadcrumb in the // editor, we need the repository. if ( $this->request_helper->is_rest_request() || \is_admin() ) { $post_id = \get_the_ID(); if ( $post_id ) { $indexable = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' ); if ( ! $indexable ) { $post = \get_post( $post_id ); $indexable = $this->indexable_repository->query()->create( [ 'object_id' => $post_id, 'object_type' => 'post', 'object_sub_type' => $post->post_type, ] ); } $context = $this->context_memoizer->get( $indexable, 'Post_Type' ); } } if ( ! isset( $context ) ) { $context = $this->context_memoizer->for_current_page(); } /** This filter is documented in src/integrations/front-end-integration.php */ $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context ); $presenter->presentation = $presentation; $presenter->replace_vars = $this->replace_vars; $presenter->helpers = $this->helpers; $class_name = 'yoast-breadcrumbs'; if ( ! empty( $attributes['className'] ) ) { $class_name .= ' ' . \esc_attr( $attributes['className'] ); } return '<div class="' . $class_name . '">' . $presenter->present() . '</div>'; } } blocks/block-categories.php 0000644 00000003477 15154134502 0011762 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Blocks; use Yoast\WP\SEO\Helpers\Wordpress_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Internal_Linking_Category block class. */ class Internal_Linking_Category implements Integration_Interface { /** * Represents the WordPress helper. * * @var Wordpress_Helper */ protected $wordpress_helper; /** * Internal_Linking_Category constructor. * * @param Wordpress_Helper $wordpress_helper The WordPress helper. */ public function __construct( Wordpress_Helper $wordpress_helper ) { $this->wordpress_helper = $wordpress_helper; } /** * {@inheritDoc} */ public static function get_conditionals() { return []; } /** * {@inheritDoc} */ public function register_hooks() { $wordpress_version = $this->wordpress_helper->get_wordpress_version(); // The 'block_categories' filter has been deprecated in WordPress 5.8 and replaced by 'block_categories_all'. if ( \version_compare( $wordpress_version, '5.8-beta0', '<' ) ) { \add_filter( 'block_categories', [ $this, 'add_block_categories' ] ); } else { \add_filter( 'block_categories_all', [ $this, 'add_block_categories' ] ); } } /** * Adds Yoast block categories. * * @param array $categories The categories. * @return array The filtered categories. */ public function add_block_categories( $categories ) { $categories[] = [ 'slug' => 'yoast-structured-data-blocks', 'title' => \sprintf( /* translators: %1$s expands to Yoast. */ \__( '%1$s Structured Data Blocks', 'wordpress-seo' ), 'Yoast' ), ]; $categories[] = [ 'slug' => 'yoast-internal-linking-blocks', 'title' => \sprintf( /* translators: %1$s expands to Yoast. */ \__( '%1$s Internal Linking Blocks', 'wordpress-seo' ), 'Yoast' ), ]; return $categories; } } blocks/abstract-dynamic-block.php 0000644 00000002365 15154134502 0013055 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Blocks; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Dynamic_Block class. */ abstract class Dynamic_Block implements Integration_Interface { /** * The name of the block. * * @var string */ protected $block_name; /** * The editor script for the block. * * @var string */ protected $script; /** * {@inheritDoc} */ public static function get_conditionals() { return []; } /** * {@inheritDoc} */ public function register_hooks() { \add_action( 'init', [ $this, 'register_block' ], 11 ); } /** * Registers the block. * * @return void */ public function register_block() { \register_block_type( 'yoast-seo/' . $this->block_name, [ 'editor_script' => $this->script, 'render_callback' => [ $this, 'present' ], 'attributes' => [ 'className' => [ 'default' => '', 'type' => 'string', ], ], ] ); } /** * Presents the block output. This is abstract because in the loop we need to be able to build the data for the * presenter in the last moment. * * @param array $attributes The block attributes. * * @return string The block output. */ abstract public function present( $attributes ); } blocks/structured-data-blocks.php 0000644 00000002676 15154134502 0013133 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Blocks; use WPSEO_Admin_Asset_Manager; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Class to load assets required for structured data blocks. */ class Structured_Data_Blocks implements Integration_Interface { /** * An instance of the WPSEO_Admin_Asset_Manager class. * * @var WPSEO_Admin_Asset_Manager */ protected $asset_manager; /** * {@inheritDoc} */ public static function get_conditionals() { return []; } /** * Structured_Data_Blocks constructor. * * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager ) { $this->asset_manager = $asset_manager; } /** * Registers hooks for Structured Data Blocks with WordPress. */ public function register_hooks() { \add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] ); } /** * Enqueue Gutenberg block assets for backend editor. */ public function enqueue_block_editor_assets() { /** * Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely. * * @api bool If false, our structured data blocks won't show. */ if ( ! \apply_filters( 'wpseo_enable_structured_data_blocks', true ) ) { return; } $this->asset_manager->enqueue_script( 'structured-data-blocks' ); $this->asset_manager->enqueue_style( 'structured-data-blocks' ); } } primary-category.php 0000644 00000003716 15154134502 0010562 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations; use stdClass; use WP_Error; use WP_Post; use WPSEO_Primary_Term; use Yoast\WP\SEO\Conditionals\Primary_Category_Conditional; /** * Adds customizations to the front end for the primary category. */ class Primary_Category implements Integration_Interface { /** * Returns the conditionals based on which this loadable should be active. * * In this case only when on the frontend, the post overview, post edit or new post admin page. * * @return array The conditionals. */ public static function get_conditionals() { return [ Primary_Category_Conditional::class ]; } /** * Registers a filter to change a post's primary category. * * @return void */ public function register_hooks() { \add_filter( 'post_link_category', [ $this, 'post_link_category' ], 10, 3 ); } /** * Filters post_link_category to change the category to the chosen category by the user. * * @param stdClass $category The category that is now used for the post link. * @param array|null $categories This parameter is not used. * @param WP_Post|null $post The post in question. * * @return array|object|WP_Error|null The category we want to use for the post link. */ public function post_link_category( $category, $categories = null, $post = null ) { $post = \get_post( $post ); if ( $post === null ) { return $category; } $primary_category = $this->get_primary_category( $post ); if ( $primary_category !== false && $primary_category !== $category->cat_ID ) { $category = \get_category( $primary_category ); } return $category; } /** * Get the id of the primary category. * * @codeCoverageIgnore It justs wraps a dependency. * * @param WP_Post $post The post in question. * * @return int Primary category id. */ protected function get_primary_category( $post ) { $primary_term = new WPSEO_Primary_Term( 'category', $post->ID ); return $primary_term->get_primary_term(); } } watchers/indexable-post-watcher.php 0000644 00000020351 15154134502 0013447 0 ustar 00 <?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\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( 'wpseo_save_indexable', [ $this, 'updated_indexable' ], \PHP_INT_MAX, 2 ); \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; } if ( $indexable->is_public ) { $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 $updated_indexable The updated indexable. * @param Indexable $old_indexable The old indexable. */ public function updated_indexable( $updated_indexable, $old_indexable ) { // Only interested in post indexables. if ( $updated_indexable->object_type !== 'post' ) { return; } $post = $this->post->get_post( $updated_indexable->object_id ); // When the indexable is public or has a change in its public state. if ( $updated_indexable->is_public || $updated_indexable->is_public !== $old_indexable->is_public ) { $this->update_relations( $post ); } $this->update_has_public_posts( $updated_indexable ); $updated_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 ); // 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(); } } 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' ); $author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id ); $author_indexable->save(); } 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 ); } /** * 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 ); $updated_at = \gmdate( 'Y-m-d H:i:s' ); foreach ( $related_indexables as $indexable ) { if ( ! $indexable->is_public ) { continue; } $indexable->updated_at = $updated_at; $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' ); foreach ( $taxonomies as $taxonomy ) { $terms = \get_the_terms( $post->ID, $taxonomy ); if ( empty( $terms ) || \is_wp_error( $terms ) ) { continue; } foreach ( $terms as $term ) { $related_indexables[] = $this->repository->find_by_id_and_type( $term->term_id, 'term', false ); } } return \array_filter( $related_indexables ); } /** * Tests if the site is multisite and switched. * * @return bool True when the site is multisite and switched */ protected function is_multisite_and_switched() { return \is_multisite() && \ms_is_switched(); } } watchers/indexable-static-home-page-watcher.php 0000644 00000004216 15154134502 0015613 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\SEO\Conditionals\Admin_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Watcher that checks for changes in the page used as homepage. * * Watches the static homepage option and updates the permalinks accordingly. */ class Indexable_Static_Home_Page_Watcher implements Integration_Interface { /** * The indexable repository. * * @var Indexable_Repository */ protected $repository; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Indexable_Static_Home_Page_Watcher constructor. * * @codeCoverageIgnore * * @param Indexable_Repository $repository The repository to use. */ public function __construct( Indexable_Repository $repository ) { $this->repository = $repository; } /** * Initializes the integration. * * This is the place to register hooks and filters. */ public function register_hooks() { \add_action( 'update_option_page_on_front', [ $this, 'update_static_homepage_permalink' ], 10, 2 ); } /** * Updates the new and previous homepage's permalink when the static home page is updated. * * @param string $old_value The previous homepage's ID. * @param int $value The new homepage's ID. */ public function update_static_homepage_permalink( $old_value, $value ) { if ( \is_string( $old_value ) ) { $old_value = (int) $old_value; } if ( $old_value === $value ) { return; } $this->update_permalink_for_page( $old_value ); $this->update_permalink_for_page( $value ); } /** * Updates the permalink based on the selected homepage settings. * * @param int $page_id The page's id. */ private function update_permalink_for_page( $page_id ) { if ( $page_id === 0 ) { return; } $indexable = $this->repository->find_by_id_and_type( $page_id, 'post', false ); if ( $indexable === false ) { return; } $indexable->permalink = \get_permalink( $page_id ); $indexable->save(); } } watchers/indexable-category-permalink-watcher.php 0000644 00000003177 15154134502 0016266 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\SEO\Config\Indexing_Reasons; /** * Watches the stripcategorybase key in wpseo_titles, in order to clear the permalink of the category indexables. */ class Indexable_Category_Permalink_Watcher extends Indexable_Permalink_Watcher { /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Checks if the stripcategorybase key in wpseo_titles has a change in value, and if so, * clears the permalink for category indexables. * * @param array $old_value The old value of the wpseo_titles option. * @param array $new_value The new value of the wpseo_titles option. * * @return void */ public function check_option( $old_value, $new_value ) { // If this is the first time saving the option, in which case its value would be false. if ( $old_value === false ) { $old_value = []; } // If either value is not an array, return. if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return; } // If both values aren't set, they haven't changed. if ( ! isset( $old_value['stripcategorybase'] ) && ! isset( $new_value['stripcategorybase'] ) ) { return; } // If a new value has been set for 'stripcategorybase', clear the category permalinks. if ( $old_value['stripcategorybase'] !== $new_value['stripcategorybase'] ) { $this->indexable_helper->reset_permalink_indexables( 'term', 'category', Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX ); } } } watchers/indexable-term-watcher.php 0000644 00000006172 15154134502 0013436 0 ustar 00 <?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 ); // Update links. $this->link_builder->build( $indexable, $term->description ); $indexable->save(); } } watchers/option-wpseo-watcher.php 0000644 00000002413 15154134502 0013173 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Watcher for the wpseo option. * * Represents the option wpseo watcher. */ class Option_Wpseo_Watcher implements Integration_Interface { use No_Conditionals; /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo', [ $this, 'check_semrush_option_disabled' ], 10, 2 ); } /** * 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 ) { if ( \array_key_exists( 'semrush_integration_active', $new_value ) && $new_value['semrush_integration_active'] === false ) { \YoastSEO()->helpers->options->set( 'semrush_tokens', [] ); return true; } return false; } } watchers/indexable-ancestor-watcher.php 0000644 00000017774 15154134502 0014317 0 ustar 00 <?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 ( $indexable->permalink === $indexable_before->permalink ) { return false; } $child_indexable_ids = $this->indexable_hierarchy_repository->find_children( $indexable ); $child_indexables = $this->indexable_repository->find_by_ids( $child_indexable_ids ); \array_walk( $child_indexables, [ $this, 'update_hierarchy_and_permalink' ] ); if ( $indexable->object_type === 'term' ) { $child_indexables_for_term = $this->get_children_for_term( $indexable->object_id, $child_indexables ); \array_walk( $child_indexables_for_term, [ $this, 'update_hierarchy_and_permalink' ] ); } return true; } /** * Finds all child indexables for the given term. * * @param int $term_id Term to fetch the indexable for. * @param Indexable[] $child_indexables The already known child indexables. * * @return array The list of additional child indexables for a given term. */ public function get_children_for_term( $term_id, array $child_indexables ) { // Finds object_ids (posts) for the term. $post_object_ids = $this->get_object_ids_for_term( $term_id, $child_indexables ); // Removes the objects that are already present in the children. $existing_post_indexables = \array_filter( $child_indexables, static function( $indexable ) { return $indexable->object_type === 'post'; } ); $existing_post_object_ids = \wp_list_pluck( $existing_post_indexables, 'object_id' ); $post_object_ids = \array_diff( $post_object_ids, $existing_post_object_ids ); // Finds the indexables for the fetched post_object_ids. $post_indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_object_ids, 'post', false ); // Finds the indexables for the posts that are attached to the term. $post_indexable_ids = \wp_list_pluck( $post_indexables, 'id' ); $additional_indexable_ids = $this->indexable_hierarchy_repository->find_children_by_ancestor_ids( $post_indexable_ids ); // Makes sure we only have indexable id's that we haven't fetched before. $additional_indexable_ids = \array_diff( $additional_indexable_ids, $post_indexable_ids ); // Finds the additional indexables. $additional_indexables = $this->indexable_repository->find_by_ids( $additional_indexable_ids ); // Merges all fetched indexables. return \array_merge( $post_indexables, $additional_indexables ); } /** * Builds the hierarchy for a post. * * @deprecated 16.4 * * @codeCoverageIgnore * * @param int $object_id The post id. * @param int $post_type The post type. */ public function build_post_hierarchy( $object_id, $post_type ) { \_deprecated_function( __METHOD__, '16.4', 'Primary_Category_Quick_Edit_Watcher::build_post_hierarchy' ); } /** * Updates the indexable hierarchy and indexable permalink. * * @param Indexable $indexable The indexable to update the hierarchy and permalink for. */ protected function update_hierarchy_and_permalink( $indexable ) { $this->indexable_hierarchy_builder->build( $indexable ); $indexable->permalink = $this->permalink_helper->get_permalink_for_indexable( $indexable ); $indexable->save(); } /** * Retrieves the object id's for a term based on the term-post relationship. * * @param int $term_id The term to get the object id's for. * @param Indexable[] $child_indexables The child indexables. * * @return array List with object ids for the term. */ protected function get_object_ids_for_term( $term_id, $child_indexables ) { $filter_terms = static function( $child ) { return $child->object_type === 'term'; }; $child_terms = \array_filter( $child_indexables, $filter_terms ); $child_object_ids = \wp_list_pluck( $child_terms, 'object_id' ); // Get the term-taxonomy id's for the term and its children. $term_taxonomy_ids = $this->wpdb->get_col( $this->wpdb->prepare( 'SELECT term_taxonomy_id FROM ' . $this->wpdb->term_taxonomy . ' WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) + 1 ), '%s' ) ) . ' )', $term_id, ...$child_object_ids ) ); // In the case of faulty data having been saved the above query can return 0 results. if ( empty( $term_taxonomy_ids ) ) { return []; } // Get the (post) object id's that are attached to the term. return $this->wpdb->get_col( $this->wpdb->prepare( 'SELECT DISTINCT object_id FROM ' . $this->wpdb->term_relationships . ' WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )', ...$term_taxonomy_ids ) ); } } watchers/addon-update-watcher.php 0000644 00000014000 15154134502 0013070 0 ustar 00 <?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 ); } /** * 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 ); } else { $this->disable_auto_updates_for_addons( $new_value ); } } /** * 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_merge( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ); \update_site_option( 'auto_update_plugins', $plugins ); } /** * Disables auto-updates for all addons. * * @param string[] $auto_updated_plugins The current list of auto-updated plugins. */ protected function disable_auto_updates_for_addons( $auto_updated_plugins ) { $plugins = \array_values( \array_diff( $auto_updated_plugins, self::ADD_ON_PLUGIN_FILES ) ); \update_site_option( 'auto_update_plugins', $plugins ); } /** * Checks whether auto updates for a plugin are enabled. * * @param string $plugin_id The plugin ID. * @param array $auto_updated_plugins The array of auto updated plugins. * * @return bool Whether auto updates for a plugin are enabled. */ protected function are_auto_updates_enabled( $plugin_id, $auto_updated_plugins ) { if ( $auto_updated_plugins === false || ! \is_array( $auto_updated_plugins ) ) { return false; } return \in_array( $plugin_id, $auto_updated_plugins, true ); } } watchers/option-titles-watcher.php 0000644 00000006174 15154134502 0013352 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\WordPress\Wrapper; /** * Watcher for the titles option. * * Represents the option titles watcher. */ class Option_Titles_Watcher implements Integration_Interface { /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Checks if one of the relevant options has been changed. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return bool Whether or not the ancestors are removed. */ public function check_option( $old_value, $new_value ) { // If this is the first time saving the option, thus when value is false. if ( $old_value === false ) { $old_value = []; } if ( ! \is_array( $old_value ) || ! \is_array( $new_value ) ) { return false; } $relevant_keys = $this->get_relevant_keys(); if ( empty( $relevant_keys ) ) { return false; } $post_types = []; foreach ( $relevant_keys as $post_type => $relevant_option ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $relevant_option ] ) && ! isset( $new_value[ $relevant_option ] ) ) { continue; } if ( $old_value[ $relevant_option ] !== $new_value[ $relevant_option ] ) { $post_types[] = $post_type; } } return $this->delete_ancestors( $post_types ); } /** * Retrieves the relevant keys. * * @return array Array with the relevant keys. */ protected function get_relevant_keys() { $post_types = \get_post_types( [ 'public' => true ], 'names' ); if ( ! \is_array( $post_types ) || $post_types === [] ) { return []; } $relevant_keys = []; foreach ( $post_types as $post_type ) { $relevant_keys[ $post_type ] = 'post_types-' . $post_type . '-maintax'; } return $relevant_keys; } /** * Removes the ancestors for given post types. * * @param array $post_types The post types to remove hierarchy for. * * @return bool True when delete query was successful. */ protected function delete_ancestors( $post_types ) { if ( empty( $post_types ) ) { return false; } $wpdb = Wrapper::get_wpdb(); $total = \count( $post_types ); $hierarchy_table = Model::get_table_name( 'Indexable_Hierarchy' ); $indexable_table = Model::get_table_name( 'Indexable' ); $result = $wpdb->query( $wpdb->prepare( " DELETE FROM `$hierarchy_table` WHERE indexable_id IN( SELECT id FROM `$indexable_table` WHERE object_type = 'post' AND object_sub_type IN( " . \implode( ', ', \array_fill( 0, $total, '%s' ) ) . ' ) )', $post_types ) ); return $result !== false; } } watchers/indexable-post-meta-watcher.php 0000644 00000005542 15154134502 0014400 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use WPSEO_Meta; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * WordPress post meta watcher. */ class Indexable_Post_Meta_Watcher implements Integration_Interface { /** * The post watcher. * * @var Indexable_Post_Watcher */ protected $post_watcher; /** * An array of post IDs that need to be updated. * * @var array */ protected $post_ids_to_update = []; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_Postmeta_Watcher constructor. * * @param Indexable_Post_Watcher $post_watcher The post watcher. */ public function __construct( Indexable_Post_Watcher $post_watcher ) { $this->post_watcher = $post_watcher; } /** * Initializes the integration. * * This is the place to register hooks and filters. */ public function register_hooks() { // Register all posts whose meta have changed. \add_action( 'added_post_meta', [ $this, 'add_post_id' ], 10, 3 ); \add_action( 'updated_post_meta', [ $this, 'add_post_id' ], 10, 3 ); \add_action( 'deleted_post_meta', [ $this, 'add_post_id' ], 10, 3 ); // Remove posts that get saved as they are handled by the Indexable_Post_Watcher. \add_action( 'wp_insert_post', [ $this, 'remove_post_id' ] ); \add_action( 'delete_post', [ $this, 'remove_post_id' ] ); \add_action( 'edit_attachment', [ $this, 'remove_post_id' ] ); \add_action( 'add_attachment', [ $this, 'remove_post_id' ] ); \add_action( 'delete_attachment', [ $this, 'remove_post_id' ] ); // Update indexables of all registered posts. \register_shutdown_function( [ $this, 'update_indexables' ] ); } /** * Adds a post id to the array of posts to update. * * @param int|string $meta_id The meta ID. * @param int|string $post_id The post ID. * @param string $meta_key The meta key. * * @return void */ public function add_post_id( $meta_id, $post_id, $meta_key ) { // Only register changes to our own meta. if ( \strpos( $meta_key, WPSEO_Meta::$meta_prefix ) !== 0 ) { return; } if ( ! \in_array( $post_id, $this->post_ids_to_update, true ) ) { $this->post_ids_to_update[] = (int) $post_id; } } /** * Removes a post id from the array of posts to update. * * @param int|string $post_id The post ID. * * @return void */ public function remove_post_id( $post_id ) { $this->post_ids_to_update = \array_diff( $this->post_ids_to_update, [ (int) $post_id ] ); } /** * Updates all indexables changed during the request. * * @return void */ public function update_indexables() { foreach ( $this->post_ids_to_update as $post_id ) { $this->post_watcher->build_indexable( $post_id ); } } } watchers/indexable-system-page-watcher.php 0000644 00000005176 15154134502 0014730 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\SEO\Builders\Indexable_Builder; use Yoast\WP\SEO\Builders\Indexable_System_Page_Builder; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Search result watcher to save the meta data to an Indexable. * * Watches the search result options to save the meta information when updated. */ class Indexable_System_Page_Watcher implements Integration_Interface { /** * The indexable repository. * * @var Indexable_Repository */ protected $repository; /** * The indexable builder. * * @var Indexable_Builder */ protected $builder; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_System_Page_Watcher constructor. * * @param Indexable_Repository $repository The repository to use. * @param Indexable_Builder $builder The post builder to use. */ public function __construct( Indexable_Repository $repository, Indexable_Builder $builder ) { $this->repository = $repository; $this->builder = $builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. */ public function register_hooks() { \add_action( 'update_option_wpseo_titles', [ $this, 'check_option' ], 10, 2 ); } /** * Checks if the home page indexable needs to be rebuild based on option values. * * @param array $old_value The old value of the option. * @param array $new_value The new value of the option. * * @return void */ public function check_option( $old_value, $new_value ) { foreach ( Indexable_System_Page_Builder::OPTION_MAPPING as $type => $options ) { foreach ( $options as $option ) { // If both values aren't set they haven't changed. if ( ! isset( $old_value[ $option ] ) && ! isset( $new_value[ $option ] ) ) { return; } // If the value was set but now isn't, is set but wasn't or is not the same it has changed. if ( ! isset( $old_value[ $option ] ) || ! isset( $new_value[ $option ] ) || $old_value[ $option ] !== $new_value[ $option ] ) { $this->build_indexable( $type ); } } } } /** * Saves the search result. * * @param string $type The type of no index page. * * @return void */ public function build_indexable( $type ) { $indexable = $this->repository->find_for_system_page( $type, false ); $this->builder->build_for_system_page( $type, $indexable ); } } watchers/indexable-permalink-watcher.php 0000644 00000017252 15154134502 0014452 0 ustar 00 <?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 Unused. The old option value. * @param string $new Unused. The new option value. * @param string $type The option name. */ public function reset_permalinks_term( $old, $new, $type ) { $subtype = $type; $reason = Indexing_Reasons::REASON_PERMALINK_SETTINGS; // When the subtype contains _base, just strip it. if ( \strstr( $subtype, '_base' ) ) { $subtype = \substr( $type, 0, -5 ); } if ( $subtype === 'tag' ) { $subtype = 'post_tag'; $reason = Indexing_Reasons::REASON_TAG_BASE_PREFIX; } if ( $subtype === 'category' ) { $reason = Indexing_Reasons::REASON_CATEGORY_BASE_PREFIX; } $this->indexable_helper->reset_permalink_indexables( 'term', $subtype, $reason ); } /** * Resets the permalink indexables automatically, if necessary. * * @return bool Whether the reset request ran. */ public function force_reset_permalinks() { if ( \get_option( 'tag_base' ) !== $this->options_helper->get( 'tag_base_url' ) ) { $this->reset_permalinks_term( null, null, 'tag_base' ); $this->options_helper->set( 'tag_base_url', \get_option( 'tag_base' ) ); } if ( \get_option( 'category_base' ) !== $this->options_helper->get( 'category_base_url' ) ) { $this->reset_permalinks_term( null, null, 'category_base' ); $this->options_helper->set( 'category_base_url', \get_option( 'category_base' ) ); } if ( $this->should_reset_permalinks() ) { $this->reset_permalinks(); return true; } $this->reset_altered_custom_taxonomies(); return true; } /** * Checks whether the permalinks should be reset after `permalink_structure` has changed. * * @return bool Whether the permalinks should be reset. */ public function should_reset_permalinks() { return \get_option( 'permalink_structure' ) !== $this->options_helper->get( 'permalink_structure' ); } /** * Resets custom taxonomies if their slugs have changed. * * @return void */ public function reset_altered_custom_taxonomies() { $taxonomies = $this->taxonomy_helper->get_custom_taxonomies(); $custom_taxonomy_bases = $this->options_helper->get( 'custom_taxonomy_slugs', [] ); $new_taxonomy_bases = []; foreach ( $taxonomies as $taxonomy ) { $taxonomy_slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy ); $new_taxonomy_bases[ $taxonomy ] = $taxonomy_slug; if ( ! \array_key_exists( $taxonomy, $custom_taxonomy_bases ) ) { continue; } if ( $taxonomy_slug !== $custom_taxonomy_bases[ $taxonomy ] ) { $this->indexable_helper->reset_permalink_indexables( 'term', $taxonomy ); } } $this->options_helper->set( 'custom_taxonomy_slugs', $new_taxonomy_bases ); } /** * Retrieves a list with the public post types. * * @return array The post types. */ protected function get_post_types() { /** * Filter: Gives the possibility to filter out post types. * * @param array $post_types The post type names. * * @return array The post types. */ $post_types = \apply_filters( 'wpseo_post_types_reset_permalinks', $this->post_type->get_public_post_types() ); return $post_types; } /** * Retrieves the taxonomies that belongs to the public post types. * * @param array $post_types The post types to get taxonomies for. * * @return array The retrieved taxonomies. */ protected function get_taxonomies_for_post_types( $post_types ) { $taxonomies = []; foreach ( $post_types as $post_type ) { $taxonomies[] = \get_object_taxonomies( $post_type, 'names' ); } $taxonomies = \array_merge( [], ...$taxonomies ); $taxonomies = \array_unique( $taxonomies ); return $taxonomies; } /** * Schedules the WP-Cron job to check the permalink_structure status. * * @return void */ protected function schedule_cron() { if ( \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) { return; } \wp_schedule_event( \time(), 'daily', 'wpseo_permalink_structure_check' ); } /** * Unschedules the WP-Cron job to check the permalink_structure status. * * @return void */ public function unschedule_cron() { if ( ! \wp_next_scheduled( 'wpseo_permalink_structure_check' ) ) { return; } \wp_clear_scheduled_hook( 'wpseo_permalink_structure_check' ); } } watchers/indexable-homeurl-watcher.php 0000644 00000005410 15154134502 0014134 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use WP_CLI; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Config\Indexing_Reasons; use Yoast\WP\SEO\Helpers\Indexable_Helper; use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Post_Type_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; /** * Home url option watcher. * * Handles updates to the home URL option for the Indexables table. */ class Indexable_HomeUrl_Watcher implements Integration_Interface { /** * Represents the options helper. * * @var Options_Helper */ protected $options_helper; /** * The post type helper. * * @var Post_Type_Helper */ private $post_type; /** * The indexable helper. * * @var Indexable_Helper */ protected $indexable_helper; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Indexable_HomeUrl_Watcher constructor. * * @param Post_Type_Helper $post_type The post type helper. * @param Options_Helper $options The options helper. * @param Indexable_Helper $indexable The indexable helper. */ public function __construct( Post_Type_Helper $post_type, Options_Helper $options, Indexable_Helper $indexable ) { $this->post_type = $post_type; $this->options_helper = $options; $this->indexable_helper = $indexable; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'update_option_home', [ $this, 'reset_permalinks' ] ); \add_action( 'wpseo_permalink_structure_check', [ $this, 'force_reset_permalinks' ] ); } /** * Resets the permalinks for everything that is related to the permalink structure. * * @return void */ public function reset_permalinks() { $this->indexable_helper->reset_permalink_indexables( null, null, Indexing_Reasons::REASON_HOME_URL_OPTION ); // Reset the home_url option. $this->options_helper->set( 'home_url', \get_home_url() ); } /** * Resets the permalink indexables automatically, if necessary. * * @return bool Whether the request ran. */ public function force_reset_permalinks() { if ( $this->should_reset_permalinks() ) { $this->reset_permalinks(); if ( \defined( 'WP_CLI' ) && \WP_CLI ) { WP_CLI::success( \__( 'All permalinks were successfully reset', 'wordpress-seo' ) ); } return true; } return false; } /** * Checks whether permalinks should be reset. * * @return bool Whether the permalinks should be reset. */ public function should_reset_permalinks() { return \get_home_url() !== $this->options_helper->get( 'home_url' ); } } watchers/primary-term-watcher.php 0000644 00000007663 15154134502 0013174 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use WP_Term; use WPSEO_Meta; use WPSEO_Primary_Term; use Yoast\WP\SEO\Builders\Primary_Term_Builder; use Yoast\WP\SEO\Conditionals\Migrations_Conditional; use Yoast\WP\SEO\Helpers\Primary_Term_Helper; use Yoast\WP\SEO\Helpers\Site_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Repositories\Primary_Term_Repository; /** * Primary Term watcher. * * Watches Posts to save the primary term when set. */ class Primary_Term_Watcher implements Integration_Interface { /** * The primary term repository. * * @var Primary_Term_Repository */ protected $repository; /** * Represents the site helper. * * @var Site_Helper */ protected $site; /** * The primary term helper. * * @var Primary_Term_Helper */ protected $primary_term; /** * The primary term builder. * * @var Primary_Term_Builder */ protected $primary_term_builder; /** * Returns the conditionals based on which this loadable should be active. * * @return array */ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } /** * Primary_Term_Watcher constructor. * * @codeCoverageIgnore It sets dependencies. * * @param Primary_Term_Repository $repository The primary term repository. * @param Site_Helper $site The site helper. * @param Primary_Term_Helper $primary_term The primary term helper. * @param Primary_Term_Builder $primary_term_builder The primary term builder. */ public function __construct( Primary_Term_Repository $repository, Site_Helper $site, Primary_Term_Helper $primary_term, Primary_Term_Builder $primary_term_builder ) { $this->repository = $repository; $this->site = $site; $this->primary_term = $primary_term; $this->primary_term_builder = $primary_term_builder; } /** * Initializes the integration. * * This is the place to register hooks and filters. */ public function register_hooks() { \add_action( 'save_post', [ $this, 'save_primary_terms' ], \PHP_INT_MAX ); \add_action( 'delete_post', [ $this, 'delete_primary_terms' ] ); } /** * Saves all selected primary terms. * * @param int $post_id Post ID to save primary terms for. */ public function save_primary_terms( $post_id ) { // Bail if this is a multisite installation and the site has been switched. if ( $this->site->is_multisite_and_switched() ) { return; } $taxonomies = $this->primary_term->get_primary_term_taxonomies( $post_id ); foreach ( $taxonomies as $taxonomy ) { $this->save_primary_term( $post_id, $taxonomy ); } $this->primary_term_builder->build( $post_id ); } /** * Saves the primary term for a specific taxonomy. * * @param int $post_id Post ID to save primary term for. * @param WP_Term $taxonomy Taxonomy to save primary term for. */ protected function save_primary_term( $post_id, $taxonomy ) { $primary_term = \filter_input( \INPUT_POST, WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_term', \FILTER_SANITIZE_NUMBER_INT ); // We accept an empty string here because we need to save that if no terms are selected. if ( $primary_term && \check_admin_referer( 'save-primary-term', WPSEO_Meta::$form_prefix . 'primary_' . $taxonomy->name . '_nonce' ) !== null ) { $primary_term_object = new WPSEO_Primary_Term( $taxonomy->name, $post_id ); $primary_term_object->set_primary_term( $primary_term ); } } /** * Deletes primary terms for a post. * * @param int $post_id The post to delete the terms of. * * @return void */ public function delete_primary_terms( $post_id ) { foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) { $primary_term = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy->name, false ); if ( ! $primary_term ) { continue; } $primary_term->delete(); } } } watchers/indexable-author-watcher.php 0000644 00000004122 15154134502 0013762 0 ustar 00 <?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 ); $this->builder->build_for_id_and_type( $user_id, 'user', $indexable ); } } watchers/primary-category-quick-edit-watcher.php 0000644 00000013272 15154134502 0016070 0 ustar 00 <?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; // phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- Base class can't be written shorter without abbreviating. /** * 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 ); } } } // phpcs:enable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded watchers/indexable-home-page-watcher.php 0000644 00000006046 15154134502 0014331 0 ustar 00 <?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. * * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- 4 words is fine. */ 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 ); $this->builder->build_for_home_page( $indexable ); } } watchers/indexable-post-type-archive-watcher.php 0000644 00000006670 15154134502 0016055 0 ustar 00 <?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 ); $this->builder->build_for_post_type_archive( $post_type, $indexable ); } } watchers/auto-update-watcher.php 0000644 00000014253 15154134502 0012765 0 ustar 00 <?php namespace Yoast\WP\SEO\Integrations\Watchers; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Helpers\Notification_Helper; use Yoast\WP\SEO\Integrations\Integration_Interface; use Yoast\WP\SEO\Presenters\Admin\Auto_Update_Notification_Presenter; use Yoast_Notification; 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; /** * The notification helper. * * @var Notification_Helper */ protected $notification_helper; /** * Auto_Update constructor. * * @param Yoast_Notification_Center $notification_center The notification center. * @param Notification_Helper $notification_helper The notification helper. */ public function __construct( Yoast_Notification_Center $notification_center, Notification_Helper $notification_helper ) { $this->notification_center = $notification_center; $this->notification_helper = $notification_helper; } /** * 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, 'auto_update_notification_not_if_dismissed' ] ); \add_action( 'update_site_option_auto_update_core_major', [ $this, 'auto_update_notification_even_if_dismissed' ] ); \add_action( 'update_site_option_auto_update_plugins', [ $this, 'auto_update_notification_not_if_dismissed' ] ); } /** * Handles the Yoast SEO auto-update notification when the user toggles the auto-update setting for WordPress Core. * * If it should be shown, this will be done even if the notification has been dismissed in the past. * * @return void */ public function auto_update_notification_even_if_dismissed() { if ( ! $this->should_show_notification() ) { $this->save_dismissal_status(); $this->maybe_remove_notification(); return; } $this->maybe_create_notification(); } /** * Handles the Yoast SEO auto-update notification on all admin pages, * as well as when the user toggles the Yoast SEO auto-update setting. * * If it should be shown, this will only be done if the notification has not been dismissed in the past. * * @return void */ public function auto_update_notification_not_if_dismissed() { if ( ! $this->should_show_notification() ) { $this->save_dismissal_status(); $this->maybe_remove_notification(); return; } $this->maybe_create_notification_if_not_dismissed(); } /** * Checks whether the Yoast SEO auto-update notification should be shown. * * @return bool Whether the notification should be shown. */ protected function should_show_notification() { $core_updates_enabled = \get_site_option( 'auto_update_core_major' ) === 'enabled'; $yoast_updates_enabled = $this->yoast_auto_updates_enabled(); return $core_updates_enabled && ! $yoast_updates_enabled; } /** * Saves the dismissal status of the notification in an option in wp_usermeta, if the notification gets dismissed. * * @return void */ protected function save_dismissal_status() { // This option exists if the notification has been dismissed at some point. $notification_dismissed = \get_user_option( 'wp_' . self::NOTIFICATION_ID ); // We wish to have its value in a different option, so we can still access it even when the notification gets removed. if ( $notification_dismissed && ! \get_user_option( 'wp_' . self::NOTIFICATION_ID . '_dismissed' ) ) { \update_user_option( \get_current_user_id(), self::NOTIFICATION_ID . '_dismissed', true ); } } /** * Removes the notification from the notification center, if it exists. * * @return void */ protected function maybe_remove_notification() { $this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID ); } /** * Creates the notification if it doesn't exist already. * * @return void */ protected function maybe_create_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 ); } } /** * Creates the notification when Yoast SEO auto-updates are enabled, if it hasn't been dismissed in the past. * * @return void */ protected function maybe_create_notification_if_not_dismissed() { $notification_dismissed = \get_user_option( 'wp_' . self::NOTIFICATION_ID . '_dismissed' ) === '1'; $yoast_updates_enabled = $this->yoast_auto_updates_enabled(); if ( $notification_dismissed && ! $yoast_updates_enabled ) { return; } $this->maybe_create_notification(); } /** * Checks whether auto-updates are enabled for Yoast SEO. * * @return bool True if they are enabled, false if not. */ protected function yoast_auto_updates_enabled() { $plugins_to_auto_update = \get_site_option( 'auto_update_plugins' ); // If no plugins are set to be automatically updated, it means that Yoast SEO isn't either. if ( ! $plugins_to_auto_update ) { return false; } // Check if the Yoast SEO plugin file is in the array of plugins for which auto-updates are enabled. return \in_array( 'wordpress-seo/wp-seo.php', $plugins_to_auto_update, true ); } /** * Returns an instance of the notification. * * @return Yoast_Notification The notification to show. */ protected function notification() { $presenter = new Auto_Update_Notification_Presenter(); return new Yoast_Notification( $presenter->present(), [ 'type' => Yoast_Notification::WARNING, 'id' => self::NOTIFICATION_ID, 'capabilities' => 'wpseo_manage_options', 'priority' => 0.8, ] ); } } watchers/indexable-date-archive-watcher.php 0000644 00000005010 15154134502 0015011 0 ustar 00 <?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 ); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка