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 / frontend / FrontendRequestPipeline.php

FrontendRequestPipeline.php in 404 Solution 4.3.0, at includes/frontend/FrontendRequestPipeline.php

228 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Frontend request pipeline for 404 processing and redirects.
9 *
10 * Orchestrator: sequences strategies and delegates the work to focused
11 * collaborators. The phases of a 404 request:
12 *
13 * 1. (process404 only) Self-heal stale DB_VERSION via FrontendDbVersionRecovery.
14 * 2. Initialize ignore values + check do-not-process list.
15 * 3. Lookup existing redirect for URL, evaluate via RedirectCandidateEvaluator.
16 * 4. tryRegexRedirect (via RedirectDispatcher).
17 * 5. Run matching engines (via MatchingEngineOrchestrator).
18 * 6. WordPress 404-permalink-guess fallback (via WordPressGuessFallback).
19 * 7. Emit 404 page (via NotFoundResponseService) and log "gave up".
20 *
21 * Construction accepts a dependency bundle so collaborator assembly stays out
22 * of the request orchestration path.
23 */
24 class ABJ_404_Solution_FrontendRequestPipeline {
25
26 /** @var ABJ_404_Solution_Functions */
27 private $f;
28
29 /** @var ABJ_404_Solution_Logging */
30 private $logger;
31
32 /** @var ABJ_404_Solution_NotFoundResponseService */
33 private $notFoundResponse;
34
35 /** @var ABJ_404_Solution_RequestIgnoreNormalizer */
36 private $requestIgnoreNormalizer;
37
38 /** @var ABJ_404_Solution_FrontendPipelineTrace */
39 private $trace;
40
41 /** @var ABJ_404_Solution_MatchingEngineOrchestrator */
42 private $matchingEngineOrchestrator;
43
44 /** @var ABJ_404_Solution_WordPressGuessFallback */
45 private $wpGuessFallback;
46
47 /** @var ABJ_404_Solution_RedirectDispatcher */
48 private $dispatcher;
49
50 /** @var ABJ_404_Solution_FrontendDbVersionRecovery */
51 private $dbVersionRecovery;
52
53 /** @var ABJ_404_Solution_FrontendPipelineTelemetry */
54 private $telemetry;
55
56 /** @var ABJ_404_Solution_FrontendHitRecorder */
57 private $hitRecorder;
58
59 /** @var ABJ_404_Solution_FrontendAsyncSuggestionTrigger */
60 private $asyncSuggestionTrigger;
61
62 /** @var ABJ_404_Solution_FrontendRuntimeOptions */
63 private $runtimeOptions;
64
65 /** @var ABJ_404_Solution_ExistingRedirectLookup */
66 private $existingRedirectLookup;
67
68 /** @var ABJ_404_Solution_AutoRedirectHandler */
69 private $autoRedirectHandler;
70
71 /**
72 * @param ABJ_404_Solution_FrontendPipelineDependencies $dependencies
73 */
74 function __construct(ABJ_404_Solution_FrontendPipelineDependencies $dependencies) {
75 $this->f = $dependencies->functions();
76 $this->logger = $dependencies->logging();
77 $this->notFoundResponse = $dependencies->notFoundResponse();
78 $this->requestIgnoreNormalizer = $dependencies->requestIgnoreNormalizer();
79 $this->trace = $dependencies->trace();
80 $this->matchingEngineOrchestrator = $dependencies->matchingEngineOrchestrator();
81 $this->telemetry = $dependencies->telemetry();
82 $this->wpGuessFallback = $dependencies->wpGuessFallback();
83 $this->dispatcher = $dependencies->dispatcher();
84 $this->dbVersionRecovery = $dependencies->dbVersionRecovery();
85 $this->hitRecorder = $dependencies->hitRecorder();
86 $this->asyncSuggestionTrigger = $dependencies->asyncSuggestionTrigger();
87 $this->runtimeOptions = $dependencies->runtimeOptions();
88 $this->existingRedirectLookup = $dependencies->existingRedirectLookup();
89 $this->autoRedirectHandler = $dependencies->autoRedirectHandler();
90 }
91
92 /**
93 * Exposed for tests that need to exercise the engine fanout in isolation
94 * (see ExclusionMetaTest). Production callers route through process404().
95 *
96 * @return ABJ_404_Solution_MatchingEngineOrchestrator
97 */
98 function getMatchingEngineOrchestrator(): ABJ_404_Solution_MatchingEngineOrchestrator {
99 return $this->matchingEngineOrchestrator;
100 }
101
102 /** @return void */
103 function processRedirectAllRequests() {
104 $this->trace->reset();
105 $options = $this->runtimeOptions->get();
106
107 $userRequest = ABJ_404_Solution_UserRequest::getInstance();
108 if ($userRequest === null) {
109 return;
110 }
111 $pathOnly = $userRequest->getPath();
112 $urlSlugOnly = $userRequest->getOnlyTheSlug();
113
114 $this->requestIgnoreNormalizer->initializeIgnoreValues($pathOnly, $urlSlugOnly);
115 $requestedURL = $userRequest->getPathWithSortedQueryString();
116
117 $this->dispatcher->tryRegexRedirect($options, $requestedURL, $this->trace);
118
119 if (is_admin() || !is_404()) {
120 $this->logger->warn('If REDIRECT_ALL_REQUESTS is turned on then a regex redirect must be in place.');
121 }
122 }
123
124 /**
125 * Process the 404 path.
126 * @return void
127 */
128 function process404() {
129 if (!is_404() || is_admin()) {
130 // SAFE_BAIL: not a 404 or in wp-admin - nothing for us to do.
131 return;
132 }
133
134 // Self-heal a stale DB_VERSION on the frontend so end users get redirects
135 // without needing an admin visit (task 233). If recovery cannot close the
136 // gap (lock held, cooldown active, or migration repeatedly throws), fall
137 // through to a degraded redirect lookup (task 234) so manual redirects
138 // keep serving instead of every 404 falling to the theme 404 page.
139 $degradedMode = false;
140 if (defined('ABJ404_VERSION')) {
141 $options = $this->runtimeOptions->get(true);
142 if (isset($options['DB_VERSION']) && $options['DB_VERSION'] != ABJ404_VERSION) {
143 $options = $this->dbVersionRecovery->recoverIfStale($options);
144 if (!isset($options['DB_VERSION']) || $options['DB_VERSION'] != ABJ404_VERSION) {
145 $degradedMode = true;
146 }
147 }
148 }
149
150 abj_service('request_context')->process_start_time = abj_clock()->nowFloat();
151 $userRequest = ABJ_404_Solution_UserRequest::getInstance();
152 if ($userRequest === null) {
153 // SAFE_BAIL: no user request context - cannot resolve a URL to look up.
154 return;
155 }
156
157 $pathOnly = $userRequest->getPath();
158 $urlSlugOnly = $userRequest->getOnlyTheSlug();
159 $this->requestIgnoreNormalizer->initializeIgnoreValues($pathOnly, $urlSlugOnly);
160 $this->trace->reset();
161
162 if (abj_service('request_context')->ignore_donotprocess) {
163 $this->trace->add('Ignore list', 'Matched - request ignored', '');
164 $this->hitRecorder->record($pathOnly, '404', 'ignore_donotprocess', null, $this->trace->getSteps());
165 $this->telemetry->emitBenchmarkHeadersIfEnabled();
166 // SAFE_BAIL: ignore_donotprocess matched - admin opted this UA out.
167 return;
168 }
169 $this->trace->add('Ignore list', 'Not ignored');
170
171 $requestedURL = $userRequest->getPathWithSortedQueryString();
172 $requestedURLWithoutComments = $requestedURL;
173 if ($this->f->strpos($requestedURL, '/comment-page-') !== false) {
174 $withoutComments = $userRequest->getRequestURIWithoutCommentsPage();
175 if (is_string($withoutComments)) {
176 $requestedURLWithoutComments = $withoutComments;
177 }
178 }
179
180 $options = $this->runtimeOptions->get();
181 $autoRedirectsAreOn = !array_key_exists('auto_redirects', $options) || $options['auto_redirects'] == '1';
182
183 if ($requestedURL != '') {
184 $deferredAutoRedirect = $this->existingRedirectLookup->dispatchManualOrFindDeferredAuto(
185 $requestedURL,
186 $requestedURLWithoutComments,
187 $options,
188 $degradedMode,
189 $this->trace
190 );
191
192 $sentTo404Page = $this->dispatcher->tryRegexRedirect($options, $requestedURL, $this->trace);
193 if ($sentTo404Page) {
194 $this->telemetry->emitBenchmarkHeadersIfEnabled();
195 return;
196 }
197
198 if ($deferredAutoRedirect !== null) {
199 $this->dispatcher->processRedirect($requestedURL, $deferredAutoRedirect, 'existing', $this->trace);
200 exit;
201 }
202
203 if ($autoRedirectsAreOn) {
204 $this->autoRedirectHandler->promoteAndRedirectIfMatched($requestedURL, $urlSlugOnly, $options, $this->trace);
205 }
206
207 if (!$autoRedirectsAreOn) {
208 $this->asyncSuggestionTrigger->triggerIfNeeded($requestedURL);
209 $this->telemetry->emitBenchmarkHeadersIfEnabled();
210 $this->notFoundResponse->sendTo404Page($requestedURL, 'Do not create redirects per the options.', true, $options);
211 return;
212 }
213 } else {
214 $redirect = $this->existingRedirectLookup->lookupForEmptyUrl($requestedURL, $options, $degradedMode);
215 $this->dispatcher->handleEmptyUrlSinglePageRedirect($requestedURL, $redirect, $options, $this->trace);
216 }
217
218 $this->wpGuessFallback->tryFallback($autoRedirectsAreOn, $requestedURL, $options, $this->trace);
219
220 $this->requestIgnoreNormalizer->tryNormalPostQuery($options);
221 $this->trace->add('Result', 'No redirect - showed 404 page');
222 $this->hitRecorder->record($requestedURL, '404', 'gave up.', null, $this->trace->getSteps());
223 $this->asyncSuggestionTrigger->triggerIfNeeded($requestedURL);
224 $this->telemetry->emitBenchmarkHeadersIfEnabled();
225 $this->notFoundResponse->sendTo404Page($requestedURL, '', true, $options);
226 }
227 }
228