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 / php / objs / UserRequest.php

UserRequest.php in 404 Solution trunk, at includes/php/objs/UserRequest.php

240 lines 9.0 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 /** Stores a message and its importance. */
9 class ABJ_404_Solution_UserRequest {
10
11 /** @var self|null */
12 private static $instance = null;
13
14 /** @var string|null */
15 private $requestURIWithoutCommentsPage = null;
16
17 /** @var string */
18 private $requestURI = null;
19
20 /** @var array<string, int|string>|null */
21 private $urlParts = null;
22
23 /** @var string */
24 private $queryString = null;
25
26 /** @var string */
27 private $commentPagePart = null;
28
29 /**
30 * Replace the singleton instance. Used by integration tests that need
31 * a pre-populated UserRequest (or a programmable test double) so the
32 * pipeline reads stable path / slug / query-string values without
33 * relying on `$_SERVER['REQUEST_URI']` being set during the test run.
34 *
35 * @param self|null $instance
36 * @return void
37 */
38 public static function setInstance($instance) {
39 self::$instance = $instance;
40 }
41
42 /** @return self|null */
43 public static function getInstance() {
44 if (self::$instance == null) {
45 if (!self::initialize()) {
46 $abj404logging = abj_service('logging');
47 $abj404logging->errorMessage('Issue initializing ' . __CLASS__,
48 new Exception('Issue initializing ' . __CLASS__));
49 }
50 }
51
52 return self::$instance;
53 }
54
55 /** @return bool */
56 public static function initialize(): bool {
57 global $wp_rewrite;
58
59 $abj404logging = abj_service('logging');
60 $f = abj_service('functions');
61 $abj404logic = abj_service('plugin_logic');
62 $sanitizer = abj_service('sanitizer');
63
64 $urlToParse = $sanitizer->normalizeUrlString($_SERVER['REQUEST_URI'] ?? '');
65
66 // if the user somehow requested an invalid URL that's too long then fix it.
67 if ($f->strlen($urlToParse) > ABJ404_MAX_URL_LENGTH) {
68 $matches = null;
69 $f->regexMatch("image (.+);base64,", $urlToParse, $matches);
70 if ($matches != null && $f->strlen($matches[0]) > 0) {
71 $instrPattern = $matches[0];
72 $truncateHere = $f->strpos($urlToParse, $instrPattern);
73 $truncatedRequest = $f->substr($urlToParse, 0, ($truncateHere !== false ? $truncateHere : null));
74 $urlToParse = $truncatedRequest;
75 }
76
77 if ($f->strlen($urlToParse) > ABJ404_MAX_URL_LENGTH) {
78 // just truncate it to something reasonable.
79 $urlToParse = $f->substr($urlToParse, 0, ABJ404_MAX_URL_LENGTH);
80 }
81 }
82
83 // hanlde the case where '///?gf_page=upload' is returned as the request URI.
84 $containsHost = $f->strpos($urlToParse, "://");
85
86 if (($containsHost === false) || ($containsHost >= 7) || (!is_array(parse_url(esc_url($urlToParse))))) {
87 // we have something like //login.php and it needs to be http://host.com/login.php
88 while ($f->strpos($urlToParse, "//") !== false) {
89 $urlToParse = $f->str_replace('//', '/', $urlToParse);
90 }
91 $urlToParse = ltrim($abj404logic->urlNormalization()->removeHomeDirectory($urlToParse), '/');
92 $urlToParse = get_site_url() . '/' . $urlToParse;
93 }
94
95 $urlParts = parse_url($urlToParse);
96 if (!is_array($urlParts)) {
97 $abj404logging->errorMessage('parse_url returned a non-array value. REQUEST_URI: "' .
98 $sanitizer->normalizeUrlString($_SERVER['REQUEST_URI']) . '", parse_url result: "' . json_encode($urlParts) . '", ' .
99 'urlToParse result: ' . $urlToParse);
100 return false;
101 }
102 // make things work with foreign languages while avoiding XSS issues.
103 foreach ($urlParts as $key => $value) {
104 if ($key === 'query') {
105 // For query strings, preserve reserved characters while removing invalid bytes.
106 parse_str($value, $queryArray);
107 $safeQueryArray = $sanitizer->sanitizeUrlComponent($queryArray);
108 $urlParts[$key] = http_build_query(is_array($safeQueryArray) ? $safeQueryArray : $queryArray);
109 } else {
110 // Sanitize path/host/etc. without stripping reserved URL characters.
111 $urlParts[$key] = $sanitizer->sanitizeUrlComponent($value);
112 }
113 }
114
115 // remove a pointless trailing /amp
116 $urlPath = isset($urlParts['path']) && is_string($urlParts['path']) ? $urlParts['path'] : '';
117 if ($urlPath !== '' &&
118 ($f->endsWithCaseInsensitive($urlPath, '/amp') ||
119 $f->endsWithCaseInsensitive($urlPath, '/amp/')
120 )
121 && $f->strlen($urlPath) >= 6) {
122 $urlParts['path'] = $f->substr($urlPath, 0, $f->strlen($urlPath) - 4);
123 }
124
125 // remove any "/comment-page-???/" if there is one.
126 /* tested with:
127 * http://localhost:8888/404solution-site/2019/02/hello-world2/comment-page-2/#comment-26
128 * http://localhost:8888/404solution-site/2019/02/hello-world2/comment-page-2/
129 * http://localhost:8888/404solution-site/2019/02/hello-world2/comment-page-2
130 * http://localhost:8888/404solution-site/2019/02/hello-world2/comment-page-2/?quer=true
131 */
132 // Fix for PHP 8.2: Handle URLs with no path component (e.g., http://example.com)
133 $urlWithoutCommentPage = (isset($urlParts['path']) && is_string($urlParts['path'])) ? $urlParts['path'] : '/';
134 $commentPagePart = '';
135 $results = array();
136 if (isset($wp_rewrite) && isset($wp_rewrite->comments_pagination_base)) {
137 $safeBase = preg_quote($wp_rewrite->comments_pagination_base);
138 $commentregex = '(.*)\/(' . $safeBase . '-[0-9]{1,})(\/|\z)?(.*)';
139 $f->regexMatch($commentregex, $urlWithoutCommentPage, $results);
140
141 if (!empty($results)) {
142 $urlWithoutCommentPage = $results[1];
143 $commentPagePart = $results[2];
144 $commentPagePart = ($commentPagePart == '') ? '' : $commentPagePart . '/';
145 }
146 }
147
148 $queryString = '';
149 if (!array_key_exists('query', $urlParts) || @$urlParts['query'] == "") {
150 $queryString = '';
151 } else {
152 $queryString = $urlParts['query'];
153 }
154
155 /** @var array<string, int|string> $urlPartsSafe */
156 $urlPartsSafe = $urlParts;
157 self::$instance = new ABJ_404_Solution_UserRequest(new ABJ_404_Solution_UserRequestParts(
158 $urlToParse, $urlPartsSafe, $urlWithoutCommentPage, $commentPagePart, $queryString));
159
160 return true;
161 }
162
163 /**
164 * @param ABJ_404_Solution_UserRequestParts $parts
165 */
166 private function __construct(ABJ_404_Solution_UserRequestParts $parts) {
167 $this->requestURI = $parts->requestURI;
168 $this->urlParts = $parts->urlParts;
169 $this->requestURIWithoutCommentsPage = $parts->urlWithoutCommentPage;
170 $this->commentPagePart = $parts->commentPagePart;
171
172 $this->queryString = $parts->queryString;
173 }
174
175 /** @return string|null */
176 function getRequestURI() {
177 return $this->requestURI;
178 }
179
180 /** @return string|null */
181 function getRequestURIWithoutCommentsPage() {
182 return $this->requestURIWithoutCommentsPage;
183 }
184
185 /** http://s.com/404solution-site/hello-world/comment-page-2/#comment-26?query_info=true becomes
186 * /404solution-site/hello-world/comment-page-2/
187 * @return string
188 */
189 function getPath() {
190 if ($this->urlParts === null || !array_key_exists('path', $this->urlParts)) {
191 // this happens for a request with no path. like http://example.com
192 return '';
193 }
194
195 return (string)($this->urlParts['path']);
196 }
197
198 /** @return string */
199 function getPathWithSortedQueryString(): string {
200 $requestedURL = $this->getPath();
201 /** @var array<string, string> $urlPartsForSort */
202 $urlPartsForSort = $this->getUrlParts() ?? array();
203 $urlParts = abj_service('query_string_helper')->sortQueryString($urlPartsForSort);
204 if ($urlParts != null && trim($urlParts) != '') {
205 $requestedURL .= '?' . $urlParts;
206 }
207
208 // otherwise various queries break.
209 $requestedURL = abj_service('url_encoder')->urlencodeEmojis($requestedURL);
210
211 return $requestedURL;
212 }
213
214 /** http://s.com/404solution-site/hello-world/comment-page-2/#comment-26?query_info=true becomes
215 * /hello-world/comment-page-2/
216 * @return string
217 */
218 function getOnlyTheSlug() {
219 $abj404logic = abj_service('plugin_logic');
220 $path = $this->getRequestURIWithoutCommentsPage();
221 return $abj404logic->urlNormalization()->removeHomeDirectory($path);
222 }
223
224 /** @return array<string, int|string>|null */
225 function getUrlParts() {
226 return $this->urlParts;
227 }
228
229 /** @return string|null */
230 function getQueryString() {
231 return $this->queryString;
232 }
233
234 /** @return string|null */
235 function getCommentPagePart() {
236 return $this->commentPagePart;
237 }
238
239 }
240