Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/google-site-kit/gtg/measurement.php
Keine Baseline-Datei – Diff nur gegen leer.
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
+