338 lines
9.1 KiB
PHP
338 lines
9.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* FirstPartyServing redirect file
|
|
*
|
|
* @package Google\FirstPartyLibrary
|
|
* @copyright 2024 Google LLC
|
|
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
|
*
|
|
* @version 288a45a
|
|
*
|
|
* NOTICE: This file has been modified from its original version in accordance with the Apache License, Version 2.0.
|
|
*/
|
|
|
|
// This file should run in isolation from any other PHP file. This means using
|
|
// minimal to no external dependencies, which leads us to suppressing the
|
|
// following linting rules:
|
|
//
|
|
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
|
|
// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
|
|
|
|
namespace Google\FirstPartyLibrary;
|
|
|
|
/* Start of Site Kit modified code. */
|
|
if ( isset( $_GET['healthCheck'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
|
echo 'ok';
|
|
exit;
|
|
}
|
|
/* End of Site Kit modified code. */
|
|
|
|
/** Core measurement.php logic. */
|
|
final class Measurement
|
|
{
|
|
private const TAG_ID_QUERY = '?id=';
|
|
private const PATH_QUERY = '&s=';
|
|
private const FPS_PATH = 'PHP_FPM_REPLACE_PATH';
|
|
|
|
private RequestHelper $helper;
|
|
|
|
/**
|
|
* Create the measurement request handler.
|
|
*
|
|
* @param RequestHelper $helper
|
|
*/
|
|
public function __construct(RequestHelper $helper)
|
|
{
|
|
$this->helper = $helper;
|
|
}
|
|
|
|
/** Run the measurement logic. */
|
|
public function run()
|
|
{
|
|
$redirectorFile = $_SERVER['SCRIPT_NAME'] ?? '';
|
|
if (empty($redirectorFile)) {
|
|
$this->helper->invalidRequest(500);
|
|
return "";
|
|
}
|
|
|
|
$parameters = self::extractParameters();
|
|
|
|
$tagId = $parameters['tag_id'];
|
|
$path = $parameters['path'];
|
|
|
|
if (empty($tagId) || empty($path)) {
|
|
$this->helper->invalidRequest(400);
|
|
return "";
|
|
}
|
|
|
|
if (!self::isScriptRequest($path)) {
|
|
$path = self::appendRequestIP($path);
|
|
}
|
|
|
|
$fpsUrl = 'https://' . $tagId . '.fps.goog/' . self::FPS_PATH . $path;
|
|
|
|
$response = $this->helper->sendRequest($fpsUrl);
|
|
if (self::isScriptResponse($response['headers'])) {
|
|
$response['body'] = str_replace(
|
|
'/' . self::FPS_PATH . '/',
|
|
$redirectorFile . self::TAG_ID_QUERY . $tagId . self::PATH_QUERY,
|
|
$response['body']
|
|
);
|
|
}
|
|
return $response;
|
|
}
|
|
|
|
private static function appendRequestIP($path)
|
|
{
|
|
if (!isset($_SERVER['REMOTE_ADDR'])) {
|
|
return $path;
|
|
}
|
|
|
|
$requestIP = $_SERVER['REMOTE_ADDR'];
|
|
// Use x-forwarded-for IP if behind a proxy
|
|
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$requestIP = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
|
}
|
|
$requestIP = urlencode($requestIP);
|
|
|
|
$gaPath = "/g/collect";
|
|
if (false !== strpos($path, $gaPath)) {
|
|
return $path . '&_uip=' . $requestIP;
|
|
} else {
|
|
return $path . '&uip=' . $requestIP;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use best effort for determining if a request path is a script request.
|
|
*
|
|
* @param string $requestPath
|
|
* @return bool
|
|
*/
|
|
private static function isScriptRequest(string $requestPath): bool
|
|
{
|
|
return substr($requestPath, 0, 7) === "/gtm.js"
|
|
|| substr($requestPath, 0, 8) === "/gtag.js"
|
|
|| substr($requestPath, 0, 8) === "/gtag/js";
|
|
}
|
|
|
|
/**
|
|
* @param string[] $headers
|
|
*/
|
|
private static function isScriptResponse(array $headers): bool
|
|
{
|
|
if (empty($headers)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($headers as $header) {
|
|
if (empty($header)) {
|
|
continue;
|
|
}
|
|
|
|
$normalizedHeader = strtolower(str_replace(' ', '', $header));
|
|
if (strpos($normalizedHeader, 'content-type:application/javascript') === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static function extractParameters(): array
|
|
{
|
|
$get = $_GET;
|
|
if (empty($get)) {
|
|
return array(
|
|
"tag_id" => '',
|
|
"path" => '',
|
|
);
|
|
}
|
|
|
|
$tagId = $get['id'] ?? '';
|
|
$path = $get['s'] ?? '';
|
|
|
|
// Validate tagId
|
|
if (!preg_match('/^[A-Za-z0-9-]*$/', $tagId)) {
|
|
return array(
|
|
"tag_id" => '',
|
|
"path" => '',
|
|
);
|
|
}
|
|
|
|
unset($get['id'], $get['s']);
|
|
|
|
if (!empty($get)) {
|
|
$containsQueryParameters = strpos($path, '?') !== false;
|
|
$paramSeparator = $containsQueryParameters ? '&' : '?';
|
|
$path .= $paramSeparator . http_build_query($get, '', '&', PHP_QUERY_RFC3986);
|
|
}
|
|
|
|
return array(
|
|
"tag_id" => $tagId,
|
|
"path" => $path,
|
|
);
|
|
}
|
|
}
|
|
|
|
// REQUEST_HELPER_START
|
|
/**
|
|
* NOTE: DO NOT edit RequestHelper directly nor remove the start and end tags.
|
|
*
|
|
* This class is copied over from src/RequestHelper.php. If any changes are
|
|
* needed, change that file and run the command `npm run copy:RequestHelper`.
|
|
*/
|
|
/**
|
|
* Isolates network requests and other methods like exit to inject into classes.
|
|
*/
|
|
class RequestHelper
|
|
{
|
|
/**
|
|
* Helper method to exit the script early and send back a status code.
|
|
*
|
|
* @param int $statusCode
|
|
*/
|
|
public function invalidRequest(int $statusCode): void
|
|
{
|
|
http_response_code($statusCode);
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Set the headers from a headers array.
|
|
*
|
|
* @param string[] $headers
|
|
*/
|
|
public function setHeaders(array $headers): void
|
|
{
|
|
foreach ($headers as $header) {
|
|
if (!empty($header)) {
|
|
header($header);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to send requests depending on the PHP environment.
|
|
*
|
|
* @param string $url
|
|
* @return array{
|
|
* body: string,
|
|
* headers: string[],
|
|
* statusCode: int,
|
|
* }
|
|
*/
|
|
public function sendRequest(string $url): array
|
|
{
|
|
if ($this->isCurlInstalled()) {
|
|
$response = $this->sendCurlRequest($url);
|
|
} else {
|
|
$response = $this->sendFileGetContents($url);
|
|
}
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* @param string $url
|
|
* @return array{
|
|
* body: string,
|
|
* headers: string[],
|
|
* statusCode: int,
|
|
* }
|
|
*/
|
|
protected function sendCurlRequest(string $url): array
|
|
{
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HEADER, true);
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
|
|
$result = curl_exec($ch);
|
|
|
|
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
|
$headersString = substr($result, 0, $headerSize);
|
|
$headers = explode("\r\n", $headersString);
|
|
$headers = $this->normalizeHeaders($headers);
|
|
|
|
$body = substr($result, $headerSize);
|
|
|
|
curl_close($ch);
|
|
|
|
return array(
|
|
'body' => $body,
|
|
'headers' => $headers,
|
|
'statusCode' => $statusCode,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string $url
|
|
* @return array{
|
|
* body: string,
|
|
* headers: string[],
|
|
* statusCode: int,
|
|
* }
|
|
*/
|
|
protected function sendFileGetContents(string $url): array
|
|
{
|
|
$streamContext = stream_context_create(array(
|
|
'http' => array(
|
|
'method' => 'GET',
|
|
)
|
|
));
|
|
|
|
// Calling file_get_contents will set the variable $http_response_header
|
|
// within the local scope.
|
|
$result = file_get_contents($url, false, $streamContext);
|
|
|
|
/** @var string[] $headers */
|
|
$headers = $http_response_header ?? [];
|
|
|
|
$statusCode = 200;
|
|
if (!empty($headers)) {
|
|
// The first element in the headers array will be the HTTP version
|
|
// and status code used, parse out the status code and remove this
|
|
// value from the headers.
|
|
preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers[0], $statusHeader);
|
|
$statusCode = intval($statusHeader[1]) ?? 200;
|
|
}
|
|
$headers = $this->normalizeHeaders($headers);
|
|
|
|
return array(
|
|
'body' => $result,
|
|
'headers' => $headers,
|
|
'statusCode' => $statusCode,
|
|
);
|
|
}
|
|
|
|
protected function isCurlInstalled(): bool
|
|
{
|
|
return extension_loaded('curl');
|
|
}
|
|
|
|
/** @param string[] $headers */
|
|
protected function normalizeHeaders(array $headers): array
|
|
{
|
|
if (empty($headers)) {
|
|
return $headers;
|
|
}
|
|
|
|
// The first element in the headers array will be the HTTP version
|
|
// and status code used, this value is not needed in the headers.
|
|
array_shift($headers);
|
|
return $headers;
|
|
}
|
|
}
|
|
// REQUEST_HELPER_END
|
|
|
|
// Skip initial run for testing
|
|
if (!defined('IS_FIRST_PARTY_MODE_TEST')) {
|
|
$requestHelper = new RequestHelper();
|
|
$response = (new Measurement($requestHelper))->run();
|
|
|
|
$requestHelper->setHeaders($response['headers']);
|
|
http_response_code($response['statusCode']);
|
|
echo $response['body'];
|
|
}
|