diff --git a/readme.txt b/readme.txt
index 8013c7c..3dae29b 100644
--- a/readme.txt
+++ b/readme.txt
@@ -24,6 +24,8 @@ Feel free to create a PR to [plugin Github repo](https://github.com/Dexerto/on-d
== Changelog ==
+= 1.2.6 =
+- feat: introduce cloudflare cache purge @MuhammedAO
= 1.2.5 =
- feat: prevent revalidate functions from running more than once within a single save_post request from @MuhammedAO
- fix: tags array populated by paths filter from @cavemon
diff --git a/src/Admin/Settings.php b/src/Admin/Settings.php
index 255b9f5..016e9e9 100644
--- a/src/Admin/Settings.php
+++ b/src/Admin/Settings.php
@@ -11,6 +11,7 @@
use OnDemandRevalidation\Admin\SettingsRegistry;
use OnDemandRevalidation\Revalidation;
+use OnDemandRevalidation\CloudflareCachePurge;
/**
* Class Settings
@@ -40,6 +41,11 @@ public function init() {
add_action( 'init', array( $this, 'register_settings' ) );
add_action( 'admin_init', array( $this, 'initialize_settings_page' ) );
+
+ if ( is_admin() ) {
+ CloudflareCachePurge::add_purge_cache_button_script();
+ }
+
if ( is_admin() ) {
Revalidation::test_revalidation_button();
}
@@ -54,8 +60,8 @@ public function init() {
public function add_options_page() {
add_options_page(
- __( 'Next.js On-Demand Revalidation', 'on-demand-revalidation' ),
- __( 'Next.js On-Demand Revalidation', 'on-demand-revalidation' ),
+ __( 'On-Demand Revalidation', 'on-demand-revalidation' ),
+ __( 'On-Demand Revalidation', 'on-demand-revalidation' ),
'manage_options',
'on-demand-revalidation',
array( $this, 'render_settings_page' )
@@ -138,6 +144,67 @@ public function register_settings() {
),
)
);
+
+ $this->settings_api->register_section(
+ 'on_demand_revalidation_cloudflare_settings',
+ array(
+ 'title' => __( 'Cloudflare Cache Purge', 'on-demand-revalidation' ),
+ 'desc' => __( 'Configure settings for Cloudflare cache purging via path or tag. These settings are optional and will only take effect if Cloudflare authentication is successfully verified.', 'on-demand-revalidation' ),
+ )
+ );
+
+ $this->settings_api->register_fields(
+ 'on_demand_revalidation_cloudflare_settings',
+ array(
+
+ array(
+ 'name' => 'cloudflare_cache_purge_enabled',
+ 'desc' => __( 'Enable cache purge on post update', 'on-demand-revalidation' ),
+ 'type' => 'checkbox',
+ ),
+
+ array(
+ 'name' => 'cloudflare_schedule_on_post_update',
+ 'desc' => __( 'Schedule purge on post update. (If unchecked, the purge will run immediately during the update, which might slow down the process.)', 'on-demand-revalidation' ),
+ 'type' => 'checkbox',
+ ),
+
+ array(
+ 'name' => 'cloudflare_zone_id',
+ 'label' => __( 'Zone ID', 'on-demand-revalidation' ),
+ 'type' => 'text',
+ 'desc' => '' . __( 'Click here for information on how to find your Zone ID and API token.', 'on-demand-revalidation' ) . '',
+ ),
+ array(
+ 'name' => 'cloudflare_api_token',
+ 'label' => __( 'API Token', 'on-demand-revalidation' ),
+ 'type' => 'password',
+ ),
+
+ array(
+ 'name' => 'cloudflare_cache_purge_paths',
+ 'label' => __( 'Paths to Purge', 'on-demand-revalidation' ),
+ 'desc' => __( 'Enter one path per line that you want to purge from Cloudflare cache.', 'on-demand-revalidation' ),
+ 'type' => 'textarea',
+ 'placeholder' => 'https://example.com/category/post',
+ ),
+
+ array(
+ 'name' => 'cloudflare_cache_purge_tags',
+ 'label' => __( 'Tags to Purge', 'on-demand-revalidation' ),
+ 'desc' => __( 'Enter one tag per line to purge related content from Cloudflare cache.', 'on-demand-revalidation' ),
+ 'type' => 'textarea',
+ 'placeholder' => 'category-page',
+ ),
+ array(
+ 'name' => 'test-config',
+ 'label' => __( 'Test Config', 'on-demand-revalidation' ),
+ 'desc' => 'Test Config',
+ 'type' => 'html',
+ ),
+
+ )
+ );
}
/**
@@ -162,7 +229,7 @@ public function render_settings_page() {
?>
Next.js On-Demand Revalidation';
+ echo '
On-Demand Revalidation
';
$this->settings_api->show_navigation();
$this->settings_api->show_forms();
?>
diff --git a/src/CloudflareCachePurge.php b/src/CloudflareCachePurge.php
new file mode 100644
index 0000000..9941dd5
--- /dev/null
+++ b/src/CloudflareCachePurge.php
@@ -0,0 +1,359 @@
+ 'Bearer ' . $api_token,
+ 'Content-Type' => 'application/json',
+ );
+
+ // Verify the token.
+ $api_token_verify_response = wp_remote_request(
+ $api_token_verify_url,
+ array(
+ 'method' => 'GET',
+ 'headers' => $headers,
+ 'timeout' => 10, //phpcs:ignore
+ )
+ );
+
+
+ if ( is_wp_error( $api_token_verify_response ) ) {
+ return false;
+ }
+
+ $api_token_verified_body = wp_remote_retrieve_body( $api_token_verify_response );
+ $api_token_verified_data = json_decode( $api_token_verified_body, true );
+
+ if ( ! isset( $api_token_verified_data['success'] ) || 1 !== (int) $api_token_verified_data['success'] ) {
+ return false;
+ }
+
+ // Validate Zone ID.
+ $zone_url = 'https://api.cloudflare.com/client/v4/zones?status=active';
+ $zone_response = wp_remote_request(
+ $zone_url,
+ array(
+ 'method' => 'GET',
+ 'headers' => $headers,
+ 'timeout' => 10, //phpcs:ignore
+ )
+ );
+
+ if ( is_wp_error( $zone_response ) ) {
+ return false;
+ }
+
+ $zone_body = wp_remote_retrieve_body( $zone_response );
+ $zone_data = json_decode( $zone_body, true );
+
+
+ if ( ! isset( $zone_data['success'] ) || 1 !== (int) $zone_data['success'] ) {
+ return false;
+ }
+
+
+ $zone_found = false;
+ foreach ( $zone_data['result'] as $zone ) {
+ if ( $zone['id'] === $zone_id ) {
+ $zone_found = true;
+ break;
+ }
+ }
+
+ if ( ! $zone_found ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Runs the purge process using the current settings and a provided post.
+ *
+ * @param \WP_Post $post The post object used for placeholder replacement.
+ * @return array The result from purge_cache().
+ */
+ public static function run_purge( $post ) {
+ $settings = get_option( 'on_demand_revalidation_cloudflare_settings' );
+
+
+ if ( ! isset( $settings['cloudflare_cache_purge_enabled'] ) || 'on' !== $settings['cloudflare_cache_purge_enabled'] ) {
+ return array(
+ 'success' => false,
+ 'message' => 'Cloudflare cache purge is disabled.',
+ );
+ }
+
+
+ $zone_id = $settings['cloudflare_zone_id'] ?? '';
+ $api_token = $settings['cloudflare_api_token'] ?? '';
+ $paths = isset( $settings['cloudflare_cache_purge_paths'] ) ? explode( "\n", trim( $settings['cloudflare_cache_purge_paths'] ) ) : array();
+ $tags = isset( $settings['cloudflare_cache_purge_tags'] ) ? explode( "\n", trim( $settings['cloudflare_cache_purge_tags'] ) ) : array();
+
+ // If no paths/tags are configured, return early.
+ if ( empty( $paths ) && empty( $tags ) ) {
+ return array(
+ 'success' => false,
+ 'message' => 'No paths or tags provided for cache purging.',
+ );
+ }
+
+ // Validate credentials.
+ if ( ! self::validate_token_zone_id( $zone_id, $api_token ) ) {
+ return array(
+ 'success' => false,
+ 'message' => 'Invalid Cloudflare API Token or Zone ID. Cache purge disabled.',
+ );
+ }
+
+ return self::purge_cache( $zone_id, $api_token, $paths, $tags, $post );
+ }
+
+ /**
+ * Purges the Cloudflare cache for the specified paths and tags, with dynamic placeholder replacement.
+ *
+ * @param string $zone_id The Cloudflare zone ID.
+ * @param string $api_token The Cloudflare API token.
+ * @param array $paths An array of URLs (paths) to purge (can contain placeholders).
+ * @param array $tags An array of tags to purge (can contain placeholders).
+ * @param \WP_Post $post The WordPress post object used to replace placeholders in paths and tags.
+ *
+ * @return array An array containing the success status and a message.
+ */
+ public static function purge_cache( $zone_id, $api_token, $paths = array(), $tags = array(), $post = null ) {
+ if ( $post instanceof \WP_Post ) {
+ $paths = Helpers::rewrite_placeholders( $paths, $post );
+ $tags = Helpers::rewrite_placeholders( $tags, $post );
+ }
+
+ // Ensure full URLs for paths if required by Cloudflare.
+ $site_url = get_site_url();
+ $paths = array_map(
+ function ( $path ) use ( $site_url ) {
+ return ( str_starts_with( $path, '/' ) ) ? trailingslashit( $site_url ) . ltrim( $path, '/' ) : $path;
+ },
+ $paths
+ );
+
+ $purge_data = array();
+ if ( ! empty( $paths ) ) {
+ $purge_data['files'] = array_values( array_unique( $paths ) );
+ }
+ if ( ! empty( $tags ) ) {
+ $purge_data['tags'] = array_values( array_unique( $tags ) );
+ }
+
+ $purge_url = "https://api.cloudflare.com/client/v4/zones/$zone_id/purge_cache";
+ $headers = array(
+ 'Authorization' => 'Bearer ' . $api_token,
+ 'Content-Type' => 'application/json',
+ );
+
+ $response = wp_remote_post(
+ $purge_url,
+ array(
+ 'method' => 'POST',
+ 'headers' => $headers,
+ 'body' => wp_json_encode( $purge_data ),
+ 'timeout' => 10, //phpcs:ignore
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return array(
+ 'success' => false,
+ 'message' => 'Cloudflare cache purge request failed: ' . $response->get_error_message(),
+ );
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+ $data = json_decode( $body, true );
+
+ if ( isset( $data['success'] ) && true === $data['success'] ) {
+ return array(
+ 'success' => true,
+ 'message' => 'Cloudflare cache purged successfully.',
+ );
+ } else {
+ $error_message = ! empty( $data['errors'] ) ? wp_json_encode( $data['errors'] ) : 'Unknown error occurred.';
+ return array(
+ 'success' => false,
+ 'message' => 'Cloudflare cache purge failed: ' . $error_message,
+ );
+ }
+ }
+
+
+ /**
+ * Automatically handle Cloudflare cache purging on post update.
+ *
+ * This function is hooked into the 'save_post' action.
+ *
+ * @param int $post_ID The post ID.
+ * @param \WP_Post $post The post object.
+ */
+ public static function handle_post_update( $post_ID, $post ) {
+ $excluded_statuses = array( 'auto-draft', 'inherit', 'draft', 'trash' );
+
+ if ( isset( $post->post_status ) && in_array( $post->post_status, $excluded_statuses, true ) ) {
+ return;
+ }
+
+ if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
+ return;
+ }
+
+ if ( false !== wp_is_post_revision( $post_ID ) ) {
+ return;
+ }
+
+ if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
+ return;
+ }
+ self::purge_post( $post );
+ }
+
+
+
+ /**
+ * Checks the "schedule on post update" setting and either purges immediately or schedules a cron event.
+ *
+ * @param \WP_Post $post The post object.
+ */
+ public static function purge_post( $post ) {
+ $settings = get_option( 'on_demand_revalidation_cloudflare_settings' );
+
+ if ( isset( $settings['cloudflare_schedule_on_post_update'] ) && 'on' === $settings['cloudflare_schedule_on_post_update'] ) {
+
+ wp_schedule_single_event( time(), 'on_demand_revalidation_cloudflare_on_post_update', array( $post->ID ) );
+ } else {
+
+ self::run_purge( $post );
+ }
+ }
+
+ /**
+ * Cron callback: Purges the cache for a post given its ID.
+ *
+ * @param int $post_ID The post ID.
+ */
+ public static function cron_purge( int $post_ID ) {
+ $post = get_post( $post_ID );
+ if ( $post ) {
+ self::run_purge( $post );
+ }
+ }
+
+
+
+
+ /**
+ * Handles the transition of post status.
+ *
+ * @param string $new_status The new status of the post.
+ * @param string $old_status The old status of the post.
+ * @param object $post The post object.
+ */
+ public static function handle_transition_post_status( $new_status, $old_status, $post ) {
+ if ( ( ( 'draft' !== $old_status && 'trash' !== $old_status ) && 'trash' === $new_status ) ||
+ ( 'publish' === $old_status && 'draft' === $new_status ) ) {
+
+ self::purge_post( $post );
+ }
+ }
+
+
+ /**
+ * Adds the jQuery script to handle the "Test Config" button click on the admin page.
+ *
+ * @return void
+ */
+ public static function add_purge_cache_button_script() {
+ add_action(
+ 'admin_footer',
+ function () { ?>
+
+ __( 'You do not have permission to manage options.', 'on-demand-revalidation' ) ) );
+ wp_die();
+ }
+ $posts = get_posts( //phpcs:ignore --suppress_filters already set to false
+ array(
+ 'numberposts' => 1,
+ 'post_status' => 'publish',
+ 'suppress_filters' => false,
+ )
+ );
+ $post = ! empty( $posts ) ? $posts[0] : null;
+
+ $response = self::run_purge( $post );
+ wp_send_json( $response );
+ wp_die();
+ }
+ );
+ }
+}