PluginProbe
404 Solution / 4.3.5
404 Solution v4.3.5
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 / 404-solution.php

404-solution.php in 404 Solution 4.3.5, at 404-solution.php

435 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /*
9 Plugin Name: 404 Solution
10 Plugin URI: https://www.ajexperience.com/404-solution/
11 Description: The smartest 404 plugin - uses intelligent matching and spell-checking to find what visitors were actually looking for, not just redirect to homepage
12 Author: Aaron J
13 Author URI: https://www.ajexperience.com/404-solution/
14
15 Version: 4.3.5
16 Requires at least: 5.0
17 Requires PHP: 7.4
18
19 License: GPL-3.0-or-later
20 License URI: https://www.gnu.org/licenses/gpl-3.0.html
21 Domain Path: /languages
22 Text Domain: 404-solution
23
24 This program is free software; you can redistribute it and/or modify
25 it under the terms of the GNU General Public License as published by
26 the Free Software Foundation; either version 2 of the License, or
27 (at your option) any later version.
28
29 This program is distributed in the hope that it will be useful,
30 but WITHOUT ANY WARRANTY; without even the implied warranty of
31 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 GNU General Public License for more details.
33
34 You should have received a copy of the GNU General Public License
35 along with this program; if not, write to the Free Software
36 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
37 */
38
39 // Guard constant definitions so unit tests (and unusual loaders) can define them first.
40 if (!defined('ABJ404_PP')) {
41 define('ABJ404_PP', 'abj404_solution');
42 }
43 if (!defined('ABJ404_FILE')) {
44 define('ABJ404_FILE', __FILE__);
45 }
46 if (!defined('ABJ404_PATH')) {
47 define('ABJ404_PATH', plugin_dir_path(ABJ404_FILE));
48 }
49 // Content-addressed release marker compiled into the earliest boot file.
50 // DiagnosticModuleManifestTest recomputes it from every covered PHP module.
51 if (!defined('ABJ404_DIAGNOSTIC_BUILD_ID')) {
52 define('ABJ404_DIAGNOSTIC_BUILD_ID', '1675af0a29435f25cc1afbaef80703108620b1f8');
53 }
54
55 // The plugin version is read from this file's own header (single source of
56 // truth) and is defined HERE rather than in Loader.php because the
57 // post-upgrade opcache guard below needs it before any other plugin file
58 // loads. Loader.php keeps its own guarded define for loaders that reach it
59 // without going through this entry point; it becomes a no-op in normal boots.
60 // get_file_data() reads from disk, so the version is correct even when this
61 // entry point is itself being served from stale bytecode.
62 if (!defined('ABJ404_VERSION') && function_exists('get_file_data')) {
63 $__abj404_header = get_file_data(ABJ404_FILE, array('Version' => 'Version'));
64 define('ABJ404_VERSION', isset($__abj404_header['Version']) ? $__abj404_header['Version'] : '');
65 unset($__abj404_header);
66 }
67
68 // POST-UPGRADE OPCACHE GUARD -- must stay the FIRST plugin file required, ahead
69 // of every other require and ahead of spl_autoload_register() below.
70 //
71 // PHP revalidates cached bytecode per file on its own schedule, so for a few
72 // seconds after an update the class graph can be MIXED: a file at a path that
73 // is new in this release compiles fresh from disk while a file whose path did
74 // not change is still the previous release's bytecode. When a parent and its
75 // subclass land on opposite sides of that split and a signature changed, PHP
76 // raises an uncatchable E_ERROR while LINKING them -- which is exactly what
77 // killed requests on a real 4.2.0 -> 4.3.1 upgrade (see the file header of
78 // includes/root-boot/OpcacheUpgradeGuard.php for the incident detail).
79 //
80 // Because the fatal happens at class-linking time, the flush is only useful if
81 // it runs before the autoloader can link anything. Anything later (the old
82 // PluginLogicVersionUpgrader::invalidateOpcacheForCriticalFiles(), which ran
83 // inside the booted plugin) is structurally incapable of saving the request
84 // that dies. The call is gated on a persisted version stamp -- the
85 // `abj404_opcache_version` option, which doubles as the support-visible record
86 // of when this last ran -- so the work happens once per upgrade, not once per
87 // request. There is no logger this early in the boot, by construction.
88 require_once __DIR__ . '/includes/root-boot/OpcacheUpgradeGuard.php';
89 abj404_opcache_refresh_after_upgrade();
90
91 require_once __DIR__ . '/includes/core/PhpErrorLogFallback.php';
92 if (!defined('ABJ404_SHORTCODE_NAME')) {
93 define('ABJ404_SHORTCODE_NAME', 'abj404_solution_page_suggestions');
94 }
95 if (!isset($GLOBALS['abj404_display_errors'])) {
96 $GLOBALS['abj404_display_errors'] = false;
97 }
98
99 // Boot state: tracks whether the plugin loaded successfully.
100 // If a required file is missing or Loader.php fails, these let us show
101 // a degraded admin page instead of a fatal error.
102 $GLOBALS['abj404_boot_ok'] = false;
103 $GLOBALS['abj404_missing_files'] = array();
104 $GLOBALS['abj404_boot_error'] = '';
105
106 // Early settings and path helpers (abj404_getUploadsDir, abj404_get_settings_options).
107 // Required before Loader.php because boot-time paths call them before the service
108 // container is available. Defined in includes/root-boot/EarlySettingsAccess.php.
109 require_once __DIR__ . '/includes/root-boot/EarlySettingsAccess.php';
110
111 // Debug whitelist - only includes localhost/development environments by default
112 // WARNING: Only add trusted domains to this list. External domains could be a security risk.
113 // This list is used to enable detailed error logging for debugging purposes.
114 $GLOBALS['abj404_whitelist'] = array('127.0.0.1', '::1', 'localhost');
115
116 // Allow filtering the whitelist for advanced users who need to add custom domains
117 // Usage: add_filter('abj404_debug_whitelist', function($whitelist) { $whitelist[] = 'yourdomain.com'; return $whitelist; });
118 if (has_filter('abj404_debug_whitelist')) {
119 $GLOBALS['abj404_whitelist'] = apply_filters('abj404_debug_whitelist', $GLOBALS['abj404_whitelist']);
120 }
121
122 // The class autoloader (abj404_autoloader) is defined in
123 // includes/root-boot/Autoloader.php. Required FIRST (plain require, before any
124 // class use) so the function is available to register below; it loads its
125 // class->collaborator dependency map from the external data file
126 // includes/data/autoloader-trait-dependencies.php.
127 require_once __DIR__ . '/includes/root-boot/Autoloader.php';
128 spl_autoload_register('abj404_autoloader');
129
130 // Boot lifecycle waypoint checkpoints (Bruno timeout cause matrix, gap G3):
131 // boot_delta_ms used to be the ONLY boot measurement, and it was written at
132 // trace construction -- AFTER auth and the rate limiter -- so a slow boot and
133 // a slow auth/DB path were indistinguishable. These checkpoints localize a
134 // slow boot to a phase instead of a total. Gated to our own table-AJAX and
135 // canary-ladder requests only (see
136 // ABJ_404_Solution_AjaxDiagnosticRequestPolicy::bootWaypointRequestId()); the frontend
137 // 404 path is hot and must never pay this write cost.
138 //
139 // 'muplugins_loaded' is not separately hooked here: WordPress fires that
140 // action (wp-settings.php, do_action('muplugins_loaded')) BEFORE a regular
141 // (non-mu) plugin's file is even require()'d, so a normal plugin can never
142 // observe it firing -- registering a callback for it here would silently
143 // never run. This waypoint, the earliest point our own code can reach the
144 // checkpoint logger, is therefore the honest measurement of that entire
145 // pre-active-plugin window: mu-plugins, every listener on muplugins_loaded,
146 // and any plugin that loads ahead of us in the active-plugins list.
147 ABJ_404_Solution_BootWaypointRecorder::record('boot_plugin_entry', array(
148 'module' => '404-solution',
149 'path' => __FILE__,
150 'build_id' => ABJ404_DIAGNOSTIC_BUILD_ID,
151 ));
152 add_action('plugins_loaded', static function () {
153 ABJ_404_Solution_BootWaypointRecorder::record('plugins_loaded', array(
154 'module' => '404-solution',
155 'path' => __FILE__,
156 'build_id' => ABJ404_DIAGNOSTIC_BUILD_ID,
157 ));
158 // Same-site concurrency census (Bruno timeout cause matrix, gap GC).
159 // Registered here rather than at the plugin file's first line because it
160 // needs the service container, which Loader.php builds further down this
161 // file; 'plugins_loaded' is the earliest hook that is guaranteed to run
162 // after that for EVERY request, including the wp-cron loopbacks and the
163 // other plugins' admin-ajax polls whose contention is the thing being
164 // counted. The census itself decides which requests are in scope (see
165 // ABJ_404_Solution_SameSiteRequestCensus::channelForThisRequest); the hot
166 // front-end 404 path is not one of them.
167 //
168 // class_exists() first (Defensive Coding #1): the safe autoloader returns
169 // SILENTLY for a missing class file, so an install with a corrupt or
170 // partially-updated plugin directory would turn this diagnostic into a
171 // fatal on every admin request. A missing diagnostic is the correct
172 // degradation; a broken admin screen is not.
173 if (class_exists('ABJ_404_Solution_SameSiteRequestCensus')) {
174 ABJ_404_Solution_SameSiteRequestCensus::join();
175 }
176 }, PHP_INT_MIN);
177 add_action('init', static function () {
178 ABJ_404_Solution_BootWaypointRecorder::record('init', array(
179 'module' => '404-solution',
180 'path' => __FILE__,
181 'build_id' => ABJ404_DIAGNOSTIC_BUILD_ID,
182 ));
183 }, PHP_INT_MIN);
184 add_action('admin_init', static function () {
185 ABJ_404_Solution_BootWaypointRecorder::record('admin_init', array(
186 'module' => '404-solution',
187 'path' => __FILE__,
188 'build_id' => ABJ404_DIAGNOSTIC_BUILD_ID,
189 ));
190 }, PHP_INT_MIN);
191
192 // Root-boot procedural functions. Each file only DEFINES functions (the
193 // add_action/add_filter/add_shortcode registrations stay below in this file).
194 // Requiring them here, right after the autoloader is registered, guarantees
195 // every function is defined before any top-level executable statement that
196 // references it runs.
197 require_once __DIR__ . '/includes/root-boot/RuntimeHelpers.php';
198 require_once __DIR__ . '/includes/root-boot/RequestBenchmark.php';
199 require_once __DIR__ . '/includes/root-boot/BootGuard.php';
200 require_once __DIR__ . '/includes/root-boot/ShortcodeAndIntegrity.php';
201 require_once __DIR__ . '/includes/root-boot/DegradedSupportRequest.php';
202 require_once __DIR__ . '/includes/root-boot/CronListeners.php';
203 require_once __DIR__ . '/includes/root-boot/LocaleAndFrontendOptions.php';
204 require_once __DIR__ . '/includes/root-boot/AdminNotices.php';
205 require_once __DIR__ . '/includes/root-boot/LocalDebugDiagnostics.php';
206 require_once __DIR__ . '/includes/root-boot/AdminInitHandlers.php';
207 require_once __DIR__ . '/includes/root-boot/AdminPageCallback.php';
208 require_once __DIR__ . '/includes/root-boot/Frontend404Listener.php';
209
210
211 add_action('doing_it_wrong_run', function($function_name, $message, $version) {
212 if (strpos($message, '404-solution') !== false &&
213 $function_name == '_load_textdomain_just_in_time') {
214
215 try {
216 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
217
218 // Prepare the plugin path from ABJ404_FILE
219 $pluginPath = trailingslashit(plugin_dir_path(ABJ404_FILE)); // e.g., /var/www/html/wp-content/plugins/404-solution/
220
221 $logMessage = '';
222 $isOurPlugin = false;
223
224 foreach ($backtrace as $index => $frame) {
225 $file = isset($frame['file']) && is_string($frame['file']) ? $frame['file'] : '[internal function]';
226 $line = isset($frame['line']) && is_scalar($frame['line']) ? (string)$frame['line'] : '';
227 $func = $frame['function'];
228
229 if (!$isOurPlugin && $file !== '[internal function]' && strpos($file, $pluginPath) !== false) {
230 $isOurPlugin = true;
231 }
232
233 $logMessage .= "#$index $func at [$file:$line]\n";
234 }
235
236 if ($isOurPlugin) {
237 $header = "=== Detected Early Translation ===\n" .
238 "Function: $function_name\n" .
239 "Message: $message\n" .
240 "Version: $version\n";
241
242 if (!isset($GLOBALS['abj404_pending_errors']) || !is_array($GLOBALS['abj404_pending_errors'])) {
243 $GLOBALS['abj404_pending_errors'] = [];
244 }
245 $GLOBALS['abj404_pending_errors'][] = $header . $logMessage;
246 }
247
248 } catch (Throwable $e) {
249 abj404_logRuntimeWarning('Failed to capture early translation stack trace', $e);
250 }
251 }
252 }, 10, 3);
253
254 // shortcode (abj404_shortCodeListener is defined in
255 // includes/root-boot/ShortcodeAndIntegrity.php).
256 add_shortcode(ABJ404_SHORTCODE_NAME, 'abj404_shortCodeListener');
257
258 // Benchmark instrumentation functions are defined in
259 // includes/root-boot/RequestBenchmark.php. Start the per-request timer now and,
260 // when benchmarking is active, register the response-header emitter. These
261 // executable statements must run unconditionally (they previously sat inside the
262 // shortcode-listener function_exists guard).
263 abj404_benchmark_bootstrap_start();
264 if (abj404_is_benchmark_request()) {
265 add_action('send_headers', 'abj404_benchmark_emit_headers', PHP_INT_MAX);
266 }
267
268 // Boot-failure shutdown handler (abj404_boot_shutdown_handler) is defined in
269 // includes/root-boot/BootGuard.php. It captures compile/parse fatals in plugin
270 // files so the degraded admin page can show them on the next request. Registered
271 // here as part of the boot sequence.
272 register_shutdown_function('abj404_boot_shutdown_handler');
273
274 // Always load Loader.php to ensure plugin constants (ABJ404_TYPE_404_DISPLAYED,
275 // ABJ404_STATUS_MANUAL, etc.) are defined in all contexts: admin, REST API, WP-CLI
276 // eval, and template_redirect. Without this, direct calls to plugin classes via
277 // wp eval fail with "Undefined constant" errors because Loader.php was previously
278 // only loaded inside is_admin(), leaving WP-CLI and other non-admin contexts
279 // without the constants they need.
280 $__abj404_loader_path = plugin_dir_path( __FILE__ ) . "includes/Loader.php";
281 if (file_exists($__abj404_loader_path)) {
282 try {
283 require_once($__abj404_loader_path);
284 $GLOBALS['abj404_boot_ok'] = true;
285 // Clear any stale boot fatal transient from a previous failed load.
286 if (function_exists('delete_transient')) {
287 delete_transient('abj404_boot_fatal');
288 }
289 } catch (\Throwable $e) {
290 $GLOBALS['abj404_boot_ok'] = false;
291 $GLOBALS['abj404_boot_error'] = $e->getMessage();
292 abj404_logRuntimeWarning('Boot failed while loading Loader.php', $e);
293 }
294 } else {
295 $GLOBALS['abj404_boot_ok'] = false;
296 $GLOBALS['abj404_missing_files'][] = $__abj404_loader_path;
297 $GLOBALS['abj404_boot_error'] = 'Loader.php is missing.';
298 }
299 unset($__abj404_loader_path);
300
301 if ($GLOBALS['abj404_boot_ok']) {
302 // Lifecycle hooks must be registered in every load context. WP-CLI loads
303 // the plugin outside wp-admin before `wp plugin activate`, then fires the
304 // callback registered during that load. Admin-only hooks remain guarded
305 // inside WordPressHookRegistrar::registerAdminHooks().
306 try {
307 ABJ_404_Solution_WordPress_Connector::init();
308 if (is_admin()) {
309 ABJ_404_Solution_AjaxAdminEndpointRegistrar::register();
310 }
311 } catch (\Throwable $e) {
312 // init() failed. On admin requests, register the degraded page so the
313 // user still gets the original error detail instead of a missing menu.
314 $GLOBALS['abj404_boot_ok'] = false;
315 $GLOBALS['abj404_boot_error'] = 'Plugin initialization failed: ' . $e->getMessage();
316 abj404_logRuntimeWarning('Plugin initialization failed', $e);
317 if (is_admin()) {
318 add_action('admin_menu', 'abj404_degraded_admin_menu');
319 add_action('admin_notices', 'abj404_degraded_admin_notice');
320 }
321 }
322
323 // REST API: deferred to rest_api_init so DataAccess/PluginLogic are only loaded on actual REST requests.
324 add_action('rest_api_init', function() {
325 try {
326 $dao = ABJ_404_Solution_DataAccess::getInstance();
327 $logic = ABJ_404_Solution_PluginLogic::getInstance();
328 $restController = new ABJ_404_Solution_RestApiController($dao, $logic);
329 $restController->registerRoutes();
330 } catch (\Throwable $e) {
331 // rest_api_init is a shared WordPress hook every plugin's REST
332 // routes register on; an uncaught error here aborts the whole
333 // request and would also block other plugins' route
334 // registration, not just ours. A transient failure resolving
335 // this plugin's services (the VRMU incident: a class
336 // momentarily missing during a plugin self-update racing a
337 // live request) must degrade to "skip registering our routes
338 // this request" instead.
339 abj404_logRuntimeWarning('rest_api_init: route registration failed', $e);
340 }
341 });
342
343 // WP-CLI commands.
344 if (defined('WP_CLI') && WP_CLI) {
345 add_action('init', function() {
346 \WP_CLI::add_command('abj404', 'ABJ_404_Solution_WPCLICommands');
347 }, 1);
348 }
349 } elseif (function_exists('is_admin') && is_admin()) {
350 // Boot failed. Register degraded admin page so the admin sees instructions
351 // instead of a white screen or missing menu item.
352 add_action('admin_menu', 'abj404_degraded_admin_menu');
353 add_action('admin_notices', 'abj404_degraded_admin_notice');
354 // Best-effort: wire the support-request flow even on the degraded
355 // boot path so an admin who lands here can still send a debug report.
356 // This is the most valuable placement for the button, because the user
357 // often cannot reach the normal plugin UI from this screen. Deferred
358 // to plugins_loaded so the function (defined in DegradedSupportRequest.php)
359 // is available regardless of source-order.
360 if (function_exists('add_action')) {
361 add_action('plugins_loaded', 'abj404_degraded_register_support_request');
362 }
363 }
364
365 // --- Degraded-mode and admin-page callbacks ---
366 // abj404_degraded_register_support_request, abj404_degraded_admin_menu,
367 // abj404_degraded_admin_notice, and abj404_degraded_admin_page are defined in
368 // includes/root-boot/DegradedSupportRequest.php. abj404_admin_page_callback and
369 // abj404_render_last_admin_fatal_notice are defined in
370 // includes/root-boot/AdminPageCallback.php.
371
372 // ----
373 // get the plugin priority to use before adding the template_redirect action.
374 $__abj404_options = abj404_get_settings_options();
375 $__abj404_redirect_priority_raw = isset($__abj404_options['template_redirect_priority']) && is_scalar($__abj404_options['template_redirect_priority']) ? $__abj404_options['template_redirect_priority'] : 9;
376 $__abj404_template_redirect_priority = absint($__abj404_redirect_priority_raw);
377 $__abj404_redirect_all = isset($__abj404_options['redirect_all_requests']) && is_scalar($__abj404_options['redirect_all_requests']) ? (string)$__abj404_options['redirect_all_requests'] : '';
378 $__abj404_update_suggest = isset($__abj404_options['update_suggest_url']) && is_scalar($__abj404_options['update_suggest_url']) ? (string)$__abj404_options['update_suggest_url'] : '';
379 $GLOBALS['abj404_frontend_runtime_flags'] = array(
380 'redirect_all_requests' => ($__abj404_redirect_all === '1'),
381 'update_suggest_url' => ($__abj404_update_suggest === '1'),
382 );
383 $__abj404_lang_override = isset($__abj404_options['plugin_language_override']) && is_string($__abj404_options['plugin_language_override']) ? $__abj404_options['plugin_language_override'] : '';
384 $GLOBALS['abj404_plugin_language_override'] = $__abj404_lang_override;
385
386 // abj404_404listener is defined in includes/root-boot/Frontend404Listener.php.
387 add_action('template_redirect', 'abj404_404listener', $__abj404_template_redirect_priority);
388
389 unset($__abj404_options);
390 unset($__abj404_template_redirect_priority);
391 abj404_benchmark_mark_bootstrap_done();
392 // ---
393
394 // Cron action registrations. The heavy-prologue cron listeners
395 // (abj404_dailyMaintenanceCronJobListener, abj404_updateLogsHitsTableListener,
396 // abj404_sendQueuedReportListener) and the
397 // secondary cron listeners are all defined in includes/root-boot/CronListeners.php;
398 // only their add_action wiring lives here so the entry-point self-heal audit and
399 // the deactivate round-trip tests can discover the hook names in one place.
400 add_action('abj404_cleanupCronAction', 'abj404_dailyMaintenanceCronJobListener');
401 add_action('abj404_updateLogsHitsTableAction', 'abj404_updateLogsHitsTableListener');
402 add_action('abj404_refresh_status_counts', 'abj404_refreshStatusCountsListener', 10, 1);
403 add_action('abj404_repair_collations', 'abj404_repairCollationsListener');
404 add_action('abj404_logsv2_canonical_backfill', 'abj404_logsv2CanonicalUrlBackfillListener');
405 add_action('abj404_redirects_denorm_backfill', 'abj404_redirectsDenormBackfillListener');
406 add_action('abj404_redirects_sort_key_backfill', 'abj404_redirectsSortKeyBackfillListener');
407 add_action('abj404_updatePermalinkCacheAction', 'abj404_updatePermalinkCacheListener', 10, 2);
408 add_action('abj404_send_digest', 'abj404_sendDigestCronListener');
409 add_action('abj404_send_queued_report', 'abj404_sendQueuedReportListener', 10, 1);
410 add_action('abj404_rebuild_ngram_cache_hook', 'abj404_rebuildNGramCacheListener', 10, 1);
411 add_action('abj404_network_activation_hook', 'abj404_networkActivationListener');
412 add_action('abj404_network_activation_background', 'abj404_networkActivationBackgroundListener');
413 add_action('abj404_network_upgrade_background', 'abj404_networkUpgradeBackgroundListener');
414 add_action('abj404_gsc_fetch_cron', 'abj404_gscFetchCronListener');
415 add_action('abj404_gsc_background_refresh', 'abj404_gscBackgroundRefreshListener');
416
417 // abj404_override_plugin_locale is defined in
418 // includes/root-boot/LocaleAndFrontendOptions.php (along with
419 // abj404_is_redirect_all_requests_enabled).
420 add_filter('plugin_locale', 'abj404_override_plugin_locale', 999, 2);
421
422 // abj404_show_runtime_integrity_notice and abj404_show_plugin_db_notice are
423 // defined in includes/root-boot/AdminNotices.php.
424 add_action('admin_notices', 'abj404_show_runtime_integrity_notice');
425 add_action('admin_notices', 'abj404_show_plugin_db_notice');
426
427 // abj404_is_local_debug_host, abj404_get_simulated_db_latency_ms, and
428 // abj404_show_diagnostic_latency_notice are defined in
429 // includes/root-boot/LocalDebugDiagnostics.php.
430
431 // abj404_load_textdomain_if_needed, abj404_maybe_refresh_runtime_integrity_cache,
432 // and abj404_loadSomethingWhenWordPressIsReady are defined in
433 // includes/root-boot/AdminInitHandlers.php.
434 add_action('admin_init', 'abj404_loadSomethingWhenWordPressIsReady');
435