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 f9e13a9..ea8a2b7 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.
*
@@ -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.
*
@@ -74,11 +85,12 @@ 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();
}
/**
@@ -86,22 +98,26 @@ public function __construct( $plugin_name, $version ) {
* 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',
+ ),
+ )
);
}
@@ -299,28 +315,81 @@ public function nginx_helper_default_settings() {
);
}
-
- 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 );
- }
+
+ 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 );
+ }
/**
* Get settings.
@@ -365,7 +434,7 @@ 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;
@@ -821,6 +890,10 @@ public function purge_all() {
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();
@@ -1128,5 +1201,108 @@ public function purge_product_cache_on_update( $product_id ) {
$nginx_purger->purge_url( $product_url );
}
}
-
+
+ /**
+ * Handles the page 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();
+ if( $result ) {
+ 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 Everything" Page Rule was created successfully.', 'nginx-helper' );
+ break;
+ case 'exists':
+ $class .= ' notice-info';
+ $message = __( 'The "Cache Everything" Page Rule already exists. No action was taken.', 'nginx-helper' );
+ break;
+ default:
+ $class .= ' notice-error';
+ $message = __( 'Failed to create the Page Rule. Please check that your API Token has Page Rules Read/Write permissions.', 'nginx-helper' );
+ break;
+ }
+
+ printf( '
', 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..71f0255
--- /dev/null
+++ b/admin/partials/easycache-cloudflare-options.php
@@ -0,0 +1,146 @@
+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();
+
+}
+
+
+$ec_site_settings = $nginx_helper_admin->get_cloudflare_settings();
+?>
+
+
+
+
+
+
+
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..49ce45e
--- /dev/null
+++ b/includes/class-cloudflare-client.php
@@ -0,0 +1,248 @@
+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|false 'created', 'exists', or false on failure.
+ */
+ public static function setupCacheRule() {
+ global $nginx_helper_admin;
+
+ if ( ! $nginx_helper_admin ) {
+ return;
+ }
+
+ $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 );
+
+ $rulesets_response = $adapter->get( sprintf( "zones/%s/rulesets", $zone_id ) );
+ $raw_existing_response = $rulesets_response->getBody() ?? [];
+ $cache_ruleset_id = null;
+
+ $existing_rules = json_decode( $raw_existing_response, true );
+
+ if( ! array_key_exists( 'result', $existing_rules ) ) {
+ return false;
+ }
+
+ $existing_rules = $existing_rules['result'];
+
+ foreach ( $existing_rules as $ruleset ) {
+ if ( 'http_request_cache_settings' === $ruleset['phase'] &&
+ isset( $ruleset['name'] ) && 'nginx_helper_cache_ruleset' === $ruleset['name'] ) {
+ return 'exists';
+ }
+
+ if ( 'http_request_cache_settings' === $ruleset['phase'] && $cache_ruleset_id === null ) {
+ $cache_ruleset_id = $ruleset->id;
+ }
+ }
+
+ $site_url = get_site_url();
+
+ $ruleset = array(
+ 'kind' => 'zone',
+ 'name' => 'nginx_helper_cache_ruleset',
+ 'phase' => 'http_request_cache_settings',
+ 'description' => 'Set\'s the edge cache rules by Nginx Helper Plugin. ',
+ 'rules' => [
+ [
+ '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,
+ 'edge_ttl' => [
+ 'mode' => 'override_origin',
+ 'default' => 3600
+ ],
+ 'browser_ttl' => [
+ 'mode' => 'override_origin',
+ 'default' => 1800
+ ]
+ ]
+ ]
+ ]
+ );
+
+ if ( $cache_ruleset_id === null ) {
+ $ruleset_resp = $adapter->post( sprintf( 'zones/%s/rulesets', $zone_id ), $ruleset );
+ } else {
+ $ruleset_resp = $adapter->put( sprintf( 'zones/%s/rulesets/%s', $zone_id, $cache_ruleset_id ), array( 'rules' => $ruleset ) );
+ }
+
+ if ( isset( $ruleset_resp->success ) && $ruleset_resp->success ) {
+ return 'created';
+ } else {
+ error_log( 'Advanced Cloudflare Cache: Failed to create/update cache rule. Response: ' . json_encode( $ruleset_resp ) );
+ return false;
+ }
+
+ } catch ( Exception $e ) {
+ error_log( 'Advanced Cloudflare Cache: Exception when setting up Cache Rule: ' . $e->getMessage() );
+ return false;
+ }
+ }
+}
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..ed53678 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.
*
@@ -253,6 +256,39 @@ 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_action( 'admin_notices', $nginx_helper_admin, 'cf_page_rule_save_display_admin_notices' );
+ $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' );
+ }
+
}
/**
@@ -357,6 +393,56 @@ public function handle_nginx_helper_upgrade() {
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 @@
+