PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / QueryStringHelper.php

QueryStringHelper.php in 404 Solution 4.3.0, at includes/core/QueryStringHelper.php

149 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * URL query-string and JSON-in-querystring transformation service.
10 *
11 * Responsibilities:
12 * - sortQueryString: produce a canonical, alphabetized rebuild of a parsed
13 * URL's `query` part so identical requests with reordered parameters
14 * hash to the same storage key.
15 * - removePageIDFromQueryString: strip `p=N` from a query string so a
16 * redirect destination does not re-trigger a 404 via WordPress's
17 * page-id parameter.
18 * - decodeComplicatedData: urldecode then json_decode an encoded request
19 * payload (with JSON.stringify single-quote unescape), used by the
20 * AJAX update-options path that ships form data as a single
21 * URL-encoded JSON blob.
22 *
23 * Extracted from ABJ_404_Solution_Functions per design-audit-2026-06-02
24 * M201 (Functions.php grab-bag split, parent task i802). The three
25 * methods share the responsibility of normalizing query-string-shaped
26 * input/output across the dispatch and admin-AJAX surfaces; they are
27 * distinct from percent-encoding (UrlEncoder), control-byte
28 * sanitization (Sanitizer), and string primitives (MbStringAdapter).
29 *
30 * Depends on Sanitizer for sanitizeUrlComponent / normalizeUrlString
31 * (sortQueryString and removePageIDFromQueryString already routed
32 * through the sibling Sanitizer service) and on Logging for the JSON
33 * decode error path.
34 */
35 class ABJ_404_Solution_QueryStringHelper {
36
37 /** @var ABJ_404_Solution_Sanitizer */
38 private $sanitizer;
39
40 /** @var ABJ_404_Solution_Logging|null */
41 private $logger;
42
43 /**
44 * @param ABJ_404_Solution_Sanitizer $sanitizer
45 * @param ABJ_404_Solution_Logging|null $logger Optional: when omitted,
46 * decodeComplicatedData() lazy-resolves through the service container
47 * so early-boot and direct test instantiation still get logging.
48 */
49 public function __construct(ABJ_404_Solution_Sanitizer $sanitizer, $logger = null) {
50 $this->sanitizer = $sanitizer;
51 $this->logger = $logger;
52 }
53
54 /**
55 * Sort the QUERY parts of the requested URL.
56 * This is in place because these are stored as part of the URL in the database and used for forwarding to another page.
57 * This is done because sometimes different query parts result in a completely different page. Therefore we have to
58 * take into account the query part of the URL (?query=part) when looking for a page to redirect to.
59 *
60 * Here we sort the query parts so that the same request will always look the same.
61 *
62 * @param array<string, string> $urlParts
63 * @return string
64 */
65 public function sortQueryString(array $urlParts): string {
66 if (!array_key_exists('query', $urlParts) || $urlParts['query'] == '') {
67 return '';
68 }
69
70 $queryParts = array();
71 parse_str($urlParts['query'], $queryParts);
72
73 ksort($queryParts);
74
75 $sanitized = $this->sanitizer->sanitizeUrlComponent($queryParts);
76 $queryParts = is_array($sanitized) ? $sanitized : $queryParts;
77 $built = http_build_query($queryParts, '', '&', PHP_QUERY_RFC3986);
78 $decoded = rawurldecode($built);
79 return $this->sanitizer->normalizeUrlString($decoded, array('decode' => false));
80 }
81
82 /**
83 * We have to remove any 'p=##' because it will cause a 404 otherwise.
84 *
85 * @param string $queryString
86 * @return string
87 */
88 public function removePageIDFromQueryString($queryString) {
89 $queryParts = array();
90 parse_str($queryString, $queryParts);
91
92 if (array_key_exists('p', $queryParts)) {
93 unset($queryParts['p']);
94 }
95
96 $sanitized = $this->sanitizer->sanitizeUrlComponent($queryParts);
97 $queryParts = is_array($sanitized) ? $sanitized : $queryParts;
98 $built = http_build_query($queryParts, '', '&', PHP_QUERY_RFC3986);
99 $decoded = rawurldecode($built);
100 return $this->sanitizer->normalizeUrlString($decoded, array('decode' => false));
101 }
102
103 /**
104 * First urldecode then json_decode the data, then return it.
105 * All of this encoding and decoding is so that [] characters are supported.
106 *
107 * @param string $data
108 * @return mixed
109 */
110 public function decodeComplicatedData($data) {
111 $dataDecoded = urldecode((string)$data);
112
113 // JSON.stringify escapes single quotes and json_decode does not want them to be escaped.
114 $dataStripped = str_replace("\'", "'", $dataDecoded);
115 $fixedData = json_decode($dataStripped, true);
116
117 $jsonErrorNumber = json_last_error();
118 if ($jsonErrorNumber != 0) {
119 $errorMsg = json_last_error_msg();
120 $lastMessagePart = ", Decoded: " . $dataDecoded;
121 if ($dataStripped != null && mb_strlen($dataStripped) > 1) {
122 $lastMessagePart = ", Stripped: " . $dataStripped;
123 }
124
125 $logger = $this->resolveLogger();
126 if ($logger !== null) {
127 $logger->errorMessage("Error " . $jsonErrorNumber . " parsing JSON in "
128 . __CLASS__ . "->" . __FUNCTION__ . "(). Error message: " . $errorMsg . $lastMessagePart);
129 }
130 }
131
132 return $fixedData;
133 }
134
135 /** @return ABJ_404_Solution_Logging|null */
136 private function resolveLogger() {
137 if ($this->logger !== null) {
138 return $this->logger;
139 }
140 if (function_exists('abj_service_optional')) {
141 $resolved = abj_service_optional('logging');
142 if ($resolved instanceof ABJ_404_Solution_Logging) {
143 return $resolved;
144 }
145 }
146 return null;
147 }
148 }
149