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 / 404-solution.php

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

312 lines 14.8 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.0
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 require_once __DIR__ . '/includes/core/PhpErrorLogFallback.php';
50 if (!defined('ABJ404_SHORTCODE_NAME')) {
51 define('ABJ404_SHORTCODE_NAME', 'abj404_solution_page_suggestions');
52 }
53 if (!isset($GLOBALS['abj404_display_errors'])) {
54 $GLOBALS['abj404_display_errors'] = false;
55 }
56
57 // Boot state: tracks whether the plugin loaded successfully.
58 // If a required file is missing or Loader.php fails, these let us show
59 // a degraded admin page instead of a fatal error.
60 $GLOBALS['abj404_boot_ok'] = false;
61 $GLOBALS['abj404_missing_files'] = array();
62 $GLOBALS['abj404_boot_error'] = '';
63
64 // Early settings and path helpers (abj404_getUploadsDir, abj404_get_settings_options).
65 // Required before Loader.php because boot-time paths call them before the service
66 // container is available. Defined in includes/root-boot/EarlySettingsAccess.php.
67 require_once __DIR__ . '/includes/root-boot/EarlySettingsAccess.php';
68
69 // Debug whitelist - only includes localhost/development environments by default
70 // WARNING: Only add trusted domains to this list. External domains could be a security risk.
71 // This list is used to enable detailed error logging for debugging purposes.
72 $GLOBALS['abj404_whitelist'] = array('127.0.0.1', '::1', 'localhost');
73
74 // Allow filtering the whitelist for advanced users who need to add custom domains
75 // Usage: add_filter('abj404_debug_whitelist', function($whitelist) { $whitelist[] = 'yourdomain.com'; return $whitelist; });
76 if (has_filter('abj404_debug_whitelist')) {
77 $GLOBALS['abj404_whitelist'] = apply_filters('abj404_debug_whitelist', $GLOBALS['abj404_whitelist']);
78 }
79
80 // The class autoloader (abj404_autoloader) is defined in
81 // includes/root-boot/Autoloader.php. Required FIRST (plain require, before any
82 // class use) so the function is available to register below; it loads its
83 // class->collaborator dependency map from the external data file
84 // includes/data/autoloader-trait-dependencies.php.
85 require_once __DIR__ . '/includes/root-boot/Autoloader.php';
86 spl_autoload_register('abj404_autoloader');
87
88 // Root-boot procedural functions. Each file only DEFINES functions (the
89 // add_action/add_filter/add_shortcode registrations stay below in this file).
90 // Requiring them here, right after the autoloader is registered, guarantees
91 // every function is defined before any top-level executable statement that
92 // references it runs.
93 require_once __DIR__ . '/includes/root-boot/RuntimeHelpers.php';
94 require_once __DIR__ . '/includes/root-boot/RequestBenchmark.php';
95 require_once __DIR__ . '/includes/root-boot/BootGuard.php';
96 require_once __DIR__ . '/includes/root-boot/ShortcodeAndIntegrity.php';
97 require_once __DIR__ . '/includes/root-boot/DegradedSupportRequest.php';
98 require_once __DIR__ . '/includes/root-boot/CronListeners.php';
99 require_once __DIR__ . '/includes/root-boot/LocaleAndFrontendOptions.php';
100 require_once __DIR__ . '/includes/root-boot/AdminNotices.php';
101 require_once __DIR__ . '/includes/root-boot/LocalDebugDiagnostics.php';
102 require_once __DIR__ . '/includes/root-boot/AdminInitHandlers.php';
103 require_once __DIR__ . '/includes/root-boot/AdminPageCallback.php';
104 require_once __DIR__ . '/includes/root-boot/Frontend404Listener.php';
105
106
107 add_action('doing_it_wrong_run', function($function_name, $message, $version) {
108 if (strpos($message, '404-solution') !== false &&
109 $function_name == '_load_textdomain_just_in_time') {
110
111 try {
112 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
113
114 // Prepare the plugin path from ABJ404_FILE
115 $pluginPath = trailingslashit(plugin_dir_path(ABJ404_FILE)); // e.g., /var/www/html/wp-content/plugins/404-solution/
116
117 $logMessage = '';
118 $isOurPlugin = false;
119
120 foreach ($backtrace as $index => $frame) {
121 $file = isset($frame['file']) && is_string($frame['file']) ? $frame['file'] : '[internal function]';
122 $line = isset($frame['line']) && is_scalar($frame['line']) ? (string)$frame['line'] : '';
123 $func = $frame['function'];
124
125 if (!$isOurPlugin && $file !== '[internal function]' && strpos($file, $pluginPath) !== false) {
126 $isOurPlugin = true;
127 }
128
129 $logMessage .= "#$index $func at [$file:$line]\n";
130 }
131
132 if ($isOurPlugin) {
133 $header = "=== Detected Early Translation ===\n" .
134 "Function: $function_name\n" .
135 "Message: $message\n" .
136 "Version: $version\n";
137
138 if (!isset($GLOBALS['abj404_pending_errors']) || !is_array($GLOBALS['abj404_pending_errors'])) {
139 $GLOBALS['abj404_pending_errors'] = [];
140 }
141 $GLOBALS['abj404_pending_errors'][] = $header . $logMessage;
142 }
143
144 } catch (Throwable $e) {
145 abj404_logRuntimeWarning('Failed to capture early translation stack trace', $e);
146 }
147 }
148 }, 10, 3);
149
150 // shortcode (abj404_shortCodeListener is defined in
151 // includes/root-boot/ShortcodeAndIntegrity.php).
152 add_shortcode(ABJ404_SHORTCODE_NAME, 'abj404_shortCodeListener');
153
154 // Benchmark instrumentation functions are defined in
155 // includes/root-boot/RequestBenchmark.php. Start the per-request timer now and,
156 // when benchmarking is active, register the response-header emitter. These
157 // executable statements must run unconditionally (they previously sat inside the
158 // shortcode-listener function_exists guard).
159 abj404_benchmark_bootstrap_start();
160 if (abj404_is_benchmark_request()) {
161 add_action('send_headers', 'abj404_benchmark_emit_headers', PHP_INT_MAX);
162 }
163
164 // Boot-failure shutdown handler (abj404_boot_shutdown_handler) is defined in
165 // includes/root-boot/BootGuard.php. It captures compile/parse fatals in plugin
166 // files so the degraded admin page can show them on the next request. Registered
167 // here as part of the boot sequence.
168 register_shutdown_function('abj404_boot_shutdown_handler');
169
170 // Always load Loader.php to ensure plugin constants (ABJ404_TYPE_404_DISPLAYED,
171 // ABJ404_STATUS_MANUAL, etc.) are defined in all contexts: admin, REST API, WP-CLI
172 // eval, and template_redirect. Without this, direct calls to plugin classes via
173 // wp eval fail with "Undefined constant" errors because Loader.php was previously
174 // only loaded inside is_admin(), leaving WP-CLI and other non-admin contexts
175 // without the constants they need.
176 $__abj404_loader_path = plugin_dir_path( __FILE__ ) . "includes/Loader.php";
177 if (file_exists($__abj404_loader_path)) {
178 try {
179 require_once($__abj404_loader_path);
180 $GLOBALS['abj404_boot_ok'] = true;
181 // Clear any stale boot fatal transient from a previous failed load.
182 if (function_exists('delete_transient')) {
183 delete_transient('abj404_boot_fatal');
184 }
185 } catch (\Throwable $e) {
186 $GLOBALS['abj404_boot_ok'] = false;
187 $GLOBALS['abj404_boot_error'] = $e->getMessage();
188 abj404_logRuntimeWarning('Boot failed while loading Loader.php', $e);
189 }
190 } else {
191 $GLOBALS['abj404_boot_ok'] = false;
192 $GLOBALS['abj404_missing_files'][] = $__abj404_loader_path;
193 $GLOBALS['abj404_boot_error'] = 'Loader.php is missing.';
194 }
195 unset($__abj404_loader_path);
196
197 if ($GLOBALS['abj404_boot_ok']) {
198 // admin
199 if (is_admin()) {
200 try {
201 ABJ_404_Solution_WordPress_Connector::init();
202 ABJ_404_Solution_AjaxAdminEndpointRegistrar::register();
203 } catch (\Throwable $e) {
204 // init() failed. Fall through to register the degraded admin page
205 // so the user still has a menu item with error details instead of nothing.
206 $GLOBALS['abj404_boot_ok'] = false;
207 $GLOBALS['abj404_boot_error'] = 'Plugin initialization failed: ' . $e->getMessage();
208 abj404_logRuntimeWarning('Admin initialization failed', $e);
209 add_action('admin_menu', 'abj404_degraded_admin_menu');
210 add_action('admin_notices', 'abj404_degraded_admin_notice');
211 }
212 }
213
214 // REST API: deferred to rest_api_init so DataAccess/PluginLogic are only loaded on actual REST requests.
215 add_action('rest_api_init', function() {
216 $dao = ABJ_404_Solution_DataAccess::getInstance();
217 $logic = ABJ_404_Solution_PluginLogic::getInstance();
218 $restController = new ABJ_404_Solution_RestApiController($dao, $logic);
219 $restController->registerRoutes();
220 });
221
222 // WP-CLI commands.
223 if (defined('WP_CLI') && WP_CLI) {
224 add_action('init', function() {
225 \WP_CLI::add_command('abj404', 'ABJ_404_Solution_WPCLICommands');
226 }, 1);
227 }
228 } elseif (function_exists('is_admin') && is_admin()) {
229 // Boot failed. Register degraded admin page so the admin sees instructions
230 // instead of a white screen or missing menu item.
231 add_action('admin_menu', 'abj404_degraded_admin_menu');
232 add_action('admin_notices', 'abj404_degraded_admin_notice');
233 // Best-effort: wire the support-request flow even on the degraded
234 // boot path so an admin who lands here can still send a debug report.
235 // This is the most valuable placement for the button, because the user
236 // often cannot reach the normal plugin UI from this screen. Deferred
237 // to plugins_loaded so the function (defined in DegradedSupportRequest.php)
238 // is available regardless of source-order.
239 if (function_exists('add_action')) {
240 add_action('plugins_loaded', 'abj404_degraded_register_support_request');
241 }
242 }
243
244 // --- Degraded-mode and admin-page callbacks ---
245 // abj404_degraded_register_support_request, abj404_degraded_admin_menu,
246 // abj404_degraded_admin_notice, and abj404_degraded_admin_page are defined in
247 // includes/root-boot/DegradedSupportRequest.php. abj404_admin_page_callback and
248 // abj404_render_last_admin_fatal_notice are defined in
249 // includes/root-boot/AdminPageCallback.php.
250
251 // ----
252 // get the plugin priority to use before adding the template_redirect action.
253 $__abj404_options = abj404_get_settings_options();
254 $__abj404_redirect_priority_raw = isset($__abj404_options['template_redirect_priority']) && is_scalar($__abj404_options['template_redirect_priority']) ? $__abj404_options['template_redirect_priority'] : 9;
255 $__abj404_template_redirect_priority = absint($__abj404_redirect_priority_raw);
256 $__abj404_redirect_all = isset($__abj404_options['redirect_all_requests']) && is_scalar($__abj404_options['redirect_all_requests']) ? (string)$__abj404_options['redirect_all_requests'] : '';
257 $__abj404_update_suggest = isset($__abj404_options['update_suggest_url']) && is_scalar($__abj404_options['update_suggest_url']) ? (string)$__abj404_options['update_suggest_url'] : '';
258 $GLOBALS['abj404_frontend_runtime_flags'] = array(
259 'redirect_all_requests' => ($__abj404_redirect_all === '1'),
260 'update_suggest_url' => ($__abj404_update_suggest === '1'),
261 );
262 $__abj404_lang_override = isset($__abj404_options['plugin_language_override']) && is_string($__abj404_options['plugin_language_override']) ? $__abj404_options['plugin_language_override'] : '';
263 $GLOBALS['abj404_plugin_language_override'] = $__abj404_lang_override;
264
265 // abj404_404listener is defined in includes/root-boot/Frontend404Listener.php.
266 add_action('template_redirect', 'abj404_404listener', $__abj404_template_redirect_priority);
267
268 unset($__abj404_options);
269 unset($__abj404_template_redirect_priority);
270 abj404_benchmark_mark_bootstrap_done();
271 // ---
272
273 // Cron action registrations. The heavy-prologue cron listeners
274 // (abj404_dailyMaintenanceCronJobListener, abj404_updateLogsHitsTableListener,
275 // abj404_sendQueuedReportListener) and the
276 // secondary cron listeners are all defined in includes/root-boot/CronListeners.php;
277 // only their add_action wiring lives here so the entry-point self-heal audit and
278 // the deactivate round-trip tests can discover the hook names in one place.
279 add_action('abj404_cleanupCronAction', 'abj404_dailyMaintenanceCronJobListener');
280 add_action('abj404_updateLogsHitsTableAction', 'abj404_updateLogsHitsTableListener');
281 add_action('abj404_logsv2_canonical_backfill', 'abj404_logsv2CanonicalUrlBackfillListener');
282 add_action('abj404_redirects_denorm_backfill', 'abj404_redirectsDenormBackfillListener');
283 add_action('abj404_redirects_sort_key_backfill', 'abj404_redirectsSortKeyBackfillListener');
284 add_action('abj404_updatePermalinkCacheAction', 'abj404_updatePermalinkCacheListener', 10, 2);
285 add_action('abj404_send_digest', 'abj404_sendDigestCronListener');
286 add_action('abj404_send_queued_report', 'abj404_sendQueuedReportListener', 10, 1);
287 add_action('abj404_rebuild_ngram_cache_hook', 'abj404_rebuildNGramCacheListener', 10, 1);
288 add_action('abj404_network_activation_hook', 'abj404_networkActivationListener');
289 add_action('abj404_network_activation_background', 'abj404_networkActivationBackgroundListener');
290 add_action('abj404_network_upgrade_background', 'abj404_networkUpgradeBackgroundListener');
291 add_action('abj404_gsc_fetch_cron', 'abj404_gscFetchCronListener');
292 add_action('abj404_gsc_background_refresh', 'abj404_gscBackgroundRefreshListener');
293
294 // abj404_override_plugin_locale is defined in
295 // includes/root-boot/LocaleAndFrontendOptions.php (along with
296 // abj404_is_redirect_all_requests_enabled).
297 add_filter('plugin_locale', 'abj404_override_plugin_locale', 999, 2);
298
299 // abj404_show_runtime_integrity_notice and abj404_show_plugin_db_notice are
300 // defined in includes/root-boot/AdminNotices.php.
301 add_action('admin_notices', 'abj404_show_runtime_integrity_notice');
302 add_action('admin_notices', 'abj404_show_plugin_db_notice');
303
304 // abj404_is_local_debug_host, abj404_get_simulated_db_latency_ms, and
305 // abj404_show_diagnostic_latency_notice are defined in
306 // includes/root-boot/LocalDebugDiagnostics.php.
307
308 // abj404_load_textdomain_if_needed, abj404_maybe_refresh_runtime_integrity_cache,
309 // and abj404_loadSomethingWhenWordPressIsReady are defined in
310 // includes/root-boot/AdminInitHandlers.php.
311 add_action('admin_init', 'abj404_loadSomethingWhenWordPressIsReady');
312