| 1 |
<?php |
| 2 |
/** |
| 3 |
* Cloudflare — thin wrapper around the Cloudflare v4 API for purging, |
| 4 |
* dev-mode toggle, and zone verification. |
| 5 |
* |
| 6 |
* Authentication: two supported modes — |
| 7 |
* token → Authorization: Bearer <api_token> (preferred — scoped) |
| 8 |
* key → X-Auth-Email + X-Auth-Key (legacy "Global API Key") |
| 9 |
* |
| 10 |
* We don't store or send the auth headers anywhere outside the |
| 11 |
* outgoing API request. No logging. The Free tier exposes Auto Purge |
| 12 |
* on Update + dev mode + manual purge; APO / Edge Cache TTL stay in |
| 13 |
* the Pro plugin per FEATURES.md "Cloudflare Integration" §8-10. |
| 14 |
* |
| 15 |
* @package XSpeed |
| 16 |
*/ |
| 17 |
|
| 18 |
declare(strict_types=1); |
| 19 |
|
| 20 |
namespace XSpeed; |
| 21 |
|
| 22 |
defined( 'ABSPATH' ) || exit; |
| 23 |
|
| 24 |
final class Cloudflare { |
| 25 |
|
| 26 |
private const API_BASE = 'https://api.cloudflare.com/client/v4'; |
| 27 |
|
| 28 |
/** |
| 29 |
* Verify the configured credentials by hitting GET /zones/{id}. |
| 30 |
* Returns parsed zone payload on success. |
| 31 |
* |
| 32 |
* @return array{ok:bool,status:int,body:array} |
| 33 |
*/ |
| 34 |
public static function verify( array $opts ): array { |
| 35 |
$zone = (string) ( $opts['zone_id'] ?? '' ); |
| 36 |
if ( '' === $zone ) { |
| 37 |
return self::fail( 0, 'Zone ID is empty.' ); |
| 38 |
} |
| 39 |
return self::request( $opts, 'GET', '/zones/' . rawurlencode( $zone ) ); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Purge everything in the configured zone. Equivalent to clicking |
| 44 |
* "Purge Everything" in the Cloudflare dashboard. |
| 45 |
* |
| 46 |
* @return array{ok:bool,status:int,body:array} |
| 47 |
*/ |
| 48 |
public static function purge_all( array $opts ): array { |
| 49 |
$zone = (string) ( $opts['zone_id'] ?? '' ); |
| 50 |
if ( '' === $zone ) { |
| 51 |
return self::fail( 0, 'Zone ID is empty.' ); |
| 52 |
} |
| 53 |
return self::request( |
| 54 |
$opts, |
| 55 |
'POST', |
| 56 |
'/zones/' . rawurlencode( $zone ) . '/purge_cache', |
| 57 |
array( 'purge_everything' => true ) |
| 58 |
); |
| 59 |
} |
| 60 |
|
| 61 |
/** |
| 62 |
* Purge a specific list of URLs. CF accepts up to 30 per call; |
| 63 |
* caller should chunk if it has more. |
| 64 |
* |
| 65 |
* @param string[] $urls |
| 66 |
* @return array{ok:bool,status:int,body:array} |
| 67 |
*/ |
| 68 |
public static function purge_urls( array $opts, array $urls ): array { |
| 69 |
$urls = array_values( array_filter( array_map( 'strval', $urls ) ) ); |
| 70 |
if ( empty( $urls ) ) { |
| 71 |
return self::fail( 0, 'No URLs supplied.' ); |
| 72 |
} |
| 73 |
$zone = (string) ( $opts['zone_id'] ?? '' ); |
| 74 |
if ( '' === $zone ) { |
| 75 |
return self::fail( 0, 'Zone ID is empty.' ); |
| 76 |
} |
| 77 |
return self::request( |
| 78 |
$opts, |
| 79 |
'POST', |
| 80 |
'/zones/' . rawurlencode( $zone ) . '/purge_cache', |
| 81 |
array( 'files' => array_slice( $urls, 0, 30 ) ) |
| 82 |
); |
| 83 |
} |
| 84 |
|
| 85 |
/** |
| 86 |
* Flip development mode on or off. CF auto-disables it after 3 |
| 87 |
* hours; that's the user's behavior, not something we model here. |
| 88 |
* |
| 89 |
* @return array{ok:bool,status:int,body:array} |
| 90 |
*/ |
| 91 |
public static function set_dev_mode( array $opts, bool $on ): array { |
| 92 |
$zone = (string) ( $opts['zone_id'] ?? '' ); |
| 93 |
if ( '' === $zone ) { |
| 94 |
return self::fail( 0, 'Zone ID is empty.' ); |
| 95 |
} |
| 96 |
return self::request( |
| 97 |
$opts, |
| 98 |
'PATCH', |
| 99 |
'/zones/' . rawurlencode( $zone ) . '/settings/development_mode', |
| 100 |
array( 'value' => $on ? 'on' : 'off' ) |
| 101 |
); |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Build the auth + content headers based on the selected auth_method. |
| 106 |
* Public for tests — they assert the right header pair is set. |
| 107 |
*/ |
| 108 |
public static function build_headers( array $opts ): array { |
| 109 |
$method = (string) ( $opts['auth_method'] ?? 'token' ); |
| 110 |
$base = array( 'Content-Type' => 'application/json' ); |
| 111 |
if ( 'token' === $method ) { |
| 112 |
$token = (string) ( $opts['api_token'] ?? '' ); |
| 113 |
if ( '' === $token ) { |
| 114 |
return $base; |
| 115 |
} |
| 116 |
$base['Authorization'] = 'Bearer ' . $token; |
| 117 |
return $base; |
| 118 |
} |
| 119 |
// key mode. |
| 120 |
$email = (string) ( $opts['email'] ?? '' ); |
| 121 |
$key = (string) ( $opts['api_key'] ?? '' ); |
| 122 |
if ( '' !== $email && '' !== $key ) { |
| 123 |
$base['X-Auth-Email'] = $email; |
| 124 |
$base['X-Auth-Key'] = $key; |
| 125 |
} |
| 126 |
return $base; |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Core request helper. Returns a normalized envelope: |
| 131 |
* ok → true when HTTP < 400 AND CF body { success: true }. |
| 132 |
* status → HTTP code (0 on transport failure). |
| 133 |
* body → decoded JSON or [ 'message' => $err ] on failure. |
| 134 |
*/ |
| 135 |
private static function request( array $opts, string $method, string $path, ?array $payload = null ): array { |
| 136 |
$headers = self::build_headers( $opts ); |
| 137 |
$args = array( |
| 138 |
'method' => $method, |
| 139 |
'headers' => $headers, |
| 140 |
'timeout' => 12, |
| 141 |
); |
| 142 |
if ( null !== $payload ) { |
| 143 |
$args['body'] = wp_json_encode( $payload ); |
| 144 |
} |
| 145 |
$url = self::API_BASE . $path; |
| 146 |
|
| 147 |
$response = wp_remote_request( $url, $args ); |
| 148 |
if ( is_wp_error( $response ) ) { |
| 149 |
return self::fail( 0, $response->get_error_message() ); |
| 150 |
} |
| 151 |
$status = (int) wp_remote_retrieve_response_code( $response ); |
| 152 |
$body = wp_remote_retrieve_body( $response ); |
| 153 |
$decoded = is_string( $body ) ? json_decode( $body, true ) : null; |
| 154 |
if ( ! is_array( $decoded ) ) { |
| 155 |
$decoded = array( 'message' => is_string( $body ) ? $body : 'Unparseable response' ); |
| 156 |
} |
| 157 |
$ok = ( $status < 400 ) && ! empty( $decoded['success'] ); |
| 158 |
return array( |
| 159 |
'ok' => $ok, |
| 160 |
'status' => $status, |
| 161 |
'body' => $decoded, |
| 162 |
); |
| 163 |
} |
| 164 |
|
| 165 |
private static function fail( int $status, string $message ): array { |
| 166 |
return array( |
| 167 |
'ok' => false, |
| 168 |
'status' => $status, |
| 169 |
'body' => array( 'success' => false, 'message' => $message ), |
| 170 |
); |
| 171 |
} |
| 172 |
} |
| 173 |
|