I’ll build this as a WordPress theme integration file, not a plugin, with safe guards so it can live inside inc/integrations/cloudflare.php. I’ll include API helpers, cache purge tools, Turnstile support, header/security helpers, development mode, APO/cache settings, and admin hooks where possible.
Below is a large advanced inc/integrations/cloudflare.php file for your rx-theme. It includes Cloudflare API v4 helpers, cache purge, automatic purge on WordPress content changes, development mode, zone setting updates, APO/cache toggle helpers, Turnstile frontend/server validation, real visitor IP handling, security/performance headers, admin bar purge button, REST endpoint, WP-CLI support, and theme-friendly hooks.
Cloudflare’s current API uses the /client/v4/zones/{zone_id}/purge_cache endpoint for cache purging, and Bearer API Token authorization is the preferred method. Turnstile must be validated server-side with Siteverify; the client widget alone is not enough.
Create this file:
<?php
/**
* RX Theme - Cloudflare Integration
*
* File: inc/integrations/cloudflare.php
*
* Advanced Cloudflare integration for WordPress themes.
*
* Features:
* - Cloudflare API v4 request wrapper
* - Purge full cache
* - Purge single URL
* - Purge multiple URLs
* - Purge by host
* - Purge by cache tags
* - Purge by prefixes
* - Auto purge on post/page/product updates
* - Auto purge related URLs: home, archives, categories, tags, author, REST, feeds
* - Development Mode on/off
* - Read/update zone settings
* - Optional APO toggle helper
* - Optional cache level/browser cache/rocket loader/minify/brotli/early hints helpers
* - Cloudflare Turnstile integration
* - Login/register/comment form Turnstile protection
* - Server-side Turnstile validation
* - Restore real visitor IP from Cloudflare headers
* - Add security/performance headers
* - Admin bar purge button
* - Dashboard tools page
* - REST endpoint for purge
* - WP-CLI commands
*
* IMPORTANT:
* Store secrets in wp-config.php, not directly in theme files.
*
* Recommended wp-config.php constants:
*
* define( 'RX_CF_API_TOKEN', 'your_cloudflare_api_token_here' );
* define( 'RX_CF_ZONE_ID', 'your_cloudflare_zone_id_here' );
* define( 'RX_CF_ACCOUNT_ID', 'your_cloudflare_account_id_here' ); // optional
* define( 'RX_CF_TURNSTILE_SITE_KEY', 'your_turnstile_site_key_here' );
* define( 'RX_CF_TURNSTILE_SECRET_KEY', 'your_turnstile_secret_key_here' );
*
* API token permission idea:
* - Zone:Cache Purge:Edit
* - Zone:Zone Settings:Edit
* - Zone:Zone:Read
*
* @package RX_Theme
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'RX_Theme_Cloudflare' ) ) :
final class RX_Theme_Cloudflare {
/**
* Cloudflare API base.
*/
const API_BASE = 'https://api.cloudflare.com/client/v4';
/**
* Cloudflare Turnstile verify endpoint.
*/
const TURNSTILE_VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
/**
* Option keys.
*/
const OPTION_GROUP = 'rx_cloudflare_options';
const OPTION_KEY = 'rx_cloudflare_settings';
const NONCE_ACTION = 'rx_cloudflare_action';
const NONCE_NAME = 'rx_cloudflare_nonce';
/**
* Singleton instance.
*
* @var RX_Theme_Cloudflare|null
*/
private static $instance = null;
/**
* Settings cache.
*
* @var array|null
*/
private $settings = null;
/**
* Runtime purge queue.
*
* @var array
*/
private $purge_queue = array();
/**
* Prevent duplicate auto purges.
*
* @var array
*/
private $purged_posts = array();
/**
* Get singleton.
*
* @return RX_Theme_Cloudflare
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
$this->hooks();
}
/**
* Register hooks.
*
* @return void
*/
private function hooks() {
add_action( 'init', array( $this, 'maybe_restore_real_ip' ), 0 );
add_action( 'send_headers', array( $this, 'send_cloudflare_friendly_headers' ), 20 );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_menu', array( $this, 'register_admin_page' ) );
add_action( 'admin_post_rx_cf_purge_everything', array( $this, 'handle_admin_purge_everything' ) );
add_action( 'admin_post_rx_cf_purge_url', array( $this, 'handle_admin_purge_url' ) );
add_action( 'admin_post_rx_cf_dev_mode_on', array( $this, 'handle_admin_dev_mode_on' ) );
add_action( 'admin_post_rx_cf_dev_mode_off', array( $this, 'handle_admin_dev_mode_off' ) );
add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 );
add_action( 'save_post', array( $this, 'auto_purge_post' ), 30, 3 );
add_action( 'deleted_post', array( $this, 'auto_purge_deleted_post' ), 30, 2 );
add_action( 'trashed_post', array( $this, 'auto_purge_trashed_post' ), 30 );
add_action( 'untrashed_post', array( $this, 'auto_purge_simple_post_id' ), 30 );
add_action( 'transition_post_status', array( $this, 'auto_purge_status_change' ), 30, 3 );
add_action( 'comment_post', array( $this, 'auto_purge_comment_post' ), 30, 3 );
add_action( 'edit_comment', array( $this, 'auto_purge_comment_by_id' ), 30 );
add_action( 'deleted_comment', array( $this, 'auto_purge_comment_by_id' ), 30 );
add_action( 'switch_theme', array( $this, 'purge_everything_silent' ) );
add_action( 'customize_save_after', array( $this, 'purge_everything_silent' ) );
add_action( 'rx_cloudflare_purge_queue', array( $this, 'process_purge_queue' ) );
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_turnstile_script' ) );
add_action( 'login_enqueue_scripts', array( $this, 'enqueue_turnstile_script' ) );
add_action( 'login_form', array( $this, 'render_turnstile_login' ) );
add_action( 'register_form', array( $this, 'render_turnstile_register' ) );
add_action( 'lostpassword_form', array( $this, 'render_turnstile_lostpassword' ) );
add_action( 'comment_form_after_fields', array( $this, 'render_turnstile_comment' ) );
add_action( 'comment_form_logged_in_after', array( $this, 'render_turnstile_comment' ) );
add_filter( 'authenticate', array( $this, 'validate_turnstile_login' ), 30, 3 );
add_filter( 'registration_errors', array( $this, 'validate_turnstile_registration' ), 30, 3 );
add_action( 'lostpassword_post', array( $this, 'validate_turnstile_lostpassword' ), 30 );
add_filter( 'preprocess_comment', array( $this, 'validate_turnstile_comment' ), 30 );
$this->register_wp_cli();
}
/**
* Default settings.
*
* @return array
*/
public function defaults() {
return array(
'enabled' => true,
'api_token' => '',
'zone_id' => '',
'account_id' => '',
'auto_purge' => true,
'auto_purge_everything' => false,
'auto_purge_urls' => true,
'purge_home' => true,
'purge_archives' => true,
'purge_feeds' => true,
'purge_rest' => true,
'purge_sitemap' => true,
'purge_author' => true,
'purge_terms' => true,
'purge_delay_seconds' => 5,
'headers_enabled' => true,
'real_ip_enabled' => true,
'turnstile_enabled' => false,
'turnstile_site_key' => '',
'turnstile_secret_key' => '',
'turnstile_login' => true,
'turnstile_register' => true,
'turnstile_lostpassword' => true,
'turnstile_comment' => true,
'turnstile_theme' => 'auto',
'turnstile_size' => 'normal',
'turnstile_failure_message' => __( 'Cloudflare Turnstile verification failed. Please try again.', 'rx-theme' ),
'admin_bar_enabled' => true,
'debug_log' => false,
'cache_tag_prefix' => 'rx',
'page_cache_header' => true,
'browser_cache_ttl_default' => 14400,
);
}
/**
* Get settings.
*
* Constants override database options.
*
* @return array
*/
public function settings() {
if ( null !== $this->settings ) {
return $this->settings;
}
$saved = get_option( self::OPTION_KEY, array() );
if ( ! is_array( $saved ) ) {
$saved = array();
}
$settings = wp_parse_args( $saved, $this->defaults() );
if ( defined( 'RX_CF_API_TOKEN' ) && RX_CF_API_TOKEN ) {
$settings['api_token'] = RX_CF_API_TOKEN;
}
if ( defined( 'RX_CF_ZONE_ID' ) && RX_CF_ZONE_ID ) {
$settings['zone_id'] = RX_CF_ZONE_ID;
}
if ( defined( 'RX_CF_ACCOUNT_ID' ) && RX_CF_ACCOUNT_ID ) {
$settings['account_id'] = RX_CF_ACCOUNT_ID;
}
if ( defined( 'RX_CF_TURNSTILE_SITE_KEY' ) && RX_CF_TURNSTILE_SITE_KEY ) {
$settings['turnstile_site_key'] = RX_CF_TURNSTILE_SITE_KEY;
}
if ( defined( 'RX_CF_TURNSTILE_SECRET_KEY' ) && RX_CF_TURNSTILE_SECRET_KEY ) {
$settings['turnstile_secret_key'] = RX_CF_TURNSTILE_SECRET_KEY;
}
$this->settings = apply_filters( 'rx_cloudflare_settings', $settings );
return $this->settings;
}
/**
* Get one setting.
*
* @param string $key Setting key.
* @param mixed $default Default value.
* @return mixed
*/
public function get_setting( $key, $default = null ) {
$settings = $this->settings();
return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default;
}
/**
* Is integration enabled?
*
* @return bool
*/
public function is_enabled() {
return (bool) $this->get_setting( 'enabled', true );
}
/**
* Has API credentials?
*
* @return bool
*/
public function has_api_credentials() {
return $this->is_enabled() && $this->get_api_token() && $this->get_zone_id();
}
/**
* API token.
*
* @return string
*/
public function get_api_token() {
return trim( (string) $this->get_setting( 'api_token', '' ) );
}
/**
* Zone ID.
*
* @return string
*/
public function get_zone_id() {
return trim( (string) $this->get_setting( 'zone_id', '' ) );
}
/**
* Account ID.
*
* @return string
*/
public function get_account_id() {
return trim( (string) $this->get_setting( 'account_id', '' ) );
}
/**
* Debug log.
*
* @param mixed $message Message.
* @param string $context Context.
* @return void
*/
public function log( $message, $context = 'cloudflare' ) {
if ( ! $this->get_setting( 'debug_log', false ) ) {
return;
}
if ( is_array( $message ) || is_object( $message ) ) {
$message = wp_json_encode( $message );
}
error_log( '[RX Theme][' . $context . '] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
/**
* Register settings.
*
* @return void
*/
public function register_settings() {
register_setting(
self::OPTION_GROUP,
self::OPTION_KEY,
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_settings' ),
'default' => $this->defaults(),
)
);
}
/**
* Sanitize settings.
*
* @param array $input Raw input.
* @return array
*/
public function sanitize_settings( $input ) {
$defaults = $this->defaults();
$output = array();
$bool_keys = array(
'enabled',
'auto_purge',
'auto_purge_everything',
'auto_purge_urls',
'purge_home',
'purge_archives',
'purge_feeds',
'purge_rest',
'purge_sitemap',
'purge_author',
'purge_terms',
'headers_enabled',
'real_ip_enabled',
'turnstile_enabled',
'turnstile_login',
'turnstile_register',
'turnstile_lostpassword',
'turnstile_comment',
'admin_bar_enabled',
'debug_log',
'page_cache_header',
);
foreach ( $defaults as $key => $default ) {
if ( in_array( $key, $bool_keys, true ) ) {
$output[ $key ] = ! empty( $input[ $key ] );
continue;
}
if ( ! isset( $input[ $key ] ) ) {
$output[ $key ] = $default;
continue;
}
switch ( $key ) {
case 'api_token':
case 'zone_id':
case 'account_id':
case 'turnstile_site_key':
case 'turnstile_secret_key':
case 'cache_tag_prefix':
$output[ $key ] = sanitize_text_field( wp_unslash( $input[ $key ] ) );
break;
case 'turnstile_theme':
$allowed = array( 'auto', 'light', 'dark' );
$value = sanitize_key( wp_unslash( $input[ $key ] ) );
$output[ $key ] = in_array( $value, $allowed, true ) ? $value : 'auto';
break;
case 'turnstile_size':
$allowed = array( 'normal', 'compact', 'flexible' );
$value = sanitize_key( wp_unslash( $input[ $key ] ) );
$output[ $key ] = in_array( $value, $allowed, true ) ? $value : 'normal';
break;
case 'turnstile_failure_message':
$output[ $key ] = sanitize_text_field( wp_unslash( $input[ $key ] ) );
break;
case 'purge_delay_seconds':
case 'browser_cache_ttl_default':
$output[ $key ] = absint( $input[ $key ] );
break;
default:
$output[ $key ] = is_scalar( $input[ $key ] ) ? sanitize_text_field( wp_unslash( $input[ $key ] ) ) : $default;
}
}
$this->settings = null;
return $output;
}
/**
* Cloudflare API request.
*
* @param string $method HTTP method.
* @param string $path API path.
* @param array $body Request body.
* @param array $query Query args.
* @return array|WP_Error
*/
public function api_request( $method, $path, $body = array(), $query = array() ) {
if ( ! $this->has_api_credentials() ) {
return new WP_Error( 'rx_cf_missing_credentials', __( 'Cloudflare API token or zone ID is missing.', 'rx-theme' ) );
}
$path = '/' . ltrim( $path, '/' );
$url = self::API_BASE . $path;
if ( ! empty( $query ) ) {
$url = add_query_arg( $query, $url );
}
$args = array(
'method' => strtoupper( $method ),
'timeout' => 25,
'redirection' => 3,
'headers' => array(
'Authorization' => 'Bearer ' . $this->get_api_token(),
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
);
if ( ! empty( $body ) && in_array( strtoupper( $method ), array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) {
$args['body'] = wp_json_encode( $body );
}
$response = wp_remote_request( esc_url_raw( $url ), $args );
if ( is_wp_error( $response ) ) {
$this->log( $response->get_error_message(), 'api_error' );
return $response;
}
$code = (int) wp_remote_retrieve_response_code( $response );
$raw = wp_remote_retrieve_body( $response );
$json = json_decode( $raw, true );
if ( null === $json ) {
return new WP_Error(
'rx_cf_invalid_json',
__( 'Cloudflare API returned invalid JSON.', 'rx-theme' ),
array(
'status' => $code,
'body' => $raw,
)
);
}
if ( $code < 200 || $code >= 300 || empty( $json['success'] ) ) {
$this->log( $json, 'api_failed' );
return new WP_Error(
'rx_cf_api_failed',
$this->format_api_error_message( $json ),
array(
'status' => $code,
'response' => $json,
)
);
}
return $json;
}
/**
* Format API errors.
*
* @param array $json API JSON.
* @return string
*/
private function format_api_error_message( $json ) {
if ( ! empty( $json['errors'] ) && is_array( $json['errors'] ) ) {
$messages = array();
foreach ( $json['errors'] as $error ) {
if ( ! empty( $error['message'] ) ) {
$messages[] = $error['message'];
}
}
if ( ! empty( $messages ) ) {
return implode( ' | ', $messages );
}
}
return __( 'Cloudflare API request failed.', 'rx-theme' );
}
/**
* Zone API path.
*
* @param string $suffix Suffix.
* @return string
*/
private function zone_path( $suffix = '' ) {
return '/zones/' . rawurlencode( $this->get_zone_id() ) . '/' . ltrim( $suffix, '/' );
}
/**
* Get zone details.
*
* @return array|WP_Error
*/
public function get_zone() {
return $this->api_request( 'GET', $this->zone_path() );
}
/**
* Verify API token.
*
* @return array|WP_Error
*/
public function verify_token() {
return $this->api_request( 'GET', '/user/tokens/verify' );
}
/**
* Purge everything.
*
* @return array|WP_Error
*/
public function purge_everything() {
$result = $this->api_request(
'POST',
$this->zone_path( 'purge_cache' ),
array(
'purge_everything' => true,
)
);
do_action( 'rx_cloudflare_after_purge_everything', $result );
return $result;
}
/**
* Silent purge everything.
*
* @return void
*/
public function purge_everything_silent() {
if ( ! $this->has_api_credentials() ) {
return;
}
$this->purge_everything();
}
/**
* Purge one URL.
*
* @param string $url URL.
* @return array|WP_Error
*/
public function purge_url( $url ) {
return $this->purge_urls( array( $url ) );
}
/**
* Purge URLs.
*
* Cloudflare commonly supports up to 30 files per request for many plans.
* This method chunks safely.
*
* @param array $urls URLs.
* @return array|WP_Error
*/
public function purge_urls( $urls ) {
$urls = $this->sanitize_urls( $urls );
if ( empty( $urls ) ) {
return new WP_Error( 'rx_cf_no_urls', __( 'No valid URLs were provided for purge.', 'rx-theme' ) );
}
$chunks = array_chunk( $urls, 30 );
$results = array();
foreach ( $chunks as $chunk ) {
$result = $this->api_request(
'POST',
$this->zone_path( 'purge_cache' ),
array(
'files' => array_values( $chunk ),
)
);
if ( is_wp_error( $result ) ) {
return $result;
}
$results[] = $result;
}
do_action( 'rx_cloudflare_after_purge_urls', $urls, $results );
return array(
'success' => true,
'result' => $results,
'urls' => $urls,
);
}
/**
* Purge hosts.
*
* @param array $hosts Hosts.
* @return array|WP_Error
*/
public function purge_hosts( $hosts ) {
$hosts = array_filter( array_map( 'sanitize_text_field', (array) $hosts ) );
if ( empty( $hosts ) ) {
return new WP_Error( 'rx_cf_no_hosts', __( 'No valid hosts were provided.', 'rx-theme' ) );
}
return $this->api_request(
'POST',
$this->zone_path( 'purge_cache' ),
array(
'hosts' => array_values( array_unique( $hosts ) ),
)
);
}
/**
* Purge cache tags.
*
* @param array $tags Cache tags.
* @return array|WP_Error
*/
public function purge_tags( $tags ) {
$tags = array_filter( array_map( 'sanitize_text_field', (array) $tags ) );
if ( empty( $tags ) ) {
return new WP_Error( 'rx_cf_no_tags', __( 'No valid cache tags were provided.', 'rx-theme' ) );
}
return $this->api_request(
'POST',
$this->zone_path( 'purge_cache' ),
array(
'tags' => array_values( array_unique( $tags ) ),
)
);
}
/**
* Purge prefixes.
*
* @param array $prefixes Prefixes.
* @return array|WP_Error
*/
public function purge_prefixes( $prefixes ) {
$prefixes = $this->sanitize_urls( $prefixes );
if ( empty( $prefixes ) ) {
return new WP_Error( 'rx_cf_no_prefixes', __( 'No valid prefixes were provided.', 'rx-theme' ) );
}
return $this->api_request(
'POST',
$this->zone_path( 'purge_cache' ),
array(
'prefixes' => array_values( array_unique( $prefixes ) ),
)
);
}
/**
* Sanitize URL list.
*
* @param array $urls URLs.
* @return array
*/
private function sanitize_urls( $urls ) {
$clean = array();
foreach ( (array) $urls as $url ) {
$url = trim( (string) $url );
if ( empty( $url ) ) {
continue;
}
$url = esc_url_raw( $url );
if ( ! wp_http_validate_url( $url ) ) {
continue;
}
$clean[] = $url;
}
return array_values( array_unique( $clean ) );
}
/**
* Get zone setting.
*
* @param string $setting Setting ID.
* @return array|WP_Error
*/
public function get_zone_setting( $setting ) {
$setting = sanitize_key( $setting );
return $this->api_request( 'GET', $this->zone_path( 'settings/' . $setting ) );
}
/**
* Update zone setting.
*
* @param string $setting Setting ID.
* @param mixed $value Setting value.
* @return array|WP_Error
*/
public function update_zone_setting( $setting, $value ) {
$setting = sanitize_key( $setting );
return $this->api_request(
'PATCH',
$this->zone_path( 'settings/' . $setting ),
array(
'value' => $value,
)
);
}
/**
* Enable development mode.
*
* Development mode bypasses Cloudflare cache temporarily and is useful while editing assets.
*
* @return array|WP_Error
*/
public function development_mode_on() {
return $this->update_zone_setting( 'development_mode', 'on' );
}
/**
* Disable development mode.
*
* @return array|WP_Error
*/
public function development_mode_off() {
return $this->update_zone_setting( 'development_mode', 'off' );
}
/**
* Set cache level.
*
* Common values: basic, simplified, aggressive, cache_everything.
*
* @param string $level Cache level.
* @return array|WP_Error
*/
public function set_cache_level( $level = 'aggressive' ) {
$allowed = array( 'basic', 'simplified', 'aggressive', 'cache_everything' );
$level = in_array( $level, $allowed, true ) ? $level : 'aggressive';
return $this->update_zone_setting( 'cache_level', $level );
}
/**
* Set browser cache TTL.
*
* @param int $seconds TTL seconds.
* @return array|WP_Error
*/
public function set_browser_cache_ttl( $seconds = 14400 ) {
$seconds = max( 0, absint( $seconds ) );
return $this->update_zone_setting( 'browser_cache_ttl', $seconds );
}
/**
* Toggle Brotli.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_brotli( $enable = true ) {
return $this->update_zone_setting( 'brotli', $enable ? 'on' : 'off' );
}
/**
* Toggle Rocket Loader.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_rocket_loader( $enable = false ) {
return $this->update_zone_setting( 'rocket_loader', $enable ? 'on' : 'off' );
}
/**
* Toggle Always Online.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_always_online( $enable = true ) {
return $this->update_zone_setting( 'always_online', $enable ? 'on' : 'off' );
}
/**
* Toggle IPv6.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_ipv6( $enable = true ) {
return $this->update_zone_setting( 'ipv6', $enable ? 'on' : 'off' );
}
/**
* Toggle HTTP/2.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_http2( $enable = true ) {
return $this->update_zone_setting( 'http2', $enable ? 'on' : 'off' );
}
/**
* Toggle HTTP/3.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_http3( $enable = true ) {
return $this->update_zone_setting( 'http3', $enable ? 'on' : 'off' );
}
/**
* Toggle 0-RTT.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_zero_rtt( $enable = true ) {
return $this->update_zone_setting( '0rtt', $enable ? 'on' : 'off' );
}
/**
* Toggle Early Hints.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_early_hints( $enable = true ) {
return $this->update_zone_setting( 'early_hints', $enable ? 'on' : 'off' );
}
/**
* Toggle Mirage.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_mirage( $enable = true ) {
return $this->update_zone_setting( 'mirage', $enable ? 'on' : 'off' );
}
/**
* Toggle Polish.
*
* @param string $mode off, lossy, lossless.
* @return array|WP_Error
*/
public function set_polish( $mode = 'lossless' ) {
$allowed = array( 'off', 'lossy', 'lossless' );
$mode = in_array( $mode, $allowed, true ) ? $mode : 'lossless';
return $this->update_zone_setting( 'polish', $mode );
}
/**
* Set minify.
*
* @param bool $html HTML.
* @param bool $css CSS.
* @param bool $js JS.
* @return array|WP_Error
*/
public function set_minify( $html = true, $css = true, $js = true ) {
return $this->update_zone_setting(
'minify',
array(
'html' => $html ? 'on' : 'off',
'css' => $css ? 'on' : 'off',
'js' => $js ? 'on' : 'off',
)
);
}
/**
* Toggle APO.
*
* Some accounts/plans may not have APO available.
*
* @param bool $enable Enable.
* @return array|WP_Error
*/
public function set_apo( $enable = true ) {
return $this->update_zone_setting( 'automatic_platform_optimization', $enable ? 'on' : 'off' );
}
/**
* Auto purge after save_post.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Is update.
* @return void
*/
public function auto_purge_post( $post_id, $post, $update ) {
if ( ! $this->should_auto_purge_post( $post_id, $post ) ) {
return;
}
$this->auto_purge_by_post_id( $post_id );
}
/**
* Auto purge deleted post.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @return void
*/
public function auto_purge_deleted_post( $post_id, $post ) {
if ( ! $post instanceof WP_Post ) {
return;
}
$this->queue_urls( $this->get_related_urls_for_post( $post_id, $post ) );
}
/**
* Auto purge trashed post.
*
* @param int $post_id Post ID.
* @return void
*/
public function auto_purge_trashed_post( $post_id ) {
$this->auto_purge_by_post_id( $post_id );
}
/**
* Auto purge simple post ID.
*
* @param int $post_id Post ID.
* @return void
*/
public function auto_purge_simple_post_id( $post_id ) {
$this->auto_purge_by_post_id( $post_id );
}
/**
* Status change purge.
*
* @param string $new_status New status.
* @param string $old_status Old status.
* @param WP_Post $post Post object.
* @return void
*/
public function auto_purge_status_change( $new_status, $old_status, $post ) {
if ( $new_status === $old_status ) {
return;
}
if ( ! $post instanceof WP_Post ) {
return;
}
if ( 'publish' === $new_status || 'publish' === $old_status ) {
$this->auto_purge_by_post_id( $post->ID );
}
}
/**
* Auto purge by post ID.
*
* @param int $post_id Post ID.
* @return void
*/
public function auto_purge_by_post_id( $post_id ) {
$post_id = absint( $post_id );
if ( isset( $this->purged_posts[ $post_id ] ) ) {
return;
}
$post = get_post( $post_id );
if ( ! $post instanceof WP_Post ) {
return;
}
if ( ! $this->should_auto_purge_post( $post_id, $post ) ) {
return;
}
$this->purged_posts[ $post_id ] = true;
if ( $this->get_setting( 'auto_purge_everything', false ) ) {
$this->schedule_purge_everything();
return;
}
$urls = $this->get_related_urls_for_post( $post_id, $post );
$this->queue_urls( $urls );
}
/**
* Should auto purge post?
*
* @param int $post_id Post ID.
* @param WP_Post $post Post.
* @return bool
*/
private function should_auto_purge_post( $post_id, $post ) {
if ( ! $this->has_api_credentials() ) {
return false;
}
if ( ! $this->get_setting( 'auto_purge', true ) ) {
return false;
}
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return false;
}
if ( ! $post instanceof WP_Post ) {
return false;
}
$skip_statuses = array( 'auto-draft', 'inherit' );
if ( in_array( $post->post_status, $skip_statuses, true ) ) {
return false;
}
$skip_types = array( 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_global_styles', 'wp_navigation' );
if ( in_array( $post->post_type, $skip_types, true ) ) {
return false;
}
return (bool) apply_filters( 'rx_cloudflare_should_auto_purge_post', true, $post_id, $post );
}
/**
* Build related URLs for post.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post.
* @return array
*/
public function get_related_urls_for_post( $post_id, $post = null ) {
$post_id = absint( $post_id );
$post = $post instanceof WP_Post ? $post : get_post( $post_id );
$urls = array();
if ( ! $post instanceof WP_Post ) {
return array();
}
$permalink = get_permalink( $post_id );
if ( $permalink ) {
$urls[] = $permalink;
$urls[] = trailingslashit( $permalink );
$urls[] = untrailingslashit( $permalink );
}
if ( $this->get_setting( 'purge_home', true ) ) {
$urls[] = home_url( '/' );
$urls[] = home_url();
}
if ( $this->get_setting( 'purge_archives', true ) ) {
$post_type_archive = get_post_type_archive_link( $post->post_type );
if ( $post_type_archive ) {
$urls[] = $post_type_archive;
}
$year = get_the_time( 'Y', $post );
$month = get_the_time( 'm', $post );
$day = get_the_time( 'd', $post );
$urls[] = get_year_link( $year );
$urls[] = get_month_link( $year, $month );
$urls[] = get_day_link( $year, $month, $day );
}
if ( $this->get_setting( 'purge_terms', true ) ) {
$taxonomies = get_object_taxonomies( $post->post_type );
foreach ( $taxonomies as $taxonomy ) {
$terms = get_the_terms( $post_id, $taxonomy );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
continue;
}
foreach ( $terms as $term ) {
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
$urls[] = $link;
}
}
}
}
if ( $this->get_setting( 'purge_author', true ) ) {
$urls[] = get_author_posts_url( (int) $post->post_author );
}
if ( $this->get_setting( 'purge_feeds', true ) ) {
$urls[] = get_feed_link();
$urls[] = get_post_comments_feed_link( $post_id );
}
if ( $this->get_setting( 'purge_rest', true ) ) {
$urls[] = rest_url();
$urls[] = rest_url( 'wp/v2/' . $post->post_type . '/' . $post_id );
}
if ( $this->get_setting( 'purge_sitemap', true ) ) {
$urls[] = home_url( '/wp-sitemap.xml' );
$urls[] = home_url( '/sitemap.xml' );
$urls[] = home_url( '/sitemap_index.xml' );
}
$urls = apply_filters( 'rx_cloudflare_related_urls_for_post', $urls, $post_id, $post );
return $this->sanitize_urls( $urls );
}
/**
* Queue URLs for purge.
*
* @param array $urls URLs.
* @return void
*/
public function queue_urls( $urls ) {
if ( ! $this->has_api_credentials() ) {
return;
}
$urls = $this->sanitize_urls( $urls );
if ( empty( $urls ) ) {
return;
}
$this->purge_queue = array_values( array_unique( array_merge( $this->purge_queue, $urls ) ) );
$delay = absint( $this->get_setting( 'purge_delay_seconds', 5 ) );
if ( ! wp_next_scheduled( 'rx_cloudflare_purge_queue' ) ) {
wp_schedule_single_event( time() + $delay, 'rx_cloudflare_purge_queue' );
}
}
/**
* Process purge queue.
*
* @return void
*/
public function process_purge_queue() {
$queue = get_transient( 'rx_cloudflare_purge_queue' );
if ( ! is_array( $queue ) ) {
$queue = array();
}
$queue = array_values( array_unique( array_merge( $queue, $this->purge_queue ) ) );
if ( empty( $queue ) ) {
return;
}
delete_transient( 'rx_cloudflare_purge_queue' );
$this->purge_urls( $queue );
}
/**
* Schedule purge everything.
*
* @return void
*/
private function schedule_purge_everything() {
if ( ! wp_next_scheduled( 'rx_cloudflare_delayed_purge_everything' ) ) {
wp_schedule_single_event( time() + absint( $this->get_setting( 'purge_delay_seconds', 5 ) ), 'rx_cloudflare_delayed_purge_everything' );
}
add_action( 'rx_cloudflare_delayed_purge_everything', array( $this, 'purge_everything_silent' ) );
}
/**
* Store queue at shutdown.
*
* @return void
*/
public function store_queue_on_shutdown() {
if ( empty( $this->purge_queue ) ) {
return;
}
$existing = get_transient( 'rx_cloudflare_purge_queue' );
if ( ! is_array( $existing ) ) {
$existing = array();
}
$merged = array_values( array_unique( array_merge( $existing, $this->purge_queue ) ) );
set_transient( 'rx_cloudflare_purge_queue', $merged, HOUR_IN_SECONDS );
}
/**
* Comment post purge.
*
* @param int $comment_id Comment ID.
* @param int|string $approved Approved.
* @param array $commentdata Comment data.
* @return void
*/
public function auto_purge_comment_post( $comment_id, $approved, $commentdata ) {
if ( empty( $commentdata['comment_post_ID'] ) ) {
return;
}
$this->auto_purge_by_post_id( absint( $commentdata['comment_post_ID'] ) );
}
/**
* Comment purge by ID.
*
* @param int $comment_id Comment ID.
* @return void
*/
public function auto_purge_comment_by_id( $comment_id ) {
$comment = get_comment( $comment_id );
if ( $comment && ! empty( $comment->comment_post_ID ) ) {
$this->auto_purge_by_post_id( absint( $comment->comment_post_ID ) );
}
}
/**
* Restore real visitor IP when behind Cloudflare.
*
* @return void
*/
public function maybe_restore_real_ip() {
if ( ! $this->get_setting( 'real_ip_enabled', true ) ) {
return;
}
if ( empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
return;
}
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return;
}
$_SERVER['REMOTE_ADDR'] = $ip;
}
/**
* Add Cloudflare-friendly headers.
*
* @return void
*/
public function send_cloudflare_friendly_headers() {
if ( headers_sent() || ! $this->get_setting( 'headers_enabled', true ) ) {
return;
}
header( 'X-RX-Theme: Cloudflare-Ready' );
if ( $this->get_setting( 'page_cache_header', true ) && ! is_user_logged_in() && ! is_admin() ) {
header( 'Cache-Control: public, max-age=0, s-maxage=31536000, stale-while-revalidate=60' );
}
header( 'X-Content-Type-Options: nosniff' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
header( 'Permissions-Policy: geolocation=(), microphone=(), camera=()' );
do_action( 'rx_cloudflare_send_headers' );
}
/**
* Generate cache tags for current page.
*
* @return array
*/
public function get_current_cache_tags() {
$prefix = sanitize_key( $this->get_setting( 'cache_tag_prefix', 'rx' ) );
$tags = array( $prefix . '-site' );
if ( is_singular() ) {
$post_id = get_queried_object_id();
$post = get_post( $post_id );
if ( $post ) {
$tags[] = $prefix . '-post-' . $post_id;
$tags[] = $prefix . '-type-' . $post->post_type;
$tags[] = $prefix . '-author-' . $post->post_author;
}
}
if ( is_home() || is_front_page() ) {
$tags[] = $prefix . '-home';
}
if ( is_category() || is_tag() || is_tax() ) {
$term = get_queried_object();
if ( $term && ! empty( $term->taxonomy ) && ! empty( $term->term_id ) ) {
$tags[] = $prefix . '-term-' . $term->taxonomy . '-' . $term->term_id;
}
}
if ( is_author() ) {
$tags[] = $prefix . '-author-' . get_queried_object_id();
}
return array_values( array_unique( apply_filters( 'rx_cloudflare_current_cache_tags', $tags ) ) );
}
/**
* Output cache tag header.
*
* @return void
*/
public function maybe_send_cache_tag_header() {
if ( headers_sent() ) {
return;
}
$tags = $this->get_current_cache_tags();
if ( ! empty( $tags ) ) {
header( 'Cache-Tag: ' . implode( ',', array_map( 'sanitize_key', $tags ) ) );
}
}
/**
* Register admin page.
*
* @return void
*/
public function register_admin_page() {
add_theme_page(
__( 'RX Cloudflare', 'rx-theme' ),
__( 'RX Cloudflare', 'rx-theme' ),
'manage_options',
'rx-cloudflare',
array( $this, 'render_admin_page' )
);
}
/**
* Render admin page.
*
* @return void
*/
public function render_admin_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'RX Theme Cloudflare Integration', 'rx-theme' ); ?></h1>
<?php $this->render_admin_notices(); ?>
<form method="post" action="options.php">
<?php settings_fields( self::OPTION_GROUP ); ?>
<h2><?php esc_html_e( 'API Settings', 'rx-theme' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><?php esc_html_e( 'Enable Cloudflare Integration', 'rx-theme' ); ?></th>
<td>
<label>
<input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled]" value="1" <?php checked( $settings['enabled'] ); ?>>
<?php esc_html_e( 'Enabled', 'rx-theme' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'API Token', 'rx-theme' ); ?></th>
<td>
<input type="password" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[api_token]" value="<?php echo esc_attr( $settings['api_token'] ); ?>" autocomplete="off">
<p class="description"><?php esc_html_e( 'Better: define RX_CF_API_TOKEN in wp-config.php.', 'rx-theme' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Zone ID', 'rx-theme' ); ?></th>
<td>
<input type="text" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[zone_id]" value="<?php echo esc_attr( $settings['zone_id'] ); ?>">
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Account ID', 'rx-theme' ); ?></th>
<td>
<input type="text" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[account_id]" value="<?php echo esc_attr( $settings['account_id'] ); ?>">
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Automatic Purge', 'rx-theme' ); ?></h2>
<table class="form-table" role="presentation">
<?php
$this->checkbox_row( 'auto_purge', __( 'Auto purge on content changes', 'rx-theme' ), $settings );
$this->checkbox_row( 'auto_purge_everything', __( 'Purge everything instead of selected URLs', 'rx-theme' ), $settings );
$this->checkbox_row( 'auto_purge_urls', __( 'Purge selected related URLs', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_home', __( 'Purge home page', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_archives', __( 'Purge archives', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_terms', __( 'Purge categories/tags/taxonomies', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_author', __( 'Purge author archive', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_feeds', __( 'Purge feeds', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_rest', __( 'Purge REST URLs', 'rx-theme' ), $settings );
$this->checkbox_row( 'purge_sitemap', __( 'Purge sitemap URLs', 'rx-theme' ), $settings );
?>
<tr>
<th scope="row"><?php esc_html_e( 'Purge Delay Seconds', 'rx-theme' ); ?></th>
<td>
<input type="number" min="0" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[purge_delay_seconds]" value="<?php echo esc_attr( $settings['purge_delay_seconds'] ); ?>">
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Headers and Real IP', 'rx-theme' ); ?></h2>
<table class="form-table" role="presentation">
<?php
$this->checkbox_row( 'headers_enabled', __( 'Send Cloudflare-friendly headers', 'rx-theme' ), $settings );
$this->checkbox_row( 'real_ip_enabled', __( 'Restore real visitor IP from CF-Connecting-IP', 'rx-theme' ), $settings );
$this->checkbox_row( 'page_cache_header', __( 'Send public CDN cache header for guest pages', 'rx-theme' ), $settings );
?>
</table>
<h2><?php esc_html_e( 'Turnstile', 'rx-theme' ); ?></h2>
<table class="form-table" role="presentation">
<?php
$this->checkbox_row( 'turnstile_enabled', __( 'Enable Turnstile', 'rx-theme' ), $settings );
$this->checkbox_row( 'turnstile_login', __( 'Protect login form', 'rx-theme' ), $settings );
$this->checkbox_row( 'turnstile_register', __( 'Protect registration form', 'rx-theme' ), $settings );
$this->checkbox_row( 'turnstile_lostpassword', __( 'Protect lost password form', 'rx-theme' ), $settings );
$this->checkbox_row( 'turnstile_comment', __( 'Protect comment form', 'rx-theme' ), $settings );
?>
<tr>
<th scope="row"><?php esc_html_e( 'Turnstile Site Key', 'rx-theme' ); ?></th>
<td>
<input type="text" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[turnstile_site_key]" value="<?php echo esc_attr( $settings['turnstile_site_key'] ); ?>">
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Turnstile Secret Key', 'rx-theme' ); ?></th>
<td>
<input type="password" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[turnstile_secret_key]" value="<?php echo esc_attr( $settings['turnstile_secret_key'] ); ?>">
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Theme', 'rx-theme' ); ?></th>
<td>
<select name="<?php echo esc_attr( self::OPTION_KEY ); ?>[turnstile_theme]">
<option value="auto" <?php selected( $settings['turnstile_theme'], 'auto' ); ?>>auto</option>
<option value="light" <?php selected( $settings['turnstile_theme'], 'light' ); ?>>light</option>
<option value="dark" <?php selected( $settings['turnstile_theme'], 'dark' ); ?>>dark</option>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Size', 'rx-theme' ); ?></th>
<td>
<select name="<?php echo esc_attr( self::OPTION_KEY ); ?>[turnstile_size]">
<option value="normal" <?php selected( $settings['turnstile_size'], 'normal' ); ?>>normal</option>
<option value="compact" <?php selected( $settings['turnstile_size'], 'compact' ); ?>>compact</option>
<option value="flexible" <?php selected( $settings['turnstile_size'], 'flexible' ); ?>>flexible</option>
</select>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Debug', 'rx-theme' ); ?></h2>
<table class="form-table" role="presentation">
<?php
$this->checkbox_row( 'admin_bar_enabled', __( 'Show admin bar Cloudflare menu', 'rx-theme' ), $settings );
$this->checkbox_row( 'debug_log', __( 'Enable debug log', 'rx-theme' ), $settings );
?>
<tr>
<th scope="row"><?php esc_html_e( 'Cache Tag Prefix', 'rx-theme' ); ?></th>
<td>
<input type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[cache_tag_prefix]" value="<?php echo esc_attr( $settings['cache_tag_prefix'] ); ?>">
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Cloudflare Tools', 'rx-theme' ); ?></h2>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-bottom: 15px;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="action" value="rx_cf_purge_everything">
<?php submit_button( __( 'Purge Everything', 'rx-theme' ), 'delete', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-bottom: 15px;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="action" value="rx_cf_purge_url">
<input type="url" class="regular-text" name="url" placeholder="<?php echo esc_attr( home_url( '/' ) ); ?>">
<?php submit_button( __( 'Purge URL', 'rx-theme' ), 'secondary', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;margin-right:10px;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="action" value="rx_cf_dev_mode_on">
<?php submit_button( __( 'Development Mode ON', 'rx-theme' ), 'secondary', 'submit', false ); ?>
</form>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="action" value="rx_cf_dev_mode_off">
<?php submit_button( __( 'Development Mode OFF', 'rx-theme' ), 'secondary', 'submit', false ); ?>
</form>
</div>
<?php
}
/**
* Checkbox row.
*
* @param string $key Setting key.
* @param string $label Label.
* @param array $settings Settings.
* @return void
*/
private function checkbox_row( $key, $label, $settings ) {
?>
<tr>
<th scope="row"><?php echo esc_html( $label ); ?></th>
<td>
<label>
<input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $key ); ?>]" value="1" <?php checked( ! empty( $settings[ $key ] ) ); ?>>
<?php esc_html_e( 'Enabled', 'rx-theme' ); ?>
</label>
</td>
</tr>
<?php
}
/**
* Render notices.
*
* @return void
*/
private function render_admin_notices() {
if ( isset( $_GET['rx_cf_status'] ) ) {
$status = sanitize_key( wp_unslash( $_GET['rx_cf_status'] ) );
$message = isset( $_GET['rx_cf_message'] ) ? sanitize_text_field( wp_unslash( $_GET['rx_cf_message'] ) ) : '';
if ( 'success' === $status ) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ? $message : __( 'Cloudflare action completed.', 'rx-theme' ) ) . '</p></div>';
} else {
echo '<div class="notice notice-error is-dismissible"><p>' . esc_html( $message ? $message : __( 'Cloudflare action failed.', 'rx-theme' ) ) . '</p></div>';
}
}
if ( ! $this->has_api_credentials() ) {
echo '<div class="notice notice-warning"><p>' . esc_html__( 'Cloudflare API token or zone ID is missing.', 'rx-theme' ) . '</p></div>';
}
}
/**
* Admin action capability and nonce check.
*
* @return void
*/
private function verify_admin_action() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'rx-theme' ) );
}
check_admin_referer( self::NONCE_ACTION, self::NONCE_NAME );
}
/**
* Redirect after admin action.
*
* @param bool|string $success Success.
* @param string $message Message.
* @return void
*/
private function admin_redirect( $success, $message ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'rx-cloudflare',
'rx_cf_status' => $success ? 'success' : 'error',
'rx_cf_message' => rawurlencode( $message ),
),
admin_url( 'themes.php' )
)
);
exit;
}
/**
* Admin purge everything.
*
* @return void
*/
public function handle_admin_purge_everything() {
$this->verify_admin_action();
$result = $this->purge_everything();
if ( is_wp_error( $result ) ) {
$this->admin_redirect( false, $result->get_error_message() );
}
$this->admin_redirect( true, __( 'Cloudflare full cache purge requested.', 'rx-theme' ) );
}
/**
* Admin purge URL.
*
* @return void
*/
public function handle_admin_purge_url() {
$this->verify_admin_action();
$url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
$result = $this->purge_url( $url );
if ( is_wp_error( $result ) ) {
$this->admin_redirect( false, $result->get_error_message() );
}
$this->admin_redirect( true, __( 'Cloudflare URL purge requested.', 'rx-theme' ) );
}
/**
* Admin development mode on.
*
* @return void
*/
public function handle_admin_dev_mode_on() {
$this->verify_admin_action();
$result = $this->development_mode_on();
if ( is_wp_error( $result ) ) {
$this->admin_redirect( false, $result->get_error_message() );
}
$this->admin_redirect( true, __( 'Cloudflare Development Mode enabled.', 'rx-theme' ) );
}
/**
* Admin development mode off.
*
* @return void
*/
public function handle_admin_dev_mode_off() {
$this->verify_admin_action();
$result = $this->development_mode_off();
if ( is_wp_error( $result ) ) {
$this->admin_redirect( false, $result->get_error_message() );
}
$this->admin_redirect( true, __( 'Cloudflare Development Mode disabled.', 'rx-theme' ) );
}
/**
* Admin bar menu.
*
* @param WP_Admin_Bar $admin_bar Admin bar.
* @return void
*/
public function admin_bar_menu( $admin_bar ) {
if ( ! is_admin_bar_showing() || ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! $this->get_setting( 'admin_bar_enabled', true ) ) {
return;
}
$admin_bar->add_node(
array(
'id' => 'rx-cloudflare',
'title' => 'RX Cloudflare',
'href' => admin_url( 'themes.php?page=rx-cloudflare' ),
)
);
$purge_url = wp_nonce_url(
admin_url( 'admin-post.php?action=rx_cf_purge_everything' ),
self::NONCE_ACTION,
self::NONCE_NAME
);
$admin_bar->add_node(
array(
'id' => 'rx-cloudflare-purge-everything',
'parent' => 'rx-cloudflare',
'title' => __( 'Purge Everything', 'rx-theme' ),
'href' => $purge_url,
)
);
}
/**
* Register REST routes.
*
* @return void
*/
public function register_rest_routes() {
register_rest_route(
'rx/v1',
'/cloudflare/purge',
array(
'methods' => 'POST',
'callback' => array( $this, 'rest_purge' ),
'permission_callback' => array( $this, 'rest_permission' ),
'args' => array(
'url' => array(
'type' => 'string',
'required' => false,
'description' => 'Single URL to purge.',
),
'urls' => array(
'type' => 'array',
'required' => false,
'description' => 'URLs to purge.',
),
'everything' => array(
'type' => 'boolean',
'required' => false,
'description' => 'Purge everything.',
),
),
)
);
}
/**
* REST permission.
*
* @return bool
*/
public function rest_permission() {
return current_user_can( 'manage_options' );
}
/**
* REST purge callback.
*
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error
*/
public function rest_purge( WP_REST_Request $request ) {
$everything = (bool) $request->get_param( 'everything' );
if ( $everything ) {
$result = $this->purge_everything();
} else {
$urls = array();
$url = $request->get_param( 'url' );
if ( $url ) {
$urls[] = $url;
}
$more_urls = $request->get_param( 'urls' );
if ( is_array( $more_urls ) ) {
$urls = array_merge( $urls, $more_urls );
}
$result = $this->purge_urls( $urls );
}
if ( is_wp_error( $result ) ) {
return $result;
}
return rest_ensure_response( $result );
}
/**
* Is Turnstile configured?
*
* @return bool
*/
public function turnstile_configured() {
return $this->get_setting( 'turnstile_enabled', false )
&& $this->get_setting( 'turnstile_site_key', '' )
&& $this->get_setting( 'turnstile_secret_key', '' );
}
/**
* Enqueue Turnstile script.
*
* @return void
*/
public function enqueue_turnstile_script() {
if ( ! $this->turnstile_configured() ) {
return;
}
wp_enqueue_script(
'cloudflare-turnstile',
'https://challenges.cloudflare.com/turnstile/v0/api.js',
array(),
null,
true
);
}
/**
* Render Turnstile widget.
*
* @return void
*/
public function render_turnstile_widget() {
if ( ! $this->turnstile_configured() ) {
return;
}
$site_key = $this->get_setting( 'turnstile_site_key', '' );
$theme = $this->get_setting( 'turnstile_theme', 'auto' );
$size = $this->get_setting( 'turnstile_size', 'normal' );
echo '<div class="rx-cloudflare-turnstile-wrap" style="margin:12px 0;">';
echo '<div class="cf-turnstile" data-sitekey="' . esc_attr( $site_key ) . '" data-theme="' . esc_attr( $theme ) . '" data-size="' . esc_attr( $size ) . '"></div>';
echo '</div>';
}
/**
* Login widget.
*
* @return void
*/
public function render_turnstile_login() {
if ( $this->get_setting( 'turnstile_login', true ) ) {
$this->render_turnstile_widget();
}
}
/**
* Register widget.
*
* @return void
*/
public function render_turnstile_register() {
if ( $this->get_setting( 'turnstile_register', true ) ) {
$this->render_turnstile_widget();
}
}
/**
* Lost password widget.
*
* @return void
*/
public function render_turnstile_lostpassword() {
if ( $this->get_setting( 'turnstile_lostpassword', true ) ) {
$this->render_turnstile_widget();
}
}
/**
* Comment widget.
*
* @return void
*/
public function render_turnstile_comment() {
if ( $this->get_setting( 'turnstile_comment', true ) ) {
$this->render_turnstile_widget();
}
}
/**
* Validate Turnstile token.
*
* @return true|WP_Error
*/
public function validate_turnstile_token() {
if ( ! $this->turnstile_configured() ) {
return true;
}
$token = isset( $_POST['cf-turnstile-response'] ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
if ( empty( $token ) ) {
return new WP_Error( 'rx_turnstile_missing', $this->get_setting( 'turnstile_failure_message' ) );
}
$remote_ip = '';
if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
$remote_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
}
$response = wp_remote_post(
self::TURNSTILE_VERIFY_ENDPOINT,
array(
'timeout' => 20,
'body' => array(
'secret' => $this->get_setting( 'turnstile_secret_key', '' ),
'response' => $token,
'remoteip' => $remote_ip,
),
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error( 'rx_turnstile_http_error', $this->get_setting( 'turnstile_failure_message' ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['success'] ) ) {
$this->log( $body, 'turnstile_failed' );
return new WP_Error( 'rx_turnstile_failed', $this->get_setting( 'turnstile_failure_message' ) );
}
return true;
}
/**
* Validate login.
*
* @param WP_User|WP_Error|null $user User.
* @param string $username Username.
* @param string $password Password.
* @return WP_User|WP_Error|null
*/
public function validate_turnstile_login( $user, $username, $password ) {
if ( ! $this->get_setting( 'turnstile_login', true ) ) {
return $user;
}
if ( empty( $_POST['log'] ) && empty( $_POST['pwd'] ) ) {
return $user;
}
$valid = $this->validate_turnstile_token();
if ( is_wp_error( $valid ) ) {
return $valid;
}
return $user;
}
/**
* Validate registration.
*
* @param WP_Error $errors Errors.
* @param string $sanitized_user_login Login.
* @param string $user_email Email.
* @return WP_Error
*/
public function validate_turnstile_registration( $errors, $sanitized_user_login, $user_email ) {
if ( ! $this->get_setting( 'turnstile_register', true ) ) {
return $errors;
}
$valid = $this->validate_turnstile_token();
if ( is_wp_error( $valid ) ) {
$errors->add( 'rx_turnstile_failed', $valid->get_error_message() );
}
return $errors;
}
/**
* Validate lost password.
*
* @param WP_Error $errors Errors.
* @return void
*/
public function validate_turnstile_lostpassword( $errors ) {
if ( ! $this->get_setting( 'turnstile_lostpassword', true ) ) {
return;
}
$valid = $this->validate_turnstile_token();
if ( is_wp_error( $valid ) ) {
$errors->add( 'rx_turnstile_failed', $valid->get_error_message() );
}
}
/**
* Validate comment.
*
* @param array $commentdata Comment data.
* @return array
*/
public function validate_turnstile_comment( $commentdata ) {
if ( ! $this->get_setting( 'turnstile_comment', true ) ) {
return $commentdata;
}
if ( is_user_logged_in() && current_user_can( 'moderate_comments' ) ) {
return $commentdata;
}
$valid = $this->validate_turnstile_token();
if ( is_wp_error( $valid ) ) {
wp_die(
esc_html( $valid->get_error_message() ),
esc_html__( 'Turnstile verification failed', 'rx-theme' ),
array(
'response' => 403,
'back_link' => true,
)
);
}
return $commentdata;
}
/**
* Register WP-CLI commands.
*
* @return void
*/
private function register_wp_cli() {
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
WP_CLI::add_command(
'rx-cloudflare purge-everything',
function() {
$result = RX_Theme_Cloudflare::instance()->purge_everything();
if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
}
WP_CLI::success( 'Cloudflare full cache purge requested.' );
}
);
WP_CLI::add_command(
'rx-cloudflare purge-url',
function( $args ) {
$url = isset( $args[0] ) ? $args[0] : '';
$result = RX_Theme_Cloudflare::instance()->purge_url( $url );
if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
}
WP_CLI::success( 'Cloudflare URL purge requested.' );
}
);
WP_CLI::add_command(
'rx-cloudflare dev-on',
function() {
$result = RX_Theme_Cloudflare::instance()->development_mode_on();
if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
}
WP_CLI::success( 'Cloudflare Development Mode enabled.' );
}
);
WP_CLI::add_command(
'rx-cloudflare dev-off',
function() {
$result = RX_Theme_Cloudflare::instance()->development_mode_off();
if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
}
WP_CLI::success( 'Cloudflare Development Mode disabled.' );
}
);
}
}
endif;
/**
* Boot Cloudflare integration.
*/
function rx_theme_cloudflare() {
return RX_Theme_Cloudflare::instance();
}
rx_theme_cloudflare();
add_action( 'shutdown', array( rx_theme_cloudflare(), 'store_queue_on_shutdown' ), 1 );
add_action( 'send_headers', array( rx_theme_cloudflare(), 'maybe_send_cache_tag_header' ), 30 );
/**
* Helper: purge everything.
*
* @return array|WP_Error
*/
function rx_cf_purge_everything() {
return rx_theme_cloudflare()->purge_everything();
}
/**
* Helper: purge URL.
*
* @param string $url URL.
* @return array|WP_Error
*/
function rx_cf_purge_url( $url ) {
return rx_theme_cloudflare()->purge_url( $url );
}
/**
* Helper: purge URLs.
*
* @param array $urls URLs.
* @return array|WP_Error
*/
function rx_cf_purge_urls( $urls ) {
return rx_theme_cloudflare()->purge_urls( $urls );
}
/**
* Helper: development mode on.
*
* @return array|WP_Error
*/
function rx_cf_dev_mode_on() {
return rx_theme_cloudflare()->development_mode_on();
}
/**
* Helper: development mode off.
*
* @return array|WP_Error
*/
function rx_cf_dev_mode_off() {
return rx_theme_cloudflare()->development_mode_off();
}
Then load it from your functions.php:
/**
* RX Theme Cloudflare Integration
*/
$rx_cloudflare_file = get_template_directory() . '/inc/integrations/cloudflare.php';
if ( file_exists( $rx_cloudflare_file ) ) {
require_once $rx_cloudflare_file;
}
Add your private keys in wp-config.php, not inside the theme file:
define( 'RX_CF_API_TOKEN', 'PASTE_YOUR_CLOUDFLARE_API_TOKEN' );
define( 'RX_CF_ZONE_ID', 'PASTE_YOUR_ZONE_ID' );
define( 'RX_CF_ACCOUNT_ID', 'PASTE_YOUR_ACCOUNT_ID_OPTIONAL' );
define( 'RX_CF_TURNSTILE_SITE_KEY', 'PASTE_TURNSTILE_SITE_KEY' );
define( 'RX_CF_TURNSTILE_SECRET_KEY', 'PASTE_TURNSTILE_SECRET_KEY' );
After adding it, go to:
Appearance → RX Cloudflare
There you can save settings, purge full cache, purge one URL, turn Development Mode on/off, and enable Turnstile.