* * @copyright Copyright (C) 2008-2019, Yoast BV * The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3. */ namespace RankMath\Sitemap; use RankMath\Helper; use RankMath\Sitemap\Sitemap; use RankMath\Traits\Hooker; defined( 'ABSPATH' ) || exit; /** * Cache_Watcher class. */ class Cache_Watcher { use Hooker; /** * Holds the options that, when updated, should cause the cache to clear. * * @var array */ protected static $cache_clear = []; /** * Holds the flag to clear all cache. * * @var boolean */ protected static $clear_all = false; /** * Holds the array of types to clear. * * @var array */ protected static $clear_types = []; /** * Holds the array of post types being imported. * * @var array */ private $importing_post_types = []; /** * The constructor. */ public function __construct() { $this->action( 'save_post', 'save_post' ); $this->action( 'transition_post_status', 'status_transition', 10, 3 ); $this->action( 'admin_footer', 'status_transition_bulk_finished' ); $this->action( 'edited_terms', 'invalidate_term', 10, 2 ); $this->action( 'clean_term_cache', 'invalidate_term', 10, 2 ); $this->action( 'clean_object_term_cache', 'invalidate_term', 10, 2 ); $this->action( 'delete_user', 'invalidate_author' ); $this->action( 'user_register', 'invalidate_author' ); $this->action( 'profile_update', 'invalidate_author' ); $this->action( 'rank_math/sitemap/invalidate_object_type', 'invalidate_object_type', 10, 2 ); add_action( 'shutdown', [ __CLASS__, 'clear_queued' ] ); add_action( 'update_option', [ __CLASS__, 'clear_on_option_update' ] ); add_action( 'deleted_term_relationships', [ __CLASS__, 'invalidate' ] ); // Clear cache when user updates any of these options. self::register_clear_on_option_update( 'home' ); self::register_clear_on_option_update( 'permalink_structure' ); self::register_clear_on_option_update( 'rank_math_modules' ); self::register_clear_on_option_update( 'rank-math-options-titles' ); self::register_clear_on_option_update( 'rank-math-options-general' ); self::register_clear_on_option_update( 'rank-math-options-sitemap' ); self::register_clear_on_option_update( 'date_format' ); } /** * Check for relevant post type before invalidation. * * @param int $post_id Post ID to possibly invalidate for. */ public function save_post( $post_id ) { $post = get_post( $post_id ); if ( ! empty( $post->post_password ) || 'auto-draft' === $post->post_status ) { return false; } update_user_meta( $post->post_author, 'last_update', get_post_modified_time( 'U', false, $post ) ); $this->invalidate_post( $post_id ); $this->invalidate_author( $post->post_author ); } /** * Hooked into transition_post_status. Will initiate search engine pings * if the post is being published, is a post type that a sitemap is built for * and is a post that is included in sitemaps. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param \WP_Post $post Post object. */ public function status_transition( $new_status, $old_status, $post ) { if ( 'publish' !== $new_status ) { return; } if ( defined( 'WP_IMPORTING' ) ) { $this->status_transition_bulk( $new_status, $old_status, $post ); return; } if ( $this->can_exclude( $post ) ) { return; } if ( WP_CACHE ) { wp_schedule_single_event( ( time() + 300 ), 'rank_math/sitemap/hit_index' ); } } /** * Can exclude post type. * * @param \WP_Post $post Post object. * * @return bool */ private function can_exclude( $post ) { $post_type = get_post_type( $post ); wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // None of our interest. // If the post type is excluded in options, we can stop. return 'nav_menu_item' === $post_type || ! Sitemap::is_object_indexable( $post->ID ); } /** * While bulk importing, just save unique `post_types`. * * When importing is done, if we have a `post_type` that is saved in the sitemap * try to ping the search engines * * @param string $new_status New post status. * @param string $old_status Old post status. * @param \WP_Post $post Post object. */ private function status_transition_bulk( $new_status, $old_status, $post ) { $this->importing_post_types[] = get_post_type( $post ); $this->importing_post_types = array_unique( $this->importing_post_types ); } /** * After import finished, walk through imported `post_types` and update info. */ public function status_transition_bulk_finished() { if ( ! defined( 'WP_IMPORTING' ) || empty( $this->importing_post_types ) ) { return; } if ( false === $this->maybe_ping_search_engines() ) { return; } if ( WP_CACHE ) { do_action( 'rank_math/sitemap/hit_index' ); } } /** * Check if we can ping search engines. * * @return bool */ private function maybe_ping_search_engines() { $ping = false; $accessible_post_types = Helper::get_accessible_post_types(); foreach ( $this->importing_post_types as $post_type ) { wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); if ( in_array( $post_type, $accessible_post_types, true ) ) { $ping = true; } } return $ping; } /** * Helper to invalidate in hooks where type is passed as second argument. * * @param int $unused Unused term ID value. * @param string $taxonomy Taxonomy to invalidate. */ public static function invalidate_term( $unused, $taxonomy ) { if ( false !== Helper::get_settings( 'sitemap.tax_' . $taxonomy . '_sitemap' ) ) { self::invalidate( $taxonomy ); } } /** * Delete cache transients for index and specific type. * * Always deletes the main index sitemaps cache, as that's always invalidated by any other change. * * @param string $type Sitemap type to invalidate. */ public static function invalidate( $type ) { self::clear( [ $type ] ); } /** * Invalidate sitemap cache for the post type of a post. * Don't invalidate for revisions. * * @param int $post_id Post ID to invalidate type for. */ public static function invalidate_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) { return; } self::invalidate( get_post_type( $post_id ) ); } /** * Invalidate sitemap cache for authors. * * @param int $user_id User ID. */ public static function invalidate_author( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( 'user_register' === current_action() || 'profile_update' === current_action() ) { update_user_meta( $user_id, 'last_update', time() ); } if ( $user && ! is_null( $user->roles ) && ! in_array( 'subscriber', $user->roles, true ) ) { self::invalidate( 'author' ); } } /** * Function to clear the Sitemap Cache. * * @param string $object_type Object type for destination where to save. * @param int $object_id Object id for destination where to save. * * @return void */ public static function invalidate_object_type( $object_type, $object_id ) { if ( 'post' === $object_type ) { self::invalidate_post( $object_id ); return; } if ( 'user' === $object_type ) { self::invalidate_author( $object_id ); return; } if ( 'term' === $object_type ) { $term = get_term( $object_id ); self::invalidate_term( $object_id, $term->taxonomy ); } } /** * Delete cache transients for given sitemaps types or all by default. * * @param array $types Set of sitemap types to delete cache transients for. */ public static function clear( $types = [] ) { if ( ! Sitemap::is_cache_enabled() ) { return; } // No types provided, clear all. if ( empty( $types ) ) { self::$clear_all = true; return; } // Always invalidate the index sitemap as well. if ( ! in_array( '1', $types, true ) ) { array_unshift( $types, '1' ); } foreach ( $types as $type ) { if ( ! in_array( $type, self::$clear_types, true ) ) { self::$clear_types[] = $type; } } } /** * Invalidate storage for cache types queued to clear. */ public static function clear_queued() { if ( self::$clear_all ) { Cache::invalidate_storage(); self::$clear_all = false; self::$clear_types = []; return; } foreach ( self::$clear_types as $type ) { Cache::invalidate_storage( $type ); } self::$clear_types = []; } /** * Adds a hook that when given option is updated, the cache is cleared. * * @param string $option Option name. * @param string $type Sitemap type. */ public static function register_clear_on_option_update( $option, $type = '' ) { self::$cache_clear[ $option ] = $type; } /** * Clears the transient cache when a given option is updated, if that option has been registered before. * * @param string $option The option name that's being updated. */ public static function clear_on_option_update( $option ) { if ( ! array_key_exists( $option, self::$cache_clear ) ) { return; } // Clear all caches. if ( empty( self::$cache_clear[ $option ] ) ) { self::clear(); return; } // Clear specific provided type(s). $types = (array) self::$cache_clear[ $option ]; self::clear( $types ); } }