| 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 |
|