Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/google-site-kit/gtg/measurement.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 +
3 + /**
4 + * GoogleTagGatewayServing measurement request proxy file
5 + *
6 + * @package Google\GoogleTagGatewayLibrary\Proxy
7 + * @copyright 2024 Google LLC
8 + * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
9 + *
10 + * @version a8ee614
11 + *
12 + * NOTICE: This file has been modified from its original version in accordance with the Apache License, Version 2.0.
13 + */
14 +
15 + // This file should run in isolation from any other PHP file. This means using
16 + // minimal to no external dependencies, which leads us to suppressing the
17 + // following linting rules:
18 + //
19 + // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
20 + // phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
21 +
22 + /* Start of Site Kit modified code. */
23 + namespace {
24 + if ( isset( $_GET['healthCheck'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
25 + echo 'ok';
26 + exit;
27 + }
28 + // Return early when including to use in external health check.
29 + // All classes will be defined but no further statements will be executed.
30 + if ( defined( 'GOOGLESITEKIT_GTG_ENDPOINT_HEALTH_CHECK' ) ) {
31 + return;
32 + }
33 + }
34 + /* End of Site Kit modified code. */
35 +
36 + namespace Google\GoogleTagGatewayLibrary\Proxy {
37 + use Google\GoogleTagGatewayLibrary\Http\RequestHelper;
38 + use Google\GoogleTagGatewayLibrary\Http\ServerRequestContext;
39 +
40 + /** Runner class to execute the proxy request. */
41 + final class Runner
42 + {
43 + /**
44 + * Request helper functions.
45 + *
46 + * @var RequestHelper
47 + */
48 + private RequestHelper $helper;
49 + /**
50 + * Measurement request helper.
51 + *
52 + * @var Measurement
53 + */
54 + private Measurement $measurement;
55 + /**
56 + * Constructor.
57 + *
58 + * @param RequestHelper $helper
59 + */
60 + public function __construct(RequestHelper $helper, Measurement $measurement)
61 + {
62 + $this->helper = $helper;
63 + $this->measurement = $measurement;
64 + }
65 + /** Run the core logic for forwarding traffic. */
66 + public function run(): void
67 + {
68 + $response = $this->measurement->run();
69 + $this->helper->setHeaders($response['headers']);
70 + http_response_code($response['statusCode']);
71 + echo $response['body'];
72 + }
73 + /** Create an instance of the runner with the system defaults. */
74 + public static function create()
75 + {
76 + $helper = new RequestHelper();
77 + $context = ServerRequestContext::create();
78 + $measurement = new Measurement($helper, $context);
79 + return new self($helper, $measurement);
80 + }
81 + }
82 + }
83 +
84 + namespace Google\GoogleTagGatewayLibrary\Http {
85 + /**
86 + * Isolates network requests and other methods like exit to inject into classes.
87 + */
88 + class RequestHelper
89 + {
90 + /**
91 + * Helper method to exit the script early and send back a status code.
92 + *
93 + * @param int $statusCode
94 + */
95 + public function invalidRequest(int $statusCode): void
96 + {
97 + http_response_code($statusCode);
98 + exit;
99 + }
100 + /**
101 + * Set the headers from a headers array.
102 + *
103 + * @param string[] $headers
104 + */
105 + public function setHeaders(array $headers): void
106 + {
107 + foreach ($headers as $header) {
108 + if (!empty($header)) {
109 + header($header);
110 + }
111 + }
112 + }
113 + /**
114 + * Sanitizes a path to a URL path.
115 + *
116 + * This function performs two critical actions:
117 + * 1. Extract ONLY the path component, discarding any scheme, host, port,
118 + * user, pass, query, or fragment.
119 + * Primary defense against Server-Side Request Forgery (SSRF).
120 + * 2. Normalize the path to resolve directory traversal segments like
121 + * '.' and '..'.
122 + * Prevents path traversal attacks.
123 + *
124 + * @param string $pathInput The raw path string.
125 + * @return string|false The sanitized and normalized URL path.
126 + */
127 + public static function sanitizePathForUrl(string $pathInput): string
128 + {
129 + if (empty($pathInput)) {
130 + return false;
131 + }
132 + // Normalize directory separators to forward slashes for Windows like directories.
133 + $path = str_replace('\\', '/', $pathInput);
134 + // 2. Normalize the path to resolve '..' and '.' segments.
135 + $parts = [];
136 + // Explode the path into segments. filter removes empty segments (e.g., from '//').
137 + $segments = explode('/', trim($path, '/'));
138 + foreach ($segments as $segment) {
139 + if ($segment === '.' || $segment === '') {
140 + // Ignore current directory and empty segments.
141 + continue;
142 + }
143 + if ($segment === '..') {
144 + // Go up one level by removing the last part.
145 + if (array_pop($parts) === null) {
146 + // If we try and traverse too far back, outside of the root
147 + // directory, this is likely an invalid configuration so
148 + // return false to have caller handle this error.
149 + return false;
150 + }
151 + } else {
152 + // Add the segment to our clean path.
153 + $parts[] = rawurlencode($segment);
154 + }
155 + }
156 + // Rebuild the final path.
157 + $sanitizedPath = implode('/', $parts);
158 + return '/' . $sanitizedPath;
159 + }
160 + /**
161 + * Helper method to send requests depending on the PHP environment.
162 + *
163 + * @param string $method
164 + * @param string $url
165 + * @param array $headers
166 + * @param string $body
167 + *
168 + * @return array{
169 + * body: string,
170 + * headers: string[],
171 + * statusCode: int,
172 + * }
173 + */
174 + public function sendRequest(string $method, string $url, array $headers = [], ?string $body = null): array
175 + {
176 + if ($this->isCurlInstalled()) {
177 + $response = $this->sendCurlRequest($method, $url, $headers, $body);
178 + } else {
179 + $response = $this->sendFileGetContents($method, $url, $headers, $body);
180 + }
181 + return $response;
182 + }
183 + /**
184 + * Send a request using curl.
185 + *
186 + * @param string $method
187 + * @param string $url
188 + * @param array $headers
189 + * @param string $body
190 + *
191 + * @return array{
192 + * body: string,
193 + * headers: string[],
194 + * statusCode: int,
195 + * }
196 + */
197 + protected function sendCurlRequest(string $method, string $url, array $headers = [], ?string $body = null): array
198 + {
199 + $ch = curl_init();
200 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
201 + curl_setopt($ch, CURLOPT_HEADER, true);
202 + curl_setopt($ch, CURLOPT_URL, $url);
203 + $method = strtoupper($method);
204 + switch ($method) {
205 + case 'GET':
206 + curl_setopt($ch, CURLOPT_HTTPGET, true);
207 + break;
208 + case 'POST':
209 + curl_setopt($ch, CURLOPT_POST, true);
210 + break;
211 + default:
212 + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
213 + break;
214 + }
215 + if (!empty($headers)) {
216 + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
217 + }
218 + if (!empty($body)) {
219 + curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
220 + }
221 + $result = curl_exec($ch);
222 + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
223 + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
224 + $headersString = substr($result, 0, $headerSize);
225 + $headers = explode("\r\n", $headersString);
226 + $headers = $this->normalizeHeaders($headers);
227 + $body = substr($result, $headerSize);
228 + curl_close($ch);
229 + return array('body' => $body, 'headers' => $headers, 'statusCode' => $statusCode);
230 + }
231 + /**
232 + * Send a request using file_get_contents.
233 + *
234 + * @param string $method
235 + * @param string $url
236 + * @param array $headers
237 + * @param string $body
238 + *
239 + * @return array{
240 + * body: string,
241 + * headers: string[],
242 + * statusCode: int,
243 + * }
244 + */
245 + protected function sendFileGetContents(string $method, string $url, array $headers = [], ?string $body = null): array
246 + {
247 + $httpContext = ['method' => strtoupper($method), 'follow_location' => 0, 'max_redirects' => 0, 'ignore_errors' => true];
248 + if (!empty($headers)) {
249 + $httpContext['header'] = implode("\r\n", $headers);
250 + }
251 + if (!empty($body)) {
252 + $httpContext['content'] = $body;
253 + }
254 + $streamContext = stream_context_create(['http' => $httpContext]);
255 + // Calling file_get_contents will set the variable $http_response_header
256 + // within the local scope.
257 + $result = file_get_contents($url, false, $streamContext);
258 + /** @var string[] $headers */
259 + $headers = $http_response_header ?? [];
260 + $statusCode = 200;
261 + if (!empty($headers)) {
262 + // The first element in the headers array will be the HTTP version
263 + // and status code used, parse out the status code and remove this
264 + // value from the headers.
265 + preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers[0], $statusHeader);
266 + $statusCode = intval($statusHeader[1]) ?? 200;
267 + }
268 + $headers = $this->normalizeHeaders($headers);
269 + return array('body' => $result, 'headers' => $headers, 'statusCode' => $statusCode);
270 + }
271 + protected function isCurlInstalled(): bool
272 + {
273 + return extension_loaded('curl');
274 + }
275 + /** @param string[] $headers */
276 + protected function normalizeHeaders(array $headers): array
277 + {
278 + if (empty($headers)) {
279 + return $headers;
280 + }
281 + // The first element in the headers array will be the HTTP version
282 + // and status code used, this value is not needed in the headers.
283 + array_shift($headers);
284 + return $headers;
285 + }
286 + /**
287 + * Takes a single URL query parameter which has not been encoded and
288 + * ensures its key & value are encoded.
289 + *
290 + * @param string $parameter Query parameter to encode.
291 + * @return string The new query parameter encoded.
292 + */
293 + public static function encodeQueryParameter(string $parameter): string
294 + {
295 + list($key, $value) = explode('=', $parameter, 2) + ['', ''];
296 + // We just manually encode to avoid any nuances that may occur as a
297 + // result of `http_build_query`. One such nuance is that
298 + // `http_build_query` will add an index to query parameters that
299 + // are repeated through an array. We would only be able to store
300 + // repeated values as an array as associative arrays cannot have the
301 + // same key multiple times. This makes `http_build_query`
302 + // undesirable as we should pass parameters through as they come in
303 + // and not modify them or change the key.
304 + $key = rawurlencode($key);
305 + $value = rawurlencode($value);
306 + return "{$key}={$value}";
307 + }
308 + }
309 + }
310 +
311 + namespace Google\GoogleTagGatewayLibrary\Http {
312 + /** Request context populated with common server set values. */
313 + final class ServerRequestContext
314 + {
315 + /**
316 + * Server set associative array. Normally the same as $_SERVER.
317 + *
318 + * @var array
319 + */
320 + private $serverParams;
321 + /**
322 + * Associative array of query parameters. Normally the same as $_GET
323 + *
324 + * @var array
325 + */
326 + private $queryParams;
327 + /**
328 + * The current server request's body.
329 + *
330 + * @var string
331 + */
332 + private $requestBody;
333 + /**
334 + * Constructor
335 + *
336 + * @param array $serverParams
337 + * @param array $queryParams
338 + * @param string $requestBody
339 + */
340 + public function __construct(array $serverParams, array $queryParams, string $requestBody)
341 + {
342 + $this->serverParams = $serverParams;
343 + $this->queryParams = $queryParams;
344 + $this->requestBody = $requestBody;
345 + }
346 + /** Create an instance with the system defaults. */
347 + public static function create()
348 + {
349 + $body = file_get_contents("php://input") ?? '';
350 + return new self($_SERVER, $_GET, $body);
351 + }
352 + /**
353 + * Fetch the current request's request body.
354 + *
355 + * @return string The current request body.
356 + */
357 + public function getBody(): string
358 + {
359 + return $this->requestBody ?? '';
360 + }
361 + public function getRedirectorFile()
362 + {
363 + $redirectorFile = $this->serverParams['SCRIPT_NAME'] ?? '';
364 + if (empty($redirectorFile)) {
365 + return '';
366 + }
367 + return RequestHelper::sanitizePathForUrl($redirectorFile);
368 + }
369 + /**
370 + * Get headers from the current request as an array of strings.
371 + * Similar to how you set headers using the `headers` function.
372 + *
373 + * @param array $filterHeaders Filter out headers from the return value.
374 + */
375 + public function getHeaders(array $filterHeaders = []): array
376 + {
377 + $headers = [];
378 + // Extra headers not prefixed with `HTTP_`
379 + $extraHeaders = ["CONTENT_TYPE" => 'content-type', "CONTENT_LENGTH" => 'content-length', "CONTENT_MD5" => 'content-md5'];
380 + foreach ($this->serverParams as $key => $value) {
381 + # Skip reserved headers
382 + if (isset($filterHeaders[$key])) {
383 + continue;
384 + }
385 + # All PHP request headers are available under the $_SERVER variable
386 + # and have a key prefixed with `HTTP_` according to:
387 + # https://www.php.net/manual/en/reserved.variables.server.php#refsect1-reserved.variables.server-description
388 + $headerKey = '';
389 + if (substr($key, 0, 5) === 'HTTP_') {
390 + # PHP defaults to every header key being all capitalized.
391 + # Format header key as lowercase with `-` as word separator.
392 + # For example: cache-control
393 + $headerKey = strtolower(str_replace('_', '-', substr($key, 5)));
394 + } elseif (isset($extraHeaders[$key])) {
395 + $headerKey = $extraHeaders[$key];
396 + }
397 + if (empty($headerKey) || empty($value)) {
398 + continue;
399 + }
400 + $headers[] = "{$headerKey}: {$value}";
401 + }
402 + // Add extra x-forwarded-for if remote address is present.
403 + if (isset($this->serverParams['REMOTE_ADDR'])) {
404 + $headers[] = "x-forwarded-for: {$this->serverParams['REMOTE_ADDR']}";
405 + }
406 + // Add extra geo if present in the query parameters.
407 + $geo = $this->getGeoParam();
408 + if (!empty($geo)) {
409 + $headers[] = "x-forwarded-countryregion: {$geo}";
410 + }
411 + return $headers;
412 + }
413 + /**
414 + * Get the request method made for the current request.
415 + *
416 + * @return string
417 + */
418 + public function getMethod()
419 + {
420 + return @$this->serverParams['REQUEST_METHOD'] ?: 'GET';
421 + }
422 + /** Get and validate the geo parameter from the request. */
423 + public function getGeoParam()
424 + {
425 + $geo = $this->queryParams['geo'] ?? '';
426 + // Basic geo validation
427 + if (!preg_match('/^[A-Za-z0-9-]+$/', $geo)) {
428 + return '';
429 + }
430 + return $geo;
431 + }
432 + /** Get the tag id query parameter from the request. */
433 + public function getTagId()
434 + {
435 + $tagId = $this->queryParams['id'] ?? '';
436 + // Validate tagId
437 + if (!preg_match('/^[A-Za-z0-9-]+$/', $tagId)) {
438 + return '';
439 + }
440 + return $tagId;
441 + }
442 + /** Get the destination query parameter from the request. */
443 + public function getDestination()
444 + {
445 + $path = $this->queryParams['s'] ?? '';
446 + // When measurement path is present it might accidentally pass an empty
447 + // path character depending on how the url rules are processed so as a
448 + // safety when path is empty we should assume that it is a request to
449 + // the root.
450 + if (empty($path)) {
451 + $path = '/';
452 + }
453 + // Remove reserved query parameters from the query string
454 + $params = $this->queryParams;
455 + unset($params['id'], $params['s'], $params['geo'], $params['mpath']);
456 + $containsQueryParameters = strpos($path, '?') !== false;
457 + if ($containsQueryParameters) {
458 + list($path, $query) = explode('?', $path, 2);
459 + $path .= '?' . RequestHelper::encodeQueryParameter($query);
460 + }
461 + if (!empty($params)) {
462 + $paramSeparator = $containsQueryParameters ? '&' : '?';
463 + $path .= $paramSeparator . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
464 + }
465 + return $path;
466 + }
467 + /**Get the measurement path query parameter from the request. */
468 + public function getMeasurementPath()
469 + {
470 + return $this->queryParams['mpath'] ?? '';
471 + }
472 + }
473 + }
474 +
475 + namespace Google\GoogleTagGatewayLibrary\Proxy {
476 + use Google\GoogleTagGatewayLibrary\Http\RequestHelper;
477 + use Google\GoogleTagGatewayLibrary\Http\ServerRequestContext;
478 + /** Core measurement.php logic. */
479 + final class Measurement
480 + {
481 + private const TAG_ID_QUERY = '?id=';
482 + private const GEO_QUERY = '&geo=';
483 + private const PATH_QUERY = '&s=';
484 + private const FPS_PATH = 'PHP_GTG_REPLACE_PATH';
485 + /**
486 + * Reserved request headers that should not be sent as part of the
487 + * measurement request.
488 + *
489 + * @var array<string, bool>
490 + */
491 + private const RESERVED_HEADERS = [
492 + # PHP managed headers which will be auto populated by curl or file_get_contents.
493 + 'HTTP_ACCEPT_ENCODING' => true,
494 + 'HTTP_CONNECTION' => true,
495 + 'HTTP_CONTENT_LENGTH' => true,
496 + 'CONTENT_LENGTH' => true,
497 + 'HTTP_EXPECT' => true,
498 + 'HTTP_HOST' => true,
499 + 'HTTP_TRANSFER_ENCODING' => true,
500 + # Sensitive headers to exclude from all requests.
501 + 'HTTP_AUTHORIZATION' => true,
502 + 'HTTP_PROXY_AUTHORIZATION' => true,
503 + 'HTTP_X_API_KEY' => true,
504 + ];
505 + /**
506 + * Request helper.
507 + *
508 + * @var RequestHelper
509 + */
510 + private RequestHelper $helper;
511 + /**
512 + * Server request context.
513 + *
514 + * @var ServerRequestContext
515 + */
516 + private ServerRequestContext $serverRequest;
517 + /**
518 + * Create the measurement request handler.
519 + *
520 + * @param RequestHelper $helper
521 + * @param ServerRequestContext $serverReqeust
522 + */
523 + public function __construct(RequestHelper $helper, ServerRequestContext $serverRequest)
524 + {
525 + $this->helper = $helper;
526 + $this->serverRequest = $serverRequest;
527 + }
528 + /** Run the measurement logic. */
529 + public function run()
530 + {
531 + $redirectorFile = $this->serverRequest->getRedirectorFile();
532 + if (empty($redirectorFile)) {
533 + $this->helper->invalidRequest(500);
534 + return "";
535 + }
536 + $tagId = $this->serverRequest->getTagId();
537 + $path = $this->serverRequest->getDestination();
538 + $geo = $this->serverRequest->getGeoParam();
539 + $mpath = $this->serverRequest->getMeasurementPath();
540 + if (empty($tagId) || empty($path)) {
541 + $this->helper->invalidRequest(400);
542 + return "";
543 + }
544 + $useMpath = empty($mpath) ? self::FPS_PATH : $mpath;
545 + $fpsUrl = 'https://' . $tagId . '.fps.goog/' . $useMpath . $path;
546 + $requestHeaders = $this->serverRequest->getHeaders(self::RESERVED_HEADERS);
547 + $method = $this->serverRequest->getMethod();
548 + $body = $this->serverRequest->getBody();
549 + $response = $this->helper->sendRequest($method, $fpsUrl, $requestHeaders, $body);
550 + if ($useMpath === self::FPS_PATH) {
551 + $substitutionMpath = $redirectorFile . self::TAG_ID_QUERY . $tagId;
552 + if (!empty($geo)) {
553 + $substitutionMpath .= self::GEO_QUERY . $geo;
554 + }
555 + $substitutionMpath .= self::PATH_QUERY;
556 + if (self::isScriptResponse($response['headers'])) {
557 + $response['body'] = str_replace('/' . self::FPS_PATH . '/', $substitutionMpath, $response['body']);
558 + } elseif (self::isRedirectResponse($response['statusCode']) && !empty($response['headers'])) {
559 + foreach ($response['headers'] as $refKey => $header) {
560 + // Ensure we are only processing strings.
561 + if (!is_string($header)) {
562 + continue;
563 + }
564 + $headerParts = explode(':', $response['headers'][$refKey], 2);
565 + if (count($headerParts) !== 2) {
566 + continue;
567 + }
568 + $key = trim($headerParts[0]);
569 + $value = trim($headerParts[1]);
570 + if (strtolower($key) !== 'location') {
571 + continue;
572 + }
573 + $newValue = str_replace('/' . self::FPS_PATH, $substitutionMpath, $value);
574 + $response['headers'][$refKey] = "{$key}: {$newValue}";
575 + break;
576 + }
577 + }
578 + }
579 + return $response;
580 + }
581 + /**
582 + * @param string[] $headers
583 + */
584 + private static function isScriptResponse(array $headers): bool
585 + {
586 + if (empty($headers)) {
587 + return false;
588 + }
589 + foreach ($headers as $header) {
590 + if (empty($header)) {
591 + continue;
592 + }
593 + $normalizedHeader = strtolower(str_replace(' ', '', $header));
594 + if (strpos($normalizedHeader, 'content-type:application/javascript') === 0) {
595 + return true;
596 + }
597 + }
598 + return false;
599 + }
600 + /**
601 + * Checks if the response is a redirect response.
602 + * @param int $statusCode
603 + */
604 + private static function isRedirectResponse(int $statusCode): bool
605 + {
606 + return $statusCode >= 300 && $statusCode < 400;
607 + }
608 + }
609 + }
610 +
611 + namespace {
612 + use Google\GoogleTagGatewayLibrary\Proxy\Runner;
613 + Runner::create()->run();
614 + }
615 +