Source: classes/AdvancedCache.php

<?php
/**
 * Admin
 *
 * @package PoweredCache
 */

namespace PoweredCache;

use PoweredCache\Async\CachePurger;
use const PoweredCache\Constants\POST_META_DISABLE_CACHE_KEY;
use function PoweredCache\Utils\clean_page_cache_dir;
use function PoweredCache\Utils\clean_site_cache_dir;
use function PoweredCache\Utils\delete_page_cache;
use function PoweredCache\Utils\get_post_related_urls;
use const PoweredCache\Constants\PURGE_CACHE_PLUGIN_NOTICE_TRANSIENT;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class Admin
 *
 * @package PoweredCache
 */
class AdvancedCache {

	/**
	 * Holds plugin settings
	 *
	 * @var array $settings
	 */
	private $settings;


	/**
	 * Instance of CachePreloader
	 *
	 * @var CachePurger
	 */
	private $cache_purger;

	/**
	 * Return an instance of the current class
	 *
	 * @return AdvancedCache
	 * @since 1.0
	 */
	public static function factory() {

		static $instance;

		if ( ! $instance ) {
			$instance = new self();
			$instance->setup();
		}

		return $instance;
	}

	/**
	 * Setup hooks
	 *
	 * @since 1.0
	 */
	public function setup() {
		$this->settings = \PoweredCache\Utils\get_settings();

		if ( ! $this->settings['enable_page_cache'] ) {
			return;
		}

		$this->cache_purger = CachePurger::factory();

		add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ) );
		add_action( 'admin_post_powered_cache_purge_page_cache', array( $this, 'purge_page_cache' ) );
		add_action( 'admin_post_powered_cache_purge_page_cache_network', array( $this, 'purge_page_cache_network_wide' ) );
		add_action( 'transition_post_status', array( $this, 'purge_on_post_update' ), 9999, 3 );
		add_action( 'switch_theme', array( $this, 'purge_on_switch_theme' ) );
		add_action( 'wp_set_comment_status', array( $this, 'purge_post_on_comment_update' ) );
		add_action( 'edit_comment', array( $this, 'purge_post_on_comment_update' ) );
		add_action( 'set_comment_cookies', array( $this, 'set_comment_cookie' ), 10, 2 );
		add_filter( 'powered_cache_post_related_urls', array( $this, 'powered_cache_post_related_urls' ) );
		add_filter( 'powered_cache_page_cache_enable', array( $this, 'maybe_caching_disabled' ) );
		add_filter( 'powered_cache_mod_rewrite', array( $this, 'maybe_disable_mod_rewrite' ), 99 );
		add_action( 'wp_update_site', array( $this, 'purge_on_site_update' ), 10, 2 );
		add_action( 'create_term', array( $this, 'purge_on_term_change' ), 10, 3 );
		add_action( 'edit_term', array( $this, 'purge_on_term_change' ), 10, 3 );
		add_action( 'delete_term', array( $this, 'purge_on_term_change' ), 10, 3 );
	}

	/**
	 * Add purge button on admin bar
	 *
	 * @param object $wp_admin_bar Admin bar object
	 *
	 * @since 1.0
	 */
	public function admin_bar_menu( $wp_admin_bar ) {
		if ( POWERED_CACHE_IS_NETWORK && current_user_can( 'manage_network' ) ) {
			$wp_admin_bar->add_menu(
				array(
					'id'     => 'advanced-cache-purge-network',
					'title'  => __( 'Purge Page Cache [Network Wide - All Sites]', 'powered-cache' ),
					'href'   => wp_nonce_url( admin_url( 'admin-post.php?action=powered_cache_purge_page_cache_network' ), 'powered_cache_purge_page_cache_network' ),
					'parent' => 'powered-cache',
				)
			);
		}

		if ( current_user_can( 'manage_options' ) ) {
			$wp_admin_bar->add_menu(
				array(
					'id'     => 'advanced-cache-purge',
					'title'  => __( 'Purge Page Cache', 'powered-cache' ),
					'href'   => wp_nonce_url( admin_url( 'admin-post.php?action=powered_cache_purge_page_cache' ), 'powered_cache_purge_page_cache' ),
					'parent' => 'powered-cache',
				)
			);

			if ( is_singular() && ! is_preview() && is_post_publicly_viewable( get_the_ID() ) ) {
				$wp_admin_bar->add_menu(
					array(
						'id'     => 'advanced-cache-current-page-purge',
						'title'  => esc_html__( 'Purge Current Page', 'powered-cache' ),
						'href'   => wp_nonce_url( admin_url( sprintf( 'admin-post.php?action=powered_cache_purge_page_cache&type=current-page&post=%d', get_the_ID() ) ), 'powered_cache_purge_page_cache' ),
						'parent' => 'powered-cache',
					)
				);
			}
		}
	}


	/**
	 * Purge page cache network wide
	 *
	 * @since 2.0
	 */
	public function purge_page_cache_network_wide() {
		if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'powered_cache_purge_page_cache_network' ) ) { // phpcs:ignore
			wp_nonce_ays( '' );
		}

		if ( current_user_can( 'manage_network' ) ) {
			if ( $this->settings['async_cache_cleaning'] ) {
				$this->cache_purger->push_to_queue( [ 'call' => 'clean_page_cache_dir' ] );
				$this->cache_purger->save()->dispatch();
			} else {
				clean_page_cache_dir();
			}
			$redirect_url = add_query_arg( 'pc_action', 'flush_page_cache_network', wp_get_referer() );
		} else {
			$redirect_url = add_query_arg( 'pc_action', 'flush_page_cache_network_err_permission', wp_get_referer() );
		}

		delete_site_transient( PURGE_CACHE_PLUGIN_NOTICE_TRANSIENT );

		wp_safe_redirect( esc_url_raw( $redirect_url ) );
		exit;
	}

	/**
	 * Purge page cache directory
	 *
	 * @since 1.0
	 * @since 1.1 clean site dir instead of root page caching dir
	 */
	public function purge_page_cache() {
		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'powered_cache_purge_page_cache' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			wp_nonce_ays( '' );
		}

		if ( current_user_can( 'manage_options' ) ) {
			if ( isset( $_GET['type'] ) && 'current-page' === $_GET['type'] && ! empty( $_GET['post'] ) ) {
				$this->purge_post( absint( $_GET['post'] ) );
			} elseif ( $this->settings['async_cache_cleaning'] ) {
				$this->cache_purger->push_to_queue( [ 'call' => 'clean_site_cache_dir' ] );
				$this->cache_purger->save()->dispatch();
			} else {
				clean_site_cache_dir();
			}

			$redirect_url = add_query_arg( 'pc_action', 'flush_page_cache', wp_get_referer() );
		} else {
			$redirect_url = add_query_arg( 'pc_action', 'flush_page_cache_err_permission', wp_get_referer() );
		}

		delete_transient( PURGE_CACHE_PLUGIN_NOTICE_TRANSIENT );

		wp_safe_redirect( esc_url_raw( $redirect_url ) );
		exit;
	}


	/**
	 * Purge cache when post updated
	 *
	 * @param string   $new_status New Post status.
	 * @param string   $old_status Old Post status.
	 * @param \WP_Post $post       Post object.
	 *
	 * @return void
	 * @since 3.4 adjusted for `transition_post_status` hook
	 * @since 1.0
	 */
	public function purge_on_post_update( $new_status, $old_status, $post ) {
		if ( 'publish' === $new_status || 'publish' === $old_status ) {
			$this->purge_post( $post->ID );
		}
	}

	/**
	 * Purge post related page cache
	 *
	 * @param int $post_id Post ID
	 *
	 * @since 3.4
	 */
	public function purge_post( $post_id ) {
		$current_post = get_post( $post_id );

		if ( ! is_a( $current_post, 'WP_Post' ) ) {
			return;
		}

		if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
			return;
		}

		if ( ! is_post_type_viewable( $current_post->post_type ) ) {
			return;
		}

		if ( ! in_array( $current_post->post_status, array( 'publish', 'private', 'trash', 'pending', 'draft' ), true ) ) {
			return;
		}

		$urls = get_post_related_urls( $post_id );

		/**
		 * Page cache purge urls.
		 *
		 * @hook   powered_cache_advanced_cache_purge_urls
		 *
		 * @param  {array} $urls The list of URLs that will purged
		 * @param  {int} $post_id Post ID.
		 *
		 * @return {array} New value
		 * @since  1.0
		 */
		$urls         = apply_filters( 'powered_cache_advanced_cache_purge_urls', $urls, $post_id );
		$deleted_urls = [];

		if ( $this->settings['async_cache_cleaning'] ) {
			$this->cache_purger->push_to_queue(
				[
					'call' => 'delete_page_cache',
					'urls' => $urls,
				]
			);
			$this->cache_purger->save()->dispatch();
		} else {
			foreach ( $urls as $url ) {
				if ( delete_page_cache( $url ) ) {
					$deleted_urls[] = $url;
				}
			}
		}

		/**
		 * Fires after purging cache on post update.
		 *
		 * @hook  powered_cache_advanced_cache_purge_post
		 *
		 * @param {int} $post_id The Post ID.
		 * @param {array} $deleted_urls The list of purged urls with the updated post.
		 *
		 * @since 1.0
		 */
		do_action( 'powered_cache_advanced_cache_purge_post', $post_id, $deleted_urls );
	}

	/**
	 * Purge post cache when comment status change
	 *
	 * @param int    $comment_id     Comment ID
	 * @param string $comment_status Comment status
	 *
	 * @return void
	 * @deprecated since 3.4.4
	 */
	public function purge_post_on_comment_status_change( $comment_id, $comment_status ) {
		// deprecate this method in favor of `purge_post_on_comment_update`
		_deprecated_function( __METHOD__, '3.4.4', 'purge_post_on_comment_update' );
		$this->purge_post_on_comment_update( $comment_id );
		$comment  = get_comment( $comment_id );
		$post_id  = $comment->comment_post_ID;
		$post_url = get_permalink( $post_id );

		do_action( 'powered_cache_advanced_cache_purge_on_comment_update', $post_id, $post_url, $comment_id );
	}

	/**
	 * Delete page cache when a comment update
	 *
	 * @param int $comment_id Comment ID
	 *
	 * @since 3.4.4
	 */
	public function purge_post_on_comment_update( $comment_id ) {
		$comment  = get_comment( $comment_id );
		$post_id  = $comment->comment_post_ID;
		$post_url = get_permalink( $post_id );
		delete_page_cache( $post_url, true );

		/**
		 * Fires after purging cache for a post that associated a comment
		 *
		 * @hook  powered_cache_advanced_cache_purge_on_comment_update
		 *
		 * @param {int} $post_id Post ID.
		 * @param {string} $post_url Post permalink.
		 * @param {int} $comment_id Comment ID.
		 *
		 * @since 3.4.4
		 */
		do_action( 'powered_cache_advanced_cache_purge_on_comment_update', $post_id, $post_url, $comment_id );
	}

	/**
	 * Purge site when switching to a new theme
	 *
	 * @since 2.0
	 */
	public function purge_on_switch_theme() {
		if ( $this->settings['async_cache_cleaning'] ) {
			$this->cache_purger->push_to_queue( [ 'call' => 'clean_site_cache_dir' ] );
			$this->cache_purger->save()->dispatch();
		} else {
			clean_site_cache_dir();
		}
	}

	/**
	 * Purge site cache when site updated (eg: archived, deleted etc...)
	 *
	 * @param \WP_Site $new_site New site object.
	 * @param \WP_Site $old_site Old site object.
	 *
	 * @return void
	 * @since 2.5.3
	 */
	public function purge_on_site_update( $new_site, $old_site ) {
		switch_to_blog( $old_site->id );
		if ( $this->settings['async_cache_cleaning'] ) {
			$this->cache_purger->push_to_queue( [ 'call' => 'clean_site_cache_dir' ] );
			$this->cache_purger->save()->dispatch();
		} else {
			clean_site_cache_dir();
		}
		restore_current_blog();
	}

	/**
	 * Leave a cookie when commenting on a post as usual and don't show the cached page.
	 * `comment_author_*` cookies are site-wide available.
	 * Don't show cached results just for left a comment 5 months ago is not seems efficient here.
	 *
	 * @param object $comment Comment object
	 * @param object $user    User Object
	 *
	 * @since 1.0
	 */
	public function set_comment_cookie( $comment, $user ) {
		$post_id = $comment->comment_post_ID;
		$path    = wp_parse_url( get_permalink( $post_id ), PHP_URL_PATH );
		setcookie( 'powered_cache_commented_posts[' . $post_id . ']', $path, ( time() + DAY_IN_SECONDS * 30 ), $path );
	}


	/**
	 * Adds custom pages to post related urls
	 *
	 * @param array $urls The list of the urls
	 *
	 * @return array urls
	 * @since 1.1
	 */
	public function powered_cache_post_related_urls( $urls ) {
		$settings = \PoweredCache\Utils\get_settings();

		$additional_pages = $settings['purge_additional_pages'];

		if ( empty( $additional_pages ) ) {
			return $urls;
		}

		// we only need relative path
		$additional_pages      = str_replace( site_url(), '', $additional_pages );
		$additional_page_paths = explode( PHP_EOL, $additional_pages );
		$additional_urls       = array();

		foreach ( $additional_page_paths as $page ) {
			$additional_urls[] = site_url( $page );
		}

		$urls = array_merge( $urls, $additional_urls );

		return $urls;
	}


	/**
	 * Get the list of rejected cookies
	 *
	 * @return mixed|void
	 * @since 2.0
	 */
	public static function get_rejected_cookies() {
		$settings = \PoweredCache\Utils\get_settings();
		$cookies  = [];

		if ( ! empty( $settings['rejected_cookies'] ) ) {
			$cookies = preg_split( '#(\r\n|\r|\n)#', $settings['rejected_cookies'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		$wp_cookies = [
			'wp-postpass',
			'wordpressuser_',
			'wordpresspass_',
			'wordpress_sec_',
			'wordpress_logged_in_',
			'powered_cache_commented_posts',
			'comment_author_',
			'comment_author_email_',
			'comment_author_url_',
		];

		if ( ! empty( $cookies ) ) {
			$wp_cookies = array_merge( $cookies, $wp_cookies );
		}

		/**
		 * Filter rejected cookie list.
		 *
		 * @hook   powered_cache_rejected_cookies
		 *
		 * @param  {array} $wp_cookies The rejected cookie list from the caching.
		 *
		 * @return {array} Rejected cookie list
		 * @since  2.0
		 */
		return apply_filters( 'powered_cache_rejected_cookies', $wp_cookies );
	}

	/**
	 * Get the list of rejected cookies
	 *
	 * @return mixed|void
	 * @since 2.0
	 */
	public static function get_vary_cookies() {
		$settings = \PoweredCache\Utils\get_settings();
		$cookies  = [];

		if ( ! empty( $settings['vary_cookies'] ) ) {
			$cookies = preg_split( '#(\r\n|\r|\n)#', $settings['vary_cookies'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		/**
		 * Filter vary cookie list.
		 *
		 * @hook   powered_cache_vary_cookies
		 *
		 * @param  {array} $cookies The varied cookie list, which allows to create separate cache based on the match.
		 *
		 * @return {array} Vary cookie list
		 * @since  2.0
		 */
		return apply_filters( 'powered_cache_vary_cookies', $cookies );
	}


	/**
	 * Get the list of rejected user agents
	 *
	 * @return mixed|void
	 * @since 2.0
	 */
	public static function get_rejected_user_agents() {
		$settings             = \PoweredCache\Utils\get_settings();
		$rejected_user_agents = [];

		if ( ! empty( $settings['rejected_user_agents'] ) ) {
			$rejected_user_agents = preg_split( '#(\r\n|\r|\n)#', $settings['rejected_user_agents'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		$rejected_user_agents[] = 'facebookexternalhit';

		/**
		 * Filter rejected user agent list.
		 *
		 * @hook   powered_cache_rejected_user_agents
		 *
		 * @param  {array} $rejected_user_agents The user agents that will not see the cached page.
		 *
		 * @return {array} Rejected user agent list.
		 * @since  2.0
		 */
		return apply_filters( 'powered_cache_rejected_user_agents', $rejected_user_agents );
	}

	/**
	 * Get the list of rejected uri that never get cached
	 *
	 * @return mixed|void
	 * @since 2.0
	 */
	public static function get_rejected_uri() {
		$settings          = \PoweredCache\Utils\get_settings();
		$rejected_uri_list = [];

		if ( ! empty( $settings['rejected_uri'] ) ) {
			$rejected_uri_list = preg_split( '#(\r\n|\r|\n)#', $settings['rejected_uri'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		/**
		 * Filter rejected uri list
		 *
		 * @hook   powered_cache_rejected_uri_list
		 *
		 * @param  {array} $rejected_uri_list The list of rejected uri that never get cached.
		 *
		 * @return {array} New value
		 * @since  2.0
		 */
		return apply_filters( 'powered_cache_rejected_uri_list', $rejected_uri_list );
	}


	/**
	 * Get the allowed cache query parameters
	 *
	 * @return mixed|null
	 * @since 3.0
	 */
	public static function get_cache_query_string() {
		$settings            = \PoweredCache\Utils\get_settings();
		$cache_query_strings = [];

		if ( ! empty( $settings['cache_query_strings'] ) ) {
			$cache_query_strings = preg_split( '#(\r\n|\r|\n)#', $settings['cache_query_strings'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		$query_strings = [
			'lang',
		];

		if ( ! empty( $cache_query_strings ) ) {
			$cache_query_strings = array_merge( $query_strings, $cache_query_strings );
		}

		/**
		 * Filter accepted query strings.
		 *
		 * @hook   powered_cache_cache_query_strings
		 *
		 * @param  {array} $query_strings The list of query strings that will be cached based on their value
		 *
		 * @return {array} New value
		 * @since  3.0
		 */
		return apply_filters( 'powered_cache_cache_query_strings', $cache_query_strings );
	}

	/**
	 * These query strings will be ignored during the caching
	 *
	 * @return array
	 * @since     3.0 deprecated
	 * @deprecated Use `self::get_ignored_query_strings` instead
	 */
	public static function get_accepted_query_strings() {
		_deprecated_function( '\PoweredCache\AdvancedCache::get_accepted_query_strings', '3.0', '\PoweredCache\AdvancedCache::get_ignored_query_strings' );

		return self::get_ignored_query_strings();
	}

	/**
	 * These query strings will be ignored during the caching
	 *
	 * @return mixed|void
	 * @since 3.0
	 */
	public static function get_ignored_query_strings() {
		$settings              = \PoweredCache\Utils\get_settings();
		$ignored_query_strings = [];

		if ( ! empty( $settings['ignored_query_strings'] ) ) {
			$ignored_query_strings = preg_split( '#(\r\n|\r|\n)#', $settings['ignored_query_strings'], - 1, PREG_SPLIT_NO_EMPTY );
		}

		$query_strings = [
			'fbclid',
			'fb_action_ids',
			'fb_action_types',
			'ref',
			'gclid',
			'fb_source',
			'utm_source',
			'utm_medium',
			'utm_campaign',
			'utm_term',
			'utm_content',
			'utm_expid',
			'_ga',
			'mc_cid',
			'mc_eid',
			'campaignid',
			'adgroupid',
			'adid',
			'age-verified',
			'usqp',
			'cn-reloaded',
			'ao_noptimize',
		];

		if ( ! empty( $ignored_query_strings ) ) {
			$query_strings = array_merge( $query_strings, $ignored_query_strings );
		}

		/**
		 * Filters ignored query strings.
		 *
		 * @hook        powered_cache_accepted_query_strings
		 *
		 * @param       {array} $query_strings The list of query strings will be ignored during the caching
		 *
		 * @return      {array} New value
		 * @since       2.0
		 * @depreacated since 3.0
		 */
		$query_strings = apply_filters( 'powered_cache_accepted_query_strings', $query_strings );

		/**
		 * Filters ignored query strings.
		 *
		 * @hook   powered_cache_ignored_query_strings
		 *
		 * @param  {array} $query_strings The list of query strings will be ignored during the caching
		 *
		 * @return {array} New value
		 * @since  3.0
		 */
		$query_strings = apply_filters( 'powered_cache_ignored_query_strings', $query_strings );

		return $query_strings;
	}


	/**
	 * Disable caching of the individual page when it marked as dont' cache
	 *
	 * @param bool $status Cache status
	 *
	 * @return bool
	 */
	public function maybe_caching_disabled( $status ) {
		if ( is_single() ) {
			$is_cache_disabled = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_CACHE_KEY, true );
			if ( $is_cache_disabled ) {
				return false;
			}
		}

		return $status;
	}

	/**
	 * Maybe disable rewrite rules based on the settings
	 *
	 * @param bool $rewrite_status mod rewrite status
	 *
	 * @return bool
	 * @since 2.0
	 */
	public function maybe_disable_mod_rewrite( $rewrite_status ) {
		$vary_cookies = self::get_vary_cookies();
		if ( ! empty( $vary_cookies ) ) {
			$rewrite_status = false;
		}

		return $rewrite_status;
	}


	/**
	 * Purge cache when term change
	 *
	 * @param int    $term_id  Term ID
	 * @param int    $tt_id    Term Taxonomy ID
	 * @param string $taxonomy Taxonomy
	 *
	 * @since 3.4.2
	 */
	public function purge_on_term_change( $term_id, $tt_id, $taxonomy ) {
		$term_taxonomy = get_taxonomy( $taxonomy );

		if ( ! $term_taxonomy->public ) {
			return;
		}

		$term_url = get_term_link( $term_id, $taxonomy );

		if ( ! is_wp_error( $term_url ) ) {
			if ( $this->settings['async_cache_cleaning'] ) {
				$this->cache_purger->push_to_queue(
					[
						'call' => 'delete_page_cache',
						'urls' => $term_url,
					]
				);
				$this->cache_purger->save()->dispatch();
			} else {
				delete_page_cache( $term_url );
			}
		}
	}

}