PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.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 / PluginLogic.php

PluginLogic.php in 404 Solution 4.2.0, at includes/PluginLogic.php

1,526 lines 61.6 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 /* the glue that holds it together / everything else. */
9
10 require_once dirname(__FILE__) . '/PluginLogicUrlNormalization.php';
11 require_once dirname(__FILE__) . '/PluginLogicAdminActions.php';
12 require_once dirname(__FILE__) . '/PluginLogicImportExport.php';
13 require_once dirname(__FILE__) . '/PluginLogicSettingsUpdate.php';
14 require_once dirname(__FILE__) . '/PluginLogicPageOrdering.php';
15 require_once dirname(__FILE__) . '/PluginLogicLifecycle.php';
16
17 /**
18 * @phpstan-type PageObject object{id: int, post_parent: int, depth: int, post_type: string, post_title: string}
19 */
20 class ABJ_404_Solution_PluginLogic {
21
22 /** @var ABJ_404_Solution_Functions */
23 private $f = null;
24
25 /** @var ABJ_404_Solution_DataAccess */
26 private $dao = null;
27
28 /** @var ABJ_404_Solution_Logging */
29 private $logger = null;
30
31 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
32 private $redirectsRepo;
33
34 /** @var ABJ_404_Solution_LogsRepositoryInterface */
35 private $logsRepo;
36
37 /** @var ABJ_404_Solution_ViewBuildOrchestratorInterface */
38 private $viewBuild;
39
40 /** @var ABJ_404_Solution_ViewReadServiceInterface */
41 private $viewRead;
42
43 /** @var ABJ_404_Solution_ContentRepositoryInterface */
44 private $contentRepo;
45
46 /** @var ABJ_404_Solution_StatsRepositoryInterface */
47 private $statsRepo;
48
49 /** @var ABJ_404_Solution_DatabaseCoreInterface */
50 private $dbCore;
51
52 /** @var ABJ_404_Solution_ImportExportService|null */
53 private $importExportService = null;
54
55 /** @var string|null */
56 private $urlHomeDirectory = null;
57
58 /** @var int|null */
59 private $urlHomeDirectoryLength = null;
60
61 /** @var array<string, mixed>|null */
62 private $options = null;
63 /** @var array<string, mixed>|null */
64 private $resolvedOptionsSkipDbCheck = null;
65 /** @var array<string, mixed>|null */
66 private $resolvedOptionsWithDbCheck = null;
67
68 /** @var self|null */
69 private static $instance = null;
70
71 /** @var string|null */
72 private static $uniqID = null;
73
74 /** Use this to avoid an infinite loop when checking if a user has admin access or not.
75 * @var bool */
76 private static $checkingIsAdmin = false;
77
78 /** @var ABJ_404_Solution_PluginLogicUrlNormalization */
79 private $urlNormalization;
80
81 /** @var ABJ_404_Solution_PluginLogicAdminActions */
82 private $adminActions;
83
84 /** @var ABJ_404_Solution_PluginLogicImportExport */
85 private $importExport;
86
87 /** @var ABJ_404_Solution_PluginLogicSettingsUpdate */
88 private $settingsUpdate;
89
90 /** @var ABJ_404_Solution_PluginLogicPageOrdering */
91 private $pageOrdering;
92
93 /** @return ABJ_404_Solution_PluginLogic The singleton instance of the class. */
94 public static function getInstance() {
95 if (self::$instance !== null) {
96 return self::$instance;
97 }
98
99 // If the DI container is initialized, prefer it.
100 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
101 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('plugin_logic');
102 if ($resolved instanceof self) {
103 self::$instance = $resolved;
104 return self::$instance;
105 }
106 }
107
108 self::$instance = new ABJ_404_Solution_PluginLogic();
109 self::$uniqID = uniqid("", true);
110
111 // these filters allow non-admins to have admin access to the plugin.
112 add_filter( 'user_has_cap',
113 'ABJ_404_Solution_PluginLogic::override_user_can_access_admin_page', 10, 4 );
114
115 return self::$instance;
116 }
117
118 /**
119 * Constructor with dependency injection.
120 *
121 * @param ABJ_404_Solution_Functions|null $functions String manipulation utilities
122 * @param ABJ_404_Solution_DataAccess|null $dataAccess Data access layer
123 * @param ABJ_404_Solution_Logging|null $logging Logging service
124 */
125 function __construct($functions = null, $dataAccess = null, $logging = null) {
126 $this->f = $functions !== null ? $functions : abj_service('functions');
127 $this->dao = $dataAccess !== null ? $dataAccess : abj_service('data_access');
128 $this->logger = $logging !== null ? $logging : abj_service('logging');
129
130 if ($this->dao instanceof ABJ_404_Solution_DataAccess && get_class($this->dao) === ABJ_404_Solution_DataAccess::class) {
131 $this->redirectsRepo = $this->dao->getRedirectsRepo();
132 $this->logsRepo = $this->dao->getLogsRepo();
133 $this->viewBuild = $this->dao->getViewBuildOrchestrator();
134 $this->viewRead = $this->dao->getViewReadService();
135 $this->contentRepo = $this->dao->getContentRepo();
136 $this->statsRepo = $this->dao->getStatsRepo();
137 $this->dbCore = $this->dao->getDbCore();
138 } else {
139 $this->redirectsRepo = $this->dao;
140 $this->logsRepo = $this->dao;
141 $this->viewBuild = $this->dao;
142 $this->viewRead = $this->dao;
143 $this->contentRepo = $this->dao;
144 $this->statsRepo = $this->dao;
145 $this->dbCore = $this->dao;
146 }
147
148 $urlPath = parse_url(get_home_url(), PHP_URL_PATH);
149 // Fix MEDIUM #1 (5th review): Distinguish between parse failure (false) and no path (null)
150 if ($urlPath === false) {
151 $this->logger->warn("Malformed home URL detected: " . get_home_url());
152 $urlPath = '';
153 } else if ($urlPath === null) {
154 $urlPath = '';
155 }
156
157 // Fix HIGH #2 (4th review): Decode subdirectory for consistency with runtime processing
158 $decodedPath = $this->f->normalizeUrlString(rtrim($urlPath, '/'));
159 if (!is_string($decodedPath)) {
160 $decodedPath = '';
161 }
162 // Fix HIGH #3 (4th review): Remove null bytes and control characters for security
163 $cleaned = preg_replace('/[\x00-\x1F\x7F]/', '', $decodedPath);
164 $this->urlHomeDirectory = is_string($cleaned) ? $cleaned : $decodedPath;
165 $this->urlHomeDirectoryLength = $this->f->strlen($this->urlHomeDirectory);
166
167 // Initialize standalone composition classes
168 $this->urlNormalization = new ABJ_404_Solution_PluginLogicUrlNormalization(
169 $this->f, $this->urlHomeDirectory, $this->urlHomeDirectoryLength
170 );
171
172 $self = $this;
173 $this->importExport = new ABJ_404_Solution_PluginLogicImportExport(function() use ($self) {
174 return $self->getImportExportService();
175 });
176
177 $this->settingsUpdate = new ABJ_404_Solution_PluginLogicSettingsUpdate(
178 $this->f, $this->logger, $this->contentRepo, $this
179 );
180
181 $this->pageOrdering = new ABJ_404_Solution_PluginLogicPageOrdering(
182 $this->f, $this->logger, $this->contentRepo, $this->statsRepo, $this->urlNormalization, $this
183 );
184
185 $this->adminActions = new ABJ_404_Solution_PluginLogicAdminActions(
186 $this->f, $this->logger, $this->redirectsRepo, $this->viewBuild, $this->viewRead,
187 $this->contentRepo, $this->dbCore, $this->dao, $this->urlNormalization, $this
188 );
189 }
190
191 /** @return ABJ_404_Solution_ImportExportService */
192 private function getImportExportService() {
193 if ($this->importExportService !== null) {
194 return $this->importExportService;
195 }
196
197 if (!class_exists('ABJ_404_Solution_ImportExportService')) {
198 require_once dirname(__FILE__) . '/ImportExportService.php';
199 }
200
201 $this->importExportService = new ABJ_404_Solution_ImportExportService(
202 abj_service('view_read_service'),
203 abj_service('redirects_repository'),
204 abj_service('content_repository'),
205 $this->logger
206 );
207 return $this->importExportService;
208 }
209
210 // =========================================================================
211 // Delegation: UrlNormalization
212 // =========================================================================
213
214 /** @param string|null $urlRequest @return string */
215 function removeHomeDirectory($urlRequest): string {
216 if (!$this->urlNormalization instanceof ABJ_404_Solution_PluginLogicUrlNormalization) {
217 $this->urlNormalization = new ABJ_404_Solution_PluginLogicUrlNormalization(
218 $this->f !== null ? $this->f : abj_service('functions'),
219 $this->urlHomeDirectory !== null ? $this->urlHomeDirectory : '',
220 $this->urlHomeDirectoryLength !== null ? $this->urlHomeDirectoryLength : 0
221 );
222 }
223 return $this->urlNormalization->removeHomeDirectory($urlRequest);
224 }
225
226 /** @param string|null $url @return string */
227 function normalizeToRelativePath($url): string {
228 if (!$this->urlNormalization instanceof ABJ_404_Solution_PluginLogicUrlNormalization) {
229 $this->urlNormalization = new ABJ_404_Solution_PluginLogicUrlNormalization(
230 $this->f !== null ? $this->f : abj_service('functions'),
231 $this->urlHomeDirectory !== null ? $this->urlHomeDirectory : '',
232 $this->urlHomeDirectoryLength !== null ? $this->urlHomeDirectoryLength : 0
233 );
234 }
235 return $this->urlNormalization->normalizeToRelativePath($url);
236 }
237
238 /** @param string|null $url @return array<int, string> */
239 function getNormalizedUrlCandidates($url) {
240 if (!$this->urlNormalization instanceof ABJ_404_Solution_PluginLogicUrlNormalization) {
241 $decoded = $this->normalizeToRelativePath($url);
242 if ($decoded === '') {
243 return array();
244 }
245 $candidates = array($decoded);
246 $lower = function_exists('mb_strtolower') ? mb_strtolower($decoded, 'UTF-8') : strtolower($decoded);
247 if ($lower !== $decoded) {
248 $candidates[] = $lower;
249 }
250 $rawDecoded = is_string($url) ? rawurldecode($url) : '';
251 if ($rawDecoded !== '' && $rawDecoded !== $decoded) {
252 $candidates[] = $this->normalizeToRelativePath($rawDecoded);
253 }
254 return array_values(array_unique($candidates));
255 }
256 return $this->urlNormalization->getNormalizedUrlCandidates($url);
257 }
258
259 /** @param string $location @param string $requestedURL @return string */
260 function maybeTranslateRedirectUrl($location, $requestedURL = '') {
261 return $this->urlNormalization->maybeTranslateRedirectUrl($location, $requestedURL);
262 }
263
264 /** @param array<string, mixed> $options @param array<string, mixed> $postData @return string */
265 public function updateWordPressSettings(array &$options, array $postData): string {
266 return $this->settingsUpdate->updateWordPressSettings($options, $postData);
267 }
268
269 public function updateDeletionSettings(array &$options, array $postData): string {
270 return $this->settingsUpdate->updateDeletionSettings($options, $postData);
271 }
272
273 public function updateSuggestionSettings(array &$options, array $postData): string {
274 return $this->settingsUpdate->updateSuggestionSettings($options, $postData);
275 }
276
277 public function updateBooleanToggles(array &$options, array $postData): string {
278 return $this->settingsUpdate->updateBooleanToggles($options, $postData);
279 }
280
281 public function translatePressIntegrationAvailable(): bool {
282 return $this->urlNormalization->translatePressIntegrationAvailable();
283 }
284
285 public function translatePressRedirectUrl(string $location, string $requestedURL) {
286 return $this->urlNormalization->translatePressRedirectUrl($location, $requestedURL);
287 }
288
289 public function getTranslatePressLanguageFromRequest(string $requestedURL): string {
290 return $this->urlNormalization->getTranslatePressLanguageFromRequest($requestedURL);
291 }
292
293 public function translatePressTranslateUrl(string $url, string $language) {
294 return $this->urlNormalization->translatePressTranslateUrl($url, $language);
295 }
296
297 public function buildFullUrlFromRequest(string $requestedURL): string {
298 return $this->urlNormalization->buildFullUrlFromRequest($requestedURL);
299 }
300
301 public function isLocalUrl(string $url): bool {
302 return $this->urlNormalization->isLocalUrl($url);
303 }
304
305 // =========================================================================
306 // Delegation: Lifecycle (static forwarding)
307 // =========================================================================
308
309 /** @return void */
310 static function doUnregisterCrons(): void {
311 ABJ_404_Solution_PluginLogicLifecycle::doUnregisterCrons();
312 }
313
314 /** @param bool $network_wide @return void */
315 static function runOnPluginActivation(bool $network_wide = false): void {
316 ABJ_404_Solution_PluginLogicLifecycle::runOnPluginActivation($network_wide);
317 }
318
319 /** @return void */
320 static function networkActivationCronHandler(): void {
321 ABJ_404_Solution_PluginLogicLifecycle::networkActivationCronHandler();
322 }
323
324 /**
325 * @param int $blog_id
326 * @param int $user_id
327 * @param string $domain
328 * @param string $path
329 * @param int $site_id
330 * @param array<string, mixed> $meta
331 * @return void
332 */
333 static function activateNewSite($blog_id, $user_id, $domain, $path, $site_id, $meta): void {
334 ABJ_404_Solution_PluginLogicLifecycle::activateNewSite($blog_id, $user_id, $domain, $path, $site_id, $meta);
335 }
336
337 /** @param mixed $site @param array<string, mixed> $args @return void */
338 static function activateNewSiteModern($site, $args): void {
339 ABJ_404_Solution_PluginLogicLifecycle::activateNewSiteModern($site, $args);
340 }
341
342 /** @param bool $network_wide @return void */
343 static function runOnPluginDeactivation(bool $network_wide = false): void {
344 ABJ_404_Solution_PluginLogicLifecycle::runOnPluginDeactivation($network_wide);
345 }
346
347 /** @param int $blog_id @param bool $drop @return void */
348 static function deleteBlogData($blog_id, $drop = false): void {
349 ABJ_404_Solution_PluginLogicLifecycle::deleteBlogData($blog_id, $drop);
350 }
351
352 /** @return void */
353 static function doRegisterCrons(): void {
354 ABJ_404_Solution_PluginLogicLifecycle::doRegisterCrons();
355 }
356
357 // =========================================================================
358 // Delegation: ImportExport
359 // =========================================================================
360
361 /** @return string */
362 function getExportFilename(string $format = 'native'): string {
363 return $this->importExport->getExportFilename($format);
364 }
365
366 /** @return void */
367 function doExport(): void {
368 $this->importExport->doExport();
369 }
370
371 /** @param string $sourceFile @param string $destinationFile @return string */
372 function convertExportCsvToRedirectionFormat($sourceFile, $destinationFile) {
373 return $this->importExport->convertExportCsvToRedirectionFormat($sourceFile, $destinationFile);
374 }
375
376 /** @return string */
377 function doImportFile(): string {
378 return $this->importExport->doImportFile();
379 }
380
381 /** @param array<string, mixed> $dataArray @param bool $dryRun @return array<int, string> */
382 function loadDataArrayFromFile(array $dataArray, bool $dryRun = false): array {
383 return $this->importExport->loadDataArrayFromFile($dataArray, $dryRun);
384 }
385
386 /** @return array<string, string> */
387 function splitCsvLine(string $line): array {
388 return $this->importExport->splitCsvLine($line);
389 }
390
391 /** @param array<int, string> $columns @return bool */
392 function isCompatibleImportHeaderRow(array $columns): bool {
393 return $this->importExport->isCompatibleImportHeaderRow($columns);
394 }
395
396 /** @param array<int, string> $columns @return array<int, string> */
397 function normalizeImportHeaders(array $columns): array {
398 return $this->importExport->normalizeImportHeaders($columns);
399 }
400
401 /** @param array<int, string> $row @param array<int, string> $normalizedHeaders @return array<string, string> */
402 function mapImportRowByHeaders(array $row, array $normalizedHeaders): array {
403 return $this->importExport->mapImportRowByHeaders($row, $normalizedHeaders);
404 }
405
406 /** @param array<int, string> $columns @return string */
407 function detectImportFormatFromHeaders(array $columns): string {
408 return $this->importExport->detectImportFormatFromHeaders($columns);
409 }
410
411 // =========================================================================
412 // Delegation: AdminActions
413 // =========================================================================
414
415 /** @param string $action @param string $sub @return string */
416 function handlePluginAction($action, &$sub) {
417 return $this->adminActions->handlePluginAction($action, $sub);
418 }
419
420 /** @return string */
421 function hanldeTrashAction() {
422 return $this->adminActions->hanldeTrashAction();
423 }
424
425 /** @return void */
426 function handleActionChangeItemsPerRow(): void {
427 $this->adminActions->handleActionChangeItemsPerRow();
428 }
429
430 /** @return void */
431 function handleActionExport(): void {
432 $this->adminActions->handleActionExport();
433 }
434
435 /** @return string|null */
436 function handleActionImportFile() {
437 return $this->adminActions->handleActionImportFile();
438 }
439
440 /** @return void */
441 function updatePerPageOption(int $rows): void {
442 $this->adminActions->updatePerPageOption($rows);
443 }
444
445 /** @return string */
446 function handleActionImportRedirects() {
447 return $this->adminActions->handleActionImportRedirects();
448 }
449
450 /** @return string */
451 function handleDeleteAction() {
452 return $this->adminActions->handleDeleteAction();
453 }
454
455 /** @return string */
456 function handleIgnoreAction() {
457 return $this->adminActions->handleIgnoreAction();
458 }
459
460 /** @return string */
461 function handleLaterAction() {
462 return $this->adminActions->handleLaterAction();
463 }
464
465 /** @param string $sub @param string $action @return string */
466 function handleActionEdit(&$sub, &$action) {
467 return $this->adminActions->handleActionEdit($sub, $action);
468 }
469
470 /** @param string $action @param array<int, int> $ids @return string */
471 function doBulkAction(string $action, array $ids): string {
472 return $this->adminActions->doBulkAction($action, $ids);
473 }
474
475 /** @param string $sub @return void */
476 function doEmptyTrash(string $sub): void {
477 $this->adminActions->doEmptyTrash($sub);
478 }
479
480 /** @return string */
481 function updateRedirectData() {
482 return $this->adminActions->updateRedirectData();
483 }
484
485 /** @return array<string, mixed> */
486 function getRedirectTypeAndDest(): array {
487 return $this->adminActions->getRedirectTypeAndDest();
488 }
489
490 /** @return string */
491 function addAdminRedirect() {
492 return $this->adminActions->addAdminRedirect();
493 }
494
495 /** @return string */
496 function handleActionUndoRegexAutoPromote() {
497 return $this->adminActions->handleActionUndoRegexAutoPromote();
498 }
499
500 // =========================================================================
501 // Delegation: SettingsUpdate
502 // =========================================================================
503
504 /** @param string $pageBeingViewed @return array<string, mixed> */
505 function getTableOptions(string $pageBeingViewed): array {
506 return $this->settingsUpdate->getTableOptions($pageBeingViewed);
507 }
508
509 /** @param array<string, mixed> $postData @param bool $restoreNewlines @return array<string, mixed> */
510 function sanitizePostData(array $postData, bool $restoreNewlines = false): array {
511 return $this->settingsUpdate->sanitizePostData($postData, $restoreNewlines);
512 }
513
514 /** @param string $str @return string */
515 function sanitizeForSQL($str) {
516 return $this->settingsUpdate->sanitizeForSQL($str);
517 }
518
519 /** @return array<string, mixed> */
520 function updateOptionsFromPOST() {
521 return $this->settingsUpdate->updateOptionsFromPOST();
522 }
523
524 /** @param array<string, mixed> $options @return bool */
525 function normalizeSuggestionTemplateOptions(array &$options): bool {
526 return $this->settingsUpdate->normalizeSuggestionTemplateOptions($options);
527 }
528
529 // =========================================================================
530 // Delegation: PageOrdering
531 // =========================================================================
532
533 /** @param string $location @param string $requestedURL @param bool $isCustom404 @return string */
534 public function buildFinalRedirectDestination($location, $requestedURL = '', $isCustom404 = false) {
535 return $this->pageOrdering->buildFinalRedirectDestination($location, $requestedURL, $isCustom404);
536 }
537
538 /** @param array<int, object> $pages @param bool $includeMissingParentPages @return array<int, object> */
539 function orderPageResults(array $pages, bool $includeMissingParentPages = false): array {
540 return $this->pageOrdering->orderPageResults($pages, $includeMissingParentPages);
541 }
542
543 /** @param array<int, object{taxonomy: string, name?: string}> $categoryRows @return array<string, array<int, object{taxonomy: string, name?: string}>> */
544 function getMapOfCustomCategories(array $categoryRows): array {
545 return $this->pageOrdering->getMapOfCustomCategories($categoryRows);
546 }
547
548 /** @param array<int, object> $pages @return array<int, mixed> */
549 function getMissingParentPageIDs(array $pages): array {
550 return $this->pageOrdering->getMissingParentPageIDs($pages);
551 }
552
553 /** @param object $a @param object $b @return int */
554 function compareByID(object $a, object $b): int {
555 return $this->pageOrdering->compareByID($a, $b);
556 }
557
558 /** @param array<int, object> $pages @return array<int, object> */
559 function setDepthAndAddChildren(array $pages): array {
560 return $this->pageOrdering->setDepthAndAddChildren($pages);
561 }
562
563 /** @param array<int, object> $pages @return array<int, object> */
564 function findAllMainPages(array $pages): array {
565 return $this->pageOrdering->findAllMainPages($pages);
566 }
567
568 /** @param array<int, object> $childPages @param array<int, object> $removeThese @return array<int, object> */
569 function removeUsedChildPages(array $childPages, array $removeThese): array {
570 return $this->pageOrdering->removeUsedChildPages($childPages, $removeThese);
571 }
572
573 /** @param array<int, object> $pages @return array<int, object> */
574 function findChildPages(array $pages): array {
575 return $this->pageOrdering->findChildPages($pages);
576 }
577
578 /** @param object $a @param object $b @return int */
579 function sortByTypeThenTitle(object $a, object $b): int {
580 return $this->pageOrdering->sortByTypeThenTitle($a, $b);
581 }
582
583 /** @return string */
584 function emailCaptured404Notification() {
585 return $this->pageOrdering->emailCaptured404Notification();
586 }
587
588 /** @param number $captured404Count @return boolean */
589 function shouldNotifyAboutCaptured404s($captured404Count) {
590 return $this->pageOrdering->shouldNotifyAboutCaptured404s($captured404Count);
591 }
592
593 /** @param string $idAndType @param string $externalLinkURL @return string */
594 function getPageTitleFromIDAndType($idAndType, $externalLinkURL) {
595 return $this->pageOrdering->getPageTitleFromIDAndType($idAndType, $externalLinkURL);
596 }
597
598 // =========================================================================
599 // Methods that remain on PluginLogic (not from traits)
600 // =========================================================================
601
602 /** This replaces the current_user_can('administrator') function.
603 * @return bool true if $abj404logic->userIsPluginAdmin()
604 */
605 function userIsPluginAdmin() {
606 if (ABJ_404_Solution_PluginLogic::$checkingIsAdmin) {
607 return false;
608 }
609
610 ABJ_404_Solution_PluginLogic::$checkingIsAdmin = true;
611 try {
612 $options = $this->getOptions(true);
613 $f = $this->f;
614 global $current_user;
615
616 $isPluginAdmin = current_user_can('manage_options') || current_user_can('administrator');
617 if (function_exists('is_multisite') && is_multisite() && function_exists('is_super_admin') && is_super_admin()) {
618 $isPluginAdmin = true;
619 }
620
621 $extraAdmins = $options['plugin_admin_users'] ?? array();
622 $current_user_name = null;
623 if (isset($current_user)) {
624 $current_user_name = $current_user->user_login;
625 }
626 if ($current_user_name != null && $current_user_name != false) {
627 $check = false;
628 if (is_array($extraAdmins)) {
629 $extraAdmins = array_filter($extraAdmins,
630 array($f, 'removeEmptyCustom'));
631 $check = true;
632 } else if (is_string($extraAdmins)) {
633 $extraAdmins = $this->f->explodeNewline($extraAdmins);
634 $check = true;
635 }
636 /** @var array<int|string, mixed> $extraAdmins */
637 if ($check && is_array($extraAdmins) && in_array($current_user_name, $extraAdmins)) {
638 $isPluginAdmin = true;
639 }
640 }
641
642 $filtered = apply_filters('abj404_userIsPluginAdmin', $isPluginAdmin);
643
644 if (!$filtered || ($filtered !== $isPluginAdmin)) {
645 $extraAdminsSummary = '';
646 $rawExtra = $options['plugin_admin_users'] ?? array();
647 if (is_array($rawExtra)) {
648 $extraAdminsSummary = implode(', ', array_filter($rawExtra));
649 } else if (is_string($rawExtra)) {
650 $extraAdminsSummary = $rawExtra;
651 }
652
653 $this->logger->debugMessage(
654 "userIsPluginAdmin detail: result=" . ($filtered ? 'true' : 'false') .
655 ", pre-filter=" . ($isPluginAdmin ? 'true' : 'false') .
656 ", manage_options=" . (current_user_can('manage_options') ? 'yes' : 'no') .
657 ", user=" . ($current_user_name ?? '(none)') .
658 ", plugin_admin_users=[" . esc_html($extraAdminsSummary) . "]" .
659 ($filtered !== $isPluginAdmin ? ", NOTE: abj404_userIsPluginAdmin filter changed the result" : "")
660 );
661 }
662
663 return $filtered;
664 } finally {
665 ABJ_404_Solution_PluginLogic::$checkingIsAdmin = false;
666 }
667 }
668
669 /**
670 * Get the current user's settings mode preference.
671 * @return string 'simple' or 'advanced'
672 */
673 function getSettingsMode() {
674 $user_id = get_current_user_id();
675 if (!$user_id) {
676 return 'simple';
677 }
678 $mode = get_user_meta($user_id, 'abj404_settings_mode', true);
679 return ($mode === 'advanced') ? 'advanced' : 'simple';
680 }
681
682 /**
683 * Set the current user's settings mode preference.
684 * @param string $mode 'simple' or 'advanced'
685 * @return bool|int Meta ID on success, false on failure
686 */
687 function setSettingsMode($mode) {
688 $user_id = get_current_user_id();
689 if (!$user_id) {
690 return false;
691 }
692 $valid_mode = ($mode === 'advanced') ? 'advanced' : 'simple';
693 return update_user_meta($user_id, 'abj404_settings_mode', $valid_mode);
694 }
695
696 /** Allow the user to be an admin for the plugin.
697 * @param array<string, bool> $allcaps
698 * @param array<int, string> $caps
699 * @param array<int, mixed> $args
700 * @param \WP_User $user
701 * @return array<string, bool> an array of the capabilities
702 */
703 static function override_user_can_access_admin_page( $allcaps, $caps, $args, $user ) {
704 if (!is_admin()) {
705 return $allcaps;
706 }
707
708 $abj404logic = abj_service('plugin_logic');
709
710 $isPluginAdmin = false;
711 $isViewing404AdminPage = false;
712
713 if ($abj404logic->userIsPluginAdmin()) {
714 $isPluginAdmin = true;
715 }
716
717 if ($isPluginAdmin) {
718 $userRequest = ABJ_404_Solution_UserRequest::getInstance();
719 $queryParts = $userRequest !== null ? $userRequest->getQueryString() : null;
720
721 if (is_string($queryParts) && strpos($queryParts, ABJ404_PP) !== false) {
722 $isViewing404AdminPage = true;
723 }
724 }
725
726 if ($isPluginAdmin && $isViewing404AdminPage) {
727 $allcaps['manage_options'] = true;
728 }
729
730 return $allcaps;
731 }
732
733 /** Forward to a real page for queries like ?p=10
734 * @param array<string, mixed> $options
735 * @return void
736 */
737 function tryNormalPostQuery(array $options): void {
738 global $wp_query;
739
740 $query = $wp_query->query;
741 if (!isset($query['p'])) {
742 return;
743 }
744 $pageid = $query['p'];
745 if (!empty($pageid)) {
746 $rawPermalink = get_permalink($pageid);
747 $permalink = $this->f->normalizeUrlString($rawPermalink !== false ? $rawPermalink : null);
748 $status = get_post_status($pageid);
749 if (($permalink != false) &&
750 (in_array($status, array('publish', 'published')))) {
751 $homeURL = get_home_url();
752 if ($homeURL == null) {
753 $homeURL = '';
754 }
755 $urlHomeDirectory = parse_url($homeURL, PHP_URL_PATH);
756 if ($urlHomeDirectory == null) {
757 $urlHomeDirectory = '';
758 }
759 $urlHomeDirectory = rtrim($urlHomeDirectory, '/');
760 $fromURL = $urlHomeDirectory . '/?p=' . $pageid;
761 $redirect = $this->redirectsRepo->getExistingRedirectForURL($fromURL);
762 $defaultRedirect = is_scalar($options['default_redirect']) ? (string)$options['default_redirect'] : '301';
763 if (!isset($redirect['id']) || $redirect['id'] == 0) {
764 $this->redirectsRepo->setupRedirect($fromURL, (string)ABJ404_STATUS_AUTO, (string)ABJ404_TYPE_POST,
765 (string)$pageid, $defaultRedirect, 0, 'page ID');
766 }
767 $this->logsRepo->logRedirectHit($fromURL, $permalink, 'page ID');
768 $this->forceRedirect($permalink, (int)$defaultRedirect);
769 exit;
770 }
771 }
772 }
773
774 /**
775 * @param string $urlRequest the requested URL
776 * @param string $urlSlugOnly only the slug
777 * @return void
778 */
779 function initializeIgnoreValues(string $urlRequest, string $urlSlugOnly): void {
780 $abj404logic = abj_service('plugin_logic');
781
782 $options = $abj404logic->getOptions();
783 $ignoreReasonDoNotProcess = null;
784 $ignoreReasonDoProcess = null;
785 $httpUserAgent = array_key_exists('HTTP_USER_AGENT', $_SERVER) ?
786 $this->f->strtolower($_SERVER['HTTP_USER_AGENT']) : '';
787
788 $adminURLRaw = parse_url(admin_url(), PHP_URL_PATH);
789 $adminURL = is_string($adminURLRaw) ? $adminURLRaw : '/wp-admin/';
790 if (is_admin() || $this->f->substr($urlRequest, 0, $this->f->strlen($adminURL)) == $adminURL) {
791 $this->logger->debugMessage("Ignoring admin URL: " . $urlRequest);
792 $ignoreReasonDoNotProcess = 'Admin URL';
793 }
794
795 $ignoreDontProcess = is_string($options['ignore_dontprocess']) ? $options['ignore_dontprocess'] : '';
796 $userAgents = $this->f->explodeNewline($ignoreDontProcess);
797
798 foreach ($userAgents as $agentToIgnore) {
799 if (stripos($httpUserAgent, trim($agentToIgnore)) !== false) {
800 $this->logger->debugMessage("Ignoring user agent (do not redirect): " .
801 esc_html($_SERVER['HTTP_USER_AGENT']) . " for URL: " . esc_html($urlRequest));
802 $ignoreReasonDoNotProcess = 'User agent (do not redirect): ' . esc_html($_SERVER['HTTP_USER_AGENT']);
803 }
804 }
805
806 $patternsToIgnore = is_array($options['folders_files_ignore_usable']) ? $options['folders_files_ignore_usable'] : array();
807 if (!empty($patternsToIgnore)) {
808 foreach ($patternsToIgnore as $patternToIgnore) {
809 $patternToIgnoreStr = is_string($patternToIgnore) ? $patternToIgnore : (string)$patternToIgnore;
810 $patternToIgnoreNoSlashes = stripslashes($patternToIgnoreStr);
811 abj_service('request_context')->debug_info = 'Applying regex pattern to ignore\"' .
812 $patternToIgnoreNoSlashes . '" to URL slug: ' . $urlSlugOnly;
813 $matches = array();
814 if ($this->f->regexMatch($patternToIgnoreNoSlashes, $urlSlugOnly, $matches)) {
815 $this->logger->debugMessage("Ignoring file/folder (do not redirect) for URL: " .
816 esc_html($urlSlugOnly) . ", pattern used: " . $patternToIgnoreNoSlashes);
817 $ignoreReasonDoNotProcess = 'Files and folders (do not redirect) pattern: ' .
818 esc_html($patternToIgnoreNoSlashes);
819 }
820 abj_service('request_context')->debug_info = 'Cleared after regex pattern to ignore.';
821 }
822 }
823 abj_service('request_context')->ignore_donotprocess = is_string($ignoreReasonDoNotProcess) ? $ignoreReasonDoNotProcess : false;
824
825 $ignoreDoProcess = is_string($options['ignore_doprocess']) ? $options['ignore_doprocess'] : '';
826 $userAgents = $this->f->explodeNewline($ignoreDoProcess);
827
828 foreach ($userAgents as $agentToIgnore) {
829 if (stripos($httpUserAgent, trim($agentToIgnore)) !== false) {
830 $this->logger->debugMessage("Ignoring user agent (process ok): " .
831 esc_html($_SERVER['HTTP_USER_AGENT']) . " for URL: " . esc_html($urlRequest));
832 $ignoreReasonDoProcess = 'User agent (process ok): ' . $agentToIgnore;
833 }
834 }
835 abj_service('request_context')->ignore_doprocess = is_string($ignoreReasonDoProcess) ? $ignoreReasonDoProcess : false;
836 }
837
838 /** @return string */
839 function readCookieWithPreviousRqeuestShort(): string {
840 $cookieName = ABJ404_PP . '_REQUEST_URI';
841 $cookieNameShort = $cookieName . '_SHORT';
842
843 if (array_key_exists($cookieNameShort, $_COOKIE) &&
844 array_key_exists($cookieName, $_COOKIE)) {
845 return $_COOKIE[$cookieName];
846 }
847
848 return '';
849 }
850
851 /** @return void */
852 function setCookieWithPreviousRequest(): void {
853
854 $requested_url_raw = $this->f->normalizeUrlString($_SERVER['REQUEST_URI']);
855
856 $requested_url_cleaned = preg_replace('/\?.*$/', '', $requested_url_raw);
857 $requested_url = is_string($requested_url_cleaned) ? $requested_url_cleaned : $requested_url_raw;
858
859 $cookieName = ABJ404_PP . '_REQUEST_URI';
860 $cookieNameShort = $cookieName . '_SHORT';
861 try {
862 setcookie($cookieName, $requested_url, time() + (60 * 4), "/");
863 setcookie($cookieNameShort, $requested_url, time() + (5), "/");
864
865 if (!isset($_COOKIE[$cookieName . '_UPDATE_URL']) ||
866 empty($_COOKIE[$cookieName . '_UPDATE_URL'])) {
867 $update_url_raw = $this->f->normalizeUrlString($_SERVER['REQUEST_URI']);
868 $update_url_cleaned = preg_replace('/\?.*$/', '', $update_url_raw);
869 $update_url = is_string($update_url_cleaned) ? $update_url_cleaned : $update_url_raw;
870 setcookie($cookieName . '_UPDATE_URL', $update_url,
871 time() + (60 * 4), "/");
872 }
873
874 } catch (Exception $e) {
875 $this->logger->debugMessage("There was an issue setting a cookie: " . $e->getMessage());
876 $expireTime = date("D, d M Y H:i:s T", time() + (60 * 4));
877 $c = "\n" . '<script>document.cookie = "' . $cookieName . '=' .
878 esc_js($requested_url) .
879 '; expires=' . $expireTime . '";</script>' . "\n";
880 echo $c;
881 }
882
883 abj_service('request_context')->requested_url = $requested_url;
884 }
885
886 /**
887 * @param string $requestedURL
888 * @param string $reason
889 * @param bool $useUserSpecified404
890 * @param array<string, mixed>|null $optionsOverride
891 * @return void
892 */
893 function sendTo404Page(string $requestedURL, string $reason = '', bool $useUserSpecified404 = true, $optionsOverride = null): void {
894 $abj404logic = abj_service('plugin_logic');
895
896 $options = (is_array($optionsOverride) ? $optionsOverride : $abj404logic->getOptions());
897
898 $behavior = isset($options['dest404_behavior']) ? $options['dest404_behavior'] : '';
899 if ($behavior === 'suggest') {
900 $systemPage = ABJ_404_Solution_SystemPage::getInstance();
901 if (!$systemPage->systemPageExists()) {
902 $systemPage->handleSystemPageDeleted();
903 $options = $this->getOptions(true);
904 }
905 }
906
907 $dest404pageRaw = isset($options['dest404page']) ? $options['dest404page'] : null;
908 $dest404page = is_string($dest404pageRaw) ? $dest404pageRaw : (ABJ404_TYPE_404_DISPLAYED . '|' . ABJ404_TYPE_404_DISPLAYED);
909
910 if ($useUserSpecified404 && $this->thereIsAUserSpecified404Page($dest404page)) {
911 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($dest404page, 0,
912 null, $options);
913
914 if (!in_array($permalink['status'], array('publish', 'published'))) {
915 $msg = __("The user specified 404 page wasn't found. " .
916 "Please update the user-specified 404 page on the Options page.",
917 '404-solution');
918 $this->logger->infoMessage($msg);
919
920 } else {
921 $redirect = $this->redirectsRepo->getExistingRedirectForURL($requestedURL);
922 $pType = is_scalar($permalink['type']) ? (string)$permalink['type'] : '';
923 $pId = is_scalar($permalink['id']) ? (string)$permalink['id'] : '';
924 $pLink = is_scalar($permalink['link']) ? (string)$permalink['link'] : '';
925 $defRedir = is_scalar($options['default_redirect']) ? (string)$options['default_redirect'] : '301';
926 if (!isset($redirect['id']) || $redirect['id'] == 0) {
927 $this->redirectsRepo->setupRedirect($requestedURL, (string)ABJ404_STATUS_CAPTURED, $pType, $pId, $defRedir, 0);
928 }
929
930 $this->logsRepo->logRedirectHit($requestedURL, $pLink, 'user specified 404 page. ' . $reason);
931
932 setcookie(ABJ404_PP . '_STATUS_404', 'true', time() + 20, "/");
933
934 $abj404logic->forceRedirect(esc_url($pLink),
935 (int)$defRedir);
936 exit;
937 }
938 }
939
940 if (@$options['capture_404'] == '1') {
941 $redirect = $this->redirectsRepo->getExistingRedirectForURL($requestedURL);
942 $defRedir2 = is_scalar($options['default_redirect']) ? (string)$options['default_redirect'] : '301';
943 if (!isset($redirect['id']) || $redirect['id'] == 0) {
944 $this->redirectsRepo->setupRedirect($requestedURL, (string)ABJ404_STATUS_CAPTURED, (string)ABJ404_TYPE_404_DISPLAYED, (string)ABJ404_TYPE_404_DISPLAYED, $defRedir2, 0);
945 }
946 } else {
947 $optionsJson = json_encode($options);
948 $this->logger->debugMessage("No permalink found to redirect to. capture_404 is off. Requested URL: " . $requestedURL .
949 " | Redirect: (none)" . " | is_single(): " . is_single() . " | " .
950 "is_page(): " . is_page() . " | is_feed(): " . is_feed() . " | is_trackback(): " .
951 is_trackback() . " | is_preview(): " . is_preview() . " | options: " . wp_kses_post(is_string($optionsJson) ? $optionsJson : ''));
952 }
953 }
954
955 /** @param string|null $dest404page @return bool */
956 function thereIsAUserSpecified404Page($dest404page): bool {
957 if ($dest404page == null) {
958 return false;
959 }
960 $check1 = ($dest404page !== (ABJ404_TYPE_404_DISPLAYED . '|' . ABJ404_TYPE_404_DISPLAYED));
961 $check2 = ($dest404page !== (string)ABJ404_TYPE_404_DISPLAYED);
962 return $check1 && $check2;
963 }
964
965 /**
966 * @param bool $skip_db_check
967 * @return array<string, mixed>
968 */
969 function getOptions(bool $skip_db_check = false) {
970 if (!$skip_db_check && is_array($this->resolvedOptionsWithDbCheck)) {
971 return $this->resolvedOptionsWithDbCheck;
972 }
973 if ($skip_db_check) {
974 if (is_array($this->resolvedOptionsSkipDbCheck)) {
975 return $this->resolvedOptionsSkipDbCheck;
976 }
977 if (is_array($this->resolvedOptionsWithDbCheck)) {
978 return $this->resolvedOptionsWithDbCheck;
979 }
980 }
981
982 if ($this->options == null) {
983 $optionResult = get_option('abj404_settings');
984 $this->options = is_array($optionResult) ? $optionResult : null;
985 }
986 $options = $this->options;
987
988 if (!is_array($options)) {
989 add_option('abj404_settings', '', '', false);
990 $options = array();
991 }
992
993 $defaults = $this->getDefaultOptions();
994 $missing = false;
995 foreach ($defaults as $key => $value) {
996 if (!isset($options[$key]) || $options[$key] === '') {
997 $options[$key] = $value;
998 $missing = true;
999 }
1000 }
1001
1002 if ($missing) {
1003 $this->updateOptions($options);
1004 }
1005
1006 if ($skip_db_check == false) {
1007 if (!array_key_exists('DB_VERSION', $options) || $options['DB_VERSION'] != ABJ404_VERSION) {
1008 $options = $this->updateToNewVersion($options);
1009 }
1010 }
1011
1012 if ($this->settingsUpdate->normalizeSuggestionTemplateOptions($options)) {
1013 $this->updateOptions($options);
1014 }
1015
1016 if ($skip_db_check) {
1017 $this->resolvedOptionsSkipDbCheck = $options;
1018 } else {
1019 $this->resolvedOptionsWithDbCheck = $options;
1020 }
1021
1022 return $options;
1023 }
1024
1025 /** @param array<string, mixed> $options @return void */
1026 function updateOptions(array $options): void {
1027 $old_options = $this->options;
1028 update_option('abj404_settings', $options);
1029 $this->options = $options;
1030 $this->resolvedOptionsSkipDbCheck = null;
1031 $this->resolvedOptionsWithDbCheck = null;
1032 }
1033
1034 /** @param array<string, mixed> $options @return array<string, mixed> */
1035 function updateToNewVersion(array $options) {
1036 self::invalidateOpcacheForCriticalFiles();
1037
1038 $syncUtils = abj_service('sync_utils');
1039
1040 $synchronizedKeyFromUser = "update_db_version";
1041 $uniqueID = $syncUtils->synchronizerAcquireLockTry($synchronizedKeyFromUser);
1042
1043 if ($uniqueID == '' || $uniqueID == null) {
1044 $this->logger->debugMessage("Avoiding infinite loop on database update.");
1045 return $options;
1046 }
1047
1048 $returnValue = $options;
1049
1050 try {
1051 $returnValue = $this->updateToNewVersionAction($options);
1052
1053 } catch (Throwable $e) {
1054 $this->logger->errorMessage("Error updating to new version. ", $e instanceof \Exception ? $e : null);
1055 throw $e;
1056 } finally {
1057 $syncUtils->synchronizerReleaseLock($uniqueID, $synchronizedKeyFromUser);
1058 }
1059
1060 $permalinkCache = abj_service('permalink_cache');
1061 $permalinkCache->updatePermalinkCache(1);
1062
1063 return $returnValue;
1064 }
1065
1066 /** @param array<string, mixed> $options @return array<string, mixed> */
1067 function updateToNewVersionAction(array $options) {
1068 global $wpdb;
1069
1070 if (!is_array($options)) {
1071 $options = array();
1072 }
1073 $options = array_merge($this->getDefaultOptions(), $options);
1074
1075 $currentDBVersion = "(unknown)";
1076 if (array_key_exists('DB_VERSION', $options) && is_string($options['DB_VERSION'])) {
1077 $currentDBVersion = $options['DB_VERSION'];
1078 }
1079 $this->logger->infoMessage(self::$uniqID . ": Updating database version from " .
1080 $currentDBVersion . " to " . ABJ404_VERSION . " (begin).");
1081
1082 $fileUtils = abj_service('functions');
1083 $fileUtils->deleteDirectoryRecursively(ABJ404_PATH . 'temp/');
1084
1085 $upgradesEtc = abj_service('database_upgrades');
1086 $upgradesEtc->runSelfHealPrologue();
1087 $upgradesEtc->createDatabaseTables(true);
1088
1089 wp_clear_scheduled_hook('abj404_duplicateCronAction');
1090
1091 ABJ_404_Solution_PluginLogic::doUnregisterCrons();
1092 ABJ_404_Solution_PluginLogic::doRegisterCrons();
1093
1094 if (version_compare($currentDBVersion, '1.9.0') < 0) {
1095 $ignoreDoProcessStr = is_string($options['ignore_doprocess']) ? $options['ignore_doprocess'] : '';
1096 $userAgents = $this->f->explodeNewline($ignoreDoProcessStr);
1097
1098 $uasForSearch = $this->f->explodeNewline($ignoreDoProcessStr);
1099
1100 foreach ($userAgents as &$str) {
1101 if ($this->f->strtolower(trim($str)) == "slurp") {
1102 $str = "Yahoo! Slurp";
1103 $this->logger->infoMessage('Changed user agent "Slurp" to "Yahoo! Slurp" in the do not log list.');
1104 }
1105 }
1106
1107 if (!in_array("seznambot", $uasForSearch)) {
1108 $userAgents[] = 'SeznamBot';
1109 $this->logger->infoMessage('Added user agent "SeznamBot" to do not log list."');
1110 }
1111 if (!in_array("pinterestbot", $uasForSearch)) {
1112 $userAgents[] = 'Pinterestbot';
1113 $this->logger->infoMessage('Added user agent "Pinterestbot" to do not log list."');
1114 }
1115 if (!in_array("uptimerobot", $uasForSearch)) {
1116 $userAgents[] = 'UptimeRobot';
1117 $this->logger->infoMessage('Added user agent "UptimeRobot" to do not log list."');
1118 }
1119
1120 $options['ignore_doprocess'] = implode("\n",$userAgents);
1121 $this->updateOptions($options);
1122 }
1123
1124 if (version_compare($currentDBVersion, '1.8.0') < 0) {
1125 $query = "SHOW TABLES LIKE '{wp_abj404_logs}'";
1126 $result = $this->dbCore->queryAndGetResults($query);
1127 $rows = $result['rows'];
1128
1129 $filteredRows = is_array($rows) ? array_filter($rows) : array();
1130 if (!empty($filteredRows)) {
1131 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/migrateToNewLogsTable.sql");
1132 $query = $this->dbCore->doTableNameReplacements($query);
1133 $result = $this->dbCore->queryAndGetResults($query);
1134
1135 if ($result['rows_affected'] > 0) {
1136 $this->logger->infoMessage($result['rows_affected'] .
1137 ' log rows were migrated to the new table structre.');
1138 $this->dbCore->queryAndGetResults('drop table ' . $this->dbCore->getLowercasePrefix() . 'abj404_logs');
1139 }
1140 }
1141 }
1142
1143 if (version_compare($currentDBVersion, '2.18.0') < 0) {
1144 $foldersIgnoreStr = is_string($options['folders_files_ignore']) ? $options['folders_files_ignore'] : '';
1145 $originalItems = $this->f->explodeNewline($foldersIgnoreStr);
1146
1147 $newItems = array("wp-content/plugins/*", "wp-content/themes/*", ".well-known/acme-challenge/*");
1148 foreach ($newItems as $newItem) {
1149 if (array_search($newItem, $originalItems) === false) {
1150 $originalItems[] = $newItem;
1151 $this->logger->infoMessage('Added ' . $newItem . ' to the list of folders to ignore."');
1152 }
1153 }
1154
1155 $options['folders_files_ignore'] = implode("\n",$originalItems);
1156 $this->updateOptions($options);
1157 }
1158
1159 $dest404page = is_string($options['dest404page']) ? $options['dest404page'] : '';
1160 if ($this->f->strpos($dest404page, '|') === false) {
1161 if ($dest404page == '0') {
1162 $dest404page .= "|" . ABJ404_TYPE_404_DISPLAYED;
1163 } else {
1164 $dest404page .= '|' . ABJ404_TYPE_POST;
1165 }
1166 $options['dest404page'] = $dest404page;
1167 $this->updateOptions($options);
1168 }
1169
1170 // @cache-write-audit: opt-out — stores a setup-completion date marker, not a query result
1171 if ($currentDBVersion !== '0.0.0' && version_compare($currentDBVersion, '3.0.7') < 0) {
1172 update_option('abj404_setup_completed', gmdate('Y-m-d'));
1173 $this->logger->infoMessage('Marked setup wizard as completed for existing user.');
1174 }
1175
1176 if (!isset($options['suggest_minscore_enabled'])) {
1177 if (isset($options['suggest_minscore']) && is_scalar($options['suggest_minscore']) && intval($options['suggest_minscore']) >= 25) {
1178 $options['suggest_minscore_enabled'] = '1';
1179 $this->logger->infoMessage('Enabled minimum score filtering based on existing suggest_minscore setting.');
1180 } else {
1181 $options['suggest_minscore_enabled'] = '0';
1182 }
1183 $this->updateOptions($options);
1184 }
1185
1186 if (!isset($options['dest404_behavior']) || $options['dest404_behavior'] === 'theme_default') {
1187 $dest = is_string($options['dest404page']) ? $options['dest404page'] : '';
1188 if ($dest === '0|' . ABJ404_TYPE_404_DISPLAYED || $dest === (string)ABJ404_TYPE_404_DISPLAYED || $dest === '') {
1189 $options['dest404_behavior'] = 'theme_default';
1190 } else if ($dest === '0|' . ABJ404_TYPE_HOME) {
1191 $options['dest404_behavior'] = 'homepage';
1192 } else if ($dest !== '') {
1193 $parts = explode('|', $dest);
1194 $pageId = isset($parts[0]) ? (int)$parts[0] : 0;
1195 if ($pageId > 0 && ABJ_404_Solution_SystemPage::isSystemPage($pageId)) {
1196 $options['dest404_behavior'] = 'suggest';
1197 } else {
1198 $options['dest404_behavior'] = 'custom';
1199 }
1200 }
1201 $this->updateOptions($options);
1202 }
1203
1204 $options = $this->doUpdateDBVersionOption($options);
1205 $this->logger->infoMessage(self::$uniqID . ": Updating database version to " .
1206 ABJ404_VERSION . " (end).");
1207
1208 return $options;
1209 }
1210
1211 /** @return array<string, mixed> */
1212 function getDefaultOptions() {
1213 $options = array(
1214 'default_redirect' => '301',
1215 'send_error_logs' => '0',
1216 'capture_404' => '1',
1217 'capture_deletion' => 1095,
1218 'manual_deletion' => '0',
1219 'log_deletion' => '365',
1220 'admin_notification' => '0',
1221 'remove_matches' => '1',
1222 'suggest_max' => '5',
1223 'suggest_title' => '<h3>{suggest_title_text}</h3>',
1224 'suggest_before' => '<ol>',
1225 'suggest_after' => '</ol>',
1226 'suggest_entrybefore' => '<li>',
1227 'suggest_entryafter' => '</li>',
1228 'suggest_noresults' => '<p>{suggest_noresults_text}</p>',
1229 'suggest_cats' => '1',
1230 'suggest_tags' => '1',
1231 'suggest_minscore' => '25',
1232 'suggest_minscore_enabled' => '0',
1233 'update_suggest_url' => '0',
1234 'auto_redirects' => '1',
1235 'auto_slugs' => '1',
1236 'auto_trash_redirect' => '0',
1237 'auto_score' => '90',
1238 'auto_score_title' => '',
1239 'auto_score_category_tag' => '',
1240 'auto_score_content' => '',
1241 'template_redirect_priority' => '9',
1242 'auto_deletion' => '1095',
1243 'auto_302_expiration_days' => '0',
1244 'auto_cats' => '1',
1245 'auto_tags' => '1',
1246 'dest404page' => '0|' . ABJ404_TYPE_404_DISPLAYED,
1247 'maximum_log_disk_usage' => '10',
1248 'ignore_dontprocess' => 'zemanta aggregator',
1249 'ignore_doprocess' => "Googlebot\nMediapartners-Google\nAdsBot-Google\ndevelopers.google.com\n"
1250 . "Bingbot\nYahoo! Slurp\nDuckDuckBot\nBaiduspider\nYandexBot\nwww.sogou.com\nSogou-Test-Spider\n"
1251 . "Exabot\nfacebot\nfacebookexternalhit\nia_archiver\nSeznamBot\nPinterestbot\nUptimeRobot\nMJ12bot",
1252 'recognized_post_types' => "page\npost\nproduct",
1253 'recognized_categories' => "",
1254 'folders_files_ignore' => implode("\n", array("wp-content/plugins/*", "wp-content/themes/*",
1255 ".well-known/acme-challenge/*")),
1256 'folders_files_ignore_usable' => "",
1257 'suggest_regex_exclusions' => "",
1258 'suggest_regex_exclusions_usable' => "",
1259 'plugin_admin_users' => "",
1260 'debug_mode' => 0,
1261 'days_wait_before_major_update' => 30,
1262 'DB_VERSION' => '0.0.0',
1263 'menuLocation' => 'underSettings',
1264 'admin_theme' => 'default',
1265 'plugin_language_override' => '',
1266 'disable_auto_dark_mode' => '0',
1267 'admin_notification_email' => '',
1268 'admin_notification_frequency' => 'instant',
1269 'admin_notification_digest_limit' => '10',
1270 'admin_notification_last_sent' => '0',
1271 'page_redirects_order_by' => 'url',
1272 'page_redirects_order' => 'ASC',
1273 'captured_order_by' => 'logshits',
1274 'captured_order' => 'DESC',
1275 'excludePages[]' => '',
1276 'dest404_behavior' => 'theme_default',
1277 'auto_trash_junk_urls' => '1',
1278 'auto_trash_junk_patterns' => implode("\n", array(
1279 '.env', '.git/', '.aws/', '.svn/', '.hg/',
1280 'xmlrpc.php', 'wlwmanifest.xml',
1281 'wp-config', 'config.php', 'config.json', 'config.bak',
1282 'phpinfo', 'phpmyadmin', 'phpMyAdmin', 'adminer',
1283 'sqladmin', 'dbadmin', 'mysqladmin',
1284 'id_rsa', '.bash_history', '.bashrc', '.DS_Store',
1285 'nginx.conf', 'httpd.conf', 'Dockerfile', 'docker-compose',
1286 '.sql', '.tar.gz', 'db_backup', 'database_backup',
1287 'setup-config.php',
1288 '/vendor/', '/node_modules/', '/tmp/',
1289 '/_profiler/', '/_debugbar/', '/debug/', '/debugbar/',
1290 '/META-INF/', '/WEB-INF/',
1291 'magento_version', 'alfa-rex.php', 'bypass.php',
1292 )),
1293 );
1294
1295 return $options;
1296 }
1297
1298 /** @param array<string, mixed>|null $options @return array<string, mixed> */
1299 function doUpdateDBVersionOption($options = null): array {
1300 if ($options == null) {
1301 $options = $this->getOptions(true);
1302 }
1303
1304 $options['DB_VERSION'] = ABJ404_VERSION;
1305
1306 $this->updateOptions($options);
1307
1308 return $options;
1309 }
1310
1311 /** @return string[] File paths that were successfully invalidated. */
1312 static function invalidateOpcacheForCriticalFiles(): array {
1313 if (!function_exists('opcache_invalidate')) {
1314 return [];
1315 }
1316
1317 $files = [
1318 ABJ404_PATH . 'includes/Functions.php',
1319 ABJ404_PATH . 'includes/php/FunctionsMBString.php',
1320 ABJ404_PATH . 'includes/php/FunctionsPreg.php',
1321 ];
1322
1323 $invalidated = [];
1324 foreach ($files as $file) {
1325 if (is_file($file) && @opcache_invalidate($file, true)) {
1326 $invalidated[] = $file;
1327 }
1328 }
1329
1330 return $invalidated;
1331 }
1332
1333
1334 /** @return string */
1335 function getDebugLogFileLink(): string {
1336 return "?page=" . ABJ404_PP . "&subpage=abj404_debugfile";
1337 }
1338
1339 /** @return string */
1340 function getCommentPartAndQueryPartOfRequest() {
1341 $requestUri = isset($_SERVER['REQUEST_URI']) ? (string)$_SERVER['REQUEST_URI'] : '';
1342 if ($requestUri !== '' &&
1343 strpos($requestUri, '?') === false &&
1344 strpos($requestUri, '/comment-page-') === false) {
1345 return '';
1346 }
1347
1348 $userRequest = ABJ_404_Solution_UserRequest::getInstance();
1349 if ($userRequest === null) {
1350 return '';
1351 }
1352 $queryString = $userRequest->getQueryString();
1353 $queryParts = $this->f->removePageIDFromQueryString(is_string($queryString) ? $queryString : '');
1354 $queryParts = ($queryParts == '') ? '' : '?' . $queryParts;
1355 $commentPart = $userRequest->getCommentPagePart();
1356 return (is_string($commentPart) ? $commentPart : '') . $queryParts;
1357 }
1358
1359 /**
1360 * @param string $location
1361 * @param int $status
1362 * @param int|string $type only 0 for sending to a 404 page
1363 * @param string $requestedURL
1364 * @param bool $isCustom404
1365 * @return bool true if the user is sent to the default 404 page.
1366 */
1367 function forceRedirect(string $location, int $status = 302, $type = -1, string $requestedURL = '', bool $isCustom404 = false): bool {
1368 // 410 Gone
1369 if ($status === 410) {
1370 status_header(410);
1371 $templatePath = __DIR__ . '/html/gone410.html';
1372 if (file_exists($templatePath)) {
1373 $siteName = function_exists('get_bloginfo') ? get_bloginfo('name') : '';
1374 $siteUrl = function_exists('home_url') ? home_url('/') : '/';
1375 $templateContent = file_get_contents($templatePath);
1376 if (is_string($templateContent)) {
1377 $templateContent = str_replace(
1378 array('{site_name}', '{site_url}', '{heading}', '{message}', '{back_home}'),
1379 array(
1380 esc_html($siteName),
1381 esc_url($siteUrl),
1382 esc_html__('This content has been permanently removed.', '404-solution'),
1383 esc_html__('The page you requested no longer exists and has not been moved to a new location.', '404-solution'),
1384 esc_html__('Back to home page', '404-solution'),
1385 ),
1386 $templateContent
1387 );
1388 echo $templateContent;
1389 }
1390 }
1391 exit;
1392 }
1393
1394 // 451 Unavailable For Legal Reasons
1395 if ($status === 451) {
1396 status_header(451);
1397 $templatePath = __DIR__ . '/html/gone451.html';
1398 if (file_exists($templatePath)) {
1399 $siteName = function_exists('get_bloginfo') ? get_bloginfo('name') : '';
1400 $siteUrl = function_exists('home_url') ? home_url('/') : '/';
1401 $templateContent = file_get_contents($templatePath);
1402 if (is_string($templateContent)) {
1403 $templateContent = str_replace(
1404 array('{site_name}', '{site_url}', '{heading}', '{message}', '{back_home}'),
1405 array(
1406 esc_html($siteName),
1407 esc_url($siteUrl),
1408 esc_html__('451 Unavailable For Legal Reasons', '404-solution'),
1409 esc_html__('This content is unavailable due to a legal demand.', '404-solution'),
1410 esc_html__('Back to home page', '404-solution'),
1411 ),
1412 $templateContent
1413 );
1414 echo $templateContent;
1415 }
1416 }
1417 exit;
1418 }
1419
1420 // Meta Refresh
1421 if ($status === 0 && $location !== '') {
1422 status_header(200);
1423 $templatePath = __DIR__ . '/html/metaRefresh.html';
1424 if (file_exists($templatePath)) {
1425 $templateContent = file_get_contents($templatePath);
1426 if (is_string($templateContent)) {
1427 $templateContent = str_replace(
1428 array('{url}', '{delay}', '{title}', '{message}'),
1429 array(
1430 esc_url($location),
1431 '0',
1432 esc_html__('Redirecting...', '404-solution'),
1433 esc_html__('You are being redirected. Click the link if not redirected automatically.', '404-solution'),
1434 ),
1435 $templateContent
1436 );
1437 echo $templateContent;
1438 }
1439 }
1440 exit;
1441 }
1442
1443 $finalDestination = $this->buildFinalRedirectDestination($location, $requestedURL, $isCustom404);
1444
1445 $previousRequest = $this->readCookieWithPreviousRqeuestShort();
1446 $schemePos = $this->f->strpos($finalDestination, '://');
1447 $finalDestNoHome = ($schemePos !== false)
1448 ? $this->f->substr($finalDestination, $schemePos + 3) : $finalDestination;
1449 $slashPos = $this->f->strpos($finalDestNoHome, '/');
1450 $finalDestNoHome = ($slashPos !== false)
1451 ? $this->f->substr($finalDestNoHome, $slashPos) : '/';
1452
1453 $schemePos2 = $this->f->strpos($location, '://');
1454 $locationNoHome = ($schemePos2 !== false)
1455 ? $this->f->substr($location, $schemePos2 + 3) : $location;
1456 $slashPos2 = $this->f->strpos($locationNoHome, '/');
1457 $locationNoHome = ($slashPos2 !== false)
1458 ? $this->f->substr($locationNoHome, $slashPos2) : '/';
1459 if (!empty($previousRequest)) {
1460 if ($previousRequest == $finalDestNoHome && $previousRequest != $locationNoHome) {
1461 $this->logger->infoMessage("Maybe avoided infite redirects to/from: " .
1462 $previousRequest);
1463 $finalDestination = $location;
1464
1465 } else if ($previousRequest == $finalDestination) {
1466 $this->logger->infoMessage("Avoided infite redirects to/from: " .
1467 $previousRequest);
1468 return false;
1469 }
1470 }
1471
1472 if ($type == ABJ404_TYPE_404_DISPLAYED) {
1473 $abj404logic = abj_service('plugin_logic');
1474 $abj404logic->sendTo404Page($requestedURL, '', false);
1475
1476 return true;
1477 }
1478
1479 $this->setCookieWithPreviousRequest();
1480 if (!headers_sent()) {
1481 if (function_exists('abj404_benchmark_emit_headers')) {
1482 abj404_benchmark_emit_headers();
1483 }
1484 $useSafe = false;
1485 if (function_exists('wp_safe_redirect')) {
1486 $destHost = parse_url($finalDestination, PHP_URL_HOST);
1487 if ($destHost === null || $destHost === false || $destHost === '') {
1488 $useSafe = true;
1489 } else {
1490 $homeHost = parse_url(home_url(), PHP_URL_HOST);
1491 if (is_string($homeHost) && $homeHost !== '' && strtolower($homeHost) === strtolower($destHost)) {
1492 $useSafe = true;
1493 }
1494 }
1495 }
1496
1497 if ($useSafe) {
1498 wp_safe_redirect($finalDestination, $status, ABJ404_NAME);
1499 } else {
1500 wp_redirect($finalDestination, $status, ABJ404_NAME);
1501 }
1502 if (!apply_filters('abj404_should_exit', true, array('source' => 'forceRedirect_header'))) {
1503 return false;
1504 }
1505 exit;
1506 }
1507
1508 if (function_exists('abj404_benchmark_emit_headers')) {
1509 abj404_benchmark_emit_headers();
1510 }
1511 $c = '<script>' . 'function doRedirect() {' . "\n" .
1512 ' window.location.replace(' . wp_json_encode($finalDestination) . ');' . "\n" .
1513 '}' . "\n" .
1514 'setTimeout(doRedirect, 1);' . "\n" .
1515 '</script>' . "\n" .
1516 'Page moved: <a href="' . esc_url($finalDestination) . '">' .
1517 esc_html($finalDestination) . '</a>';
1518 echo $c;
1519 if (!apply_filters('abj404_should_exit', true, array('source' => 'forceRedirect_js'))) {
1520 return false;
1521 }
1522 exit;
1523 }
1524
1525 }
1526