PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.2
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / includes / class-cloudflare.php

class-cloudflare.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.2, at includes/class-cloudflare.php

173 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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