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 / Functions.php

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

497 lines 17.3 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 /* Static functions that can be used from anywhere. */
9 class ABJ_404_Solution_Functions {
10
11 /** @var self|null */
12 private static $instance = null;
13 /**
14 * Test seam: install or clear the cached singleton instance without
15 * private-field reflection. Pass null to reset between tests; pass a
16 * configured instance (or double) to install it. Mirrors the setInstance()
17 * contract on DataAccess / PluginLogic (M105 singleton-reset seam).
18 *
19 * @param self|null $instance
20 * @return void
21 */
22 public static function setInstance($instance) {
23 self::$instance = $instance;
24 }
25
26
27 /** @var ABJ_404_Solution_Logging|null */
28 protected $injectedLogging = null;
29
30 /** @var ABJ_404_Solution_RequestContext|null */
31 protected $injectedRequestContext = null;
32
33 /** @var ABJ_404_Solution_MbStringAdapter */
34 protected $mbAdapter;
35
36 /** @var ABJ_404_Solution_RegexHelper */
37 protected $regexHelper;
38
39 /**
40 * Collaborators are passed in by the DI container's 'functions' factory
41 * (see bootstrap.php). Nulls are tolerated for early-boot and direct
42 * test instantiation; logging() and requestContext() lazy-resolve in
43 * that case as a singular bootstrap-only fallback. The mbstring
44 * adapter and regex helper default to the platform-appropriate
45 * implementation so tests and early-boot callers do not have to wire
46 * them up explicitly.
47 *
48 * @param ABJ_404_Solution_Logging|null $logging
49 * @param ABJ_404_Solution_RequestContext|null $requestContext
50 * @param ABJ_404_Solution_MbStringAdapter|null $mbAdapter
51 * @param ABJ_404_Solution_RegexHelper|null $regexHelper
52 */
53 public function __construct($logging = null, $requestContext = null, $mbAdapter = null, $regexHelper = null) {
54 $this->injectedLogging = $logging;
55 $this->injectedRequestContext = $requestContext;
56 $this->mbAdapter = $mbAdapter !== null
57 ? $mbAdapter
58 : (extension_loaded('mbstring')
59 ? ABJ_404_Solution_MbStringAdapterMb::getInstance()
60 : ABJ_404_Solution_MbStringAdapterPreg::getInstance());
61 $this->regexHelper = $regexHelper !== null
62 ? $regexHelper
63 : (extension_loaded('mbstring')
64 ? ABJ_404_Solution_RegexHelperMb::getInstance()
65 : ABJ_404_Solution_RegexHelperPreg::getInstance());
66 }
67
68 /** @return self */
69 public static function getInstance() {
70 if (self::$instance !== null) {
71 return self::$instance;
72 }
73
74 // If the DI container is initialized, prefer it.
75 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
76 $service = ABJ_404_Solution_ServiceContainer::safeGet('functions');
77 if ($service instanceof self) {
78 self::$instance = $service;
79 return self::$instance;
80 }
81 }
82
83 self::$instance = new self();
84 return self::$instance;
85 }
86
87 /**
88 * Returns the polymorphic mbstring/preg adapter. Useful for callers
89 * that only need the string primitives and want to depend on a smaller
90 * interface than ABJ_404_Solution_Functions.
91 *
92 * @return ABJ_404_Solution_MbStringAdapter
93 */
94 public function getMbStringAdapter() {
95 return $this->mbAdapter;
96 }
97
98 /**
99 * Returns the polymorphic regex helper. Useful for callers that only
100 * need the regex primitives and want to depend on a smaller interface
101 * than ABJ_404_Solution_Functions.
102 *
103 * @return ABJ_404_Solution_RegexHelper
104 */
105 public function getRegexHelper() {
106 return $this->regexHelper;
107 }
108
109 /**
110 * Returns the injected Logging service, falling back to the locator
111 * when constructed outside the DI container (early boot / tests).
112 * Production paths go through DI via the bootstrap factory.
113 *
114 * @return ABJ_404_Solution_Logging
115 */
116 protected function logging() {
117 if ($this->injectedLogging !== null) {
118 return $this->injectedLogging;
119 }
120 return abj_service('logging');
121 }
122
123 /**
124 * Returns the injected RequestContext, falling back to the locator
125 * when constructed outside the DI container (early boot / tests).
126 *
127 * @return ABJ_404_Solution_RequestContext
128 */
129 protected function requestContext() {
130 if ($this->injectedRequestContext !== null) {
131 return $this->injectedRequestContext;
132 }
133 return abj_service('request_context');
134 }
135
136 /** Uses explode() to return an array.
137 * @param string $string
138 * @return array<int, string>
139 */
140 function explodeNewline(string $string): array {
141 $normalized = str_replace("\r\n", "\n", $string);
142 $normalized = str_replace('\n', "\n", $normalized);
143 $result = array_filter(explode("\n", $this->strtolower($normalized)),
144 array($this, 'removeEmptyCustom'));
145
146 return $result;
147 }
148
149 /**
150 * Like {@see explodeNewline()} but tolerates comma-separated input.
151 * Use for textarea-backed settings whose values are slug-like tokens
152 * (post type names, taxonomy slugs) that cannot legitimately contain
153 * commas. A site owner who types "post,page,product" instead of one
154 * per line should not silently break the suggestion engine (i321).
155 *
156 * Do NOT use for free-form values where commas are valid content
157 * (User-Agent strings, regex patterns, paths). Those callers must
158 * keep {@see explodeNewline()}.
159 *
160 * @param string $string
161 * @return array<int, string>
162 */
163 function explodeNewlineOrComma(string $string): array {
164 $normalized = str_replace("\r\n", "\n", $string);
165 $normalized = str_replace('\n', "\n", $normalized);
166 $normalized = str_replace(',', "\n", $normalized);
167 $result = array_filter(
168 array_map('trim', explode("\n", $this->strtolower($normalized))),
169 array($this, 'removeEmptyCustom')
170 );
171 return $result;
172 }
173
174 /**
175 * @param string|array<int, string> $needle
176 * @param string|array<int, mixed>|null $replacement
177 * @param string $haystack
178 * @return string
179 */
180 function str_replace($needle, $replacement, string $haystack): string {
181 if ($replacement === null) {
182 $replacement = '';
183 }
184 /** @var string $result */
185 $result = str_replace($needle, $replacement, $haystack);
186 return $result;
187 }
188
189 /**
190 * @param string $needle
191 * @param string $replacement
192 * @param string $haystack
193 * @return string
194 */
195 function single_str_replace(string $needle, string $replacement, string $haystack): string {
196 if ($haystack == "" || $this->strlen($haystack) == 0) {
197 return "";
198
199 } else if ($needle === '' || $this->strpos($haystack, $needle) === false) {
200 return $haystack;
201 }
202
203 $splitResult = explode($needle, $haystack);
204 $implodeResult = implode($replacement, $splitResult);
205
206 return $implodeResult;
207 }
208
209 /** Hash the last octet of an IP address.
210 * @param string $ip
211 * @return string
212 */
213 function md5lastOctet($ip) {
214 if (trim($ip) == "") {
215 return $ip;
216 }
217 $partsToStrip = 1;
218 $separatorChar = ".";
219
220 // split into parts
221 $parts = explode(".", $ip);
222 if (count($parts) == 1) {
223 $parts = explode(":", $ip);
224 // if exploding on : worked then assume we have an IPv6.
225 if (count($parts) > 1) {
226 $partsToStrip = max(count($parts) - 3, 1);
227 $separatorChar = ":";
228 }
229 }
230 $firstPart = implode($separatorChar, array_slice($parts, 0, count($parts) - $partsToStrip));
231 $partToHash = $parts[count($parts) - $partsToStrip];
232 $lastPart = $separatorChar . substr(base_convert(md5($partToHash), 16,32), 0, 12);
233
234 return $firstPart . $lastPart;
235 }
236
237 // =========================================================================
238 // mbstring / preg primitives - delegated to ABJ_404_Solution_MbStringAdapter
239 // =========================================================================
240
241 /** @return int */
242 function ord(string $char): int {
243 return $this->mbAdapter->ord($char);
244 }
245
246 /** @return string */
247 function strtolower(string $string): string {
248 return $this->mbAdapter->strtolower($string);
249 }
250
251 /** @return int */
252 function strlen(string $string): int {
253 return $this->mbAdapter->strlen($string);
254 }
255
256 /** @return int|false */
257 function strpos(string $haystack, string $needle, int $offset = 0) {
258 return $this->mbAdapter->strpos($haystack, $needle, $offset);
259 }
260
261 /** @return string */
262 function substr(?string $str, int $start, ?int $length = null): string {
263 return $this->mbAdapter->substr($str, $start, $length);
264 }
265
266 /**
267 * @param string|null $string
268 * @return string
269 */
270 function sanitizeInvalidUTF8(?string $string): string {
271 return $this->mbAdapter->sanitizeInvalidUTF8($string);
272 }
273
274 // =========================================================================
275 // Regex primitives - delegated to ABJ_404_Solution_RegexHelper
276 // =========================================================================
277
278 /**
279 * @param string $pattern
280 * @param string $string
281 * @param array<int, string>|null $regs
282 * @return bool|int
283 */
284 function regexMatch(string $pattern, string $string, ?array &$regs = null) {
285 return $this->regexHelper->regexMatch($pattern, $string, $regs);
286 }
287
288 /**
289 * @param string $pattern
290 * @param string $string
291 * @param array<int, string>|null $regs
292 * @return bool|int
293 */
294 function regexMatchi(string $pattern, string $string, ?array &$regs = null) {
295 return $this->regexHelper->regexMatchi($pattern, $string, $regs);
296 }
297
298 /**
299 * @param string $pattern
300 * @param string $replacement
301 * @param string $string
302 * @return string|null
303 */
304 function regexReplace($pattern, $replacement, $string) {
305 return $this->regexHelper->regexReplace($pattern, $replacement, $string);
306 }
307
308 /** Used with array_filter()
309 * @param string $value
310 * @return boolean
311 */
312 function removeEmptyCustom($value) {
313 if ($value == null) {
314 return false;
315 }
316 return trim($value) !== '';
317 }
318
319 /**
320 * @return float|string
321 */
322 function getExecutionTime() {
323 $startTime = $this->requestContext()->process_start_time;
324 if ($startTime !== null) {
325 $elapsedTime = abj_clock()->nowFloat() - $startTime;
326
327 return $elapsedTime;
328 }
329
330 return '';
331 }
332
333 /** Replace constants and translations.
334 * @param string $text
335 * @return string
336 */
337 function doNormalReplacements($text) {
338 global $wpdb;
339
340 // known strings that do not exist in the translation file.
341 $knownReplacements = array(
342 '{ABJ404_STATUS_AUTO}' => ABJ404_STATUS_AUTO,
343 '{ABJ404_STATUS_MANUAL}' => ABJ404_STATUS_MANUAL,
344 '{ABJ404_STATUS_CAPTURED}' => ABJ404_STATUS_CAPTURED,
345 '{ABJ404_STATUS_IGNORED}' => ABJ404_STATUS_IGNORED,
346 '{ABJ404_STATUS_LATER}' => ABJ404_STATUS_LATER,
347 '{ABJ404_STATUS_REGEX}' => ABJ404_STATUS_REGEX,
348 '{ABJ404_TYPE_404_DISPLAYED}' => ABJ404_TYPE_404_DISPLAYED,
349 '{ABJ404_TYPE_POST}' => ABJ404_TYPE_POST,
350 '{ABJ404_TYPE_CAT}' => ABJ404_TYPE_CAT,
351 '{ABJ404_TYPE_TAG}' => ABJ404_TYPE_TAG,
352 '{ABJ404_TYPE_EXTERNAL}' => ABJ404_TYPE_EXTERNAL,
353 '{ABJ404_TYPE_HOME}' => ABJ404_TYPE_HOME,
354 '{ABJ404_HOME_URL}' => ABJ404_HOME_URL,
355 '{PLUGIN_NAME}' => PLUGIN_NAME,
356 '{ABJ404_VERSION}' => ABJ404_VERSION,
357 '{PHP_VERSION}' => phpversion(),
358 '{WP_VERSION}' => get_bloginfo('version'),
359 '{MYSQL_VERSION}' => $wpdb->db_version(),
360 '{ABJ404_MAX_AJAX_DROPDOWN_SIZE}' => ABJ404_MAX_AJAX_DROPDOWN_SIZE,
361 '{WP_MEMORY_LIMIT}' => WP_MEMORY_LIMIT,
362 '{MBSTRING}' => extension_loaded('mbstring') ? 'true' : 'false',
363 );
364
365 // replace known strings that do not exist in the translation file.
366 $text = $this->str_replace(array_keys($knownReplacements), array_values($knownReplacements), $text);
367
368 // Find the strings to replace in the content.
369 $re = '/\{(.+?)\}/x';
370 $stringsToReplace = array();
371 // TODO does this need to be $f->regexMatch?
372 preg_match_all($re, $text, $stringsToReplace, PREG_PATTERN_ORDER);
373
374 // Iterate through each string to replace.
375 foreach ($stringsToReplace[1] as $stringToReplace) {
376 $regexSearchString = '{' . $stringToReplace . '}';
377 // External HTML template placeholders are extracted and checked by
378 // TranslationCoverageTest because they do not live in PHP call sites.
379 $translated = function_exists('translate') ? translate($stringToReplace, '404-solution') : $stringToReplace;
380 $text = $this->str_replace($regexSearchString, $translated, $text);
381 }
382
383 return $text;
384 }
385
386
387 /**
388 * @param string $haystack
389 * @param string $needle
390 * @return bool
391 */
392 function endsWithCaseInsensitive(string $haystack, string $needle): bool {
393 $length = $this->strlen($needle);
394 if ($this->strlen($haystack) < $length) {
395 return false;
396 }
397
398 $lowerNeedle = $this->strtolower($needle);
399 $lowerHay = $this->strtolower($haystack);
400
401 return ($this->substr($lowerHay, -$length) == $lowerNeedle);
402 }
403
404 /**
405 * @param string $haystack
406 * @param string $needle
407 * @return bool
408 */
409 function endsWithCaseSensitive(string $haystack, string $needle): bool {
410 $length = $this->strlen($needle);
411 if ($this->strlen($haystack) < $length) {
412 return false;
413 }
414
415 return ($this->substr($haystack, -$length) == $needle);
416 }
417
418 // =========================================================================
419 // Request parameter sanitization (relocated from DataAccessTrait_Stats, Phase 4)
420 // =========================================================================
421
422 /**
423 * @param string $name The key to retrieve the value for.
424 * @param string|null $defaultValue The value to return if the value is not set.
425 * @return string The sanitized value.
426 */
427 function getPostOrGetSanitize($name, $defaultValue = null) {
428 $returnValue = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
429 if ($returnValue === null && $name === 'action') {
430 $returnValue = isset($_GET['abj404action']) ? $_GET['abj404action'] : (isset($_POST['abj404action']) ? $_POST['abj404action'] : null);
431 }
432 $returnValue = self::applyBulkActionFallback($name, $returnValue);
433 if ($returnValue !== null) {
434 if (is_array($returnValue)) {
435 $returnValue = array_map('sanitize_text_field', $returnValue);
436 } else {
437 $returnValue = sanitize_text_field($returnValue);
438 }
439 }
440 $finalValue = $returnValue ?? $defaultValue;
441 return is_string($finalValue) ? $finalValue : (is_string($defaultValue) ? $defaultValue : '');
442 }
443
444 /**
445 * Native WP_List_Table renders bulk-action <select>s at top and bottom of
446 * the table using name="action" and name="action2". The 404 Solution
447 * wrappers mirror this with abj404action (top) and abj404action2 (bottom).
448 * When the top select is empty (default placeholder), fall back to the
449 * bottom select's value so Apply submits from either utility row.
450 *
451 * @param string $name
452 * @param mixed $current
453 * @return mixed
454 */
455 private static function applyBulkActionFallback($name, $current) {
456 if ($name !== 'abj404action') {
457 return $current;
458 }
459 if ($current !== null && $current !== '' && $current !== '-1') {
460 return $current;
461 }
462 $alt = isset($_GET['abj404action2']) ? $_GET['abj404action2'] : (isset($_POST['abj404action2']) ? $_POST['abj404action2'] : null);
463 if ($alt === null || $alt === '' || $alt === '-1') {
464 return $current;
465 }
466 return $alt;
467 }
468
469 /**
470 * @param string $name The key to retrieve the value for.
471 * @param string|null $defaultValue The value to return if the value is not set.
472 * @return string|array<string>|null The normalized URL value.
473 */
474 function getPostOrGetSanitizeUrl($name, $defaultValue = null) {
475 $returnValue = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
476 if ($returnValue === null) {
477 return $defaultValue;
478 }
479
480 $sanitizer = abj_service('sanitizer');
481 $unslash = function($value) {
482 return function_exists('wp_unslash') ? wp_unslash($value) : $value;
483 };
484
485 if (is_array($returnValue)) {
486 return array_map(function($value) use ($sanitizer, $unslash) {
487 $value = $unslash($value);
488 return $sanitizer->normalizeUrlString($value);
489 }, $returnValue);
490 }
491
492 $returnValue = $unslash($returnValue);
493 return $sanitizer->normalizeUrlString($returnValue);
494 }
495
496 }
497