diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e34b3b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[{.jshintrc,*.json,*.yml}] +indent_style = space +indent_size = 2 + +[{*.txt,wp-config-sample.php}] +end_of_line = crlf + diff --git a/README.md b/README.md index 1eac0b6..0780b38 100644 --- a/README.md +++ b/README.md @@ -587,6 +587,10 @@ Fix url escaping [#82](https://github.com/rtCamp/nginx-helper/pull/82) - by * First release +## Credits ## + +This plugin’s Cloudflare edge cache purging and cache tag architecture is inspired by the excellent work in [pantheon-systems/pantheon-advanced-page-cache](https://github.com/pantheon-systems/pantheon-advanced-page-cache). + ## Upgrade Notice ## ### 2.2.3 ### diff --git a/admin/class-nginx-helper-admin.php b/admin/class-nginx-helper-admin.php index a35ca9d..da38542 100644 --- a/admin/class-nginx-helper-admin.php +++ b/admin/class-nginx-helper-admin.php @@ -9,6 +9,8 @@ * @subpackage nginx-helper/admin */ +use EasyCache\Cloudflare_Client; + /** * The admin-specific functionality of the plugin. * @@ -20,7 +22,7 @@ * @author rtCamp */ class Nginx_Helper_Admin { - + /** * The ID of this plugin. * @@ -29,7 +31,7 @@ class Nginx_Helper_Admin { * @var string $plugin_name The ID of this plugin. */ private $plugin_name; - + /** * The version of this plugin. * @@ -38,7 +40,7 @@ class Nginx_Helper_Admin { * @var string $version The current version of this plugin. */ private $version; - + /** * Various settings tabs. * @@ -47,7 +49,7 @@ class Nginx_Helper_Admin { * @var string $settings_tabs Various settings tabs. */ private $settings_tabs; - + /** * Purge options. * @@ -56,7 +58,16 @@ class Nginx_Helper_Admin { * @var string[] $options Purge options. */ public $options; - + + /** + * Purge options. + * + * @since 2.0.0 + * @access public + * @var string[] $options Cloudflare options. + */ + public $cf_options; + /** * WP-CLI Command. * @@ -65,7 +76,7 @@ class Nginx_Helper_Admin { * @var string $options WP-CLI Command. */ const WP_CLI_COMMAND = 'nginx-helper'; - + /** * Initialize the class and set its properties. * @@ -74,37 +85,42 @@ class Nginx_Helper_Admin { * @param string $version The version of this plugin. */ public function __construct( $plugin_name, $version ) { - + $this->plugin_name = $plugin_name; $this->version = $version; - - $this->options = $this->nginx_helper_settings(); + + $this->options = $this->nginx_helper_settings(); + $this->cf_options = $this->get_cloudflare_settings(); } - + /** * Initialize the settings tab. * Required since i18n is used in the settings tab which can be invoked only after init hook since WordPress 6.7 */ public function initialize_setting_tab() { - + /** * Define settings tabs */ $this->settings_tabs = apply_filters( - 'rt_nginx_helper_settings_tabs', - array( - 'general' => array( - 'menu_title' => __( 'General', 'nginx-helper' ), - 'menu_slug' => 'general', - ), - 'support' => array( - 'menu_title' => __( 'Support', 'nginx-helper' ), - 'menu_slug' => 'support', - ), - ) + 'rt_nginx_helper_settings_tabs', + array( + 'general' => array( + 'menu_title' => __( 'General', 'nginx-helper' ), + 'menu_slug' => 'general', + ), + 'support' => array( + 'menu_title' => __( 'Support', 'nginx-helper' ), + 'menu_slug' => 'support', + ), + 'cloudflare' => array( + 'menu_title' => __( 'Cloudflare', 'nginx-helper' ), + 'menu_slug' => 'cloudflare', + ), + ) ); } - + /** * Register the stylesheets for the admin area. * @@ -113,7 +129,7 @@ public function initialize_setting_tab() { * @param string $hook The current admin page. */ public function enqueue_styles( $hook ) { - + /** * This function is provided for demonstration purposes only. * @@ -125,16 +141,16 @@ public function enqueue_styles( $hook ) { * between the defined hooks and the functions defined in this * class. */ - + if ( 'settings_page_nginx' !== $hook ) { return; } - + wp_enqueue_style( $this->plugin_name . '-icons', plugin_dir_url( __FILE__ ) . 'icons/css/nginx-fontello.css', array(), $this->version, 'all' ); wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/nginx-helper-admin.css', array(), $this->version, 'all' ); - + } - + /** * Register the JavaScript for the admin area. * @@ -143,7 +159,7 @@ public function enqueue_styles( $hook ) { * @param string $hook The current admin page. */ public function enqueue_scripts( $hook ) { - + /** * This function is provided for demonstration purposes only. * @@ -155,29 +171,29 @@ public function enqueue_scripts( $hook ) { * between the defined hooks and the functions defined in this * class. */ - + if ( 'settings_page_nginx' !== $hook ) { return; } - + wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/nginx-helper-admin.js', array( 'jquery' ), $this->version, false ); - + $do_localize = array( 'purge_confirm_string' => esc_html__( 'Purging entire cache is not recommended. Would you like to continue?', 'nginx-helper' ), ); wp_localize_script( $this->plugin_name, 'nginx_helper', $do_localize ); - + } - + /** * Add admin menu. * * @since 2.0.0 */ public function nginx_helper_admin_menu() { - + if ( is_multisite() ) { - + add_submenu_page( 'settings.php', __( 'Nginx Helper', 'nginx-helper' ), @@ -186,9 +202,9 @@ public function nginx_helper_admin_menu() { 'nginx', array( &$this, 'nginx_helper_setting_page' ) ); - + } else { - + add_submenu_page( 'options-general.php', __( 'Nginx Helper', 'nginx-helper' ), @@ -197,22 +213,22 @@ public function nginx_helper_admin_menu() { 'nginx', array( &$this, 'nginx_helper_setting_page' ) ); - + } - + } - + /** * Function to add toolbar purge link. * * @param object $wp_admin_bar Admin bar object. */ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) { - + if ( ! current_user_can( 'Nginx Helper | Purge cache' ) ) { return; } - + if ( is_admin() ) { $nginx_helper_urls = 'all'; $link_title = __( 'Purge Cache', 'nginx-helper' ); @@ -220,7 +236,7 @@ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) { $nginx_helper_urls = 'current-url'; $link_title = __( 'Purge Current Page', 'nginx-helper' ); } - + $purge_url = add_query_arg( array( 'nginx_helper_action' => 'purge', @@ -228,9 +244,9 @@ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) { 'nginx_helper_dismiss' => get_transient( 'rt_wp_nginx_helper_suggest_purge_notice' ), ) ); - + $nonced_url = wp_nonce_url( $purge_url, 'nginx_helper-purge_all' ); - + $wp_admin_bar->add_menu( array( 'id' => 'nginx-helper-purge-all', @@ -239,9 +255,9 @@ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) { 'meta' => array( 'title' => $link_title ), ) ); - + } - + /** * Display settings. * @@ -252,7 +268,7 @@ public function nginx_helper_toolbar_purge_link( $wp_admin_bar ) { public function nginx_helper_setting_page() { include plugin_dir_path( __FILE__ ) . 'partials/nginx-helper-admin-display.php'; } - + /** * Default settings. * @@ -260,7 +276,7 @@ public function nginx_helper_setting_page() { * @return array */ public function nginx_helper_default_settings() { - + return array( 'enable_purge' => 0, 'cache_method' => 'enable_fastcgi', @@ -297,30 +313,83 @@ public function nginx_helper_default_settings() { 'roles_with_purge_cap' => array(), 'purge_woo_products' => 0, ); - + + } + + public function store_default_options() { + $options = get_site_option( 'rt_wp_nginx_helper_options', array() ); + $default_settings = $this->nginx_helper_default_settings(); + + $removable_default_settings = array( + 'redis_port', + 'redis_prefix', + 'redis_hostname', + 'redis_database', + 'redis_unix_socket' + ); + + // Remove all the keys that are not to be stored by default. + foreach ( $removable_default_settings as $removable_key ) { + unset( $default_settings[ $removable_key ] ); + } + + $diffed_options = wp_parse_args( $options, $default_settings ); + + add_site_option( 'rt_wp_nginx_helper_options', $diffed_options ); + + $this->store_cloudflare_settings(); + } + + /** + * Gets the default settings for cloudflare. + * + * @return array An array of settings. + */ + public function get_cloudflare_default_settings() { + return array( + 'api_token' => '', + 'zone_id' => '', + 'default_cache_ttl' => 604800, + 'api_token_enabled_by_constant' => false, + ); + } + + /** + * Gets the current cloudflare settings. + * + * @return array The current settings. + */ + public function get_cloudflare_settings() { + $default_settings = $this->get_cloudflare_default_settings(); + + $stored_options = get_site_option( 'easycache_cf_settings', array() ); + + if ( defined( 'EASYCACHE_CLOUDFLARE_API_TOKEN' ) && !empty( EASYCACHE_CLOUDFLARE_API_TOKEN ) ) { + $stored_options['api_token'] = EASYCACHE_CLOUDFLARE_API_TOKEN; + $stored_options['api_token_enabled_by_constant'] = true; + } + + $diff_options = wp_parse_args( $stored_options, $default_settings ); + + $diff_options['is_enabled'] = ! empty( $diff_options['api_token'] ) && ! empty( $diff_options['zone_id'] ); + + return $diff_options; + } + + /** + * Stores the cloudflare settings. + * + * @return array The current settings. + */ + public function store_cloudflare_settings() { + $default_settings = $this->get_cloudflare_default_settings(); + + $stored_options = get_site_option( 'easycache_cf_settings', array() ); + + $diff_options = wp_parse_args( $stored_options, $default_settings ); + + add_site_option( 'easycache_cf_settings', $diff_options ); } - - public function store_default_options() { - $options = get_site_option( 'rt_wp_nginx_helper_options', array() ); - $default_settings = $this->nginx_helper_default_settings(); - - $removable_default_settings = array( - 'redis_port', - 'redis_prefix', - 'redis_hostname', - 'redis_database', - 'redis_unix_socket' - ); - - // Remove all the keys that are not to be stored by default. - foreach ( $removable_default_settings as $removable_key ) { - unset( $default_settings[ $removable_key ] ); - } - - $diffed_options = wp_parse_args( $options, $default_settings ); - - add_site_option( 'rt_wp_nginx_helper_options', $diffed_options ); - } /** * Get settings. @@ -328,7 +397,7 @@ public function store_default_options() { * @since 2.0.0 */ public function nginx_helper_settings() { - + $options = get_site_option( 'rt_wp_nginx_helper_options', array( @@ -338,18 +407,18 @@ public function nginx_helper_settings() { 'redis_database' => 0, ) ); - + $data = wp_parse_args( $options, $this->nginx_helper_default_settings() ); - + $is_redis_enabled = ( defined( 'RT_WP_NGINX_HELPER_REDIS_HOSTNAME' ) && defined( 'RT_WP_NGINX_HELPER_REDIS_PORT' ) && defined( 'RT_WP_NGINX_HELPER_REDIS_PREFIX' ) ); - + $data['redis_acl_enabled_by_constant'] = defined('RT_WP_NGINX_HELPER_REDIS_USERNAME') && defined('RT_WP_NGINX_HELPER_REDIS_PASSWORD'); $data['redis_socket_enabled_by_constant'] = defined('RT_WP_NGINX_HELPER_REDIS_UNIX_SOCKET'); $data['redis_unix_socket'] = $data['redis_socket_enabled_by_constant'] ? RT_WP_NGINX_HELPER_REDIS_UNIX_SOCKET : $data['redis_unix_socket']; @@ -365,13 +434,13 @@ public function nginx_helper_settings() { $data['cache_method'] = 'enable_redis'; $data['redis_hostname'] = RT_WP_NGINX_HELPER_REDIS_HOSTNAME; $data['redis_port'] = RT_WP_NGINX_HELPER_REDIS_PORT; - $data['redis_prefix'] = RT_WP_NGINX_HELPER_REDIS_PREFIX; + $data['redis_prefix'] = RT_WP_NGINX_HELPER_REDIS_PREFIX; $data['redis_database'] = defined('RT_WP_NGINX_HELPER_REDIS_DATABASE') ? RT_WP_NGINX_HELPER_REDIS_DATABASE : 0; return $data; - + } - + /** * Nginx helper setting link function. * @@ -380,20 +449,20 @@ public function nginx_helper_settings() { * @return mixed */ public function nginx_helper_settings_link( $links ) { - + if ( is_network_admin() ) { $setting_page = 'settings.php'; } else { $setting_page = 'options-general.php'; } - + $settings_link = '' . __( 'Settings', 'nginx-helper' ) . ''; array_unshift( $links, $settings_link ); - + return $links; - + } - + /** * Check if the nginx log is enabled. * @@ -401,20 +470,20 @@ public function nginx_helper_settings_link( $links ) { * @return boolean */ public function is_nginx_log_enabled() { - + $options = get_site_option( 'rt_wp_nginx_helper_options', array() ); - + if ( ! empty( $options['enable_log'] ) && 1 === (int) $options['enable_log'] ) { return true; } - + if ( defined( 'NGINX_HELPER_LOG' ) && true === NGINX_HELPER_LOG ) { return true; } - + return false; } - + /** * Retrieve the asset path. * @@ -422,13 +491,13 @@ public function is_nginx_log_enabled() { * @return string asset path of the plugin. */ public function functional_asset_path() { - + $log_path = WP_CONTENT_DIR . '/uploads/nginx-helper/'; - + return apply_filters( 'nginx_asset_path', $log_path ); - + } - + /** * Retrieve the asset url. * @@ -436,36 +505,36 @@ public function functional_asset_path() { * @return string asset url of the plugin. */ public function functional_asset_url() { - + $log_url = WP_CONTENT_URL . '/uploads/nginx-helper/'; - + return apply_filters( 'nginx_asset_url', $log_url ); - + } - + /** * Get latest news. * * @since 2.0.0 */ public function nginx_helper_get_feeds() { - + // Get RSS Feed(s). require_once ABSPATH . WPINC . '/feed.php'; - + $maxitems = 0; $rss_items = array(); - + // Get a SimplePie feed object from the specified feed source. $rss = fetch_feed( 'https://rtcamp.com/blog/feed/' ); - + if ( ! is_wp_error( $rss ) ) { // Checks that the object is created correctly. - + // Figure out how many total items there are, but limit it to 5. $maxitems = $rss->get_item_quantity( 5 ); // Build an array of all the items, starting with element 0 (first element). $rss_items = $rss->get_items( 0, $maxitems ); - + } ?> options['enable_purge'] || 1 !== (int) $this->options['enable_stamp'] ) { return; } - + if ( ! empty( $pagenow ) && 'wp-login.php' === $pagenow ) { return; } - + foreach ( headers_list() as $header ) { list( $key, $value ) = explode( ':', $header, 2 ); $key = strtolower( $key ); @@ -528,32 +597,32 @@ public function add_timestamps() { break; } } - + /** * Don't add timestamp if run from ajax, cron or wpcli. */ if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return; } - + if ( defined( 'DOING_CRON' ) && DOING_CRON ) { return; } - + if ( defined( 'WP_CLI' ) && WP_CLI ) { return; } - + $timestamps = "\n\n" . ''; - + echo wp_kses( $timestamps, array() ); - + } - + /** * Get map * @@ -562,83 +631,83 @@ public function add_timestamps() { * @return string */ public function get_map() { - + if ( ! $this->options['enable_map'] ) { return; } - + if ( is_multisite() ) { - + global $wpdb; - + $rt_all_blogs = $wpdb->get_results( $wpdb->prepare( 'SELECT blog_id, domain, path FROM ' . $wpdb->blogs . " WHERE site_id = %d AND archived = '0' AND mature = '0' AND spam = '0' AND deleted = '0'", $wpdb->siteid ) ); - + $wpdb->dmtable = $wpdb->base_prefix . 'domain_mapping'; - + $rt_domain_map_sites = ''; - + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->dmtable}'" ) === $wpdb->dmtable ) { // phpcs:ignore $rt_domain_map_sites = $wpdb->get_results( "SELECT blog_id, domain FROM {$wpdb->dmtable} ORDER BY id DESC" ); } - + $rt_nginx_map = ''; $rt_nginx_map_array = array(); - + if ( $rt_all_blogs ) { - + foreach ( $rt_all_blogs as $blog ) { - + if ( true === SUBDOMAIN_INSTALL ) { $rt_nginx_map_array[ $blog->domain ] = $blog->blog_id; } else { - + if ( 1 !== $blog->blog_id ) { $rt_nginx_map_array[ $blog->path ] = $blog->blog_id; } } } } - + if ( $rt_domain_map_sites ) { - + foreach ( $rt_domain_map_sites as $site ) { $rt_nginx_map_array[ $site->domain ] = $site->blog_id; } } - + foreach ( $rt_nginx_map_array as $domain => $domain_id ) { $rt_nginx_map .= "\t" . $domain . "\t" . $domain_id . ";\n"; } - + return $rt_nginx_map; - + } - + } - + /** * Update map */ public function update_map() { - + if ( is_multisite() ) { - + $rt_nginx_map = $this->get_map(); - + $fp = fopen( $this->functional_asset_path() . 'map.conf', 'w+' ); if ( $fp ) { fwrite( $fp, $rt_nginx_map ); fclose( $fp ); } } - + } - + /** * Purge url when post status is changed. * @@ -650,7 +719,7 @@ public function update_map() { * @param object $post Post object. */ public function set_future_post_option_on_future_status( $new_status, $old_status, $post ) { - + global $blog_id, $nginx_purger; $exclude_post_types = apply_filters( 'rt_nginx_helper_exclude_post_types', array( 'nav_menu_item' ) ); @@ -658,20 +727,20 @@ public function set_future_post_option_on_future_status( $new_status, $old_statu if ( in_array( $post->post_type, $exclude_post_types, true ) ) { return; } - + if ( ! $this->options['enable_purge'] || $this->is_import_request() ) { return; } - + $purge_status = array( 'publish', 'future' ); - + if ( in_array( $old_status, $purge_status, true ) || in_array( $new_status, $purge_status, true ) ) { - + $nginx_purger->log( 'Purge post on transition post STATUS from ' . $old_status . ' to ' . $new_status ); $nginx_purger->purge_post( $post->ID ); - + } - + if ( 'future' === $new_status && $post && 'future' === $post->post_status && ( @@ -682,15 +751,15 @@ public function set_future_post_option_on_future_status( $new_status, $old_statu ) ) ) { - + $nginx_purger->log( 'Set/update future_posts option ( post id = ' . $post->ID . ' and blog id = ' . $blog_id . ' )' ); $this->options['future_posts'][ $blog_id ][ $post->ID ] = strtotime( $post->post_date_gmt ) + 60; update_site_option( 'rt_wp_nginx_helper_options', $this->options ); - + } - + } - + /** * Unset future post option on delete * @@ -700,9 +769,9 @@ public function set_future_post_option_on_future_status( $new_status, $old_statu * @param int $post_id Post id. */ public function unset_future_post_option_on_delete( $post_id ) { - + global $blog_id, $nginx_purger; - + if ( ! $this->options['enable_purge'] || empty( $this->options['future_posts'] ) || @@ -712,18 +781,18 @@ public function unset_future_post_option_on_delete( $post_id ) { ) { return; } - + $nginx_purger->log( 'Unset future_posts option ( post id = ' . $post_id . ' and blog id = ' . $blog_id . ' )' ); - + unset( $this->options['future_posts'][ $blog_id ][ $post_id ] ); - + if ( ! count( $this->options['future_posts'][ $blog_id ] ) ) { unset( $this->options['future_posts'][ $blog_id ] ); } - + update_site_option( 'rt_wp_nginx_helper_options', $this->options ); } - + /** * Update map when new blog added in multisite. * @@ -732,18 +801,18 @@ public function unset_future_post_option_on_delete( $post_id ) { * @param string $blog_id blog id. */ public function update_new_blog_options( $blog_id ) { - + global $nginx_purger; - + $nginx_purger->log( "New site added ( id $blog_id )" ); $this->update_map(); $nginx_purger->log( "New site added to nginx map ( id $blog_id )" ); $helper_options = $this->nginx_helper_default_settings(); update_blog_option( $blog_id, 'rt_wp_nginx_helper_options', $helper_options ); $nginx_purger->log( "Default options updated for the new blog ( id $blog_id )" ); - + } - + /** * Purge all urls. * Purge current page cache when purging is requested from front @@ -752,18 +821,18 @@ public function update_new_blog_options( $blog_id ) { * @global object $nginx_purger */ public function purge_all() { - + if ( $this->is_import_request() ) { return; } - + global $nginx_purger, $wp; - + $method = null; if ( isset( $_SERVER['REQUEST_METHOD'] ) ) { $method = wp_strip_all_tags( $_SERVER['REQUEST_METHOD'] ); } - + $action = ''; if ( 'POST' === $method ) { if ( isset( $_POST['nginx_helper_action'] ) ) { @@ -774,34 +843,34 @@ public function purge_all() { $action = wp_strip_all_tags( $_GET['nginx_helper_action'] ); } } - + if ( empty( $action ) ) { return; } - + if ( ! current_user_can( 'Nginx Helper | Purge cache' ) ) { wp_die( 'Sorry, you do not have the necessary privileges to edit these options.' ); } - + if ( 'done' === $action ) { - + add_action( 'admin_notices', array( &$this, 'display_notices' ) ); add_action( 'network_admin_notices', array( &$this, 'display_notices' ) ); return; - + } - + check_admin_referer( 'nginx_helper-purge_all' ); - + $current_url = user_trailingslashit( home_url( $wp->request ) ); - + if ( ! is_admin() ) { $action = 'purge_current_page'; $redirect_url = $current_url; } else { $redirect_url = add_query_arg( array( 'nginx_helper_action' => 'done' ) ); } - + switch ( $action ) { case 'purge': $nginx_purger->purge_all(); @@ -810,30 +879,34 @@ public function purge_all() { $nginx_purger->purge_url( $current_url ); break; } - + if ( 'purge' === $action ) { - + /** * Fire an action after the entire cache has been purged whatever caching type is used. * * @since 2.2.2 */ do_action( 'rt_nginx_helper_after_purge_all' ); - + + } + + if( $this->cf_options['is_enabled'] ) { + Cloudflare_Client::purgeEverything(); } - + wp_redirect( esc_url_raw( $redirect_url ) ); exit(); - + } - + /** * Dispay plugin notices. */ public function display_notices() { echo '

' . esc_html__( 'Purge initiated', 'nginx-helper' ) . '

'; } - + /** * Preloads the cache for the website. * @@ -842,42 +915,42 @@ public function display_notices() { public function preload_cache() { $is_cache_preloaded = $this->options['is_cache_preloaded']; $preload_cache_enabled = $this->options['preload_cache']; - + if ( $preload_cache_enabled && false === boolval( $is_cache_preloaded ) ) { $this->options['is_cache_preloaded'] = true; - + update_site_option( 'rt_wp_nginx_helper_options', $this->options ); $this->preload_cache_from_sitemap(); } } - + /** * This function preloads the cache from sitemap url. * * @return void */ private function preload_cache_from_sitemap() { - + $sitemap_urls = $this->get_index_sitemap_urls(); $all_urls = array(); - + foreach ( $sitemap_urls as $sitemap_url ) { $urls = $this->extract_sitemap_urls( $sitemap_url ); $all_urls = array_merge( $all_urls, $urls ); } - + $args = array( 'timeout' => 1, 'blocking' => false, 'sslverify' => false, ); - + foreach ( $all_urls as $url ) { wp_remote_get( esc_url_raw( $url ), $args ); } - + } - + /** * Fetches all the sitemap urls for the site. * @@ -891,7 +964,7 @@ private function get_index_sitemap_urls() { } return $urls; } - + /** * Parse sitemap content and extract all URLs. * @@ -900,35 +973,35 @@ private function get_index_sitemap_urls() { */ private function extract_sitemap_urls( $sitemap_url ) { $response = wp_remote_get( $sitemap_url ); - + $urls = array(); - + if ( is_wp_error( $response ) ) { return $urls; } - + $sitemap_content = wp_remote_retrieve_body( $response ); - + libxml_use_internal_errors( true ); $xml = simplexml_load_string( $sitemap_content ); - + if ( false === $xml ) { return new WP_Error( 'sitemap_parse_error', esc_html__( 'Failed to parse the sitemap XML', 'nginx-helper' ) ); } - + $urls = array(); - + if ( false === $xml ) { return $urls; } - + foreach ( $xml->url as $url ) { $urls[] = (string) $url->loc; } - + return $urls; } - + /** * Determines if the current request is for importing Posts/ WordPress content. * @@ -937,7 +1010,7 @@ private function extract_sitemap_urls( $sitemap_url ) { public function is_import_request() { $import_query_var = sanitize_text_field( wp_unslash( $_GET['import'] ?? '' ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is already in the admin dashboard. $has_import_started = did_action( 'import_start' ); - + return ( defined( 'WP_IMPORTING' ) && true === WP_IMPORTING ) || 0 !== $has_import_started || ! empty( $import_query_var ); @@ -1112,21 +1185,123 @@ public function purge_product_cache_on_purchase( $order ) { public function purge_product_cache_on_update( $product_id ) { global $nginx_purger; - if ( empty( $nginx_purger ) ) { - return; + if ( empty( $nginx_purger ) ) { + return; } if ( ! $this->options['enable_purge'] ) { return; } - + $nginx_purger->log( 'WooCommerce product update - purging cache for product ID: ' . $product_id ); - + $product_url = get_permalink( $product_id ); - + if ( $product_url ) { $nginx_purger->purge_url( $product_url ); } } - + + /** + * Handles the cache rule update on Cloudflare tab. + * + * @return void + */ + public function handle_cf_cache_rule_update() { + $nonce = isset( $_POST['easycache_add_cache_rule_nonce'] ) ? wp_unslash( $_POST['easycache_add_cache_rule_nonce'] ) : ''; + + if ( wp_verify_nonce( $nonce, 'easycache_add_cache_rule_nonce' ) ) { + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $result = EasyCache\Cloudflare_Client::setupCacheRule(); + + set_transient( 'ec_page_rule_save_state_admin_notice', $result, 60 ); + } + } + + /** + * Display admin notices for cloudflare page save rules. + */ + public function cf_page_rule_save_display_admin_notices() { + if ( $result = get_transient( 'ec_page_rule_save_state_admin_notice' ) ) { + $class = 'notice'; + $message = ''; + + switch ( $result ) { + case 'created': + $class .= ' notice-success'; + $message = __( 'The Cloudflare Cache Rule was created successfully.', 'nginx-helper' ); + break; + case 'exists': + $class .= ' notice-info'; + $message = __( 'The Cache Rule already exists. No action was taken.', 'nginx-helper' ); + break; + default: + $class .= ' notice-error'; + $message = __( 'Failed to create the Cache Rule. Please check that your API Token has Cache Rules Read/Write permissions.', 'nginx-helper' ); + break; + } + + printf( '

%2$s

', esc_attr( $class ), esc_html( $message ) ); + delete_transient( 'ec_page_rule_save_state_admin_notice' ); + } + } + + /** + * Register a toolbar button to purge the cache for the current page. + * + * @param object $wp_admin_bar Instance of WP_Admin_Bar. + */ + public static function add_cloudflare_admin_bar_purge( $wp_admin_bar ) { + if ( is_admin() || ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { + return; + } + + if ( ! empty( $_GET['message'] ) && 'ec-cleared-url-cache' === $_GET['message'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $title = esc_html__( 'URL Cache Cleared', 'easycache' ); + } else { + $title = esc_html__( 'Clear URL Cache', 'easycache' ); + } + + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( $_SERVER['REQUEST_URI'] ) : ''; + $wp_admin_bar->add_menu( [ + 'parent' => '', + 'id' => 'clear-page-cache', + 'title' => $title, + 'meta' => [ + 'title' => __( 'Purge the current URL from Cloudflare cache.', 'easycache' ), + ], + 'href' => wp_nonce_url( admin_url( 'admin-ajax.php?action=ec_clear_url_cache&path=' . rawurlencode( home_url( $request_uri ) ) ), 'ec-clear-url-cache' ), + ] ); + } + + /** + * Handle an admin-ajax request to clear the URL cache for Cloudflare. + * + * @return void + */ + public static function handle_cloudflare_clear_cache_ajax() { + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( $_GET['_wpnonce'] ) : ''; + if ( empty( $nonce ) + || ! wp_verify_nonce( $nonce, 'ec-clear-url-cache' ) + || ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( "You shouldn't be doing this.", 'easycache' ) ); + } + + $path = isset( $_GET['path'] ) ? esc_url_raw( $_GET['path'] ) : ''; + if ( empty( $path ) ) { + wp_die( esc_html__( 'No path provided.', 'easycache' ) ); + } + + $ret = Cloudflare_Client::purgeByUrls( [ $path ] ); + if ( ! $ret ) { + wp_die( esc_html__( 'Failed to clear URL cache.', 'easycache' ) ); + } + + wp_safe_redirect( add_query_arg( 'message', 'ec-cleared-url-cache', $path ) ); + exit; + } } diff --git a/admin/partials/easycache-cloudflare-options.php b/admin/partials/easycache-cloudflare-options.php new file mode 100644 index 0000000..4e4a26e --- /dev/null +++ b/admin/partials/easycache-cloudflare-options.php @@ -0,0 +1,150 @@ +get_cloudflare_default_settings(); + + $args = wp_parse_args( $all_inputs, $default_args ); + + update_site_option( 'easycache_cf_settings', $args ); + + echo '

' . esc_html__( 'Settings saved.', 'nginx-helper' ) . '

'; +} + +if( isset( $nginx_helper_admin ) && method_exists( $nginx_helper_admin, 'handle_cf_cache_rule_update' ) ) { + $nginx_helper_admin->handle_cf_cache_rule_update(); +} + +// Display any updates from Cloudflare Purge Rules. +if( isset( $nginx_helper_admin ) && method_exists( $nginx_helper_admin, 'cf_page_rule_save_display_admin_notices' ) ) { + $nginx_helper_admin->cf_page_rule_save_display_admin_notices(); +} + + +$ec_site_settings = $nginx_helper_admin->get_cloudflare_settings(); +?> + +
+
+ + +
+

+
+ + + + + + + + + + + + + + + +
+

+
+
+ + + +
+ + name="api_token" id="cf_api_token" type="password" class="password-input" + value=""/> + + + + +

+

+
+

+
+
+ + + +
+ +
+

+
+
+ + + +
+ +

+
+
+ +
+ +
+
+ + +
+ + +
+ + diff --git a/admin/partials/nginx-helper-admin-display.php b/admin/partials/nginx-helper-admin-display.php index bd15371..44251db 100644 --- a/admin/partials/nginx-helper-admin-display.php +++ b/admin/partials/nginx-helper-admin-display.php @@ -49,6 +49,9 @@ case 'support': include plugin_dir_path( __FILE__ ) . 'nginx-helper-support-options.php'; break; + case 'cloudflare': + include plugin_dir_path( __FILE__ ) . 'easycache-cloudflare-options.php'; + break; } ?> diff --git a/composer.json b/composer.json index f215446..fcb7e63 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,32 @@ { "name": "rtcamp/nginx-helper", - "description": "Cleans nginx's fastcgi/proxy cache or redis-cache whenever a post is edited/published. Also does a few more things.", - "keywords": ["wordpress", "plugin", "nginx", "nginx-helper", "fastcgi", "redis-cache", "redis", "cache"], + "type": "wordpress-plugin", + "description": "Cleans nginx's fastcgi/proxy cache or redis-cache whenever a post is edited/published. Also provides cloudflare edge cache purging with Cache-Tags.", + "keywords": [ + "wordpress", + "plugin", + "nginx", + "nginx-helper", + "fastcgi", + "redis-cache", + "redis", + "cache", + "cloudflare", + "cloudflare-edge-cache", + "cache-tags", + "easycache" + ], "homepage": "https://rtcamp.com/nginx-helper/", "license": "GPL-2.0+", - "authors": [{ - "name": "rtCamp", - "email": "support@rtcamp.com", - "homepage": "https://rtcamp.com" - }], + "authors": [ + { + "name": "rtCamp", + "email": "support@rtcamp.com", + "homepage": "https://rtcamp.com" + } + ], "minimum-stability": "dev", "prefer-stable": true, - "type": "wordpress-plugin", "support": { "issues": "https://github.com/rtCamp/nginx-helper/issues", "forum": "https://wordpress.org/support/plugin/nginx-helper", @@ -20,9 +35,15 @@ }, "require": { "php": ">=5.3.2", - "composer/installers": "^1.0" + "composer/installers": "^1.0", + "cloudflare/sdk": "^1.1" }, "require-dev": { "wpreadme2markdown/wpreadme2markdown": "*" + }, + "config": { + "allow-plugins": { + "composer/installers": true + } } } diff --git a/composer.lock b/composer.lock index 589b071..1e4cbd2 100644 --- a/composer.lock +++ b/composer.lock @@ -1,42 +1,98 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "f8ee8d46fadaee8c9cc194ef126e7404", + "content-hash": "9a39933154bb8e65621b25ca590a1e84", "packages": [ + { + "name": "cloudflare/sdk", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/cloudflare/cloudflare-php.git", + "reference": "2d3f198773e865b5de2357d7bdbc52bdf42e8f97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cloudflare/cloudflare-php/zipball/2d3f198773e865b5de2357d7bdbc52bdf42e8f97", + "reference": "2d3f198773e865b5de2357d7bdbc52bdf42e8f97", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^7.0.1", + "php": ">=7.2.5", + "psr/http-message": "~1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.6", + "phpmd/phpmd": "@stable", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cloudflare\\API\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Junade Ali", + "email": "junade@cloudflare.com" + } + ], + "description": "PHP binding for v4 of the Cloudflare Client API.", + "support": { + "issues": "https://github.com/cloudflare/cloudflare-php/issues", + "source": "https://github.com/cloudflare/cloudflare-php/tree/1.4.0" + }, + "time": "2024-12-17T23:18:20+00:00" + }, { "name": "composer/installers", - "version": "v1.0.6", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "b3bd071ea114a57212c75aa6a2eef5cfe0cc798f" + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/b3bd071ea114a57212c75aa6a2eef5cfe0cc798f", - "reference": "b3bd071ea114a57212c75aa6a2eef5cfe0cc798f", + "url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19", "shasum": "" }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, "replace": { + "roundcube/plugin-installer": "*", "shama/baton": "*" }, "require-dev": { - "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "3.7.*" + "composer/composer": "1.6.* || ^2.0", + "composer/semver": "^1 || ^3", + "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan-phpunit": "^0.12.16", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.3" }, - "type": "composer-installer", + "type": "composer-plugin", "extra": { - "class": "Composer\\Installers\\Installer", + "class": "Composer\\Installers\\Plugin", "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "1.x-dev" } }, "autoload": { - "psr-0": { - "Composer\\Installers\\": "src/" + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" } }, "notification-url": "https://packagist.org/downloads/", @@ -47,77 +103,512 @@ { "name": "Kyle Robinson Young", "email": "kyle@dontkry.com", - "homepage": "https://github.com/shama", - "role": "Developer" + "homepage": "https://github.com/shama" } ], "description": "A multi-framework Composer library installer", - "homepage": "http://composer.github.com/installers/", + "homepage": "https://composer.github.io/installers/", "keywords": [ - "TYPO3 CMS", - "TYPO3 Flow", - "TYPO3 Neos", + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", "cakephp", + "chef", + "cockpit", "codeigniter", + "concrete5", + "croogo", + "dokuwiki", "drupal", + "eZ Platform", + "elgg", + "expressionengine", "fuelphp", + "grav", "installer", + "itop", "joomla", + "known", "kohana", "laravel", - "li3", + "lavalite", "lithium", + "magento", + "majima", "mako", + "mediawiki", + "miaoxing", "modulework", + "modx", + "moodle", + "osclass", + "pantheon", "phpbb", + "piwik", "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", "silverstripe", + "sydes", + "sylius", "symfony", + "tastyigniter", + "typo3", "wordpress", - "zend" + "yawik", + "zend", + "zikula" ], - "time": "2013-08-20 04:37:09" - } - ], - "packages-dev": [ + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.12.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-09-13T08:19:44+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, { - "name": "symfony/console", - "version": "v2.7.1", + "name": "guzzlehttp/psr7", + "version": "2.8.0", "source": { "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "564398bc1f33faf92fc2ec86859983d30eb81806" + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/564398bc1f33faf92fc2ec86859983d30eb81806", - "reference": "564398bc1f33faf92fc2ec86859983d30eb81806", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", - "symfony/process": "~2.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -126,39 +617,213 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "time": "2015-06-10 15:30:22" - }, + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + } + ], + "packages-dev": [ { "name": "wpreadme2markdown/wpreadme2markdown", - "version": "2.0.0", + "version": "4.1.1", "source": { "type": "git", - "url": "https://github.com/benbalter/WP-Readme-to-Github-Markdown.git", - "reference": "dceae108111232949affc9107c98276c6fa6c98f" + "url": "https://github.com/wpreadme2markdown/wp-readme-to-markdown.git", + "reference": "abe788b2a15d13073e47100f8a5312b8175e78a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/benbalter/WP-Readme-to-Github-Markdown/zipball/dceae108111232949affc9107c98276c6fa6c98f", - "reference": "dceae108111232949affc9107c98276c6fa6c98f", + "url": "https://api.github.com/repos/wpreadme2markdown/wp-readme-to-markdown/zipball/abe788b2a15d13073e47100f8a5312b8175e78a7", + "reference": "abe788b2a15d13073e47100f8a5312b8175e78a7", "shasum": "" }, "require": { - "php": ">= 5.3.3", - "symfony/console": "~2.4" + "guzzlehttp/guzzle": "^7.3", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "*" }, - "bin": [ - "bin/wp2md" - ], "type": "library", "autoload": { "psr-4": { @@ -171,11 +836,11 @@ ], "authors": [ { - "name": "Christian Archer", - "email": "chrstnarchr@aol.com" + "name": "Benjamin J. Balter" }, { - "name": "Benjamin J. Balter" + "name": "Christian Archer", + "email": "sunchaser@sunchaser.info" } ], "description": "Convert WordPress Plugin readme.txt to Markdown", @@ -185,16 +850,21 @@ "readme", "wordpress" ], - "time": "2014-05-28 21:28:31" + "support": { + "issues": "https://github.com/wpreadme2markdown/wp-readme-to-markdown/issues", + "source": "https://github.com/wpreadme2markdown/wp-readme-to-markdown/tree/4.1.1" + }, + "time": "2024-12-16T19:44:24+00:00" } ], "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=5.3.2" }, - "platform-dev": [] + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/includes/class-cli.php b/includes/class-cli.php new file mode 100644 index 0000000..21cc8c4 --- /dev/null +++ b/includes/class-cli.php @@ -0,0 +1,101 @@ +... + * : One or more cache tags. + * + * ## EXAMPLES + * + * # Purge the 'post-1' cache tag from Cloudflare. + * $ wp cloudflare cache purge-tag post-1 + * Success: Purged tag. + * + * @subcommand purge-tag + */ + public function purge_tag( $args ) { + $ret = Cloudflare_Client::purgeByTags( $args ); + if ( ! $ret ) { + WP_CLI::error( 'Failed to purge tags.' ); + } else { + $message = count( $args ) > 1 ? 'Purged tags.' : 'Purged tag.'; + WP_CLI::success( $message ); + } + } + + /** + * Purge one or more paths from Cloudflare. + * + * ## OPTIONS + * + * ... + * : One or more paths. + * + * ## EXAMPLES + * + * # Purge the homepage from Cloudflare. + * $ wp cloudflare cache purge-path '/' + * Success: Purged path. + * + * @subcommand purge-path + */ + public function purge_path( $args ) { + $ret = Cloudflare_Client::purgeByUrls( $args ); + if ( ! $ret ) { + WP_CLI::error( 'Failed to purge paths.' ); + } else { + $message = count( $args ) > 1 ? 'Purged paths.' : 'Purged path.'; + WP_CLI::success( $message ); + } + } + + /** + * Purge the entire Cloudflare cache for the zone. + * + * WARNING! Purging the entire page cache can have a severe performance + * impact on a high-traffic site. We encourage you to explore other options + * first. + * + * ## OPTIONS + * + * [--yes] + * : Answer yes to the confirmation message. + * + * ## EXAMPLES + * + * # Purging the entire page cache will display a confirmation prompt. + * $ wp cloudflare cache purge-all + * Are you sure you want to purge the entire page cache? [y/n] y + * Success: Purged page cache. + * + * @subcommand purge-all + */ + public function purge_all( $_, $assoc_args ) { + WP_CLI::confirm( 'Are you sure you want to purge the entire page cache?', $assoc_args ); + $ret = Cloudflare_Client::purgeEverything(); + if ( ! $ret ) { + WP_CLI::error( 'Failed to purge all.' ); + } else { + WP_CLI::success( 'Purged page cache.' ); + } + } +} diff --git a/includes/class-cloudflare-client.php b/includes/class-cloudflare-client.php new file mode 100644 index 0000000..c4d9338 --- /dev/null +++ b/includes/class-cloudflare-client.php @@ -0,0 +1,286 @@ +get_cloudflare_settings(); + $token = isset( $options['api_token'] ) ? $options['api_token'] : ''; + $zone_id = isset( $options['zone_id'] ) ? $options['zone_id'] : ''; + + if ( empty( $token ) || empty( $zone_id ) ) { + error_log( 'Advanced Cloudflare Cache: API Token or Zone ID not configured.' ); + + return false; + } + + try { + $key = new APIToken( $token ); + $adapter = new Guzzle( $key ); + $zones = new Zones( $adapter ); + + $result = $zones->cachePurge( $zone_id, null, $tags, null ); + + if ( $result ) { + error_log( 'Advanced Cloudflare Cache: Successfully purged by tags: ' . implode( ', ', $tags ) ); + + return true; + } else { + error_log( 'Advanced Cloudflare Cache: Failed to purge by tags: ' . implode( ', ', $tags ) ); + + return false; + } + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when purging by tags: ' . $e->getMessage() ); + + return false; + } + } + + /** + * Purge the entire cache for the zone. + * + * @return bool True on success, false on failure. + */ + public static function purgeEverything() { + global $nginx_helper_admin; + + $options = $nginx_helper_admin->get_cloudflare_settings(); + $token = isset( $options['api_token'] ) ? $options['api_token'] : ''; + $zone_id = isset( $options['zone_id'] ) ? $options['zone_id'] : ''; + + if ( empty( $token ) || empty( $zone_id ) ) { + error_log( 'Advanced Cloudflare Cache: API Token or Zone ID not configured.' ); + + return false; + } + + try { + $key = new APIToken( $token ); + $adapter = new Guzzle( $key ); + $zones = new Zones( $adapter ); + + $result = $zones->cachePurgeEverything( $zone_id ); + + if ( $result ) { + error_log( 'Advanced Cloudflare Cache: Successfully purged everything.' ); + + return true; + } else { + error_log( 'Advanced Cloudflare Cache: Failed to purge everything.' ); + + return false; + } + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when purging everything: ' . $e->getMessage() ); + + return false; + } + } + + /** + * Purge the cache for a given set of URLs. + * + * @param array $urls The URLs to purge. + * + * @return bool True on success, false on failure. + */ + public static function purgeByUrls( array $urls ) { + if ( empty( $urls ) ) { + return false; + } + + global $nginx_helper_admin; + + $options = $nginx_helper_admin->get_cloudflare_settings(); + $token = isset( $options['api_token'] ) ? $options['api_token'] : ''; + $zone_id = isset( $options['zone_id'] ) ? $options['zone_id'] : ''; + + if ( empty( $token ) || empty( $zone_id ) ) { + error_log( 'Advanced Cloudflare Cache: API Token or Zone ID not configured.' ); + + return false; + } + + try { + $key = new APIToken( $token ); + $adapter = new Guzzle( $key ); + $zones = new Zones( $adapter ); + + $result = $zones->cachePurge( $zone_id, $urls, null, null ); + + if ( $result ) { + error_log( 'Advanced Cloudflare Cache: Successfully purged by URLs: ' . implode( ', ', $urls ) ); + + return true; + } else { + error_log( 'Advanced Cloudflare Cache: Failed to purge by URLs: ' . implode( ', ', $urls ) ); + + return false; + } + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when purging by URLs: ' . $e->getMessage() ); + + return false; + } + } + + /** + * Sets up the "Cache Rule" required to purge the edge cache. + * + * @return string 'created', 'exists', or 'failed'. + */ + public static function setupCacheRule() { + global $nginx_helper_admin; + + if ( ! $nginx_helper_admin ) { + return 'failed'; + } + + $options = $nginx_helper_admin->get_cloudflare_settings(); + $token = isset( $options['api_token'] ) ? sanitize_text_field( $options['api_token'] ) : ''; + $zone_id = isset( $options['zone_id'] ) ? sanitize_text_field( $options['zone_id'] ) : ''; + + if ( empty( $token ) || empty( $zone_id ) ) { + error_log( 'Advanced Cloudflare Cache: API Token or Zone ID not configured.' ); + return 'failed'; + } + + $key = new APIToken( $token ); + $adapter = new Guzzle( $key ); + + try { + $rulesets_response = $adapter->get( sprintf( 'zones/%s/rulesets', esc_attr( $zone_id ) ) ); + $raw_response = $rulesets_response->getBody() ?? ''; + $response_data = json_decode( $raw_response, true ); + + if ( ! is_array( $response_data ) || ! array_key_exists( 'result', $response_data ) ) { + error_log( 'Advanced Cloudflare Cache: Invalid response when fetching rulesets.' ); + return 'failed'; + } + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when fetching rulesets: ' . esc_html( $e->getMessage() ) ); + return 'failed'; + } + + $cache_ruleset_id = null; + foreach ( $response_data['result'] as $ruleset ) { + if ( 'http_request_cache_settings' === $ruleset['phase'] ) { + $cache_ruleset_id = sanitize_text_field( $ruleset['id'] ); + break; + } + } + + $site_url = esc_url( get_site_url() ); + $rule = [ + 'expression' => '(http.request.full_uri wildcard "' . $site_url . '/*" and not http.cookie contains "wordpress_logged" and not http.cookie contains "NO_CACHE" and not http.cookie contains "S+ESS" and not http.cookie contains "fbs" and not http.cookie contains "SimpleSAML" and not http.cookie contains "PHPSESSID" and not http.cookie contains "wordpress" and not http.cookie contains "wp-" and not http.cookie contains "comment_author_" and not http.cookie contains "duo_wordpress_auth_cookie" and not http.cookie contains "duo_secure_wordpress_auth_cookie" and not http.cookie contains "bp_completed_create_steps" and not http.cookie contains "bp_new_group_id" and not http.cookie contains "wp-resetpass-" and not http.cookie contains "woocommerce" and not http.cookie contains "amazon_Login_")', + 'action' => 'set_cache_settings', + 'action_parameters' => [ + 'cache' => true, + ], + 'description' => 'EasyEngine Cache Manager Ruleset', + ]; + + // If no cache rule exist then we can directly create a new. + if ( null === $cache_ruleset_id ) { + $ruleset = [ + 'name' => 'default', + 'kind' => 'zone', + 'phase' => 'http_request_cache_settings', + 'description' => 'Set\'s the edge cache rules by Nginx-Helper Cache Manager.', + 'rules' => [ $rule ], + ]; + + try { + $ruleset_resp = $adapter->post( sprintf( 'zones/%s/rulesets', esc_attr( $zone_id ) ), $ruleset ); + $raw_ruleset_body = $ruleset_resp->getBody(); + $ruleset_body = json_decode( $raw_ruleset_body ); + + if ( isset( $ruleset_body->success ) && true === $ruleset_body->success ) { + return 'created'; + } + + error_log( 'Advanced Cloudflare Cache: Failed to create cache rule. Response: ' . wp_json_encode( $ruleset_body ) ); + return 'failed'; + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when creating cache ruleset: ' . esc_html( $e->getMessage() ) ); + return 'failed'; + } + } + + // Get the existing rule for cache and then update it to add our new rule. + try { + $ruleset_resp = $adapter->get( sprintf( 'zones/%s/rulesets/%s', esc_attr( $zone_id ), esc_attr( $cache_ruleset_id ) ) ); + + if ( 200 !== $ruleset_resp->getStatusCode() ) { + error_log( 'Advanced Cloudflare Cache: Failed to fetch existing cache rule. Ruleset ID: ' . wp_json_encode( $cache_ruleset_id ) ); + return 'failed'; + } + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when fetching existing ruleset: ' . esc_html( $e->getMessage() ) ); + return 'failed'; + } + + $raw_ruleset_body = $ruleset_resp->getBody(); + $ruleset_body = json_decode( $raw_ruleset_body, true ); + + $existing_rules = is_array( $ruleset_body['result']['rules'] ) ? $ruleset_body['result']['rules'] : []; + + $rule_exists = false; + foreach ( $existing_rules as $existing_rule ) { + if ( isset( $existing_rule['description'] ) && 'EasyEngine Cache Manager Ruleset' === $existing_rule['description'] ) { + $rule_exists = true; + break; + } + } + + if ( $rule_exists ) { + return 'exists'; + } + + $existing_rules[] = $rule; + + try { + $ruleset_resp = $adapter->put( sprintf( 'zones/%s/rulesets/%s', esc_attr( $zone_id ), esc_attr( $cache_ruleset_id ) ), [ 'rules' => $existing_rules ] ); + $raw_ruleset_body = $ruleset_resp->getBody(); + $ruleset_body = json_decode( $raw_ruleset_body ); + + if ( isset( $ruleset_body->success ) && true === $ruleset_body->success ) { + return 'created'; + } + + error_log( 'Advanced Cloudflare Cache: Failed to update cache rule. Response: ' . wp_json_encode( $ruleset_body ) ); + return 'failed'; + } catch ( Exception $e ) { + error_log( 'Advanced Cloudflare Cache: Exception when updating cache ruleset: ' . esc_html( $e->getMessage() ) ); + return 'failed'; + } + } +} diff --git a/includes/class-cloudflare-purger.php b/includes/class-cloudflare-purger.php new file mode 100644 index 0000000..be7cccf --- /dev/null +++ b/includes/class-cloudflare-purger.php @@ -0,0 +1,436 @@ +post_status ) { + return; + } + self::purge_post_with_related( $post ); + } + + /** + * Purge cache tags associated with a post being published or unpublished. + * + * @param string $new_status New status for the post. + * @param string $old_status Old status for the post. + * @param WP_Post $post Post object. + */ + public function action_transition_post_status( $new_status, $old_status, $post ) { + if ( 'publish' !== $new_status && 'publish' !== $old_status ) { + return; + } + self::purge_post_with_related( $post ); + if ( 'publish' === $old_status ) { + return; + } + // Targets 404 pages that could be cached with no cache tags (i.e. + // a drafted post going live after the 404 has been cached). + self::clear_post_path( $post ); + } + + + /** + * Purge the cache for a given post's path + * + * @param WP_Post $post Post object. + * + * @since 1.0.0 + */ + public function clear_post_path( $post ) { + $post_path = get_permalink( $post->ID ); + $parsed_url = parse_url( $post_path ); + $path = $parsed_url['path']; + $paths = [ trailingslashit( $path ), untrailingslashit( $path ) ]; + + /** + * Paths possibly without cache tags purges + * + * @param array $paths paths to clear. + */ + $paths = apply_filters( 'ec_clear_post_path', $paths ); + Cloudflare_Client::purgeByUrls( $paths ); + } + + /** + * Purge cache tags associated with a post being deleted. + * + * @param integer $post_id ID for the post to be deleted. + */ + public function action_before_delete_post( $post_id ) { + $post = get_post( $post_id ); + self::purge_post_with_related( $post ); + } + + /** + * Purge cache tags associated with an attachment being deleted. + * + * @param integer $post_id ID for the modified attachment. + */ + public function action_delete_attachment( $post_id ) { + $post = get_post( $post_id ); + self::purge_post_with_related( $post ); + } + + /** + * Purge the post's cache tag when the post cache is cleared. + * + * @param integer $post_id ID for the modified post. + */ + public function action_clean_post_cache( $post_id ) { + $type = get_post_type( $post_id ); + + /** + * Allow specific post types to ignore the purge process. + * + * @param array $ignored_post_types Post types to ignore. + * + * @return array + * @since 1.0.0 + */ + $ignored_post_types = apply_filters( 'ec_purge_post_type_ignored', [ 'revision' ] ); + + if ( $type && in_array( $type, $ignored_post_types, true ) ) { + return; + } + + $keys = [ + 'post-' . $post_id, + 'rest-post-' . $post_id, + 'post-huge', + 'rest-post-huge', + ]; + + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when clearing post cache. + * + * @param array $keys cache tags. + * @param array $post_id ID for purged post. + */ + $keys = apply_filters( 'ec_purge_clean_post_cache', $keys, $post_id ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge cache tags associated with a term being created. + * + * @param integer $term_id ID for the created term. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy slug. + */ + public function action_created_term( $term_id, $tt_id, $taxonomy ) { + self::purge_term( $term_id ); + $keys = [ 'rest-' . $taxonomy . '-collection' ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when creating a new term. + * + * @param array $keys cache tags. + * @param array $term_id ID for new term. + * @param array $tt_id Term taxonomy ID for new term. + * @param string $taxonomy Taxonomy for the new term. + */ + $keys = apply_filters( 'ec_purge_create_term', $keys, $term_id, $tt_id, $taxonomy ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge cache tags associated with a term being edited. + * + * @param integer $term_id ID for the edited term. + */ + public function action_edited_term( $term_id ) { + self::purge_term( $term_id ); + } + + /** + * Purge cache tags associated with a term being deleted. + * + * @param integer $term_id ID for the deleted term. + */ + public function action_delete_term( $term_id ) { + self::purge_term( $term_id ); + } + + /** + * Purge the term's archive cache tag when the term is modified. + * + * @param integer $term_ids One or more IDs of modified terms. + */ + public function action_clean_term_cache( $term_ids ) { + $keys = []; + $term_ids = is_array( $term_ids ) ? $term_ids : [ $term_ids ]; + foreach ( $term_ids as $term_id ) { + $keys[] = 'term-' . $term_id; + $keys[] = 'rest-term-' . $term_id; + } + $keys[] = 'term-huge'; + $keys[] = 'rest-term-huge'; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when clearing term cache. + * + * @param array $keys cache tags. + * @param array $term_ids IDs for purged terms. + */ + $keys = apply_filters( 'ec_purge_clean_term_cache', $keys, $term_ids ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge cache tags when an approved comment is updated. + * + * @param integer $id The comment ID. + * @param WP_Comment $comment Comment object. + */ + public function action_wp_insert_comment( $id, $comment ) { + if ( 1 !== (int) $comment->comment_approved ) { + return; + } + $keys = [ + 'rest-comment-' . $comment->comment_ID, + 'rest-comment-collection', + 'rest-comment-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when inserting a new comment. + * + * @param array $keys cache tags. + * @param integer $id Comment ID. + * @param WP_Comment $comment Comment to be inserted. + */ + $keys = apply_filters( 'ec_purge_insert_comment', $keys, $id, $comment ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge cache tags when a comment is approved or unapproved. + * + * @param int|string $new_status The new comment status. + * @param int|string $old_status The old comment status. + * @param object $comment The comment data. + */ + public function action_transition_comment_status( $new_status, $old_status, $comment ) { + $keys = [ + 'rest-comment-' . $comment->comment_ID, + 'rest-comment-collection', + 'rest-comment-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when transitioning a comment status. + * + * @param array $keys cache tags. + * @param string $new_status New comment status. + * @param string $old_status Old comment status. + * @param WP_Comment $comment Comment being transitioned. + */ + $keys = apply_filters( 'ec_purge_transition_comment_status', $keys, $new_status, $old_status, $comment ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge the comment's cache tag when the comment is modified. + * + * @param integer $comment_id Modified comment id. + */ + public function action_clean_comment_cache( $comment_id ) { + $keys = [ + 'rest-comment-' . $comment_id, + 'rest-comment-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when cleaning comment cache. + * + * @param array $keys cache tags. + * @param integer $id Comment ID. + */ + $keys = apply_filters( 'ec_purge_clean_comment_cache', $keys, $comment_id ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge the cache tags associated with a post being modified. + * + * @param object $post Object representing the modified post. + */ + private function purge_post_with_related( $post ) { + /** + * Allow specific post types to ignore the purge process. + * + * @param array $ignored_post_types Post types to ignore. + * + * @return array + * @since 1.0.0 + */ + $ignored_post_types = apply_filters( 'ec_purge_post_type_ignored', [ 'revision' ] ); + + if ( in_array( $post->post_type, $ignored_post_types, true ) ) { + return; + } + + $keys = [ + 'post-' . $post->ID, + $post->post_type . '-archive', + 'rest-' . $post->post_type . '-collection', + 'home', + 'front', + '404', + 'feed', + 'post-huge', + ]; + + if ( post_type_supports( $post->post_type, 'author' ) ) { + $keys[] = 'user-' . $post->post_author; + $keys[] = 'user-huge'; + } + + if ( post_type_supports( $post->post_type, 'comments' ) ) { + $keys[] = 'rest-comment-post-' . $post->ID; + $keys[] = 'rest-comment-post-huge'; + } + + $taxonomies = wp_list_filter( + get_object_taxonomies( $post->post_type, 'objects' ), + [ 'public' => true ] + ); + + foreach ( $taxonomies as $taxonomy ) { + $terms = get_the_terms( $post, $taxonomy->name ); + if ( $terms ) { + foreach ( $terms as $term ) { + $keys[] = 'term-' . $term->term_id; + } + $keys[] = 'term-huge'; + } + } + + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * Related cache tags purged when purging a post. + * + * @param array $keys cache tags. + * @param WP_Post $post Post object. + */ + $keys = apply_filters( 'ec_purge_post_with_related', $keys, $post ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge the cache tags associated with a term being modified. + * + * @param integer $term_id ID for the modified term. + */ + private function purge_term( $term_id ) { + $keys = [ + 'term-' . $term_id, + 'rest-term-' . $term_id, + 'post-term-' . $term_id, + 'term-huge', + 'rest-term-huge', + 'post-term-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when purging a term. + * + * @param array $keys cache tags. + * @param integer $term_id Term ID. + */ + $keys = apply_filters( 'ec_purge_term', $keys, $term_id ); + Cloudflare_Client::purgeByTags( $keys ); + } + + + /** + * Purge a variety of cache tags when a user is modified. + * + * @param integer $user_id ID for the modified user. + */ + public function action_clean_user_cache( $user_id ) { + $keys = [ + 'user-' . $user_id, + 'rest-user-' . $user_id, + 'user-huge', + 'rest-user-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when clearing user cache. + * + * @param array $keys cache tags. + * @param array $user_id ID for purged user. + */ + $keys = apply_filters( 'ec_purge_clean_user_cache', $keys, $user_id ); + Cloudflare_Client::purgeByTags( $keys ); + } + + /** + * Purge a variety of cache tags when an option is modified. + * + * @param string $option Name of the updated option. + */ + public function action_updated_option( $option ) { + if ( ! function_exists( 'get_registered_settings' ) ) { + return; + } + $settings = get_registered_settings(); + if ( empty( $settings[ $option ] ) || empty( $settings[ $option ]['show_in_rest'] ) ) { + return; + } + $rest_name = ! empty( $settings[ $option ]['show_in_rest']['name'] ) ? $settings[ $option ]['show_in_rest']['name'] : $option; + $keys = [ + 'rest-setting-' . $rest_name, + 'rest-setting-huge', + ]; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + /** + * cache tags purged when updating an option cache. + * + * @param array $keys cache tags. + * @param string $option Option name. + */ + $keys = apply_filters( 'ec_purge_updated_option', $keys, $option ); + Cloudflare_Client::purgeByTags( $keys ); + } +} diff --git a/includes/class-cloudflare-tag-emitter.php b/includes/class-cloudflare-tag-emitter.php new file mode 100644 index 0000000..4022f20 --- /dev/null +++ b/includes/class-cloudflare-tag-emitter.php @@ -0,0 +1,462 @@ + true ], 'objects' ) as $post_type ) { + add_filter( "rest_prepare_{$post_type->name}", [ $this, 'filter_rest_prepare_post' ], 10, 3 ); + $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; + self::get_instance()->rest_api_collection_endpoints[ '/wp/v2/' . $base ] = $post_type->name; + } + foreach ( get_taxonomies( [ 'show_in_rest' => true ], 'objects' ) as $taxonomy ) { + add_filter( "rest_prepare_{$taxonomy->name}", [ $this, 'filter_rest_prepare_term' ], 10, 3 ); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + self::get_instance()->rest_api_collection_endpoints[ '/wp/v2/' . $base ] = $taxonomy->name; + } + add_filter( 'rest_prepare_comment', [ $this, 'filter_rest_prepare_comment' ], 10, 3 ); + self::get_instance()->rest_api_collection_endpoints['/wp/v2/comments'] = 'comment'; + add_filter( 'rest_prepare_user', [ $this, 'filter_rest_prepare_user' ], 10, 3 ); + add_filter( 'rest_pre_get_setting', [ $this, 'filter_rest_pre_get_setting' ], 10, 2 ); + self::get_instance()->rest_api_collection_endpoints['/wp/v2/users'] = 'user'; + } + + /** + * Reset cache tags before a REST API response is generated. + * + * @param mixed $result Response to replace the requested version with. + * @param WP_REST_Server $server Server instance. + * @param WP_REST_Request $request Request used to generate the response. + */ + public function filter_rest_pre_dispatch( $result, $server, $request ) { + if ( isset( self::get_instance()->rest_api_collection_endpoints[ $request->get_route() ] ) ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-' . self::get_instance()->rest_api_collection_endpoints[ $request->get_route() ] . '-collection'; + } + + return $result; + } + + /** + * Render cache tags after a REST API response is prepared + * + * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response. + * @param WP_REST_Server $server Server instance. + */ + public function filter_rest_post_dispatch( $result, $server ) { + $keys = self::get_rest_api_cache_tags(); + if ( ! empty( $keys ) && $result instanceof \WP_REST_Response ) { + $result->header( self::HEADER_KEY, implode( ' ', $keys ) ); + } + + return $result; + } + + /** + * Determine which posts are present in a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + public function filter_rest_prepare_post( $response, $post, $request ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-post-' . $post->ID; + + return $response; + } + + /** + * Determine which terms are present in a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $term Term object. + * @param WP_REST_Request $request Request object. + */ + public function filter_rest_prepare_term( $response, $term, $request ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-term-' . $term->term_id; + + return $response; + } + + /** + * Determine which comments are present in a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $comment The original comment object. + * @param WP_REST_Request $request Request used to generate the response. + */ + public function filter_rest_prepare_comment( $response, $comment, $request ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-comment-' . $comment->comment_ID; + self::get_instance()->rest_api_cache_tags[] = 'rest-comment-post-' . $comment->comment_post_ID; + + return $response; + } + + /** + * Determine which users are present in a REST API response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $user User object. + * @param WP_REST_Request $request Request object. + */ + public function filter_rest_prepare_user( $response, $user, $request ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-user-' . $user->ID; + + return $response; + } + + /** + * Determine which settings are present in a REST API request + * + * @param mixed $result Value to use for the requested setting. Can be a scalar + * matching the registered schema for the setting, or null to + * follow the default get_option() behavior. + * @param string $name Setting name (as shown in REST API responses). + */ + public function filter_rest_pre_get_setting( $result, $name ) { + self::get_instance()->rest_api_cache_tags[] = 'rest-setting-' . $name; + + return $result; + } + + /** + * Get the cache tags to be included in this view. + * + * cache tags are generated based on the main WP_Query. + * + * @return array + */ + public function get_main_query_cache_tags() { + global $wp_query; + + $keys = []; + if ( is_front_page() ) { + $keys[] = 'front'; + } + if ( is_home() ) { + $keys[] = 'home'; + } + if ( is_404() ) { + $keys[] = '404'; + } + if ( is_feed() ) { + $keys[] = 'feed'; + } + if ( is_date() ) { + $keys[] = 'date'; + } + if ( is_paged() ) { + $keys[] = 'paged'; + } + if ( is_search() ) { + $keys[] = 'search'; + if ( $wp_query->found_posts ) { + $keys[] = 'search-results'; + } else { + $keys[] = 'search-no-results'; + } + } + + if ( ! empty( $wp_query->posts ) ) { + foreach ( $wp_query->posts as $p ) { + $keys[] = 'post-' . $p->ID; + if ( $wp_query->is_singular() ) { + if ( post_type_supports( $p->post_type, 'author' ) ) { + $keys[] = 'post-user-' . $p->post_author; + } + + /** + * Filter ec_should_add_terms + * Gives the option to skip taxonomy terms for a given post + * + * @param $add_terms whether or not to create cache tags for a given post's taxonomy terms. + * @param $wp_query the full WP_Query object. + * + * @return bool + * usage: add_filter( 'ec_should_add_terms',"__return_false", 10, 2); + */ + $add_terms = apply_filters( 'ec_should_add_terms', true, $wp_query ); + if ( ! $add_terms ) { + continue; + } + + foreach ( get_object_taxonomies( $p ) as $tax ) { + $terms = get_the_terms( $p->ID, $tax ); + if ( $terms && ! is_wp_error( $terms ) ) { + foreach ( $terms as $t ) { + $keys[] = 'post-term-' . $t->term_id; + } + } + } + } + } + } + + if ( is_singular() ) { + $keys[] = 'single'; + if ( is_attachment() ) { + $keys[] = 'attachment'; + } + } elseif ( is_archive() ) { + $keys[] = 'archive'; + if ( is_post_type_archive() ) { + $keys[] = 'post-type-archive'; + $post_types = get_query_var( 'post_type' ); + // If multiple post types are queried, create a surrogate key for each. + if ( is_array( $post_types ) ) { + foreach ( $post_types as $post_type ) { + $keys[] = "$post_type-archive"; + } + } else { + $keys[] = "$post_types-archive"; + } + } elseif ( is_author() ) { + $user_id = get_queried_object_id(); + if ( $user_id ) { + $keys[] = 'user-' . $user_id; + } + } elseif ( is_category() || is_tag() || is_tax() ) { + $term_id = get_queried_object_id(); + if ( $term_id ) { + $keys[] = 'term-' . $term_id; + } + } + } + + // Don't emit cache tags in the admin, unless defined by the filter. + if ( is_admin() ) { + $keys = []; + } + + /** + * Customize cache tags sent in the header. + * + * @param array $keys Existing cache tags generated by the plugin. + */ + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + $keys = apply_filters( 'ec_main_query_cache_tags', $keys ); + $keys = array_unique( $keys ); + $keys = self::filter_huge_cache_tags_list( $keys ); + + return $keys; + } + + /** + * Get the cache tags to be included in this view. + * + * cache tags are generated based on filters added to REST API controllers. + * + * @return array + */ + public function get_rest_api_cache_tags() { + + /** + * Customize cache tags sent in the REST API header. + * + * @param array $keys Existing cache tags generated by the plugin. + */ + $keys = self::get_instance()->rest_api_cache_tags; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + $keys = apply_filters( 'ec_rest_api_cache_tags', $keys ); + $keys = array_unique( $keys ); + $keys = self::filter_huge_cache_tags_list( $keys ); + + return $keys; + } + + /** + * Reset cache tags stored on the instance. + */ + public function reset_rest_api_cache_tags() { + self::get_instance()->rest_api_cache_tags = []; + } + + /** + * Filter the cache tags to ensure that the length doesn't exceed what nginx can handle. + * + * @param array $keys Existing cache tags generated by the plugin. + * + * @return array + */ + public function filter_huge_cache_tags_list( $keys ) { + $output = implode( ' ', $keys ); + if ( strlen( $output ) <= self::HEADER_MAX_LENGTH ) { + return $keys; + } + + $keycats = []; + foreach ( $keys as $k ) { + $p = strrpos( $k, '-' ); + if ( false === $p ) { + $keycats[ $k ][] = $k; + continue; + } + $cat = substr( $k, 0, $p + 1 ); + $keycats[ $cat ][] = $k; + } + + // Sort by the output length of the key category. + uasort( + $keycats, + function ( $a, $b ) { + $ca = strlen( implode( ' ', $a ) ); + $cb = strlen( implode( ' ', $b ) ); + if ( $ca === $cb ) { + return 0; + } + + return $ca > $cb ? - 1 : 1; + } + ); + + $cats = array_keys( $keycats ); + foreach ( $cats as $c ) { + $keycats[ $c ] = [ $c . 'huge' ]; + $keyout = []; + foreach ( $keycats as $v ) { + $keyout = array_merge( $keyout, $v ); + } + $output = implode( ' ', $keyout ); + if ( strlen( $output ) <= self::HEADER_MAX_LENGTH ) { + return $keyout; + } + } + + return $keyout; + } + + /** + * Inspect the model and get the right cache tags. + * + * @param WPGraphQL\Model\Model|mixed $model Model object, array, etc. + */ + public function filter_graphql_dataloader_get_model( $model ) { + if ( ! $model instanceof \WPGraphQL\Model\Model ) { + return $model; + } + + $reflect = new \ReflectionClass( $model ); + $class_short_name = $reflect->getShortName(); + $cache_tag_prefix = strtolower( $class_short_name ); + if ( isset( $model->id ) ) { + if ( ! empty( $model->databaseId ) ) { + self::get_instance()->graphql_cache_tags[] = $cache_tag_prefix . '-' . $model->databaseId; + } + } + + return $model; + } + + /** + * Get the cache tags to be included in this view. + * + * cache tags are generated based on filters added to GraphQL controllers. + * + * @return array + */ + public function get_graphql_cache_tags() { + + /** + * Customize cache tags sent in the GraphQL header. + * + * @param array $keys Existing cache tags generated by the plugin. + */ + $keys = self::get_instance()->graphql_cache_tags; + $keys[] = 'graphql-collection'; + $keys = ec_cf_prefix_cache_tags_with_blog_id( $keys ); + $keys = apply_filters( 'ec_graphql_cache_tags', $keys ); + $keys = array_unique( $keys ); + $keys = self::filter_huge_cache_tags_list( $keys ); + + return $keys; + } + + /** + * Send additional headers to graphql response. + * + * @param array $headers Existing headers as set by graphql plugin. + */ + public function filter_graphql_response_headers_to_send( $headers ) { + $keys = self::get_graphql_cache_tags(); + if ( ! empty( $keys ) ) { + $headers[ self::HEADER_KEY ] = implode( ' ', $keys ); + } + + return $headers; + } +} diff --git a/includes/class-nginx-helper.php b/includes/class-nginx-helper.php index 7cf8cb0..085e7a2 100644 --- a/includes/class-nginx-helper.php +++ b/includes/class-nginx-helper.php @@ -12,6 +12,9 @@ * @subpackage nginx-helper/includes */ +use EasyCache\Cloudflare_Purger; +use EasyCache\CloudFlare_Tag_Emitter; + /** * The core plugin class. * @@ -237,7 +240,7 @@ private function define_admin_hooks() { // expose action to allow other plugins to purge the cache. $this->loader->add_action( 'rt_nginx_helper_purge_all', $nginx_purger, 'purge_all' ); - + // add action to preload the cache $this->loader->add_action( 'admin_init', $nginx_helper_admin, 'preload_cache' ); $this->loader->add_action( 'plugins_loaded', $this, 'handle_nginx_helper_upgrade' ); @@ -253,6 +256,38 @@ private function define_admin_hooks() { // WooCommerce integration. $this->loader->add_action( 'plugins_loaded', $nginx_helper_admin, 'init_woocommerce_hooks' ); + if ( $nginx_helper_admin->cf_options['is_enabled'] ) { + $this->loader->add_filter( 'wp_headers', $this, 'handle_cloudflare_headers', 999 ); + $this->loader->add_action( 'admin_bar_menu', $nginx_helper_admin, 'add_cloudflare_admin_bar_purge', 100 ); + $this->loader->add_action( 'wp_ajax_ec_clear_url_cache', $nginx_helper_admin, 'handle_cloudflare_clear_cache_ajax' ); + + // Add the cache tags. + $this->loader->add_filter( 'wp', CloudFlare_Tag_Emitter::get_instance(), 'action_wp' ); + $this->loader->add_action( 'rest_api_init', CloudFlare_Tag_Emitter::get_instance(), 'action_rest_api_init' ); + $this->loader->add_filter( 'rest_pre_dispatch', CloudFlare_Tag_Emitter::get_instance(), 'filter_rest_pre_dispatch', 10, 3 ); + $this->loader->add_filter( 'rest_post_dispatch', CloudFlare_Tag_Emitter::get_instance(), 'filter_rest_post_dispatch', 10, 2 ); + $this->loader->add_filter( 'graphql_dataloader_get_model', CloudFlare_Tag_Emitter::get_instance(), 'filter_graphql_dataloader_get_model' ); + $this->loader->add_filter( 'graphql_response_headers_to_send', CloudFlare_Tag_Emitter::get_instance(), 'filter_graphql_response_headers_to_send' ); + + /** + * Clears cache tags when various modification behaviors are performed. + */ + $this->loader->add_action( 'wp_insert_post', Cloudflare_Purger::get_instance(), 'action_wp_insert_post', 10, 2 ); + $this->loader->add_action( 'transition_post_status', Cloudflare_Purger::get_instance(), 'action_transition_post_status', 10, 3 ); + $this->loader->add_action( 'before_delete_post', Cloudflare_Purger::get_instance(), 'action_before_delete_post' ); + $this->loader->add_action( 'delete_attachment', Cloudflare_Purger::get_instance(), 'action_delete_attachment' ); + $this->loader->add_action( 'clean_post_cache', Cloudflare_Purger::get_instance(), 'action_clean_post_cache' ); + $this->loader->add_action( 'created_term', Cloudflare_Purger::get_instance(), 'action_created_term', 10, 3 ); + $this->loader->add_action( 'edited_term', Cloudflare_Purger::get_instance(), 'action_edited_term' ); + $this->loader->add_action( 'delete_term', Cloudflare_Purger::get_instance(), 'action_delete_term' ); + $this->loader->add_action( 'clean_term_cache', Cloudflare_Purger::get_instance(), 'action_clean_term_cache' ); + $this->loader->add_action( 'wp_insert_comment', Cloudflare_Purger::get_instance(), 'action_wp_insert_comment', 10, 2 ); + $this->loader->add_action( 'transition_comment_status', Cloudflare_Purger::get_instance(), 'action_transition_comment_status', 10, 3 ); + $this->loader->add_action( 'clean_comment_cache', Cloudflare_Purger::get_instance(), 'action_clean_comment_cache' ); + $this->loader->add_action( 'clean_user_cache', Cloudflare_Purger::get_instance(), 'action_clean_user_cache' ); + $this->loader->add_action( 'updated_option', Cloudflare_Purger::get_instance(), 'action_updated_option' ); + } + } /** @@ -351,12 +386,62 @@ public function handle_nginx_helper_upgrade() { $installed_version = get_option( 'nginx_helper_version', '0' ); if ( version_compare( $installed_version, $this->get_version(), '<' ) ) { - + require_once NGINX_HELPER_BASEPATH . 'includes/class-nginx-helper-activator.php'; Nginx_Helper_Activator::set_user_caps(); update_option( 'nginx_helper_version', $this->get_version() ); } + } + + /** + * Manage the cache headers for Cloudflare. + * + * @param array $headers The headers of the site. + * + * @return array The modified headers for cache. + */ + public function handle_cloudflare_headers( $headers ) { + + // Defensively remove any Cache-Control or Expires headers set by the server or other plugins. + // This ensures our plugin has the final say. + if ( isset( $headers['Cache-Control'] ) ) { + unset( $headers['Cache-Control'] ); + } + if ( isset( $headers['Expires'] ) ) { + unset( $headers['Expires'] ); + } + + // Conditions for NOT caching (logged-in, admin, search, etc.) + $do_not_cache = is_user_logged_in() || is_admin() || is_search() || is_404() || is_customize_preview(); + + // Also check for common dynamic cookies + if ( ! $do_not_cache && ! empty( $_COOKIE ) ) { + foreach ( array_keys( $_COOKIE ) as $cookie_key ) { + if ( strpos( $cookie_key, 'wordpress_logged_in' ) !== false || strpos( $cookie_key, 'woocommerce_items_in_cart' ) !== false ) { + $do_not_cache = true; + break; + } + } + } + + if ( $do_not_cache ) { + // User is logged in or page is dynamic. Send explicit NO CACHE headers. + $headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'; + $headers['Pragma'] = 'no-cache'; // For legacy HTTP/1.0 compatibility + $headers['Expires'] = 'Wed, 11 Jan 1984 05:00:00 GMT'; // Date in the past + } else { + // Page is for an anonymous user and is cacheable. + $options = get_option( 'easycache_cf_settings' ); + $ttl = isset( $options['default_cache_ttl'] ) ? (int) $options['default_cache_ttl'] : 0; + + if ( $ttl > 0 ) { + // Send CDN-friendly caching headers. + $headers['Cache-Control'] = 'public, max-age=0, s-maxage=' . $ttl; + } + } + + return $headers; } } diff --git a/nginx-helper.php b/nginx-helper.php index bb3ff52..d2e610b 100644 --- a/nginx-helper.php +++ b/nginx-helper.php @@ -21,6 +21,11 @@ die; } +// Load Composer dependencies. +if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) { + require_once __DIR__ . '/vendor/autoload.php'; +} + /** * Base URL of plugin */ @@ -42,6 +47,9 @@ define( 'NGINX_HELPER_BASEPATH', plugin_dir_path( __FILE__ ) ); } +require_once NGINX_HELPER_BASEPATH . '/utils/functions.php'; +require_once NGINX_HELPER_BASEPATH . '/utils/autoloader.php'; + /** * The code that runs during plugin activation. * This action is documented in includes/class-nginx-helper-activator.php @@ -90,7 +98,7 @@ function run_nginx_helper() { require_once NGINX_HELPER_BASEPATH . 'class-nginx-helper-wp-cli-command.php'; \WP_CLI::add_command( 'nginx-helper', 'Nginx_Helper_WP_CLI_Command' ); - + \WP_CLI::add_command( 'cloudflare cache', 'EasyCache\\CLI' ); } } diff --git a/utils/autoloader.php b/utils/autoloader.php new file mode 100644 index 0000000..94125dc --- /dev/null +++ b/utils/autoloader.php @@ -0,0 +1,29 @@ +