PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta
16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / vendor / wp-php-toolkit / reprint-exporter / src / class-http-server.php
jetpack / vendor / wp-php-toolkit / reprint-exporter / src Last commit date
class-file-tree-producer.php 1 week ago class-hmac-client.php 1 week ago class-hmac-server.php 1 week ago class-http-server.php 1 week ago class-mysql-dump-producer.php 1 week ago class-pdo-polyfill.php 1 week ago class-sqlite-driver-pdo.php 1 week ago class-staged-artifacts.php 1 week ago class-staged-endpoints.php 1 week ago class-staged-push-stream-protocol.php 1 week ago class-wpdb-driver-pdo.php 1 week ago export.php 1 week ago utils.php 1 week ago
class-http-server.php
394 lines
1 <?php
2
3 use function WordPress\Reprint\Exporter\parse_size;
4
5 /**
6 * HTTP dispatcher for the Site Export API.
7 */
8 final class Site_Export_HTTP_Server {
9
10 /** @var array<string, callable> */
11 private $handlers;
12
13 /** @var callable */
14 private $budget_factory;
15
16 /** @var callable */
17 private $body_reader;
18
19 /** @var string */
20 private $cursor_header_name;
21
22 /** @var string|null */
23 private $default_directory;
24
25 /** @var string[] Endpoints dispatched without a resource budget. */
26 private $no_budget_endpoints = ['preflight'];
27
28 public function __construct(array $options = []) {
29 $this->handlers = $options['handlers'] ?? $this->default_handlers();
30 $this->budget_factory = $options['budget_factory'] ?? [$this, 'default_budget_factory'];
31 $this->body_reader = $options['body_reader'] ?? static function (): string {
32 $body = file_get_contents('php://input');
33 return $body === false ? '' : $body;
34 };
35 $this->cursor_header_name = $options['cursor_header_name'] ?? 'HTTP_X_EXPORT_CURSOR';
36 $this->default_directory = $options['default_directory'] ?? null;
37
38 if (isset($options['staged']) && is_array($options['staged'])) {
39 $this->register_staged_handlers(new Site_Export_Staged_Endpoints($options['staged']));
40 }
41 }
42
43 public function handle_request(array $request = []): void {
44 $server = $request['server'] ?? $_SERVER;
45 $get = $request['get'] ?? $_GET;
46 $post = $request['post'] ?? $_POST;
47 if (array_key_exists('body', $request)) {
48 $body = (string) $request['body'];
49 } else {
50 $endpoint = (string) ( $get['endpoint'] ?? $post['endpoint'] ?? '' );
51 // Data-plane staged routes carry raw bytes and must only be read
52 // by their handlers. Other JSON requests still feed config parsing.
53 $body = $endpoint !== 'staged_push' && $this->is_json_content_type($server)
54 ? call_user_func($this->body_reader)
55 : '';
56 }
57 $config = $request['config'] ?? $this->parse_http_config(
58 $get,
59 $post,
60 $server,
61 $body
62 );
63 $config = $this->normalize_config($config, $server);
64 $this->dispatch($config);
65 }
66
67 /**
68 * Emits CORS headers and terminates OPTIONS preflight requests.
69 *
70 * Must be called BEFORE authentication runs — browsers send
71 * preflight OPTIONS without credentials, so the consumer must not
72 * require auth headers before this check passes.
73 *
74 * A wildcard origin ('*') is safe when authentication happens
75 * out-of-band (e.g., HMAC with a pre-shared secret) — an attacker
76 * without the secret cannot export anything regardless of origin.
77 *
78 * For OPTIONS requests this terminates the process. For all other
79 * methods it just emits the headers and returns so the caller can
80 * continue with authentication and dispatch.
81 *
82 * @param string|true $origin The Access-Control-Allow-Origin value,
83 * or true as a shorthand for '*'.
84 * @param string $allow_headers The Access-Control-Allow-Headers value.
85 * Defaults to '*' to permit all headers. Pass a comma-separated
86 * list to restrict (e.g. 'Content-Type, X-Auth-Signature').
87 * @param array<string, mixed> $server Request server array (defaults to $_SERVER).
88 * @param array<string, callable>|null $io Optional overrides for
89 * 'header' (emitter) and 'exit' (preflight terminator). Used
90 * only by tests.
91 */
92 public static function handle_cors_headers_and_terminate_on_options(
93 $origin = '*',
94 string $allow_headers = '*',
95 array $server = [],
96 ?array $io = null
97 ): void {
98 if ($origin === true) {
99 $origin = '*';
100 }
101 if (!is_string($origin) || $origin === '') {
102 throw new InvalidArgumentException(
103 'CORS origin must be a non-empty string or true'
104 );
105 }
106
107 $emit_header = ($io['header'] ?? null) ?? static function (string $h): void {
108 header($h);
109 };
110 $terminate = ($io['exit'] ?? null) ?? static function (): void {
111 exit;
112 };
113
114 $emit_header('Access-Control-Allow-Origin: ' . $origin);
115 $emit_header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
116 $emit_header('Access-Control-Allow-Headers: ' . $allow_headers);
117
118 $request_server = $server === [] ? $_SERVER : $server;
119 $method = isset($request_server['REQUEST_METHOD']) ? (string) $request_server['REQUEST_METHOD'] : '';
120 if (strtoupper($method) !== 'OPTIONS') {
121 return;
122 }
123
124 $emit_header('Allow: GET, POST, OPTIONS');
125 $terminate();
126 }
127
128 /**
129 * One-call convenience entry point: loads export.php, constructs
130 * the server, and dispatches the current request.
131 *
132 * Equivalent to:
133 *
134 * require_once __DIR__ . '/export.php';
135 * $server = new Site_Export_HTTP_Server($options);
136 * $server->handle_request();
137 *
138 * export.php is only required once. Callers that need to run CORS
139 * or their own authentication must do that before calling this method.
140 *
141 * @param array<string, mixed> $options Forwarded to the constructor.
142 */
143 public static function serve(array $options = []): void {
144 // endpoint_preflight is defined by export.php — use it as a
145 // cheap sentinel to detect whether the runtime is already
146 // loaded. require_once would be safe either way, but this
147 // avoids re-running the stat() on hot paths.
148 if (!function_exists('endpoint_preflight')) {
149 require_once __DIR__ . '/export.php';
150 }
151
152 $server = new self($options);
153 $server->handle_request();
154 }
155
156 /**
157 * @param array<string, mixed> $get
158 * @param array<string, mixed> $post
159 * @param array<string, mixed> $server
160 * @return array<string, mixed>
161 */
162 public function parse_http_config(array $get = [], array $post = [], array $server = [], string $body = ''): array {
163 $config = [];
164 $params = array_merge($get, $post);
165
166 $content_type = $server['CONTENT_TYPE'] ?? '';
167 $content_type_main = strtolower(trim((string) strtok((string) $content_type, ';')));
168 if ($content_type_main === 'application/json' && $body !== '') {
169 $json_data = json_decode($body, true);
170 if (is_array($json_data)) {
171 $params = array_merge($json_data, $params);
172 }
173 }
174
175 foreach ($params as $key => $value) {
176 $key = str_replace('-', '_', (string) $key);
177
178 if (
179 in_array($key, [
180 'max_execution_time',
181 'min_ctime',
182 'chunk_size',
183 'fragments_per_batch',
184 'batch_size',
185 'db_query_time_limit',
186 'tables_per_batch',
187 ], true)
188 ) {
189 $value = (int) $value;
190 } elseif (in_array($key, ['memory_threshold'], true)) {
191 $value = (float) $value;
192 } elseif (in_array($key, ['create_table_query', 'db_unbuffered', 'follow_symlinks'], true)) {
193 $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
194 } elseif ($key === 'paths' && is_string($value)) {
195 $decoded = json_decode($value, true);
196 if (is_array($decoded)) {
197 $value = $decoded;
198 }
199 }
200
201 $config[$key] = $value;
202 }
203
204 return $config;
205 }
206
207 /**
208 * @param array<string, mixed> $config
209 * @param array<string, mixed> $server
210 * @return array<string, mixed>
211 */
212 public function normalize_config(array $config, array $server = []): array {
213 if (
214 $this->default_directory !== null &&
215 !isset($config['directory'])
216 ) {
217 $config['directory'] = $this->default_directory;
218 }
219
220 if (!isset($config['cursor']) && isset($server[$this->cursor_header_name])) {
221 $config['cursor'] = $server[$this->cursor_header_name];
222 }
223
224 if (isset($config['cursor']) && $config['cursor'] !== '' && $config['cursor'] !== null) {
225 $config['cursor'] = $this->decode_cursor((string) $config['cursor']);
226 }
227
228 $endpoint = $config['endpoint'] ?? null;
229 if (!is_string($endpoint) || $endpoint === '') {
230 throw new InvalidArgumentException(
231 "endpoint parameter is required. Valid endpoints: " . $this->get_valid_endpoints_message()
232 );
233 }
234
235 return $config;
236 }
237
238 public function decode_cursor(string $cursor_b64): string {
239 $cursor_json = base64_decode($cursor_b64, true);
240 if ($cursor_json === false) {
241 throw new InvalidArgumentException(
242 'Cursor must be base64-encoded. Received invalid base64: ' . substr($cursor_b64, 0, 50)
243 );
244 }
245
246 $cursor_data = json_decode($cursor_json, true);
247 if ($cursor_data === null && json_last_error() !== JSON_ERROR_NONE) {
248 throw new InvalidArgumentException(
249 'Cursor must be valid JSON after base64 decoding. JSON error: ' . json_last_error_msg()
250 );
251 }
252
253 return $cursor_json;
254 }
255
256 /**
257 * @param array<string, mixed> $config
258 * @return mixed
259 */
260 public function create_resource_budget(array $config) {
261 return call_user_func($this->budget_factory, $config);
262 }
263
264 /**
265 * @param array<string, mixed> $config
266 * @param mixed $budget
267 */
268 public function dispatch(array $config, $budget = null): void {
269 $endpoint = $config['endpoint'] ?? null;
270 if (!is_string($endpoint) || $endpoint === '') {
271 throw new InvalidArgumentException(
272 "endpoint parameter is required. Valid endpoints: " . $this->get_valid_endpoints_message()
273 );
274 }
275
276 if (!isset($this->handlers[$endpoint])) {
277 throw new InvalidArgumentException(
278 "Invalid endpoint: '{$endpoint}'. Valid endpoints: " . $this->get_valid_endpoints_message()
279 );
280 }
281
282 $handler = $this->handlers[$endpoint];
283 if (in_array($endpoint, $this->no_budget_endpoints, true)) {
284 call_user_func($handler, $config);
285 return;
286 }
287
288 if ($budget === null) {
289 $budget = $this->create_resource_budget($config);
290 }
291
292 call_user_func($handler, $config, $budget);
293 }
294
295 /**
296 * @return array<string, callable>
297 */
298 private function default_handlers(): array {
299 return [
300 'file_index' => 'endpoint_file_index',
301 'file_fetch' => 'endpoint_file_fetch',
302 'sql_chunk' => 'endpoint_sql_chunk',
303 'db_index' => 'endpoint_db_index',
304 'preflight' => 'endpoint_preflight',
305 ];
306 }
307
308 /**
309 * Wire the staged artifact routes to the shared dispatcher.
310 *
311 * Explicitly-passed handlers win over these, matching how the
312 * handlers option replaces the default map.
313 */
314 private function register_staged_handlers(Site_Export_Staged_Endpoints $endpoints): void {
315 $routes = [
316 'staged_push' => static function (array $config) use ($endpoints): void {
317 $input = @fopen('php://input', 'rb');
318 try {
319 self::emit_json_response(
320 $endpoints->push_stream($config, $_SERVER, $input === false ? null : $input)
321 );
322 } finally {
323 if (is_resource($input)) {
324 fclose($input);
325 }
326 }
327 },
328 'staged_finalize' => static function (array $config) use ($endpoints): void {
329 self::emit_json_response($endpoints->finalize($config, $_SERVER));
330 },
331 'staged_status' => static function (array $config) use ($endpoints): void {
332 self::emit_json_response($endpoints->status($config));
333 },
334 'staged_discard' => static function (array $config) use ($endpoints): void {
335 self::emit_json_response($endpoints->discard($config, $_SERVER));
336 },
337 ];
338
339 foreach ($routes as $endpoint => $handler) {
340 if (!isset($this->handlers[$endpoint])) {
341 $this->handlers[$endpoint] = $handler;
342 }
343 $this->no_budget_endpoints[] = $endpoint;
344 }
345 }
346
347 /**
348 * @param array{http_code:int,body:array} $response
349 */
350 private static function emit_json_response(array $response): void {
351 http_response_code($response['http_code']);
352 header('Content-Type: application/json');
353 echo json_encode($response['body']);
354 }
355
356 private function is_json_content_type(array $server): bool {
357 $content_type = (string) ( $server['CONTENT_TYPE'] ?? '' );
358 return strtolower(trim( (string) strtok($content_type, ';'))) === 'application/json';
359 }
360
361 /**
362 * @param array<string, mixed> $config
363 * @return mixed
364 */
365 private function default_budget_factory(array $config) {
366 $max_execution_time = require_int_range(
367 'max_execution_time',
368 (int) ($config['max_execution_time'] ?? 5),
369 1,
370 60
371 );
372 $memory_threshold = require_float_range(
373 'memory_threshold',
374 (float) ($config['memory_threshold'] ?? 0.8),
375 0.1,
376 0.95
377 );
378
379 $memory_limit = ini_get('memory_limit');
380 $max_memory = $memory_limit === '-1' ? PHP_INT_MAX : parse_size((string) $memory_limit);
381
382 return new ResourceBudget(
383 microtime(true),
384 $max_execution_time,
385 $max_memory,
386 $memory_threshold
387 );
388 }
389
390 private function get_valid_endpoints_message(): string {
391 return "'" . implode("', '", array_keys($this->handlers)) . "'";
392 }
393 }
394