PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 trunk, at includes/core/QueryStringHelper.php

153 lines 5.8 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 // Tolerate a caller that did not unslash first. wp_magic_quotes()
114 // escapes the apostrophes encodeURI() leaves literal, and json_decode
115 // rejects the resulting backslash-apostrophe as an escape sequence.
116 // Ajax_UpdateOptions unslashes at the boundary now; this stays so the
117 // parser never breaks on valid input from any other caller.
118 $dataStripped = str_replace("\'", "'", $dataDecoded);
119 $fixedData = json_decode($dataStripped, true);
120
121 $jsonErrorNumber = json_last_error();
122 if ($jsonErrorNumber != 0) {
123 $errorMsg = json_last_error_msg();
124 $lastMessagePart = ", Decoded: " . $dataDecoded;
125 if ($dataStripped != null && mb_strlen($dataStripped) > 1) {
126 $lastMessagePart = ", Stripped: " . $dataStripped;
127 }
128
129 $logger = $this->resolveLogger();
130 if ($logger !== null) {
131 $logger->errorMessage("Error " . $jsonErrorNumber . " parsing JSON in "
132 . __CLASS__ . "->" . __FUNCTION__ . "(). Error message: " . $errorMsg . $lastMessagePart);
133 }
134 }
135
136 return $fixedData;
137 }
138
139 /** @return ABJ_404_Solution_Logging|null */
140 private function resolveLogger() {
141 if ($this->logger !== null) {
142 return $this->logger;
143 }
144 if (function_exists('abj_service_optional')) {
145 $resolved = abj_service_optional('logging');
146 if ($resolved instanceof ABJ_404_Solution_Logging) {
147 return $resolved;
148 }
149 }
150 return null;
151 }
152 }
153