File: /home/dartdocs/public_html/insarag.dartdocs.app/wp-content/mu-plugins/endurance-page-cache.php
<?php
/**
* Plugin Name: Endurance Page Cache
* Description: This cache plugin is primarily for cache purging of the additional layers of cache that may be available on your hosting account.
* Version: 2.3.0
* Author: Mike Hansen
* Author URI: https://www.mikehansen.me/
* License: GPLv2 or later
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
*
* @package EndurancePageCache
*/
/**
* Endurance Page Cache is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
*
* Endurance Page Cache is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with Endurance Page Cache; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @license GPL-v2-or-later
* @link https://github.com/bluehost/endurance-page-cache/LICENSE
* (If this plugin was installed as a single file, a copy of the license is available in the distribution repository in the link above)
*/
// Do not access file directly!
if ( ! defined( 'WPINC' ) ) {
die;
}
define( 'EPC_VERSION', '2.3.0' );
if ( ! class_exists( 'Endurance_Page_Cache' ) ) {
// Marker for all EPC-managed rules
if ( ! defined( 'NFD_EPC_MARKER' ) ) {
define( 'NFD_EPC_MARKER', 'NFD EPC' );
}
/**
* Class Endurance_Page_Cache
*/
class Endurance_Page_Cache {
/**
* The directory where cached files are stored.
*
* @var string
*/
public $cache_dir;
/**
* A collection of tokens which, if contained in a URI, will prevent caching.
*
* @var array
*/
public $cache_exempt = array( 'checkout', 'cart', 'wp-admin' );
/**
* Cache level.
*
* @var int
*/
public $cache_level = 2;
/**
* Cloudflare enabled
*
* @var bool
*/
public $cloudflare_enabled = false;
/**
* Cloudflare tier
*
* @var string
*/
public $cloudflare_tier = 'basic';
/**
* File Based enabled
*
* @var bool
*/
public $file_based_enabled = false;
/**
* Whether or not to force a purge.
*
* @var bool
*/
public $force_purge = false;
/**
* A collection of throttled items grouped by type where the key is a hash of the URI and the value is the expiration timestamp.
*
* @var array
*/
public $throttled = array();
/**
* Whether or not to update list of throttled items (transient: epc_throttled).
*
* @var bool
*/
public $should_update_throttled_items = false;
/**
* Record keeping for which triggers have fired
*
* @var array
*/
public $triggers = array();
/**
* UDEV Purge Buffer
*
* This parameter determines whether to hit the UDEV Cache Purge API.
*
* Set to false, no request is made.
* Set to true or an empty array all cached resources are purged.
* Set to array of relative paths to purge specified resources only.
*
* @var boolean|array
*/
protected $udev_purge_buffer = false;
/**
* UDEV Cache Purge API Root URL.
*
* @var string
*/
protected static $udev_api_root = 'https://cachepurge.bluehost.com';
/**
* UDEV Cache Purge API version string. First tag v0.
*
* @var string
*/
protected static $udev_api_version = 'v0';
/**
* UDEV Cache Purge API endpoint
*
* @var string
*/
protected static $udev_api_endpoint = 'purge';
/**
* UDEV Cache Purge API services.
*
* PARAMETERS:
* 'cf' => 1|0 (default 1)
* 'epc' => 1|0 (default 0)
*
* @var array
*/
public $udev_api_services = array(
'cf' => 1,
'epc' => 0,
);
/**
* The hook name for scheduling a cache purge event.
*
* @var string
*/
public $epc_scheduled_purge_all_hook = 'epc_scheduled_purge_all';
/**
* Endurance_Page_Cache constructor.
*/
public function __construct() {
if ( isset( $_GET['doing_wp_cron'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
add_action( $this->epc_scheduled_purge_all_hook, array( __CLASS__, 'scheduled_purge_all' ) );
return;
}
$this->throttled = array_filter( (array) get_transient( 'epc_throttled' ) );
$this->cache_level = $this->get_cache_level();
$this->cache_dir = WP_CONTENT_DIR . '/endurance-page-cache';
$cloudflare_state = get_option( 'endurance_cloudflare_enabled', false );
$this->cloudflare_enabled = (bool) $cloudflare_state;
$this->cloudflare_tier = ( is_numeric( $cloudflare_state ) && $cloudflare_state ) ? 'basic' : $cloudflare_state;
$this->udev_api_services['cf'] = $this->cloudflare_tier;
$path = defined( 'ABSPATH' ) ? ABSPATH : __DIR__;
$this->file_based_enabled = (bool) get_option( 'endurance_file_enabled', false === strpos( $path, 'public_html' ) );
array_push( $this->cache_exempt, rest_get_url_prefix() );
$this->hooks();
}
/**
* Retrieves the cache level from the database
*
* If cache level is set higher than 3, then it will reset it down to level 3
*
* @return int
*/
public function get_cache_level() {
$level = absint( get_option( 'endurance_cache_level', 2 ) );
if ( $level > 3 ) {
$level = 3;
update_option( 'endurance_cache_level', $level );
}
return $level;
}
/**
* Setup all WordPress actions and filters.
*/
public function hooks() {
if ( $this->is_enabled( 'page' ) ) {
add_action( 'init', array( $this, 'start' ) );
add_action( 'shutdown', array( $this, 'finish' ) );
add_action( 'shutdown', array( $this, 'shutdown' ) );
add_action( 'generate_rewrite_rules', array( $this, 'config_nginx' ) );
}
// Reconcile in safe contexts (no frontend cost)
add_action( 'admin_init', array( $this, 'nfd_reconcile_epc_htaccess' ) );
add_action( 'rest_api_init', array( $this, 'nfd_reconcile_epc_htaccess' ) );
add_action(
'init',
function () {
if ( ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() )
|| ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) ) {
$this->nfd_reconcile_epc_htaccess();
}
},
5
);
// Run reconcile late in shutdown so it happens after EPC's own finish/shutdown (priority 20)
add_action( 'shutdown', array( $this, 'nfd_reconcile_epc_htaccess' ), 20 );
// Reconcile when options that affect rules change
add_action( 'update_option_endurance_cache_level', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'delete_option_endurance_cache_level', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'update_option_epc_skip_404_handling', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'delete_option_epc_skip_404_handling', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
// These also influence composition of rules
add_action( 'update_option_mm_cache_settings', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'update_option_endurance_file_enabled', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'update_option_epc_filetype_expirations', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
add_action( 'delete_option_epc_filetype_expirations', array( $this, 'nfd_reconcile_epc_htaccess' ), 10, 0 );
// Other hooks
add_action( 'admin_init', array( $this, 'register_cache_settings' ) );
add_action( 'transition_post_status', array( $this, 'save_post' ), 10, 3 );
add_action( 'edit_terms', array( $this, 'edit_terms' ) );
add_action( 'comment_post', array( $this, 'comment' ) );
add_action( 'updated_option', array( $this, 'option_handler' ), 10, 3 );
add_action( 'epc_purge', array( $this, 'purge_all' ) );
add_action( 'epc_purge_request', array( $this, 'purge_request' ) );
add_action( 'wp_update_nav_menu', array( $this, 'purge_all' ) );
add_action( 'admin_bar_menu', array( $this, 'admin_toolbar' ), 99 );
add_action( 'init', array( $this, 'do_purge' ) );
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'status_link' ) );
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'update' ) );
add_filter( 'pre_update_option_mm_cache_settings', array( $this, 'cache_type_change' ), 10, 2 );
add_filter( 'pre_update_option_endurance_cache_level', array( $this, 'cache_level_change' ), 10, 2 );
add_filter( 'got_rewrite', array( $this, 'force_rewrite' ) );
add_action( 'shutdown', array( $this, 'udev_cache_purge_via_buffer' ) ); // keep (priority 10)
}
/**
* Customize the WP Admin Bar.
*
* @param \WP_Admin_Bar $wp_admin_bar Instance of the admin bar.
*/
public function admin_toolbar( $wp_admin_bar ) {
if ( current_user_can( 'manage_options' ) && $this->is_enabled() ) {
$args = array(
'id' => 'epc_purge_menu',
'title' => 'Caching',
);
$wp_admin_bar->add_node( $args );
$args = array(
'id' => 'epc_purge_menu-purge_all',
'title' => 'Purge All',
'parent' => 'epc_purge_menu',
'href' => add_query_arg( array( 'epc_purge_all' => true ) ),
);
$wp_admin_bar->add_node( $args );
if ( ! is_admin() ) {
$args = array(
'id' => 'epc_purge_menu-purge_single',
'title' => 'Purge This Page',
'parent' => 'epc_purge_menu',
'href' => add_query_arg( array( 'epc_purge_single' => true ) ),
);
$wp_admin_bar->add_node( $args );
}
$args = array(
'id' => 'epc_purge_menu-cache_settings',
'title' => 'Cache Settings',
'parent' => 'epc_purge_menu',
'href' => admin_url( 'options-general.php#epc_settings' ),
);
$wp_admin_bar->add_node( $args );
}
}
/**
* Register fields for cache settings.
*/
public function register_cache_settings() {
$section_name = 'epc_settings_section';
add_settings_section(
$section_name,
'<span id="epc_settings">Endurance Cache</span>',
'__return_false',
'general'
);
add_settings_field(
'endurance_cache_level',
'Cache Level',
array( $this, 'output_cache_settings' ),
'general',
$section_name,
array( 'field' => 'endurance_cache_level' )
);
add_settings_field(
'epc_skip_404_handling',
'Skip WordPress 404 Handling For Static Files',
function () {
echo '<input type="checkbox" name="epc_skip_404_handling" value="1"' . checked( (bool) get_option( 'epc_skip_404_handling' ), true, false ) . ' />';
},
'general',
$section_name,
array( 'field' => 'epc_skip_404_handling' )
);
register_setting( 'general', 'endurance_cache_level' );
register_setting( 'general', 'epc_skip_404_handling' );
}
/**
* Output the cache options.
*
* @param array $args Settings
*/
public function output_cache_settings( $args ) {
$cache_level = absint( get_option( $args['field'], 2 ) );
echo '<select name="' . esc_attr( $args['field'] ) . '">';
$cache_levels = array(
0 => 'Off',
1 => 'Assets Only',
2 => 'Normal',
3 => 'Advanced',
);
foreach ( $cache_levels as $i => $label ) {
if ( $i !== $cache_level ) {
echo '<option value="' . absint( $i ) . '"">';
} else {
echo '<option value="' . absint( $i ) . '" selected="selected">';
}
echo esc_html( $label ) . ' (Level ' . absint( $i ) . ')';
echo '</option>';
}
echo '</select>';
}
/**
* Convert a string to studly case.
*
* @param string $value String to be converted.
*
* @return string
*/
public function to_studly_case( $value ) {
return str_replace( ' ', '', ucwords( str_replace( array( '-', '_' ), ' ', $value ) ) );
}
/**
* Convert a string to snake case.
*
* @param string $value String to be converted.
* @param string $delimiter Delimiter (can be a dash for conversion to kebab case).
*
* @return string
*/
public function to_snake_case( $value, $delimiter = '_' ) {
if ( ! ctype_lower( $value ) ) {
$value = preg_replace( '/(\s+)/u', '', ucwords( $value ) );
$value = trim( mb_strtolower( preg_replace( '/([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)/u', '$1' . $delimiter, $value ), 'UTF-8' ), $delimiter );
}
return $value;
}
/**
* Checks if this environment caches requests on the current filesystem
*
* @return bool True if uses file system to cache
*/
public function use_file_cache() {
return $this->file_based_enabled && $this->cache_level;
}
/**
* Whether or not to skip 404 handling for static files.
*
* Enable via WP-CLI: wp option set epc_skip_404_handling 1
*
* @return bool
*/
public function skip_404_handling() {
return (bool) get_option( 'epc_skip_404_handling' );
}
/**
* Handlers that listens for changes to options and checks to see, based on the option name, if the cache should
* be purged.
*
* @param string $option Option name
* @param mixed $old_value Old option value
* @param mixed $new_value New option value
*
* @return bool
*/
public function option_handler( $option, $old_value, $new_value ) {
// No need to process if nothing was updated
if ( $old_value === $new_value ) {
return false;
}
$exempt_if_equals = array(
'active_plugins' => true,
'html_type' => true,
'fs_accounts' => true,
'rewrite_rules' => true,
'uninstall_plugins' => true,
'wp_user_roles' => true,
);
// If we have an exact match, we can just stop here.
if ( array_key_exists( $option, $exempt_if_equals ) ) {
return false;
}
$force_if_contains = array(
'html',
'css',
'style',
'query',
'queries',
);
$exempt_if_contains = array(
'_active',
'_activated',
'_activation',
'_attempts',
'_available',
'_blacklist',
'_cache_validator',
'_check_',
'_checksum',
'_config',
'_count',
'_dectivated',
'_disable',
'_enable',
'_errors',
'_hash',
'_inactive',
'_installed',
'_key',
'_last_',
'_license',
'_log_',
'_mode',
'_options',
'_pageviews',
'_redirects',
'_rules',
'_schedule',
'_session',
'_settings',
'_shown',
'_stats',
'_status',
'_statistics',
'_supports',
'_sync',
'_task',
'_time',
'_token',
'_traffic',
'_transient',
'_url_',
'_version',
'_views',
'_visits',
'_whitelist',
'404s',
'cron',
'limit_login_',
'nonce',
'user_roles',
);
$force_purge = false;
if ( ctype_upper( str_replace( array( '-', '_' ), '', $option ) ) ) {
$option = strtolower( $option );
}
$option_name = '_' . $this->to_snake_case( $this->to_studly_case( $option ) ) . '_';
foreach ( $force_if_contains as $slug ) {
if ( false !== strpos( $option_name, $slug ) ) {
$force_purge = true;
break;
}
}
if ( ! $force_purge ) {
foreach ( $exempt_if_contains as $slug ) {
if ( false !== strpos( $option_name, $slug ) ) {
return false;
}
}
}
$this->add_trigger( 'option_handler' );
// Schedule a purge if not already scheduled.
$this->schedule_purge_all();
return true;
}
/**
* Schedules a single event for purging the cache.
*
* @return void
*/
public function schedule_purge_all() {
if ( ! wp_next_scheduled( $this->epc_scheduled_purge_all_hook ) ) {
wp_schedule_single_event( time() + 60, $this->epc_scheduled_purge_all_hook );
}
}
/**
* Static cron job handler to execute a purge all.
*/
public static function scheduled_purge_all() {
$instance = self::get_instance();
if ( $instance ) {
$instance->purge_all();
}
}
/**
* Purge single post when a comment is updated.
*
* @param int $comment_id ID of the comment.
*/
public function comment( $comment_id ) {
$comment = get_comment( $comment_id );
if ( $comment && property_exists( $comment, 'comment_post_ID' ) ) {
$post_url = get_permalink( $comment->comment_post_ID );
$this->purge_single( $post_url );
}
}
/**
* Purge appropriate caches when post when post is updated.
*
* @param string $old_status The previous post status
* @param string $new_status The new post status
* @param WP_Post $post The post object of the edited or created post
*/
public function save_post( $old_status, $new_status, $post ) {
$post_type_object = get_post_type_object( $post->post_type );
// Skip purging for non-public post types
if ( ! $post_type_object || ! $post_type_object->public ) {
return;
}
// Skip purging if the post wasn't public before and isn't now
if ( 'publish' !== $old_status && 'publish' !== $new_status ) {
return;
}
// Purge post URL when post is updated.
$permalink = get_permalink( $post );
if ( $permalink ) {
$this->purge_single( $permalink );
}
// Purge taxonomy term URLs for related terms.
$taxonomies = get_post_taxonomies( $post );
foreach ( $taxonomies as $taxonomy ) {
if ( $this->is_public_taxonomy( $taxonomy ) ) {
$terms = get_the_terms( $post, $taxonomy );
if ( is_array( $terms ) ) {
foreach ( $terms as $term ) {
$term_link = get_term_link( $term );
$this->purge_single( $term_link );
}
}
}
}
// Purge post type archive URL when post is updated.
$post_type_archive = get_post_type_archive_link( $post->post_type );
if ( $post_type_archive ) {
$this->purge_single( $post_type_archive );
}
// Purge date archive URL when post is updated.
$year_archive = get_year_link( (int) get_the_date( 'y', $post ) );
$year_archive_path = str_replace( get_site_url(), '', $year_archive );
$this->purge_dir( $year_archive_path );
}
/**
* Checks if a post is public.
*
* @param int $post_id The post ID.
*
* @return boolean
*/
public function is_public_post( $post_id ) {
$public = false;
if ( false === wp_is_post_autosave( $post_id ) ) {
$post_type = get_post_type( $post_id );
if ( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object && isset( $post_type_object->public ) ) {
$public = $post_type_object->public;
}
}
}
return $public;
}
/**
* Checks if a taxonomy is public.
*
* @param string $taxonomy Taxonomy name.
*
* @return boolean
*/
public function is_public_taxonomy( $taxonomy ) {
$public = false;
$taxonomy_object = get_taxonomy( $taxonomy );
if ( $taxonomy_object && isset( $taxonomy_object->public ) ) {
$public = $taxonomy_object->public;
}
return $public;
}
/**
* Purge taxonomy term URL when a term is updated.
*
* @param int $term_id Term ID
*/
public function edit_terms( $term_id ) {
$url = get_term_link( $term_id );
if ( ! is_wp_error( $url ) ) {
$this->purge_single( $url );
}
}
/**
* Write page content to cache.
*
* @param string $page Page content to be cached.
*
* @return string
*/
public function write( $page ) {
$base = wp_parse_url( trailingslashit( get_option( 'home' ) ), PHP_URL_PATH );
if ( ! empty( $page ) ) {
$path = WP_CONTENT_DIR . '/endurance-page-cache' . str_replace( get_option( 'home' ), '', esc_url( $_SERVER['REQUEST_URI'] ) );
$path = str_replace( '/endurance-page-cache' . $base, '/endurance-page-cache/', $path );
$path = str_replace( '//', '/', $path );
if ( file_exists( $path . '_index.html' ) && filemtime( $path . '_index.html' ) > time() - HOUR_IN_SECONDS ) {
return $page;
}
if ( false !== strpos( $page, '</html>' ) ) {
$page .= "\n<!--Generated by Endurance Page Cache-->";
}
if ( $this->use_file_cache() ) {
if ( ! is_dir( $path ) ) {
mkdir( $path, 0755, true );
}
file_put_contents( $path . '_index.html', $page, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
} else {
nocache_headers();
}
return $page;
}
/**
* Make a request to purge the entire Sitelock CDN
*/
public function purge_cdn() {
if ( ! $this->force_purge && true === $this->should_throttle( 'sitelock_cdn', __METHOD__ ) ) {
return;
}
if ( true === $this->cloudflare_enabled ) {
return;
}
if ( 'BlueHost' === get_option( 'mm_brand' ) ) {
$endpoint = 'https://my.bluehost.com/cgi/wpapi/cdn_purge';
$domain = wp_parse_url( get_option( 'siteurl' ), PHP_URL_HOST );
$query = add_query_arg( array( 'domain' => $domain ), $endpoint );
$refresh_token = get_option( '_mm_refresh_token' );
if ( false === $refresh_token ) {
return;
}
$path = ABSPATH;
$path = explode( 'public_html/', $path );
if ( 2 === count( $path ) ) {
$path = '/public_html/' . $path[1];
} else {
return;
}
$path_hash = bin2hex( $path );
$headers = array(
'x-api-refresh-token' => $refresh_token,
'x-api-path' => $path_hash,
);
$args = array(
'timeout' => 1,
'blocking' => false,
'headers' => $headers,
);
wp_remote_get( $query, $args );
}
}
/**
* Purge CDN based on pattern.
*
* A purge pattern is any string of literal characters, and will be searched for within filenames. For example,
* a pattern of "ndex" will match "index.html" and "spandex.php". For more fine-grained control, it is possible
* to specify the standard PCRE anchor characters "^" and "$" at the beginning and/or end, respectively, of a
* pattern, in order to anchor to that portion of the string. For example, "html$" will match "index.html" but
* not "learn_html.php".
*
* @param string $pattern (Optional) Pattern used to match assets that should be purged.
*/
public function purge_cdn_single( $pattern = '' ) {
if ( ! $this->force_purge && true === $this->should_throttle( $pattern, __METHOD__ ) ) {
return;
}
if ( 'BlueHost' === get_option( 'mm_brand' ) ) {
$pattern = rawurlencode( $pattern );
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
wp_remote_request(
"https://my.bluehost.com/api/domains/{$domain}/caches/sitelock/{$pattern}",
array(
'method' => 'PUT',
'blocking' => false,
'headers' => array(
'X-MOJO-TOKEN' => get_option( '_mm_refresh_token' ),
),
)
);
}
}
/**
* Ensure that a URI isn't purged more than once per minute.
*
* @param string $uri URI being purged
* @param string $type The type of throttling
*
* @return bool True if additional purges should be avoided, false otherwise.
*/
public function should_throttle( $uri, $type ) {
if ( is_null( $uri ) ) {
return true;
}
$should_throttle = false;
$this->should_update_throttled_items = true;
$hash = md5( $uri );
if ( isset( $this->throttled[ $type ], $this->throttled[ $type ][ $hash ] ) ) {
if ( $this->is_timestamp_valid( $this->throttled[ $type ][ $hash ] ) ) {
$should_throttle = true;
}
}
if ( ! $should_throttle ) {
$this->throttled[ $type ][ $hash ] = time() + MINUTE_IN_SECONDS;
}
return $should_throttle;
}
/**
* Actions that should take place when the page is done loading.
*/
public function shutdown() {
if ( $this->should_update_throttled_items ) {
$throttled = array();
foreach ( $this->throttled as $type => $group ) {
$throttled[ $type ] = array_filter( $group, array( $this, 'is_timestamp_valid' ) );
}
set_transient( 'epc_throttled', $throttled, 60 );
}
}
/**
* Returns true when a timestamp is in the future, or false when it is in the past (expired).
*
* @param int $timestamp Timestamp
*
* @return bool
*/
public function is_timestamp_valid( $timestamp ) {
return $timestamp > time();
}
/**
* Send a cache purge request.
*
* @param string $uri URI to be purged.
*/
public function purge_request( $uri ) {
global $wp_version;
if ( ! $this->force_purge && true === $this->should_throttle( $uri, __METHOD__ ) ) {
return;
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
if ( empty( $this->triggers ) ) {
$this->add_trigger( current_action() );
}
$args = array(
'method' => 'PURGE',
'timeout' => '5',
'blocking' => false,
'sslverify' => false,
'headers' => array(
'host' => $domain,
),
'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url() . '; EPC/v' . EPC_VERSION . '/' . $this->get_trigger(),
);
wp_remote_request( $this->get_purge_request_url( $uri, 'http' ), $args );
wp_remote_request( $this->get_purge_request_url( $uri, 'https' ), $args );
$this->udev_cache_populate_buffer( $uri );
if ( preg_match( '/\.\*$/', $uri ) ) {
$this->purge_cdn();
}
}
/**
* Get URL to be used for purge requests.
*
* @param string $uri The original URI
* @param string $scheme The scheme to be used
*
* @return string
*/
public function get_purge_request_url( $uri, $scheme = 'http' ) {
// Default scheme to http; only allow two values
if ( 'http' !== $scheme && 'https' !== $scheme ) {
$scheme = 'http';
}
$base = ( 'http' === $scheme ) ? 'http://127.0.0.1:8080' : 'https://127.0.0.1:8443';
if ( 0 === strpos( $uri, '/' ) ) {
return $base . $uri;
}
return str_replace( str_replace( wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '', home_url() ), $base, $uri );
}
/**
* Purge everything in a specific directory.
*
* @param string|null $dir Directory to be purged
*/
public function purge_dir( $dir = null ) {
if ( ! $this->force_purge && true === $this->should_throttle( $dir, __METHOD__ ) ) {
return;
}
if ( $this->use_file_cache() ) {
if ( is_null( $dir ) || ! is_dir( $dir ) ) {
$dir = WP_CONTENT_DIR . '/endurance-page-cache';
}
$dir = str_replace( '_index.html', '', $dir );
if ( is_dir( $dir ) ) {
$files = scandir( $dir );
if ( is_array( $files ) ) {
$files = array_diff( $files, array( '.', '..' ) );
}
if ( is_array( $files ) ) {
foreach ( $files as $file ) {
if ( is_dir( $dir . '/' . $file ) ) {
$this->purge_dir( $dir . '/' . $file );
} elseif ( file_exists( $dir . '/' . $file ) ) {
unlink( $dir . '/' . $file );
}
}
if ( 2 === count( scandir( $dir ) ) ) {
rmdir( $dir );
}
}
}
} else {
$this->purge_request( get_option( 'siteurl' ) . $dir . '/.*' );
}
}
/**
* Purge the cache for entire site
*/
public function purge_all() {
if ( ! $this->force_purge && true === $this->should_throttle( 'all', __METHOD__ ) ) {
return;
}
if ( $this->use_file_cache() ) {
$this->purge_dir();
} else {
$this->udev_purge_buffer = array();
$this->purge_request( get_option( 'siteurl' ) . '/.*' );
}
}
/**
* Purge a single URI.
*
* @param string $uri URI to be purged.
*/
public function purge_single( $uri ) {
if ( ! $this->force_purge && true === $this->should_throttle( $uri, __METHOD__ ) ) {
return;
}
$this->purge_request( $uri );
$this->purge_request( home_url() );
$cache_file = $this->uri_to_cache( $uri );
// Purge CDN
$path = wp_parse_url( $uri, PHP_URL_PATH );
$this->purge_cdn_single( $path . '$' );
// Purge Image Assets from CDN
if ( file_exists( $cache_file ) ) {
$content = file_get_contents( $cache_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( ! empty( $content ) ) {
$image_urls = $this->extract_image_urls( $content );
foreach ( $image_urls as $image_url ) {
$this->purge_cdn_single( wp_parse_url( $image_url, PHP_URL_PATH ) . '$' );
if ( ! empty( $this->udev_purge_buffer ) ) {
$this->udev_purge_buffer[] = wp_parse_url( $image_url, PHP_URL_PATH );
}
}
}
}
// Purge requested file
if ( file_exists( $cache_file ) ) {
unlink( $cache_file );
}
// Purge front page file
if ( file_exists( $this->cache_dir . '/_index.html' ) ) {
unlink( $this->cache_dir . '/_index.html' );
}
}
/**
* Extract image URLs from post content.
*
* @param string $content The post content
*
* @return array
*/
public function extract_image_urls( $content ) {
$urls = array();
preg_match_all( '#<img src="(.*?)"#', $content, $matches );
if ( isset( $matches, $matches[1] ) ) {
$urls = $matches[1];
}
return $urls;
}
/**
* Get the URI to cache.
*
* @param string $uri URI
*
* @return string
*/
public function uri_to_cache( $uri ) {
$path = str_replace( get_site_url(), '', $uri );
return $this->cache_dir . $path . '_index.html';
}
/**
* Check if current request is cachable.
*
* @param string $type Cache type
*
* @return bool
*/
public function is_cachable( $type = 'default' ) {
global $wp_query;
$return = true;
if ( 'file' === $type ) {
if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE === true ) {
$return = false;
} elseif ( defined( 'DOING_AJAX' ) ) {
$return = false;
} elseif ( 'private' === get_post_status() ) {
$return = false;
} elseif ( isset( $wp_query ) && is_404() ) {
$return = false;
} elseif ( is_admin() ) {
$return = false;
} elseif ( false === get_option( 'permalink_structure' ) ) {
$return = false;
} elseif ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
$return = false;
} elseif ( isset( $_GET ) && ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$return = false;
} elseif ( isset( $_POST ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$return = false;
} elseif ( isset( $wp_query ) && is_feed() ) {
$return = false;
}
$cache_exempt = array_merge( $this->cache_exempt, array( '@', '%', ':', ';', '&', '=', '.' ) );
} else {
if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE === true ) {
$return = false;
} elseif ( defined( 'DOING_AJAX' ) ) {
$return = false;
} elseif ( 'private' === get_post_status() ) {
$return = false;
} elseif ( isset( $wp_query ) && is_404() ) {
$return = false;
} elseif ( is_admin() ) {
$return = false;
} elseif ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
$return = false;
} elseif ( isset( $_POST ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$return = false;
} elseif ( isset( $wp_query ) && is_feed() ) {
$return = false;
}
$cache_exempt = $this->cache_exempt;
}
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
$return = false;
} else {
$cache_exempt = apply_filters( 'epc_exempt_uri_contains', $cache_exempt );
foreach ( $cache_exempt as $exclude ) {
if ( false !== strpos( $_SERVER['REQUEST_URI'], $exclude ) ) {
$return = false;
}
}
}
return (bool) apply_filters( 'epc_is_cachable', $return );
}
/**
* Start output buffering for cachable requests.
*/
public function start() {
if ( $this->file_based_enabled && $this->is_cachable( 'file' ) ) {
ob_start( array( $this, 'write' ) );
} elseif ( $this->is_cachable() === false ) {
nocache_headers();
}
}
/**
* End output buffering for cachable requests.
*/
public function finish() {
if ( $this->is_cachable( 'file' ) && $this->file_based_enabled && ob_get_contents() ) {
ob_end_clean();
}
}
/**
* Update .htaccess to reflect updates.
*/
public function update_htaccess() {
if ( ! function_exists( 'save_mod_rewrite_rules' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
save_mod_rewrite_rules();
$this->nfd_scrub_inside_wp_block();
}
/**
* Modify the .htaccess file with custom rewrite rules based on caching level.
*
* @param string $rules .htaccess content
*
* @return string
*/
public function htaccess_contents_rewrites( $rules ) {
// Never inject inside the WordPress block; we manage a separate marked block.
if ( did_action( 'mod_rewrite_rules' ) || doing_action( 'mod_rewrite_rules' ) ) {
return $rules;
}
$base = wp_parse_url( trailingslashit( get_option( 'home' ) ), PHP_URL_PATH );
$cache_url = $base . str_replace( get_option( 'home' ), '', WP_CONTENT_URL . '/endurance-page-cache' );
$cache_url = str_replace( '//', '/', $cache_url );
$additions = '';
if ( $this->use_file_cache() ) {
$additions .= <<<HTACCESS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase {$base}
RewriteRule ^{$cache_url}/ - [L]
RewriteCond %{REQUEST_METHOD} !POST
RewriteCond %{QUERY_STRING} !.*=.*
RewriteCond %{HTTP_COOKIE} !(wordpress_test_cookie|comment_author|wp\-postpass|wordpress_logged_in|wptouch_switch_toggle|wp_woocommerce_session_) [NC]
RewriteCond %{DOCUMENT_ROOT}{$cache_url}/$1/_index.html -f
RewriteRule ^(.*)\$ {$cache_url}/$1/_index.html [L]
</IfModule>
HTACCESS;
$additions .= PHP_EOL;
}
if ( $this->skip_404_handling() ) {
$additions .= <<<HTACCESS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !(robots\.txt|[a-z0-9_\-]*sitemap[a-z0-9_\.\-]*\.(xml|xsl|html)(\.gz)?)
RewriteCond %{REQUEST_URI} \.(css|htc|less|js|js2|js3|js4|html|htm|rtf|rtx|txt|xsd|xsl|xml|asf|asx|wax|wmv|wmx|avi|avif|avifs|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|webp|json|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|webm|mpp|otf|_otf|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|tif|tiff|ttf|ttc|_ttf|wav|wma|wri|woff|woff2|xla|xls|xlsx|xlt|xlw|zip)$ [NC]
RewriteRule .* - [L]
</IfModule>
HTACCESS;
$additions .= PHP_EOL;
}
return $additions . $rules;
}
/**
* Modify the .htaccess file with custom expiration rules based on caching level.
*
* @param string $rules .htaccess content
*
* @return string
*/
public function htaccess_contents_expirations( $rules ) {
// Never inject inside the WordPress block; we manage a separate marked block.
if ( did_action( 'mod_rewrite_rules' ) || doing_action( 'mod_rewrite_rules' ) ) {
return $rules;
}
if ( ! $this->is_enabled( 'browser' ) || $this->cache_level < 1 ) {
return $rules;
}
$default_files = array(
'image/jpg' => '1 year',
'image/jpeg' => '1 year',
'image/gif' => '1 year',
'image/png' => '1 year',
'text/css' => '1 month',
'application/pdf' => '1 month',
'text/javascript' => '1 month',
'text/html' => '2 hours',
);
$file_types = wp_parse_args( get_option( 'epc_filetype_expirations', array() ), $default_files );
$additions = "<IfModule mod_expires.c>\n\tExpiresActive On\n\t";
foreach ( $file_types as $file_type => $expires ) {
if ( 'default' !== $file_type ) {
$additions .= 'ExpiresByType ' . $file_type . ' "access plus ' . $expires . '"' . "\n\t";
}
}
$additions .= "ExpiresByType image/x-icon \"access plus 1 year\"\n\t";
if ( isset( $file_types['default'] ) ) {
$additions .= 'ExpiresDefault "access plus ' . $file_types['default'] . "\"\n";
} else {
$additions .= "ExpiresDefault \"access plus 6 hours\"\n";
}
$additions .= "</IfModule>\n";
return $additions . $rules;
}
/**
* Check if a specific caching type is enabled.
*
* @param string $type Caching type (e.g. page or browser).
*
* @return bool
*/
public function is_enabled( $type = 'page' ) {
$plugins = get_option( 'active_plugins', array() );
if ( ! empty( $plugins ) ) {
$plugins = implode( ' ', $plugins );
if ( strpos( $plugins, 'cach' ) || strpos( $plugins, 'wp-rocket' ) ) {
return false;
}
}
$active_theme = array(
'stylesheet' => get_option( 'stylesheet' ),
'template' => get_option( 'template' ),
);
$active_theme = implode( ' ', $active_theme );
$incompatible_themes = array( 'headway', 'prophoto' );
foreach ( $incompatible_themes as $theme ) {
if ( false !== strpos( $active_theme, $theme ) ) {
return false;
}
}
$cache_settings = get_option( 'mm_cache_settings', array() );
if ( 'page' === $type ) {
if ( isset( $_GET['epc_toggle'] ) && is_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification
$valid_values = array( 'enabled', 'disabled' );
if ( in_array( $_GET['epc_toggle'], $valid_values, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$cache_settings['page'] = $_GET['epc_toggle']; // phpcs:ignore WordPress.Security.NonceVerification
update_option( 'mm_cache_settings', $cache_settings );
header( 'Location: ' . admin_url( 'plugins.php?plugin_status=mustuse' ) );
}
}
if ( isset( $cache_settings['page'] ) && 'disabled' === $cache_settings['page'] ) {
return false;
} else {
return true;
}
}
if ( 'browser' === $type ) {
if ( isset( $_GET['epc_toggle'] ) && is_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification
$valid_values = array( 'enabled', 'disabled' );
if ( in_array( $_GET['epc_toggle'], $valid_values, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$cache_settings['browser'] = $_GET['epc_toggle']; // phpcs:ignore WordPress.Security.NonceVerification
update_option( 'mm_cache_settings', $cache_settings );
header( 'Location: ' . admin_url( 'plugins.php?plugin_status=mustuse' ) );
}
}
if ( isset( $cache_settings['browser'] ) && 'disabled' === $cache_settings['browser'] ) {
return false;
} else {
return true;
}
}
return false;
}
/**
* Add plugin action links.
*
* @param array $links Action links
*
* @return array
*/
public function status_link( $links ) {
if ( $this->is_enabled() ) {
$links[] = '<a href="' . add_query_arg( array( 'epc_toggle' => 'disabled' ) ) . '">Disable</a>';
} else {
$links[] = '<a href="' . add_query_arg( array( 'epc_toggle' => 'enabled' ) ) . '">Enable</a>';
}
$links[] = '<a href="' . add_query_arg( array( 'epc_purge_all' => 'true' ) ) . '">Purge Cache</a>';
return $links;
}
/**
* Listens for purge actions and handles based on type.
*/
public function do_purge() {
if ( ( isset( $_GET['epc_purge_all'] ) || isset( $_GET['epc_purge_single'] ) ) && is_user_logged_in() && current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->force_purge = true;
if ( isset( $_GET['epc_purge_all'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->add_trigger( 'toolbar_manual_all' );
$this->purge_all();
} else {
$this->add_trigger( 'toolbar_manual_single' );
$this->purge_single( $this->get_current_single_purge_url() );
}
header( 'Location: ' . remove_query_arg( array( 'epc_purge_single', 'epc_purge_all' ) ) );
}
}
/**
* Get the current URI for a single purge request.
*
* @return string
*/
public function get_current_single_purge_url() {
$host = str_replace( wp_parse_url( home_url(), PHP_URL_PATH ), '', home_url() );
$path = remove_query_arg( array( 'epc_purge_single', 'epc_purge_all' ) );
return $host . $path;
}
/**
* Update the appropriate option when cache settings are changed.
*
* @param array $new_cache_settings New cache settings
* @param array $old_cache_settings Old Cache settings
*
* @return array
*/
public function cache_type_change( $new_cache_settings, $old_cache_settings ) {
$new_page_cache_value = 0;
if ( is_array( $new_cache_settings ) && isset( $new_cache_settings['page'] ) ) {
$new_page_cache_value = ( 'enabled' === $new_cache_settings['page'] ) ? 1 : 0;
}
if ( false === get_option( 'endurance_cache_level' ) ) {
if ( 1 === $new_page_cache_value ) {
update_option( 'endurance_cache_level', 2 );
} else {
update_option( 'endurance_cache_level', 0 );
}
}
return $new_cache_settings;
}
/**
* Handle cache level change.
*
* @param int $new_cache_level New cache level
* @param int $old_cache_level Old cache level
*
* @return int
*/
public function cache_level_change( $new_cache_level, $old_cache_level ) {
$cache_settings = get_option( 'mm_cache_settings', array() );
if ( 0 === $new_cache_level ) {
$cache_settings['page'] = 'disabled';
$cache_settings['browser'] = 'disabled';
} else {
$cache_settings['page'] = 'enabled';
$cache_settings['browser'] = 'enabled';
}
remove_filter( 'pre_update_option_mm_cache_settings', array( $this, 'cache_type_change' ), 10 );
update_option( 'mm_cache_settings', $cache_settings );
add_filter( 'pre_update_option_mm_cache_settings', array( $this, 'cache_type_change' ), 10, 2 );
$this->cache_level = $new_cache_level;
$this->toggle_nginx( $new_cache_level );
$this->update_level_expirations( $new_cache_level );
return (int) $new_cache_level;
}
/**
* Update cache expirations rules in .htaccess based on cache level.
*
* @param int $level Cache level
*/
public function update_level_expirations( $level ) {
$level = (int) $level;
$original_expirations = get_option( 'epc_filetype_expirations', array() );
switch ( $level ) {
case 3:
$new_expirations = array(
'image/jpg' => '1 week',
'image/jpeg' => '1 week',
'image/gif' => '1 week',
'image/png' => '1 week',
'text/css' => '1 week',
'application/pdf' => '1 week',
'text/javascript' => '1 month',
'text/html' => '8 hours',
'default' => '1 week',
);
break;
case 2:
$new_expirations = array(
'image/jpg' => '24 hours',
'image/jpeg' => '24 hours',
'image/gif' => '24 hours',
'image/png' => '24 hours',
'text/css' => '24 hours',
'application/pdf' => '1 week',
'text/javascript' => '24 hours',
'text/html' => '2 hours',
'default' => '24 hours',
);
break;
case 1:
$new_expirations = array(
'image/jpg' => '1 hour',
'image/jpeg' => '1 hour',
'image/gif' => '1 hour',
'image/png' => '1 hour',
'text/css' => '1 hour',
'application/pdf' => '6 hours',
'text/javascript' => '1 hour',
'text/html' => '0 seconds',
'default' => '5 minutes',
);
break;
default:
$new_expirations = array();
break;
}
$expirations = wp_parse_args( $new_expirations, $original_expirations );
if ( 0 === $level ) {
delete_option( 'epc_filetype_expirations' );
} else {
update_option( 'epc_filetype_expirations', $expirations );
}
}
/**
* Configure caching in nginx.
*/
public function config_nginx() {
$this->toggle_nginx( $this->cache_level );
}
/**
* Toggle nginx caching.
*
* @param int $new_value Cache level
*/
public function toggle_nginx( $new_value = 0 ) {
if ( ! $this->use_file_cache() ) {
$domain = wp_parse_url( get_option( 'siteurl' ), PHP_URL_HOST );
$domain = str_replace( 'www.', '', $domain );
$path = explode( 'public_html', __DIR__ );
if ( 2 !== count( $path ) ) {
return;
}
$user = basename( $path[0] );
$path = $path[0];
if ( ! is_dir( $path . '.cpanel/proxy_conf' ) ) {
mkdir( $path . '.cpanel/proxy_conf' );
}
if ( true === $this->cloudflare_enabled ) {
$new_value = '-1';
}
@file_put_contents( $path . '.cpanel/proxy_conf/' . $domain, 'cache_level=' . $new_value ); // phpcs:ignore WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors
@touch( '/etc/proxy_notify/' . $user ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
}
}
/**
* Handle checking for plugin updates.
*
* @param \stdClass $checked_data Plugin update data.
*
* @return \stdClass
*/
public function update( $checked_data ) {
$muplugins_details = get_transient( 'mojo_plugin_assets' );
if ( ! $muplugins_details ) {
$muplugins_details = wp_remote_get( 'https://cdn.hiive.space/bluehost/mu-plugins.json' );
if ( ! is_wp_error( $muplugins_details ) ) {
set_transient( 'mojo_plugin_assets', $muplugins_details, 6 * HOUR_IN_SECONDS );
}
}
if ( is_wp_error( $muplugins_details ) || ! isset( $muplugins_details['body'] ) ) {
return $checked_data;
}
$mu_plugin = json_decode( $muplugins_details['body'], true );
if ( ! is_null( $mu_plugin ) ) {
foreach ( $mu_plugin as $slug => $info ) {
if ( isset( $info['constant'] ) && defined( $info['constant'] ) ) {
if ( version_compare( $info['version'], constant( $info['constant'] ), '>' ) ) {
$file = wp_remote_get( $info['source'] );
if ( ! is_wp_error( $file ) && isset( $file['body'] ) && strpos( $file['body'], $info['constant'] ) && is_writable( WP_CONTENT_DIR . $info['destination'] ) ) {
file_put_contents( WP_CONTENT_DIR . $info['destination'], $file['body'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
}
}
}
}
return $checked_data;
}
/**
* Filter to force got_mod_rewrite() to true
*
* On CLI requests, mod_rewrite is unavailable, so it fails to update
* the .htaccess file when save_mod_rewrite_rules() is called. This
* forces that to be true so updates from WP CLI work.
*
* @param bool $got_rewrite Value of apache_mod_loaded('mod_rewrite')
*
* @return bool true for WP CLI requests
*/
public function force_rewrite( $got_rewrite ) {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return true;
}
return $got_rewrite;
}
/**
* Add trigger for record keeping.
*
* @param string $trigger Typically an action but can be manually set in the event of a force purge.
*
* @return void
*/
protected function add_trigger( $trigger ) {
$this->triggers[] = $trigger;
}
/**
* Retrieves the most recent trigger
*
* @return string of the most recent trigger from the collection
*/
protected function get_trigger() {
return end( $this->triggers );
}
/**
* Retrieves all the triggers to send with bundled requests
*
* @param string $include_duplicates Determines if the array should be unique
*
* @return array
*/
protected function get_triggers( $include_duplicates = false ) {
if ( ! $include_duplicates ) {
return array_values( array_unique( $this->triggers ) );
} else {
return $this->triggers;
}
}
/**
* Primary function for the UDEV Purge Cache API. Makes non-blocking request for current install cache purges.
*
* Calling this method with *no* parameters triggers a full cache wipe for the domain.
* Calling this method with relative paths to resources will purge just those resources.
*
* @param array $resources (Site paths, image assets, scripts, styles, files, etc)
* @param array $override_services (see defaults on self::$udev_api_services)
*
* @return void
*/
protected function udev_cache_purge( $resources = array(), $override_services = array() ) {
global $wp_version;
if ( $this->use_file_cache() || false === $this->cloudflare_enabled ) {
return;
}
$throttle_key = md5( wp_json_encode( $resources ) );
if ( ! $this->force_purge && true === $this->should_throttle( $throttle_key, __METHOD__ ) ) {
return;
}
$hosts = array( wp_parse_url( home_url(), PHP_URL_HOST ) );
$services = ! empty( $override_services ) ? $override_services : $this->udev_api_services;
if ( $services['cf'] && $this->cloudflare_enabled ) {
$services['cf'] = $this->cloudflare_tier;
}
wp_remote_post(
$this->udev_cache_api_uri( $services ),
array(
'blocking' => false,
'body' => $this->udev_create_request_body( $hosts, $resources ),
'compress' => true,
'headers' => array(
'X-EPC-PLUGIN-PURGE' => 1,
'content-type' => 'application/json',
),
'sslverify' => false,
'user-agent' => 'WordPress/' . $wp_version . '; ' . wp_parse_url( home_url(), PHP_URL_HOST ) . '; EPC/v' . EPC_VERSION,
)
);
}
/**
* Build request URL and params for UDEV Purge Cache API.
*
* @param array $services List of services
*
* @return string URI to use for the udev cache API
*/
protected function udev_cache_api_uri( $services ) {
return trailingslashit( static::$udev_api_root ) . trailingslashit( static::$udev_api_version ) . static::$udev_api_endpoint . '?' . http_build_query( $services );
}
/**
* Take hosts (and perhaps specific resources) to purge and encode JSON for request body.
*
* @param array $hosts List of hosts
* @param array $resources List of resources
*
* @return string|false
*/
protected function udev_create_request_body( $hosts, $resources ) {
$request = array( 'hosts' => $hosts );
if ( ! empty( $resources ) ) {
$request['assets'] = array_values( array_unique( array_filter( $resources ) ) );
}
$request['triggers'] = $this->triggers;
return wp_json_encode( $request );
}
/**
* Takes full URI and adds to $this->udev_purge_buffer. Typically fires in $this->purge_request().
* Note that $this->purge_all() presets an empty array, which denotes a full domain purge.
*
* @param string $uri URI to add to udev purge buffer
*
* @return void
*/
protected function udev_cache_populate_buffer( $uri ) {
if ( is_array( $this->udev_purge_buffer ) && ! empty( $this->udev_purge_buffer ) ) {
$this->udev_purge_buffer[] = wp_parse_url( $uri, PHP_URL_PATH );
} elseif ( false === $this->udev_purge_buffer ) {
$this->udev_purge_buffer = array( wp_parse_url( $uri, PHP_URL_PATH ) );
}
}
/**
* Takes all specified resources in $this->udev_purge_buffer (or empty array denoting full purge)
* and makes cache purge request to UDEV Cache API.
*
* @return void
*/
public function udev_cache_purge_via_buffer() {
if ( ! empty( $this->udev_purge_buffer ) || is_array( $this->udev_purge_buffer ) ) {
$this->udev_cache_purge( $this->udev_purge_buffer );
}
}
/**
* Retrieve the singleton instance of the class.
*
* @return Endurance_Page_Cache
*/
public static function get_instance() {
static $instance = null;
if ( null === $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Get the path to the .htaccess file.
*/
private function nfd_htaccess_path() {
return trailingslashit( ABSPATH ) . '.htaccess'; }
/**
* Check if we are in a context safe to modify .htaccess
* (admin, ajax, cron, WP-CLI, REST API).
*/
private function nfd_safe_context() {
return ( is_admin()
|| ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() )
|| ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() )
|| ( defined( 'WP_CLI' ) && WP_CLI )
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
}
/**
* Reconcile .htaccess contents with current settings.
*
* @return array of actions taken
*/
private function nfd_epc_lines() {
$rw = $this->htaccess_contents_rewrites( '' ); // file-cache rewrites + skip404 (if enabled)
$exp = $this->htaccess_contents_expirations( '' ); // browser expirations (if enabled)
$txt = trim( $rw . "\n" . $exp );
$txt = preg_replace( "/\n{3,}/", "\n\n", $txt );
return $txt ? explode( "\n", $txt ) : array();
}
/**
* Ensure .htaccess has correct EPC block, or remove it if not needed.
*
* @return boolean True if .htaccess was modified, false if no change made
*/
private function nfd_remove_block() {
$path = $this->nfd_htaccess_path();
if ( ! file_exists( $path ) || ! is_writable( $path ) ) { return false; }
$contents = file_get_contents( $path );
$pattern = '/^\h*# BEGIN ' . preg_quote( NFD_EPC_MARKER, '/' ) . '\R.*?\R# END ' . preg_quote( NFD_EPC_MARKER, '/' ) . '\R?/ms';
$new = preg_replace( $pattern, '', $contents ); // remove ALL occurrences
if ( $new !== $contents ) {
file_put_contents( $path, ltrim( $new, "\r\n" ) );
return true;
}
return false;
}
/**
* Write or update the EPC block in .htaccess, or remove it if no longer needed.
*
* @return boolean True if .htaccess was modified, false if no change made
*/
private function nfd_write_block() {
$path = $this->nfd_htaccess_path();
if ( ! file_exists( $path ) ) { @touch( $path ); }
if ( ! is_writable( $path ) ) { return false; }
if ( ! function_exists( 'insert_with_markers' ) ) {
require_once ABSPATH . 'wp-admin/includes/misc.php';
}
$lines = $this->nfd_epc_lines();
// If nothing to write, ensure our block is removed
if ( empty( $lines ) ) { return $this->nfd_remove_block(); }
$contents = file_get_contents( $path );
// Purge ALL existing NFD blocks
$pattern = '/^\h*# BEGIN ' . preg_quote( NFD_EPC_MARKER, '/' ) . '\R.*?\R# END ' . preg_quote( NFD_EPC_MARKER, '/' ) . '\R?/ms';
$contents = preg_replace( $pattern, '', $contents );
// Normalize RewriteRule leading slash for per-dir .htaccess
$lines = array_map(
function ( $l ) {
$l = preg_replace( '/^(\s*RewriteRule\s+\^)\//', '$1', $l );
return rtrim( $l );
},
$lines
);
$block = '# BEGIN ' . NFD_EPC_MARKER . "\n" . implode( "\n", $lines ) . "\n# END " . NFD_EPC_MARKER . "\n";
$wp_beg = '# BEGIN WordPress';
if ( strpos( $contents, $wp_beg ) !== false ) {
$parts = explode( $wp_beg, $contents, 2 );
$before = rtrim( $parts[0] );
$after = $wp_beg . $parts[1];
// Use no leading separator if file begins with WP block
$sep_before = ( '' === $before ) ? '' : "\n\n";
// Ensure a single newline between our block and WP block
$new = $before . $sep_before . $block . $after;
file_put_contents( $path, $new );
return true;
}
// No WP block: append with at most one blank line before our block
$trimmed = rtrim( $contents );
$sep_before = ( '' === $trimmed ) ? '' : "\n\n";
file_put_contents( $path, $trimmed . $sep_before . $block );
return true;
}
/**
* Clean stray EPC rules inside the core WordPress block and normalize formatting.
*
* - Never removes the native WP rewrite block (the one with "^index\.php$ - [L]").
* - Preserves brand inline patches (e.g., "NFD PATCH", "Newfold", "nfd.skip404.static").
* - Removes only EPC legacy blocks (file-cache + skip-404 with "RewriteRule .* - [L]").
*
* @return bool True if the file was modified, false otherwise.
*/
private function nfd_scrub_inside_wp_block() {
$path = $this->nfd_htaccess_path();
if ( ! file_exists( $path ) || ! is_writable( $path ) ) {
return false;
}
$contents = file_get_contents( $path );
// Find the WordPress block.
if ( ! preg_match( '/(# BEGIN WordPress\b)([\s\S]*?)(# END WordPress\b)/', $contents, $m ) ) {
return false;
}
list( $full, $wp_begin, $wp_body, $wp_end ) = $m;
$orig_body = $wp_body;
$had_native_wp_rule = (bool) preg_match( '/RewriteRule\s+\^index\.php\$\s+-\s+\[L\]/i', $wp_body );
// Normalize some formatting glitches we’ve seen in the wild.
$wp_body = preg_replace( '/(will be overwritten\.)\s*(?=<)/i', "$1\n", $wp_body ); // newline after comment
$wp_body = preg_replace( '/<\/IfModule>\s*(?=<IfModule\b)/i', "</IfModule>\n", $wp_body ); // split glued tags
$wp_body = preg_replace( '/Options\s+-Indexes(?=<IfModule\b)/i', "Options -Indexes\n", $wp_body );
// Remove simple junk that never belongs in the WP block.
$wp_body = preg_replace( '/^\h*Options\s+-Indexes\s*$/im', '', $wp_body ); // stray Options -Indexes
$wp_body = preg_replace(
'/\s*<IfModule\s+mod_headers\.c>[\s\S]*?(?:^|\R)\h*Header\s+set\s+(?:X-Endurance-Cache-Level|X-nginx-cache)\b[\s\S]*?<\/IfModule>\s*/im',
'',
$wp_body
); // old EPC headers
$wp_body = preg_replace( '/\s*<IfModule\s+mod_expires\.c>[\s\S]*?<\/IfModule>\s*/i', '', $wp_body ); // browser cache belongs outside
// Detect brand markers; when present, we never strip rewrite lines in-place.
$has_brand_markers = (
stripos( $wp_body, 'NFD PATCH' ) !== false
|| stripos( $wp_body, 'Newfold' ) !== false
|| stripos( $wp_body, 'nfd.skip404.static' ) !== false
);
if ( ! $has_brand_markers ) {
// Process each mod_rewrite block *individually*.
$wp_body = preg_replace_callback(
'/\s*<IfModule\s+mod_rewrite\.c>([\s\S]*?)<\/IfModule>\s*/i',
function ( $mm ) {
$inner = $mm[1];
// 1) Never touch the native WP rewrite block.
if ( preg_match( '/RewriteRule\s+\^index\.php\$\s+-\s+\[L\]/i', $inner ) ) {
return $mm[0];
}
// 2) Remove EPC file-cache rewrite blocks outright.
if ( preg_match( '/endurance-page-cache|_index\.html/i', $inner ) ) {
return '';
}
// 3) Remove EPC skip-404 blocks ONLY if they include the sentinel "RewriteRule .* - [L]".
if ( preg_match( '/^\h*RewriteRule\s+\.\*\s+-\s+\[L\]\s*$/im', $inner ) ) {
// Strip that sentinel rule and its paired conditions, but leave anything else alone.
$inner = preg_replace( '/^\h*RewriteRule\s+\.\*\s+-\s+\[L\]\s*$/im', '', $inner );
$inner = preg_replace( '/^\h*RewriteCond\s+\%\{REQUEST_FILENAME\}\s+!-f\s*$/im', '', $inner );
$inner = preg_replace( '/^\h*RewriteCond\s+\%\{REQUEST_FILENAME\}\s+!-d\s*$/im', '', $inner );
$inner = preg_replace(
'/^\h*RewriteCond\s+\%\{REQUEST_URI\}\s+!\(robots\\\.txt\|[a-z0-9_\-]*sitemap[a-z0-9_\.\-]*\\\.(xml|xsl|html)\\\(\\\.gz\\\)\?\)\s*$/im',
'',
$inner
);
$inner = preg_replace(
'/^\h*RewriteCond\s+\%\{REQUEST_URI\}\s+\\\.\((?:css|htc|less|js|js2|js3|js4|html|htm|rtf|rtx|txt|xsd|xsl|xml|asf|asx|wax|wmv|wmx|avi|avif|avifs|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|webp|json|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|webm|mpp|otf|_otf|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|tif|tiff|ttf|ttc|_ttf|wav|wma|wri|woff|woff2|xla|xls|xlsx|xlt|xlw)\)\\\)\$\s+\[NC\]\s*$/im',
'',
$inner
);
// If the block is now empty (no RewriteCond/RewriteRule), drop it entirely.
if ( ! preg_match( '/Rewrite(Cond|Rule)\b/i', $inner ) ) {
return '';
}
// Otherwise keep the reduced block wrapped.
return "<IfModule mod_rewrite.c>\n" . trim( $inner ) . "\n</IfModule>\n";
}
// Anything else: leave untouched.
return $mm[0];
},
$wp_body
);
}
// Compact excessive blank lines.
$wp_body = preg_replace( '/\R{3,}/', "\n\n", $wp_body );
// SAFETY: if we had the native WP rule before, we must still have it after. If not, abort.
$still_has_native_wp_rule = (bool) preg_match( '/RewriteRule\s+\^index\.php\$\s+-\s+\[L\]/i', $wp_body );
if ( $had_native_wp_rule && ! $still_has_native_wp_rule ) {
return false;
}
if ( $wp_body === $orig_body ) {
return false;
}
// Write the updated WP block back.
$new = preg_replace(
'/# BEGIN WordPress\b[\s\S]*?# END WordPress\b/',
$wp_begin . $wp_body . $wp_end,
$contents
);
file_put_contents( $path, $new );
return true;
}
/**
* Main method to ensure .htaccess EPC block is correct or removed if not needed.
*
* @return void
*/
public function nfd_reconcile_epc_htaccess() {
if ( ! $this->nfd_safe_context() ) { return;
}
$path = $this->nfd_htaccess_path();
if ( ! file_exists( $path ) && ! @touch( $path ) ) { return;
}
// Decide if we *should* have any EPC rules at all (holistic)
$level = (int) get_option( 'endurance_cache_level', 0 );
$skip_404 = (bool) get_option( 'epc_skip_404_handling', 0 );
$page_on = ( $this->is_enabled( 'page' ) && $this->use_file_cache() );
$browser_on = ( $this->is_enabled( 'browser' ) && $this->cache_level >= 1 );
$should_have_rules = ( $page_on || $browser_on || $skip_404 );
// Always scrub stray EPC lines out of the WordPress block (if any slipped in previously)
$this->nfd_scrub_inside_wp_block();
if ( ! $should_have_rules ) {
$this->nfd_remove_block();
return;
}
// Build expected lines and compare with current block; rewrite if missing or drifted
$expected = $this->nfd_epc_lines();
// If nothing to write (edge cases), remove block
if ( empty( $expected ) ) { $this->nfd_remove_block();
return; }
// Read existing block (if any)
$contents = file_get_contents( $path );
$pattern = '/# BEGIN ' . preg_quote( NFD_EPC_MARKER, '/' ) . '\R(.*?)\R# END ' . preg_quote( NFD_EPC_MARKER, '/' ) . '/s';
$matches = array();
$current_lines = array();
if ( preg_match( $pattern, $contents, $matches ) && isset( $matches[1] ) ) {
// Normalize whitespace for comparison
$body = preg_replace( "/\r\n?/", "\n", trim( $matches[1] ) );
$current_lines = array_map( 'rtrim', explode( "\n", $body ) );
}
// If block missing or different, rewrite the canonical block
if ( array_map( 'rtrim', $expected ) !== $current_lines ) {
$this->nfd_write_block();
}
}
}
$epc = new Endurance_Page_Cache();
}