cloudflare.php

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.

Leave a Reply

Your email address will not be published. Required fields are marked *