| 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.1.18 |
| 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 |
if (!defined('ABJ404_SHORTCODE_NAME')) { |
| 50 |
define('ABJ404_SHORTCODE_NAME', 'abj404_solution_page_suggestions'); |
| 51 |
} |
| 52 |
if (!isset($GLOBALS['abj404_display_errors'])) { |
| 53 |
$GLOBALS['abj404_display_errors'] = false; |
| 54 |
} |
| 55 |
|
| 56 |
// Boot state: tracks whether the plugin loaded successfully. |
| 57 |
// If a required file is missing or Loader.php fails, these let us show |
| 58 |
// a degraded admin page instead of a fatal error. |
| 59 |
$GLOBALS['abj404_boot_ok'] = false; |
| 60 |
$GLOBALS['abj404_missing_files'] = array(); |
| 61 |
$GLOBALS['abj404_boot_error'] = ''; |
| 62 |
|
| 63 |
// Used by multiple classes during early admin initialization (e.g. upgrade/migration paths). |
| 64 |
// This must be defined before any Loader.php initialization that might touch Logging/SynchronizationUtils. |
| 65 |
if (!function_exists('abj404_getUploadsDir')) { |
| 66 |
/** @return string */ |
| 67 |
function abj404_getUploadsDir() { |
| 68 |
$uploadsDirArray = wp_upload_dir(null, false); |
| 69 |
$uploadsDir = $uploadsDirArray['basedir']; |
| 70 |
$uploadsDir .= DIRECTORY_SEPARATOR . 'temp_' . ABJ404_PP . DIRECTORY_SEPARATOR; |
| 71 |
return $uploadsDir; |
| 72 |
} |
| 73 |
} |
| 74 |
|
| 75 |
if (!function_exists('abj404_get_settings_options')) { |
| 76 |
/** |
| 77 |
* Centralized settings read so call sites don't repeat option-shape checks. |
| 78 |
* |
| 79 |
* @return array<string, mixed> |
| 80 |
*/ |
| 81 |
function abj404_get_settings_options() { |
| 82 |
$options = get_option('abj404_settings'); |
| 83 |
return is_array($options) ? $options : array(); |
| 84 |
} |
| 85 |
} |
| 86 |
|
| 87 |
// Debug whitelist - only includes localhost/development environments by default |
| 88 |
// WARNING: Only add trusted domains to this list. External domains could be a security risk. |
| 89 |
// This list is used to enable detailed error logging for debugging purposes. |
| 90 |
$GLOBALS['abj404_whitelist'] = array('127.0.0.1', '::1', 'localhost'); |
| 91 |
|
| 92 |
// Allow filtering the whitelist for advanced users who need to add custom domains |
| 93 |
// Usage: add_filter('abj404_debug_whitelist', function($whitelist) { $whitelist[] = 'yourdomain.com'; return $whitelist; }); |
| 94 |
if (has_filter('abj404_debug_whitelist')) { |
| 95 |
$GLOBALS['abj404_whitelist'] = apply_filters('abj404_debug_whitelist', $GLOBALS['abj404_whitelist']); |
| 96 |
} |
| 97 |
|
| 98 |
if (!function_exists('abj404_autoloader')) { |
| 99 |
/** |
| 100 |
* @param string $class |
| 101 |
* @return void |
| 102 |
*/ |
| 103 |
function abj404_autoloader($class) { |
| 104 |
// some people were having issues with possibly parent classes not being loaded before their children. |
| 105 |
$childParentMap = [ |
| 106 |
'ABJ_404_Solution_FunctionsMBString' => 'ABJ_404_Solution_Functions', |
| 107 |
'ABJ_404_Solution_FunctionsPreg' => 'ABJ_404_Solution_Functions', |
| 108 |
]; |
| 109 |
|
| 110 |
// only pay attention if it's for us. don't bother for other things. |
| 111 |
if (substr($class, 0, 16) !== 'ABJ_404_Solution') { |
| 112 |
return; |
| 113 |
} |
| 114 |
|
| 115 |
// Use a deterministic classmap to avoid runtime glob() scans on real sites. |
| 116 |
static $abj404_autoLoaderClassMap = null; |
| 117 |
if ($abj404_autoLoaderClassMap === null) { |
| 118 |
$mapFile = __DIR__ . '/includes/classmap.php'; |
| 119 |
$abj404_autoLoaderClassMap = file_exists($mapFile) ? require $mapFile : array(); |
| 120 |
} |
| 121 |
|
| 122 |
if (!array_key_exists($class, $abj404_autoLoaderClassMap)) { |
| 123 |
return; |
| 124 |
} |
| 125 |
|
| 126 |
// Trait dependency pre-check: classes that use require_once for trait files at file |
| 127 |
// scope will cause an uncatchable compile-time fatal if any trait file is missing. |
| 128 |
// Verify all trait files exist BEFORE loading the parent class. |
| 129 |
static $traitDependencies = null; |
| 130 |
if ($traitDependencies === null) { |
| 131 |
// Use __DIR__ (not ABJ404_PATH) to match the classmap's path resolution. |
| 132 |
$inc = __DIR__ . '/includes/'; |
| 133 |
$traitDependencies = array( |
| 134 |
'ABJ_404_Solution_View' => array( |
| 135 |
$inc . 'ViewTrait_Shared.php', |
| 136 |
$inc . 'ViewTrait_UI.php', |
| 137 |
$inc . 'ViewTrait_Stats.php', |
| 138 |
$inc . 'ViewTrait_Settings.php', |
| 139 |
$inc . 'ViewTrait_Redirects.php', |
| 140 |
$inc . 'ViewTrait_RedirectsTable.php', |
| 141 |
$inc . 'ViewTrait_Logs.php', |
| 142 |
), |
| 143 |
'ABJ_404_Solution_DataAccess' => array( |
| 144 |
$inc . 'DataAccessTrait_Maintenance.php', |
| 145 |
$inc . 'DataAccessTrait_Connection.php', |
| 146 |
$inc . 'DataAccessTrait_ViewMetadata.php', |
| 147 |
$inc . 'DataAccessTrait_ViewQueries.php', |
| 148 |
$inc . 'DataAccessTrait_ViewQueriesStaged.php', |
| 149 |
$inc . 'DataAccessTrait_ViewBuildStageRunner.php', |
| 150 |
$inc . 'DataAccessTrait_ViewBuildStageCallbacks.php', |
| 151 |
$inc . 'DataAccessTrait_ViewQueriesStagedRead.php', |
| 152 |
$inc . 'DataAccessTrait_ViewBuildAdaptive.php', |
| 153 |
$inc . 'DataAccessTrait_ViewBuildHelpers.php', |
| 154 |
$inc . 'DataAccessTrait_ViewBuildLockAndCron.php', |
| 155 |
$inc . 'DataAccessTrait_ViewBuildPhpEnvProbe.php', |
| 156 |
$inc . 'DataAccessTrait_ViewBuildSessionEnvProbe.php', |
| 157 |
$inc . 'DataAccessTrait_ViewBuildHostFailurePolicy.php', |
| 158 |
$inc . 'DataAccessTrait_ViewSnapshotCache.php', |
| 159 |
$inc . 'DataAccessTrait_Logs.php', |
| 160 |
$inc . 'DataAccessTrait_LogsHitsRebuild.php', |
| 161 |
$inc . 'DataAccessTrait_Redirects.php', |
| 162 |
$inc . 'DataAccessTrait_PublishedContent.php', |
| 163 |
$inc . 'DataAccessTrait_Stats.php', |
| 164 |
$inc . 'DataAccessTrait_ErrorClassification.php', |
| 165 |
$inc . 'DataAccessTrait_SqlErrorReporting.php', |
| 166 |
$inc . 'DataAccessTrait_QueryTimeouts.php', |
| 167 |
), |
| 168 |
'ABJ_404_Solution_PluginLogic' => array( |
| 169 |
$inc . 'PluginLogicTrait_UrlNormalization.php', |
| 170 |
$inc . 'PluginLogicTrait_AdminActions.php', |
| 171 |
$inc . 'PluginLogicTrait_ImportExport.php', |
| 172 |
$inc . 'PluginLogicTrait_SettingsUpdate.php', |
| 173 |
$inc . 'PluginLogicTrait_PageOrdering.php', |
| 174 |
$inc . 'PluginLogicTrait_Lifecycle.php', |
| 175 |
), |
| 176 |
'ABJ_404_Solution_SpellChecker' => array( |
| 177 |
$inc . 'SpellCheckerTrait_PostListeners.php', |
| 178 |
$inc . 'SpellCheckerTrait_URLMatching.php', |
| 179 |
$inc . 'SpellCheckerTrait_CandidateFiltering.php', |
| 180 |
$inc . 'SpellCheckerTrait_LevenshteinEngine.php', |
| 181 |
), |
| 182 |
'ABJ_404_Solution_DatabaseUpgradesEtc' => array( |
| 183 |
$inc . 'DatabaseUpgradesEtcTrait_NGram.php', |
| 184 |
$inc . 'DatabaseUpgradesEtcTrait_Maintenance.php', |
| 185 |
$inc . 'DatabaseUpgradesEtcTrait_PluginUpdate.php', |
| 186 |
$inc . 'DatabaseUpgradesEtcTrait_TableRepair.php', |
| 187 |
$inc . 'DatabaseUpgradesEtcTrait_Indexes.php', |
| 188 |
), |
| 189 |
// AJAX handler classes that pull in shared traits via `use`. |
| 190 |
// Without these entries, a corrupted upload that loses the trait |
| 191 |
// file would cause an uncatchable compile fatal in the host class. |
| 192 |
'ABJ_404_Solution_Ajax_TrashLink' => array( |
| 193 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 194 |
), |
| 195 |
'ABJ_404_Solution_Ajax_TrendData' => array( |
| 196 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 197 |
), |
| 198 |
'ABJ_404_Solution_Ajax_CrossPluginImporter' => array( |
| 199 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 200 |
), |
| 201 |
'ABJ_404_Solution_Ajax_EngineProfiles' => array( |
| 202 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 203 |
), |
| 204 |
'ABJ_404_Solution_Ajax_SettingsModeToggle' => array( |
| 205 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 206 |
), |
| 207 |
'ABJ_404_Solution_Ajax_SupportRequest' => array( |
| 208 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 209 |
), |
| 210 |
'ABJ_404_Solution_Ajax_SupportRequestPreview' => array( |
| 211 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 212 |
), |
| 213 |
'ABJ_404_Solution_ViewUpdater' => array( |
| 214 |
$inc . 'ajax/AjaxFailureLoggingTrait.php', |
| 215 |
), |
| 216 |
'ABJ_404_Solution_FeedbackTransport' => array( |
| 217 |
$inc . 'FeedbackTransportTrait_EnvironmentExtras.php', |
| 218 |
), |
| 219 |
); |
| 220 |
} |
| 221 |
|
| 222 |
if (isset($traitDependencies[$class])) { |
| 223 |
foreach ($traitDependencies[$class] as $traitFile) { |
| 224 |
if (!file_exists($traitFile)) { |
| 225 |
$GLOBALS['abj404_missing_files'][] = $traitFile; |
| 226 |
// Don't load the parent class — the compile-time fatal is uncatchable. |
| 227 |
return; |
| 228 |
} |
| 229 |
} |
| 230 |
} |
| 231 |
|
| 232 |
// Ensure the parent class is loaded first. |
| 233 |
if (array_key_exists($class, $childParentMap)) { |
| 234 |
$parentClass = $childParentMap[$class]; |
| 235 |
if (!class_exists($parentClass, false) && array_key_exists($parentClass, $abj404_autoLoaderClassMap)) { |
| 236 |
$parentFile = $abj404_autoLoaderClassMap[$parentClass]; |
| 237 |
if (!file_exists($parentFile)) { |
| 238 |
$GLOBALS['abj404_missing_files'][] = $parentFile; |
| 239 |
return; |
| 240 |
} |
| 241 |
require_once $parentFile; |
| 242 |
} |
| 243 |
} |
| 244 |
|
| 245 |
$classFile = $abj404_autoLoaderClassMap[$class]; |
| 246 |
if (!file_exists($classFile)) { |
| 247 |
$GLOBALS['abj404_missing_files'][] = $classFile; |
| 248 |
return; |
| 249 |
} |
| 250 |
|
| 251 |
require_once $classFile; |
| 252 |
} |
| 253 |
} |
| 254 |
spl_autoload_register('abj404_autoloader'); |
| 255 |
|
| 256 |
|
| 257 |
add_action('doing_it_wrong_run', function($function_name, $message, $version) { |
| 258 |
if (strpos($message, '404-solution') !== false && |
| 259 |
$function_name == '_load_textdomain_just_in_time') { |
| 260 |
|
| 261 |
try { |
| 262 |
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
| 263 |
|
| 264 |
// Prepare the plugin path from ABJ404_FILE |
| 265 |
$pluginPath = trailingslashit(plugin_dir_path(ABJ404_FILE)); // e.g., /var/www/html/wp-content/plugins/404-solution/ |
| 266 |
|
| 267 |
$logMessage = ''; |
| 268 |
$isOurPlugin = false; |
| 269 |
|
| 270 |
foreach ($backtrace as $index => $frame) { |
| 271 |
$file = isset($frame['file']) ? $frame['file'] : '[internal function]'; |
| 272 |
$line = isset($frame['line']) ? $frame['line'] : ''; |
| 273 |
$func = $frame['function']; |
| 274 |
|
| 275 |
if (!$isOurPlugin && is_string($file) && strpos($file, $pluginPath) !== false) { |
| 276 |
$isOurPlugin = true; |
| 277 |
} |
| 278 |
|
| 279 |
$logMessage .= "#$index $func at [$file:$line]\n"; |
| 280 |
} |
| 281 |
|
| 282 |
if ($isOurPlugin) { |
| 283 |
$header = "=== Detected Early Translation ===\n" . |
| 284 |
"Function: $function_name\n" . |
| 285 |
"Message: $message\n" . |
| 286 |
"Version: $version\n"; |
| 287 |
|
| 288 |
if (!isset($GLOBALS['abj404_pending_errors'])) { |
| 289 |
$GLOBALS['abj404_pending_errors'] = []; |
| 290 |
} |
| 291 |
$GLOBALS['abj404_pending_errors'][] = $header . $logMessage; |
| 292 |
} |
| 293 |
|
| 294 |
} catch (Throwable $e) { |
| 295 |
error_log('404 Solution: failed to capture early translation stack trace: ' . |
| 296 |
$e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); |
| 297 |
} |
| 298 |
} |
| 299 |
}, 10, 3); |
| 300 |
|
| 301 |
// shortcode |
| 302 |
add_shortcode(ABJ404_SHORTCODE_NAME, 'abj404_shortCodeListener'); |
| 303 |
if (!function_exists('abj404_shortCodeListener')) { |
| 304 |
/** |
| 305 |
* @param array<string, mixed>|string $atts |
| 306 |
* @return string |
| 307 |
*/ |
| 308 |
function abj404_shortCodeListener($atts) { |
| 309 |
if (!$GLOBALS['abj404_boot_ok']) { |
| 310 |
return ''; |
| 311 |
} |
| 312 |
abj404_load_textdomain_if_needed(); |
| 313 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 314 |
/** @var array<string, mixed> $safeAtts */ |
| 315 |
$safeAtts = is_array($atts) ? $atts : array(); |
| 316 |
return ABJ_404_Solution_ShortCode::shortcodePageSuggestions($safeAtts); |
| 317 |
} |
| 318 |
|
| 319 |
if (!function_exists('abj404_get_required_runtime_files')) { |
| 320 |
/** |
| 321 |
* Files required for a healthy runtime/plugin package. |
| 322 |
* Covers boot-critical PHP files and essential SQL templates. |
| 323 |
* |
| 324 |
* @return array<int, string> |
| 325 |
*/ |
| 326 |
function abj404_get_required_runtime_files() { |
| 327 |
$inc = ABJ404_PATH . 'includes/'; |
| 328 |
return array( |
| 329 |
// Boot-critical |
| 330 |
$inc . 'Loader.php', |
| 331 |
$inc . 'bootstrap.php', |
| 332 |
$inc . 'classmap.php', |
| 333 |
$inc . 'ServiceContainer.php', |
| 334 |
$inc . 'Clock.php', |
| 335 |
$inc . 'ErrorHandler.php', |
| 336 |
// Core classes |
| 337 |
$inc . 'WordPress_Connector.php', |
| 338 |
$inc . 'Functions.php', |
| 339 |
$inc . 'Logging.php', |
| 340 |
$inc . 'FrontendRequestPipeline.php', |
| 341 |
$inc . 'ImportExportService.php', |
| 342 |
$inc . 'QueryBudgetInstrumentation.php', |
| 343 |
// Support-request button + AJAX. Listed here so that a |
| 344 |
// corrupt install which lost any of these files is |
| 345 |
// surfaced to the admin (the "missing files" list on |
| 346 |
// the degraded admin page is what tells the user what |
| 347 |
// to re-upload). The degraded admin page itself uses |
| 348 |
// these files when present to render the in-page |
| 349 |
// support button. |
| 350 |
$inc . 'SupportRequestButton.php', |
| 351 |
$inc . 'FeedbackTransport.php', |
| 352 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 353 |
$inc . 'ajax/Ajax_SupportRequest.php', |
| 354 |
$inc . 'ajax/Ajax_SupportRequestPreview.php', |
| 355 |
$inc . 'ajax/SupportRequest.js', |
| 356 |
$inc . 'js/support-request-button.js', |
| 357 |
// View + traits |
| 358 |
$inc . 'View.php', |
| 359 |
$inc . 'ViewTrait_Shared.php', |
| 360 |
$inc . 'ViewTrait_UI.php', |
| 361 |
$inc . 'ViewTrait_Stats.php', |
| 362 |
$inc . 'ViewTrait_Settings.php', |
| 363 |
$inc . 'ViewTrait_Redirects.php', |
| 364 |
$inc . 'ViewTrait_RedirectsTable.php', |
| 365 |
$inc . 'ViewTrait_Logs.php', |
| 366 |
// DataAccess + traits |
| 367 |
$inc . 'DataAccess.php', |
| 368 |
$inc . 'DataAccessTrait_Maintenance.php', |
| 369 |
$inc . 'DataAccessTrait_ViewQueries.php', |
| 370 |
$inc . 'DataAccessTrait_Logs.php', |
| 371 |
$inc . 'DataAccessTrait_Redirects.php', |
| 372 |
$inc . 'DataAccessTrait_PublishedContent.php', |
| 373 |
$inc . 'DataAccessTrait_Stats.php', |
| 374 |
// PluginLogic + traits |
| 375 |
$inc . 'PluginLogic.php', |
| 376 |
$inc . 'PluginLogicTrait_UrlNormalization.php', |
| 377 |
$inc . 'PluginLogicTrait_AdminActions.php', |
| 378 |
$inc . 'PluginLogicTrait_ImportExport.php', |
| 379 |
$inc . 'PluginLogicTrait_SettingsUpdate.php', |
| 380 |
$inc . 'PluginLogicTrait_PageOrdering.php', |
| 381 |
$inc . 'PluginLogicTrait_Lifecycle.php', |
| 382 |
// SpellChecker + traits |
| 383 |
$inc . 'SpellChecker.php', |
| 384 |
$inc . 'SpellCheckerTrait_PostListeners.php', |
| 385 |
$inc . 'SpellCheckerTrait_URLMatching.php', |
| 386 |
$inc . 'SpellCheckerTrait_CandidateFiltering.php', |
| 387 |
$inc . 'SpellCheckerTrait_LevenshteinEngine.php', |
| 388 |
// DatabaseUpgradesEtc + traits |
| 389 |
$inc . 'DatabaseUpgradesEtc.php', |
| 390 |
$inc . 'DatabaseUpgradesEtcTrait_NGram.php', |
| 391 |
$inc . 'DatabaseUpgradesEtcTrait_Maintenance.php', |
| 392 |
$inc . 'DatabaseUpgradesEtcTrait_PluginUpdate.php', |
| 393 |
// SQL templates — all files required for correct operation. |
| 394 |
// A test (SqlFileIntegrityListCompletenessTest) verifies this list |
| 395 |
// stays in sync with the actual files in includes/sql/. |
| 396 |
$inc . 'sql/correctLookupTableIssue.sql', |
| 397 |
$inc . 'sql/createEngineProfilesTable.sql', |
| 398 |
$inc . 'sql/createLogTable.sql', |
| 399 |
$inc . 'sql/createLogsHitsPreAggTempTable.sql', |
| 400 |
$inc . 'sql/createLogsHitsTempTable.sql', |
| 401 |
$inc . 'sql/createLookupTable.sql', |
| 402 |
$inc . 'sql/createNGramCacheTable.sql', |
| 403 |
$inc . 'sql/createPermalinkCacheTable.sql', |
| 404 |
$inc . 'sql/createRedirectConditionsTable.sql', |
| 405 |
$inc . 'sql/createRedirectsTable.sql', |
| 406 |
$inc . 'sql/createSpellingCacheTable.sql', |
| 407 |
$inc . 'sql/createViewBuildTable.sql', |
| 408 |
$inc . 'sql/createViewCacheTable.sql', |
| 409 |
$inc . 'sql/deleteOldLogs.sql', |
| 410 |
$inc . 'sql/getAdditionalPostData.sql', |
| 411 |
$inc . 'sql/getIDsNeededForPermalinkCache.sql', |
| 412 |
$inc . 'sql/getLogRecords.sql', |
| 413 |
$inc . 'sql/getLogsCount.sql', |
| 414 |
$inc . 'sql/getDistinctLoggedUrls.sql', |
| 415 |
$inc . 'sql/getLogsIDandURL.sql', |
| 416 |
$inc . 'sql/getLogsIDandURLForAjax.sql', |
| 417 |
$inc . 'sql/getMostUnusedRedirects.sql', |
| 418 |
$inc . 'sql/getOrphanedAutoRedirects.sql', |
| 419 |
$inc . 'sql/getPermalinkFromURL.sql', |
| 420 |
$inc . 'sql/getPostsNeedingContentKeywords.sql', |
| 421 |
$inc . 'sql/getPublishedCategories.sql', |
| 422 |
$inc . 'sql/getPublishedImageIDs.sql', |
| 423 |
$inc . 'sql/getPublishedPagesAndPostsIDs.sql', |
| 424 |
$inc . 'sql/getPublishedTags.sql', |
| 425 |
$inc . 'sql/getRedirectsExport.sql', |
| 426 |
$inc . 'sql/getRedirectsForView.sql', |
| 427 |
$inc . 'sql/getRedirectsForViewTempTable.sql', |
| 428 |
$inc . 'sql/getRedirectsWithLogs.sql', |
| 429 |
$inc . 'sql/importDataFromPluginRedirectioner.sql', |
| 430 |
$inc . 'sql/insertPermalinkCache.sql', |
| 431 |
$inc . 'sql/insertSpellingCache.sql', |
| 432 |
$inc . 'sql/logsSetMinLogID.sql', |
| 433 |
$inc . 'sql/migrateToNewLogsTable.sql', |
| 434 |
$inc . 'sql/selectTableEngines.sql', |
| 435 |
$inc . 'sql/updatePermalinkCache.sql', |
| 436 |
$inc . 'sql/updatePermalinkCacheParentPages.sql', |
| 437 |
); |
| 438 |
} |
| 439 |
} |
| 440 |
|
| 441 |
if (!function_exists('abj404_verify_runtime_integrity')) { |
| 442 |
/** |
| 443 |
* Validate that required plugin files are present. |
| 444 |
* |
| 445 |
* @return array<int, string> Missing file paths. |
| 446 |
*/ |
| 447 |
function abj404_verify_runtime_integrity() { |
| 448 |
$missing = array(); |
| 449 |
foreach (abj404_get_required_runtime_files() as $path) { |
| 450 |
if (!file_exists($path)) { |
| 451 |
$missing[] = $path; |
| 452 |
} |
| 453 |
} |
| 454 |
return $missing; |
| 455 |
} |
| 456 |
} |
| 457 |
|
| 458 |
if (!function_exists('abj404_is_benchmark_request')) { |
| 459 |
/** |
| 460 |
* Benchmark instrumentation is disabled by default and only enabled per-request. |
| 461 |
* |
| 462 |
* @return bool |
| 463 |
*/ |
| 464 |
function abj404_is_benchmark_request() { |
| 465 |
return isset($_GET['abj404_bench']) && (string)$_GET['abj404_bench'] === '1'; |
| 466 |
} |
| 467 |
} |
| 468 |
|
| 469 |
if (!function_exists('abj404_benchmark_bootstrap_start')) { |
| 470 |
/** @return void */ |
| 471 |
function abj404_benchmark_bootstrap_start() { |
| 472 |
if (!abj404_is_benchmark_request()) { |
| 473 |
return; |
| 474 |
} |
| 475 |
if (!isset($GLOBALS['abj404_benchmark_state']) || !is_array($GLOBALS['abj404_benchmark_state'])) { |
| 476 |
$GLOBALS['abj404_benchmark_state'] = array( |
| 477 |
'start' => microtime(true), |
| 478 |
'bootstrap_done' => 0.0, |
| 479 |
'db_query_count' => 0, |
| 480 |
'db_query_ms' => 0.0, |
| 481 |
'redirect_lookup_ms' => 0.0, |
| 482 |
); |
| 483 |
} |
| 484 |
} |
| 485 |
} |
| 486 |
|
| 487 |
if (!function_exists('abj404_benchmark_mark_bootstrap_done')) { |
| 488 |
/** @return void */ |
| 489 |
function abj404_benchmark_mark_bootstrap_done() { |
| 490 |
if (!abj404_is_benchmark_request() || !isset($GLOBALS['abj404_benchmark_state'])) { |
| 491 |
return; |
| 492 |
} |
| 493 |
$GLOBALS['abj404_benchmark_state']['bootstrap_done'] = microtime(true); |
| 494 |
} |
| 495 |
} |
| 496 |
|
| 497 |
if (!function_exists('abj404_benchmark_record_db_query')) { |
| 498 |
/** |
| 499 |
* @param float $elapsedMs |
| 500 |
* @return void |
| 501 |
*/ |
| 502 |
function abj404_benchmark_record_db_query($elapsedMs) { |
| 503 |
if (!abj404_is_benchmark_request() || !isset($GLOBALS['abj404_benchmark_state'])) { |
| 504 |
return; |
| 505 |
} |
| 506 |
$elapsedMs = max(0.0, (float)$elapsedMs); |
| 507 |
$GLOBALS['abj404_benchmark_state']['db_query_count']++; |
| 508 |
$GLOBALS['abj404_benchmark_state']['db_query_ms'] += $elapsedMs; |
| 509 |
} |
| 510 |
} |
| 511 |
|
| 512 |
if (!function_exists('abj404_benchmark_record_redirect_lookup')) { |
| 513 |
/** |
| 514 |
* @param float $elapsedMs |
| 515 |
* @return void |
| 516 |
*/ |
| 517 |
function abj404_benchmark_record_redirect_lookup($elapsedMs) { |
| 518 |
if (!abj404_is_benchmark_request() || !isset($GLOBALS['abj404_benchmark_state'])) { |
| 519 |
return; |
| 520 |
} |
| 521 |
$GLOBALS['abj404_benchmark_state']['redirect_lookup_ms'] += max(0.0, (float)$elapsedMs); |
| 522 |
} |
| 523 |
} |
| 524 |
|
| 525 |
if (!function_exists('abj404_benchmark_emit_headers')) { |
| 526 |
/** @return void */ |
| 527 |
function abj404_benchmark_emit_headers() { |
| 528 |
if (!abj404_is_benchmark_request() || headers_sent() || !isset($GLOBALS['abj404_benchmark_state'])) { |
| 529 |
return; |
| 530 |
} |
| 531 |
$state = $GLOBALS['abj404_benchmark_state']; |
| 532 |
$start = isset($state['start']) ? (float)$state['start'] : 0.0; |
| 533 |
$bootstrapDone = isset($state['bootstrap_done']) ? (float)$state['bootstrap_done'] : 0.0; |
| 534 |
$now = microtime(true); |
| 535 |
$totalMs = ($start > 0.0) ? (($now - $start) * 1000.0) : 0.0; |
| 536 |
$bootstrapMs = ($start > 0.0 && $bootstrapDone > 0.0) ? (($bootstrapDone - $start) * 1000.0) : 0.0; |
| 537 |
$dbQueryCount = isset($state['db_query_count']) ? (int)$state['db_query_count'] : 0; |
| 538 |
$dbQueryMs = isset($state['db_query_ms']) ? (float)$state['db_query_ms'] : 0.0; |
| 539 |
$redirectLookupMs = isset($state['redirect_lookup_ms']) ? (float)$state['redirect_lookup_ms'] : 0.0; |
| 540 |
|
| 541 |
header( |
| 542 |
'X-ABJ404-Benchmark: ' . |
| 543 |
'total_ms=' . round($totalMs, 3) . ';' . |
| 544 |
'bootstrap_ms=' . round($bootstrapMs, 3) . ';' . |
| 545 |
'db_query_count=' . $dbQueryCount . ';' . |
| 546 |
'db_query_ms=' . round($dbQueryMs, 3) . ';' . |
| 547 |
'redirect_lookup_ms=' . round($redirectLookupMs, 3) |
| 548 |
); |
| 549 |
} |
| 550 |
} |
| 551 |
|
| 552 |
abj404_benchmark_bootstrap_start(); |
| 553 |
if (abj404_is_benchmark_request()) { |
| 554 |
add_action('send_headers', 'abj404_benchmark_emit_headers', PHP_INT_MAX); |
| 555 |
} |
| 556 |
} |
| 557 |
|
| 558 |
// Minimal shutdown handler: catches compile/parse fatals in plugin files and |
| 559 |
// stores them in a transient so the degraded admin page can display the error |
| 560 |
// on the next request. This is important for PHP 7.4 where syntax errors in |
| 561 |
// required files produce uncatchable E_COMPILE_ERROR. |
| 562 |
if (!function_exists('abj404_boot_shutdown_handler')) { |
| 563 |
/** @return void */ |
| 564 |
function abj404_boot_shutdown_handler() { |
| 565 |
if ($GLOBALS['abj404_boot_ok']) { |
| 566 |
return; |
| 567 |
} |
| 568 |
$error = error_get_last(); |
| 569 |
if ($error === null) { |
| 570 |
return; |
| 571 |
} |
| 572 |
// Only capture fatal/compile errors in our plugin files. |
| 573 |
$fatalTypes = E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR; |
| 574 |
if (!($error['type'] & $fatalTypes)) { |
| 575 |
return; |
| 576 |
} |
| 577 |
$pluginDir = defined('ABJ404_PATH') ? ABJ404_PATH : __DIR__ . '/'; |
| 578 |
if (strpos($error['file'], $pluginDir) === false) { |
| 579 |
return; |
| 580 |
} |
| 581 |
$errorInfo = array( |
| 582 |
'message' => $error['message'], |
| 583 |
'file' => $error['file'], |
| 584 |
'line' => $error['line'], |
| 585 |
'type' => $error['type'], |
| 586 |
'time' => time(), |
| 587 |
); |
| 588 |
// Use update_option as a fallback — set_transient might not be available |
| 589 |
// during a fatal shutdown. |
| 590 |
if (function_exists('set_transient')) { |
| 591 |
set_transient('abj404_boot_fatal', $errorInfo, 3600); |
| 592 |
} |
| 593 |
} |
| 594 |
} |
| 595 |
register_shutdown_function('abj404_boot_shutdown_handler'); |
| 596 |
|
| 597 |
// Always load Loader.php to ensure plugin constants (ABJ404_TYPE_404_DISPLAYED, |
| 598 |
// ABJ404_STATUS_MANUAL, etc.) are defined in all contexts: admin, REST API, WP-CLI |
| 599 |
// eval, and template_redirect. Without this, direct calls to plugin classes via |
| 600 |
// wp eval fail with "Undefined constant" errors because Loader.php was previously |
| 601 |
// only loaded inside is_admin() — leaving WP-CLI and other non-admin contexts |
| 602 |
// without the constants they need. |
| 603 |
$__abj404_loader_path = plugin_dir_path( __FILE__ ) . "includes/Loader.php"; |
| 604 |
if (file_exists($__abj404_loader_path)) { |
| 605 |
try { |
| 606 |
require_once($__abj404_loader_path); |
| 607 |
$GLOBALS['abj404_boot_ok'] = true; |
| 608 |
// Clear any stale boot fatal transient from a previous failed load. |
| 609 |
if (function_exists('delete_transient')) { |
| 610 |
delete_transient('abj404_boot_fatal'); |
| 611 |
} |
| 612 |
} catch (\Throwable $e) { |
| 613 |
$GLOBALS['abj404_boot_ok'] = false; |
| 614 |
$GLOBALS['abj404_boot_error'] = $e->getMessage(); |
| 615 |
error_log('404 Solution: boot failed — ' . $e->getMessage()); |
| 616 |
} |
| 617 |
} else { |
| 618 |
$GLOBALS['abj404_boot_ok'] = false; |
| 619 |
$GLOBALS['abj404_missing_files'][] = $__abj404_loader_path; |
| 620 |
$GLOBALS['abj404_boot_error'] = 'Loader.php is missing.'; |
| 621 |
} |
| 622 |
unset($__abj404_loader_path); |
| 623 |
|
| 624 |
if ($GLOBALS['abj404_boot_ok']) { |
| 625 |
// admin |
| 626 |
if (is_admin()) { |
| 627 |
try { |
| 628 |
ABJ_404_Solution_WordPress_Connector::init(); |
| 629 |
ABJ_404_Solution_ViewUpdater::init(); |
| 630 |
} catch (\Throwable $e) { |
| 631 |
// init() failed — fall through to register the degraded admin page |
| 632 |
// so the user still has a menu item with error details instead of nothing. |
| 633 |
$GLOBALS['abj404_boot_ok'] = false; |
| 634 |
$GLOBALS['abj404_boot_error'] = 'Plugin initialization failed: ' . $e->getMessage(); |
| 635 |
error_log('404 Solution: admin initialization failed: ' . |
| 636 |
$e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); |
| 637 |
add_action('admin_menu', 'abj404_degraded_admin_menu'); |
| 638 |
add_action('admin_notices', 'abj404_degraded_admin_notice'); |
| 639 |
} |
| 640 |
} |
| 641 |
|
| 642 |
// REST API — deferred to rest_api_init so DataAccess/PluginLogic are only loaded on actual REST requests. |
| 643 |
add_action('rest_api_init', function() { |
| 644 |
$dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 645 |
$logic = ABJ_404_Solution_PluginLogic::getInstance(); |
| 646 |
$restController = new ABJ_404_Solution_RestApiController($dao, $logic); |
| 647 |
$restController->registerRoutes(); |
| 648 |
}); |
| 649 |
|
| 650 |
// WP-CLI commands. |
| 651 |
if (defined('WP_CLI') && WP_CLI) { |
| 652 |
add_action('init', function() { |
| 653 |
\WP_CLI::add_command('abj404', 'ABJ_404_Solution_WPCLICommands'); |
| 654 |
}, 1); |
| 655 |
} |
| 656 |
} elseif (function_exists('is_admin') && is_admin()) { |
| 657 |
// Boot failed. Register degraded admin page so the admin sees instructions |
| 658 |
// instead of a white screen or missing menu item. |
| 659 |
add_action('admin_menu', 'abj404_degraded_admin_menu'); |
| 660 |
add_action('admin_notices', 'abj404_degraded_admin_notice'); |
| 661 |
// Best-effort: wire the support-request flow even on the degraded |
| 662 |
// boot path so an admin who lands here can still send a debug report. |
| 663 |
// This is the most valuable placement for the button, because the user |
| 664 |
// often cannot reach the normal plugin UI from this screen. Deferred |
| 665 |
// to plugins_loaded so the function (defined below) is available |
| 666 |
// regardless of source-order; PHP does not hoist function definitions |
| 667 |
// out of conditional blocks. |
| 668 |
if (function_exists('add_action')) { |
| 669 |
add_action('plugins_loaded', 'abj404_degraded_register_support_request'); |
| 670 |
} |
| 671 |
} |
| 672 |
|
| 673 |
// --- Degraded-mode functions (always defined, no plugin class dependencies) --- |
| 674 |
|
| 675 |
if (!function_exists('abj404_degraded_register_support_request')) { |
| 676 |
/** |
| 677 |
* Wire the support-request flow on the degraded boot path so an admin |
| 678 |
* stuck on the corrupt-install screen can still send a debug log. The |
| 679 |
* normal registration runs inside WordPress_Connector::registerAdminHooks() |
| 680 |
* which only executes on a successful boot; without this helper, the |
| 681 |
* AJAX handler that the modal POSTs to does not exist on the degraded |
| 682 |
* path and the click silently 400s. |
| 683 |
* |
| 684 |
* Strictly best-effort. Every step is guarded with file_exists() and |
| 685 |
* class_exists() so a corrupted install that is missing any of the |
| 686 |
* support-request files just falls back to the mailto link in the |
| 687 |
* degraded admin page. We must not throw a fatal here, because we |
| 688 |
* already ARE on the degraded path. |
| 689 |
* |
| 690 |
* @return void |
| 691 |
*/ |
| 692 |
function abj404_degraded_register_support_request() { |
| 693 |
$inc = ABJ404_PATH . 'includes/'; |
| 694 |
$supportFiles = array( |
| 695 |
$inc . 'ajax/AjaxSecurityTrait.php', |
| 696 |
$inc . 'FeedbackTransport.php', |
| 697 |
$inc . 'SupportRequestButton.php', |
| 698 |
$inc . 'ajax/Ajax_SupportRequest.php', |
| 699 |
$inc . 'ajax/Ajax_SupportRequestPreview.php', |
| 700 |
); |
| 701 |
foreach ($supportFiles as $file) { |
| 702 |
if (!file_exists($file)) { |
| 703 |
return; |
| 704 |
} |
| 705 |
} |
| 706 |
try { |
| 707 |
foreach ($supportFiles as $file) { |
| 708 |
require_once $file; |
| 709 |
} |
| 710 |
// allow-silent-catch: degraded boot path. If any of the support-request files compile-fatals on require we want to fall through to the mailto fallback rather than crash the corrupt-install screen the user is here to read. |
| 711 |
} catch (\Throwable $e) { |
| 712 |
return; |
| 713 |
} |
| 714 |
if (!class_exists('ABJ_404_Solution_Ajax_SupportRequest') |
| 715 |
|| !class_exists('ABJ_404_Solution_Ajax_SupportRequestPreview')) { |
| 716 |
return; |
| 717 |
} |
| 718 |
// Register the AJAX handlers directly. The normal init() path goes |
| 719 |
// through ABJ_404_Solution_WPUtils::safeAddAction() but that helper |
| 720 |
// may itself be missing on a corrupt install; falling back to |
| 721 |
// add_action() avoids that dependency. |
| 722 |
$supportInstance = ABJ_404_Solution_Ajax_SupportRequest::getInstance(); |
| 723 |
$previewInstance = ABJ_404_Solution_Ajax_SupportRequestPreview::getInstance(); |
| 724 |
add_action('wp_ajax_abj404_support_request', array($supportInstance, 'handleRequest')); |
| 725 |
add_action('wp_ajax_abj404_support_request_preview', array($previewInstance, 'handleRequest')); |
| 726 |
// Enqueue the JS assets on the degraded admin page only. Using a |
| 727 |
// closure keeps this self-contained without registering a new |
| 728 |
// global function on the corrupted boot path. |
| 729 |
add_action('admin_enqueue_scripts', function($hook) use ($inc) { |
| 730 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 731 |
$isOurPage = is_string($hook) && ( |
| 732 |
strpos($hook, $ppSlug) !== false |
| 733 |
|| strpos($hook, 'abj404_solution') !== false |
| 734 |
); |
| 735 |
if (!$isOurPage) { |
| 736 |
return; |
| 737 |
} |
| 738 |
$clientJs = $inc . 'ajax/SupportRequest.js'; |
| 739 |
$buttonJs = $inc . 'js/support-request-button.js'; |
| 740 |
if (!file_exists($clientJs) || !file_exists($buttonJs)) { |
| 741 |
return; |
| 742 |
} |
| 743 |
$baseUrl = plugin_dir_url(__FILE__) . 'includes/'; |
| 744 |
$ver = defined('ABJ404_VERSION') ? ABJ404_VERSION : (string)time(); |
| 745 |
wp_enqueue_script('abj404-support-request-client', |
| 746 |
$baseUrl . 'ajax/SupportRequest.js', array(), $ver, true); |
| 747 |
wp_enqueue_script('abj404-support-request-button', |
| 748 |
$baseUrl . 'js/support-request-button.js', |
| 749 |
array('abj404-support-request-client'), $ver, true); |
| 750 |
$supportNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequest::NONCE_ACTION); |
| 751 |
$previewNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequestPreview::NONCE_ACTION); |
| 752 |
$ajaxUrl = admin_url('admin-ajax.php'); |
| 753 |
$payload = wp_json_encode(array( |
| 754 |
'ajaxurl' => $ajaxUrl, |
| 755 |
'nonces' => array( |
| 756 |
'support_request' => $supportNonce, |
| 757 |
'support_request_preview' => $previewNonce, |
| 758 |
), |
| 759 |
)); |
| 760 |
$bootstrap = 'window.ABJ404=window.ABJ404||{};Object.assign(window.ABJ404,' |
| 761 |
. (is_string($payload) ? $payload : '{}') . ');'; |
| 762 |
wp_add_inline_script('abj404-support-request-client', $bootstrap, 'before'); |
| 763 |
}); |
| 764 |
} |
| 765 |
} |
| 766 |
|
| 767 |
if (!function_exists('abj404_degraded_admin_menu')) { |
| 768 |
/** @return void */ |
| 769 |
function abj404_degraded_admin_menu() { |
| 770 |
$options = function_exists('get_option') ? get_option('abj404_settings') : false; |
| 771 |
$options = is_array($options) ? $options : array(); |
| 772 |
|
| 773 |
$menuName = '404 Solution'; |
| 774 |
$badge = " <span class='update-plugins count-1'><span class='plugin-count'>!</span></span>"; |
| 775 |
|
| 776 |
if (isset($options['menuLocation']) && $options['menuLocation'] === 'settingsLevel') { |
| 777 |
add_menu_page('404 Solution', $menuName . $badge, 'manage_options', 'abj404_solution', 'abj404_degraded_admin_page'); |
| 778 |
} else { |
| 779 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 780 |
add_submenu_page('options-general.php', '404 Solution', $menuName . $badge, 'manage_options', $ppSlug, 'abj404_degraded_admin_page'); |
| 781 |
} |
| 782 |
} |
| 783 |
} |
| 784 |
|
| 785 |
if (!function_exists('abj404_degraded_admin_notice')) { |
| 786 |
/** @return void */ |
| 787 |
function abj404_degraded_admin_notice() { |
| 788 |
if (!current_user_can('manage_options')) { |
| 789 |
return; |
| 790 |
} |
| 791 |
echo '<div class="notice notice-error"><p><strong>404 Solution:</strong> '; |
| 792 |
echo 'Plugin files are missing or corrupt. '; |
| 793 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 794 |
echo '<a href="' . esc_url(admin_url('options-general.php?page=' . $ppSlug)) . '">View details</a>'; |
| 795 |
echo '</p></div>'; |
| 796 |
} |
| 797 |
} |
| 798 |
|
| 799 |
if (!function_exists('abj404_degraded_admin_page')) { |
| 800 |
/** @return void */ |
| 801 |
function abj404_degraded_admin_page() { |
| 802 |
if (!current_user_can('manage_options')) { |
| 803 |
echo '<div class="wrap">'; |
| 804 |
echo '<h1>404 Solution</h1>'; |
| 805 |
echo '<div class="notice notice-error"><p>'; |
| 806 |
echo '<strong>Permission denied.</strong> '; |
| 807 |
echo 'Your user account does not have permission to access this page.'; |
| 808 |
echo '</p><p>'; |
| 809 |
echo 'Please verify that your WordPress role has the <code>manage_options</code> capability.'; |
| 810 |
echo '</p></div></div>'; |
| 811 |
return; |
| 812 |
} |
| 813 |
|
| 814 |
$missingFiles = isset($GLOBALS['abj404_missing_files']) ? $GLOBALS['abj404_missing_files'] : array(); |
| 815 |
$bootError = isset($GLOBALS['abj404_boot_error']) ? $GLOBALS['abj404_boot_error'] : ''; |
| 816 |
$pluginDir = defined('ABJ404_PATH') ? ABJ404_PATH : dirname(__FILE__) . '/'; |
| 817 |
|
| 818 |
// Check for a stored fatal from a previous request. |
| 819 |
$fatalInfo = function_exists('get_transient') ? get_transient('abj404_boot_fatal') : false; |
| 820 |
|
| 821 |
echo '<div class="wrap">'; |
| 822 |
echo '<h1>404 Solution — Plugin Files Missing</h1>'; |
| 823 |
|
| 824 |
echo '<div class="notice notice-error inline"><p>'; |
| 825 |
echo '<strong>The 404 Solution plugin cannot start</strong> because one or more required files are missing or corrupt. '; |
| 826 |
echo 'This usually happens after a failed plugin update or incomplete file upload.'; |
| 827 |
echo '</p></div>'; |
| 828 |
|
| 829 |
if (!empty($missingFiles)) { |
| 830 |
echo '<div class="card" style="max-width:800px;">'; |
| 831 |
echo '<h2>Missing Files</h2>'; |
| 832 |
echo '<ul style="list-style:disc;padding-left:20px;">'; |
| 833 |
foreach ($missingFiles as $file) { |
| 834 |
// Show relative path for readability. |
| 835 |
$relative = str_replace($pluginDir, '', $file); |
| 836 |
echo '<li><code>' . esc_html($relative) . '</code></li>'; |
| 837 |
} |
| 838 |
echo '</ul>'; |
| 839 |
echo '</div>'; |
| 840 |
} |
| 841 |
|
| 842 |
if ($bootError !== '') { |
| 843 |
echo '<div class="card" style="max-width:800px;">'; |
| 844 |
echo '<h2>Error Details</h2>'; |
| 845 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;">' . esc_html($bootError) . '</pre>'; |
| 846 |
echo '</div>'; |
| 847 |
} |
| 848 |
|
| 849 |
if (is_array($fatalInfo) && !empty($fatalInfo['message'])) { |
| 850 |
echo '<div class="card" style="max-width:800px;">'; |
| 851 |
echo '<h2>Fatal Error (previous request)</h2>'; |
| 852 |
$fatalFile = isset($fatalInfo['file']) ? str_replace($pluginDir, '', $fatalInfo['file']) : 'unknown'; |
| 853 |
$fatalLine = isset($fatalInfo['line']) ? $fatalInfo['line'] : '?'; |
| 854 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;">' . esc_html($fatalInfo['message']) . "\n" . esc_html($fatalFile) . ':' . esc_html((string)$fatalLine) . '</pre>'; |
| 855 |
echo '</div>'; |
| 856 |
} |
| 857 |
|
| 858 |
echo '<div class="card" style="max-width:800px;">'; |
| 859 |
echo '<h2>How to Fix</h2>'; |
| 860 |
echo '<ol>'; |
| 861 |
echo '<li>Go to <strong>Plugins → Installed Plugins</strong>, deactivate <strong>404 Solution</strong>, then delete it.</li>'; |
| 862 |
echo '<li>Reinstall from the WordPress plugin directory: '; |
| 863 |
$installUrl = admin_url('plugin-install.php?s=404+solution&tab=search'); |
| 864 |
echo '<a href="' . esc_url($installUrl) . '" class="button button-primary">Search “404 Solution”</a>'; |
| 865 |
echo '</li>'; |
| 866 |
echo '<li>Activate the fresh copy. Your redirects and settings are stored in the database and will not be lost.</li>'; |
| 867 |
echo '</ol>'; |
| 868 |
echo '</div>'; |
| 869 |
|
| 870 |
// Support-request card. This is the most valuable placement for |
| 871 |
// the "Send debug log to developer" button: an admin who reached |
| 872 |
// this screen needs help and may not be able to navigate the |
| 873 |
// normal plugin UI. When the SupportRequestButton class is |
| 874 |
// present (most corruption is partial), render the mount div so |
| 875 |
// the JS component can take over. Always emit a mailto fallback |
| 876 |
// underneath in case the JS files are themselves among the |
| 877 |
// missing files. |
| 878 |
echo '<div class="card" style="max-width:800px;">'; |
| 879 |
echo '<h2>Need help? Contact the developer</h2>'; |
| 880 |
echo '<p>Send the developer a one-time diagnostic report including the missing-file list above. Your redirects and settings are not shared.</p>'; |
| 881 |
$missingCount = is_array($missingFiles) ? count($missingFiles) : 0; |
| 882 |
$contextSummary = 'Corrupt install: ' . $missingCount . ' missing file(s)'; |
| 883 |
$bootErrorForSummary = is_scalar($bootError) ? (string)$bootError : ''; |
| 884 |
if ($bootErrorForSummary !== '') { |
| 885 |
$contextSummary .= '. Boot error: ' . substr($bootErrorForSummary, 0, 200); |
| 886 |
} |
| 887 |
if (class_exists('ABJ_404_Solution_SupportRequestButton')) { |
| 888 |
echo ABJ_404_Solution_SupportRequestButton::render('system_corrupt_install', $contextSummary); |
| 889 |
} |
| 890 |
$mailEmail = defined('ABJ404_AUTHOR_EMAIL') ? (string)ABJ404_AUTHOR_EMAIL : '404solution@ajexperience.com'; |
| 891 |
$mailSubject = rawurlencode('404 Solution: corrupt install report'); |
| 892 |
$homeUrlText = '(unknown)'; |
| 893 |
if (function_exists('home_url')) { |
| 894 |
$homeUrlVal = home_url(); |
| 895 |
$homeUrlText = is_string($homeUrlVal) ? $homeUrlVal : '(unknown)'; |
| 896 |
} |
| 897 |
$missingFileLines = ''; |
| 898 |
if (is_array($missingFiles)) { |
| 899 |
$stringMissingFiles = array(); |
| 900 |
foreach ($missingFiles as $entry) { |
| 901 |
$stringMissingFiles[] = is_scalar($entry) ? (string)$entry : ''; |
| 902 |
} |
| 903 |
$missingFileLines = implode("\n", $stringMissingFiles); |
| 904 |
} |
| 905 |
$bootErrorText = is_scalar($bootError) ? (string)$bootError : ''; |
| 906 |
$mailBody = rawurlencode("Site URL: " . $homeUrlText . "\n" |
| 907 |
. "Missing files (" . (int)$missingCount . "):\n" |
| 908 |
. $missingFileLines |
| 909 |
. "\n\nBoot error:\n" . $bootErrorText); |
| 910 |
echo '<p style="margin-top:8px;">Or email manually: '; |
| 911 |
echo '<a href="mailto:' . esc_attr($mailEmail) . '?subject=' . $mailSubject . '&body=' . $mailBody . '">'; |
| 912 |
echo esc_html($mailEmail) . '</a></p>'; |
| 913 |
echo '</div>'; |
| 914 |
|
| 915 |
echo '</div>'; // .wrap |
| 916 |
} |
| 917 |
} |
| 918 |
|
| 919 |
if (!function_exists('abj404_admin_page_callback')) { |
| 920 |
/** |
| 921 |
* Show one-time admin fatal diagnostics captured during shutdown. |
| 922 |
* |
| 923 |
* @return void |
| 924 |
*/ |
| 925 |
function abj404_render_last_admin_fatal_notice() { |
| 926 |
if (!function_exists('current_user_can') || !current_user_can('manage_options')) { |
| 927 |
return; |
| 928 |
} |
| 929 |
|
| 930 |
$fatalInfo = function_exists('get_transient') ? get_transient('abj404_admin_fatal') : false; |
| 931 |
if ($fatalInfo === false && function_exists('get_option')) { |
| 932 |
$fatalInfo = get_option('abj404_admin_fatal_fallback', false); |
| 933 |
} |
| 934 |
if (!is_array($fatalInfo) || empty($fatalInfo['message'])) { |
| 935 |
return; |
| 936 |
} |
| 937 |
|
| 938 |
if (function_exists('delete_transient')) { |
| 939 |
delete_transient('abj404_admin_fatal'); |
| 940 |
} |
| 941 |
if (function_exists('delete_option')) { |
| 942 |
delete_option('abj404_admin_fatal_fallback'); |
| 943 |
} |
| 944 |
|
| 945 |
$pluginDir = defined('ABJ404_PATH') ? ABJ404_PATH : __DIR__ . '/'; |
| 946 |
$fatalFile = isset($fatalInfo['file']) ? str_replace($pluginDir, '', (string)$fatalInfo['file']) : '(unknown file)'; |
| 947 |
$fatalLine = isset($fatalInfo['line']) ? (int)$fatalInfo['line'] : 0; |
| 948 |
|
| 949 |
echo '<div class="wrap">'; |
| 950 |
echo '<div class="notice notice-error">'; |
| 951 |
echo '<p><strong>404 Solution:</strong> A fatal error occurred while rendering the previous admin request.</p>'; |
| 952 |
echo '<details><summary>Show error details</summary>'; |
| 953 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;max-width:100%;margin:6px 0;">' . |
| 954 |
esc_html((string)$fatalInfo['message'] . "\n" . $fatalFile . ':' . (string)$fatalLine) . |
| 955 |
'</pre>'; |
| 956 |
echo '</details>'; |
| 957 |
echo '</div>'; |
| 958 |
echo '</div>'; |
| 959 |
} |
| 960 |
|
| 961 |
/** |
| 962 |
* Safe wrapper for the admin page callback. Falls back to the degraded |
| 963 |
* page if the View class was not loaded during boot. |
| 964 |
* |
| 965 |
* @return void |
| 966 |
*/ |
| 967 |
function abj404_admin_page_callback() { |
| 968 |
abj404_render_last_admin_fatal_notice(); |
| 969 |
|
| 970 |
// The false parameter avoids triggering the autoloader — if View was not |
| 971 |
// loaded during boot, we don't want to attempt loading it again here. |
| 972 |
if (class_exists('ABJ_404_Solution_View', false)) { |
| 973 |
ob_start(); |
| 974 |
$renderError = null; |
| 975 |
try { |
| 976 |
ABJ_404_Solution_View::handleMainAdminPageActionAndDisplay(); |
| 977 |
} catch (\Throwable $e) { |
| 978 |
$renderError = $e; |
| 979 |
error_log('404 Solution: admin page rendering failed: ' . |
| 980 |
$e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); |
| 981 |
} |
| 982 |
$output = ob_get_clean(); |
| 983 |
|
| 984 |
if ($renderError !== null) { |
| 985 |
echo '<div class="wrap">'; |
| 986 |
echo '<div class="notice notice-error">'; |
| 987 |
echo '<p><strong>404 Solution:</strong> An error occurred while rendering this page.</p>'; |
| 988 |
echo '<details><summary>Show error details</summary>'; |
| 989 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;max-width:100%;margin:6px 0;">' . esc_html($renderError->getMessage() . "\n" . $renderError->getTraceAsString()) . '</pre>'; |
| 990 |
echo '</details>'; |
| 991 |
echo '</div>'; |
| 992 |
echo '</div>'; |
| 993 |
} elseif ($output === '' || $output === false) { |
| 994 |
// The View class was loaded and didn't throw, but produced zero output. |
| 995 |
// Show a diagnostic instead of a blank page. |
| 996 |
echo '<div class="wrap">'; |
| 997 |
echo '<h1>404 Solution</h1>'; |
| 998 |
echo '<div class="notice notice-error"><p>'; |
| 999 |
echo '<strong>This page produced no output.</strong> '; |
| 1000 |
echo 'This can happen when a required dependency failed to initialize or a template file is missing.'; |
| 1001 |
echo '</p><p>'; |
| 1002 |
echo 'Try deactivating and reactivating the plugin. If the problem persists, '; |
| 1003 |
echo 'delete the plugin and reinstall it from the WordPress plugin directory.'; |
| 1004 |
echo '</p></div></div>'; |
| 1005 |
} else { |
| 1006 |
echo $output; |
| 1007 |
} |
| 1008 |
} else { |
| 1009 |
abj404_degraded_admin_page(); |
| 1010 |
} |
| 1011 |
} |
| 1012 |
} |
| 1013 |
|
| 1014 |
// ---- |
| 1015 |
// get the plugin priority to use before adding the template_redirect action. |
| 1016 |
$__abj404_options = abj404_get_settings_options(); |
| 1017 |
$__abj404_redirect_priority_raw = isset($__abj404_options['template_redirect_priority']) && is_scalar($__abj404_options['template_redirect_priority']) ? $__abj404_options['template_redirect_priority'] : 9; |
| 1018 |
$__abj404_template_redirect_priority = absint($__abj404_redirect_priority_raw); |
| 1019 |
$__abj404_redirect_all = isset($__abj404_options['redirect_all_requests']) && is_scalar($__abj404_options['redirect_all_requests']) ? (string)$__abj404_options['redirect_all_requests'] : ''; |
| 1020 |
$__abj404_update_suggest = isset($__abj404_options['update_suggest_url']) && is_scalar($__abj404_options['update_suggest_url']) ? (string)$__abj404_options['update_suggest_url'] : ''; |
| 1021 |
$GLOBALS['abj404_frontend_runtime_flags'] = array( |
| 1022 |
'redirect_all_requests' => ($__abj404_redirect_all === '1'), |
| 1023 |
'update_suggest_url' => ($__abj404_update_suggest === '1'), |
| 1024 |
); |
| 1025 |
$__abj404_lang_override = isset($__abj404_options['plugin_language_override']) && is_string($__abj404_options['plugin_language_override']) ? $__abj404_options['plugin_language_override'] : ''; |
| 1026 |
$GLOBALS['abj404_plugin_language_override'] = $__abj404_lang_override; |
| 1027 |
|
| 1028 |
add_action('template_redirect', 'abj404_404listener', $__abj404_template_redirect_priority); |
| 1029 |
|
| 1030 |
unset($__abj404_options); |
| 1031 |
unset($__abj404_template_redirect_priority); |
| 1032 |
abj404_benchmark_mark_bootstrap_done(); |
| 1033 |
// --- |
| 1034 |
|
| 1035 |
// 404 |
| 1036 |
if (!function_exists('abj404_404listener')) { |
| 1037 |
/** @return void */ |
| 1038 |
function abj404_404listener() { |
| 1039 |
if (!$GLOBALS['abj404_boot_ok']) { |
| 1040 |
return; |
| 1041 |
} |
| 1042 |
$is404 = is_404(); |
| 1043 |
if (!$is404) { |
| 1044 |
// Performance: do NOT load the whole plugin on every frontend request unless we must. |
| 1045 |
if (!empty($GLOBALS['abj404_frontend_runtime_flags']['redirect_all_requests'])) { |
| 1046 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1047 |
$connector = ABJ_404_Solution_WordPress_Connector::getInstance(); |
| 1048 |
$connector->processRedirectAllRequests(); |
| 1049 |
return; |
| 1050 |
} |
| 1051 |
|
| 1052 |
$updateSuggestEnabled = !empty($GLOBALS['abj404_frontend_runtime_flags']['update_suggest_url']); |
| 1053 |
$cookieName404 = ABJ404_PP . '_STATUS_404'; |
| 1054 |
$has404StatusCookie = (isset($_COOKIE[$cookieName404]) && $_COOKIE[$cookieName404] == 'true'); |
| 1055 |
|
| 1056 |
// Fast path: if none of the non-404 features are active, bail immediately. |
| 1057 |
if (!$updateSuggestEnabled && !$has404StatusCookie) { |
| 1058 |
return; |
| 1059 |
} |
| 1060 |
|
| 1061 |
/** If we're currently redirecting to a custom 404 page and we are about to show page |
| 1062 |
* suggestions then update the URL displayed to the user. */ |
| 1063 |
$cookieName = ABJ404_PP . '_REQUEST_URI_UPDATE_URL'; |
| 1064 |
$queryParamName = ABJ404_PP . '_ref'; |
| 1065 |
|
| 1066 |
$hasUpdateCookie = !empty($_COOKIE[$cookieName]); |
| 1067 |
$hasUpdateParam = !empty($_GET[$queryParamName]); |
| 1068 |
|
| 1069 |
// Fast path: nothing pending from prior plugin-driven redirects. |
| 1070 |
if (!$hasUpdateCookie && !$hasUpdateParam && !$has404StatusCookie) { |
| 1071 |
return; |
| 1072 |
} |
| 1073 |
|
| 1074 |
if ($has404StatusCookie) { |
| 1075 |
// clear the cookie |
| 1076 |
setcookie($cookieName404, 'false', time() - 5, "/"); |
| 1077 |
// we're going to a custom 404 page so set the status to 404. |
| 1078 |
status_header(404); |
| 1079 |
} |
| 1080 |
|
| 1081 |
if (!$updateSuggestEnabled) { |
| 1082 |
return; |
| 1083 |
} |
| 1084 |
|
| 1085 |
// Check cookie first, then query param fallback (for 301 redirects where cookies don't survive) |
| 1086 |
$originalURL = null; |
| 1087 |
if ($hasUpdateCookie) { |
| 1088 |
$originalURL = $_COOKIE[$cookieName]; |
| 1089 |
} elseif ($hasUpdateParam) { |
| 1090 |
$originalURL = urldecode($_GET[$queryParamName]); |
| 1091 |
} |
| 1092 |
|
| 1093 |
if ($originalURL !== null) { |
| 1094 |
// clear the cookie - sanitize before writing to $_REQUEST |
| 1095 |
$sanitizedOriginal = sanitize_text_field($originalURL); |
| 1096 |
$_REQUEST[ABJ404_PP . '_REQUEST_URI'] = $sanitizedOriginal; |
| 1097 |
$_REQUEST[ABJ404_PP . '_REQUEST_URI_UPDATE_URL'] = $sanitizedOriginal; |
| 1098 |
setcookie($cookieName, '', time() - 5, "/"); |
| 1099 |
|
| 1100 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1101 |
add_action('wp_head', 'ABJ_404_Solution_ShortCode::updateURLbarIfNecessary'); |
| 1102 |
} |
| 1103 |
return; |
| 1104 |
} |
| 1105 |
|
| 1106 |
// ignore admin screens and login requests on 404 processing path. |
| 1107 |
// $_SERVER['SCRIPT_NAME'] is not guaranteed (CLI, some test runners, some proxies). |
| 1108 |
// Use a direct script-name check to avoid invoking wp_login_url() filters. |
| 1109 |
$scriptName = $_SERVER['SCRIPT_NAME'] ?? ''; |
| 1110 |
$requestUri = $_SERVER['REQUEST_URI'] ?? ''; |
| 1111 |
$isLoginScreen = ( |
| 1112 |
($scriptName !== '' && stripos($scriptName, 'wp-login.php') !== false) || |
| 1113 |
($requestUri !== '' && stripos($requestUri, 'wp-login.php') !== false) |
| 1114 |
); |
| 1115 |
if (is_admin() || $isLoginScreen) { |
| 1116 |
return; |
| 1117 |
} |
| 1118 |
|
| 1119 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1120 |
$connector = ABJ_404_Solution_WordPress_Connector::getInstance(); |
| 1121 |
$connector->process404(); |
| 1122 |
} |
| 1123 |
} |
| 1124 |
|
| 1125 |
if (!function_exists('abj404_is_redirect_all_requests_enabled')) { |
| 1126 |
/** |
| 1127 |
* Small helper for testability and to keep option-parsing logic consistent. |
| 1128 |
* |
| 1129 |
* @param mixed $options Value returned by get_option('abj404_settings') |
| 1130 |
* @return bool |
| 1131 |
*/ |
| 1132 |
function abj404_is_redirect_all_requests_enabled($options) { |
| 1133 |
return is_array($options) && |
| 1134 |
array_key_exists('redirect_all_requests', $options) && |
| 1135 |
(string)$options['redirect_all_requests'] === '1'; |
| 1136 |
} |
| 1137 |
} |
| 1138 |
|
| 1139 |
if (!function_exists('abj404_dailyMaintenanceCronJobListener')) { |
| 1140 |
/** @return void */ |
| 1141 |
function abj404_dailyMaintenanceCronJobListener() { |
| 1142 |
try { |
| 1143 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1144 |
$abj404dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 1145 |
$abj404dao->deleteOldRedirectsCron(); |
| 1146 |
|
| 1147 |
$dbUpgrades = ABJ_404_Solution_DatabaseUpgradesEtc::getInstance(); |
| 1148 |
$dbUpgrades->runDatabaseMaintenanceTasks(); |
| 1149 |
} catch (\Throwable $e) { |
| 1150 |
error_log('404 Solution cron (maintenance): ' . $e->getMessage()); |
| 1151 |
} |
| 1152 |
} |
| 1153 |
} |
| 1154 |
|
| 1155 |
if (!function_exists('abj404_updateLogsHitsTableListener')) { |
| 1156 |
/** @return void */ |
| 1157 |
function abj404_updateLogsHitsTableListener() { |
| 1158 |
try { |
| 1159 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1160 |
$abj404dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 1161 |
$abj404dao->createRedirectsForViewHitsTable(); |
| 1162 |
} catch (\Throwable $e) { |
| 1163 |
error_log('404 Solution cron (logs/hits): ' . $e->getMessage()); |
| 1164 |
} |
| 1165 |
} |
| 1166 |
} |
| 1167 |
if (!function_exists('abj404_logsv2CanonicalUrlBackfillListener')) { |
| 1168 |
/** @return void */ |
| 1169 |
function abj404_logsv2CanonicalUrlBackfillListener() { |
| 1170 |
try { |
| 1171 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1172 |
$dbUpgrades = ABJ_404_Solution_DatabaseUpgradesEtc::getInstance(); |
| 1173 |
$dbUpgrades->backfillLogsv2CanonicalUrl(); |
| 1174 |
} catch (\Throwable $e) { |
| 1175 |
error_log('404 Solution cron (log canonical URL backfill): ' . $e->getMessage()); |
| 1176 |
} |
| 1177 |
} |
| 1178 |
} |
| 1179 |
if (!function_exists('abj404_updatePermalinkCacheListener')) { |
| 1180 |
/** |
| 1181 |
* @param int $maxExecutionTime |
| 1182 |
* @param int $executionCount |
| 1183 |
* @return void |
| 1184 |
*/ |
| 1185 |
function abj404_updatePermalinkCacheListener($maxExecutionTime, $executionCount = 1) { |
| 1186 |
try { |
| 1187 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1188 |
$permalinkCache = ABJ_404_Solution_PermalinkCache::getInstance(); |
| 1189 |
$permalinkCache->updatePermalinkCache($maxExecutionTime, $executionCount); |
| 1190 |
} catch (\Throwable $e) { |
| 1191 |
error_log('404 Solution cron (permalink cache): ' . $e->getMessage()); |
| 1192 |
} |
| 1193 |
} |
| 1194 |
} |
| 1195 |
if (!function_exists('abj404_rebuildNGramCacheListener')) { |
| 1196 |
/** |
| 1197 |
* @param int $offset |
| 1198 |
* @return void |
| 1199 |
*/ |
| 1200 |
function abj404_rebuildNGramCacheListener($offset = 0) { |
| 1201 |
try { |
| 1202 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1203 |
$dbUpgrades = ABJ_404_Solution_DatabaseUpgradesEtc::getInstance(); |
| 1204 |
$dbUpgrades->rebuildNGramCacheAsync($offset); |
| 1205 |
} catch (\Throwable $e) { |
| 1206 |
error_log('404 Solution cron (ngram cache): ' . $e->getMessage()); |
| 1207 |
} |
| 1208 |
} |
| 1209 |
} |
| 1210 |
if (!function_exists('abj404_networkActivationListener')) { |
| 1211 |
/** @return void */ |
| 1212 |
function abj404_networkActivationListener() { |
| 1213 |
try { |
| 1214 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1215 |
ABJ_404_Solution_PluginLogic::networkActivationCronHandler(); |
| 1216 |
} catch (\Throwable $e) { |
| 1217 |
error_log('404 Solution cron (network activation): ' . $e->getMessage()); |
| 1218 |
} |
| 1219 |
} |
| 1220 |
} |
| 1221 |
if (!function_exists('abj404_networkActivationBackgroundListener')) { |
| 1222 |
/** @return void */ |
| 1223 |
function abj404_networkActivationBackgroundListener() { |
| 1224 |
try { |
| 1225 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1226 |
$upgradesEtc = ABJ_404_Solution_DatabaseUpgradesEtc::getInstance(); |
| 1227 |
$upgradesEtc->processMultisiteActivationBatch(); |
| 1228 |
} catch (\Throwable $e) { |
| 1229 |
error_log('404 Solution cron (multisite activation): ' . $e->getMessage()); |
| 1230 |
} |
| 1231 |
} |
| 1232 |
} |
| 1233 |
if (!function_exists('abj404_networkUpgradeBackgroundListener')) { |
| 1234 |
/** @return void */ |
| 1235 |
function abj404_networkUpgradeBackgroundListener() { |
| 1236 |
try { |
| 1237 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1238 |
$upgradesEtc = ABJ_404_Solution_DatabaseUpgradesEtc::getInstance(); |
| 1239 |
$upgradesEtc->processMultisiteUpgradeBatch(); |
| 1240 |
} catch (\Throwable $e) { |
| 1241 |
error_log('404 Solution cron (multisite upgrade): ' . $e->getMessage()); |
| 1242 |
} |
| 1243 |
} |
| 1244 |
} |
| 1245 |
if (!function_exists('abj404_rebuildViewDoneListener')) { |
| 1246 |
/** @return void */ |
| 1247 |
function abj404_rebuildViewDoneListener() { |
| 1248 |
try { |
| 1249 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1250 |
$abj404dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 1251 |
$abj404dao->rebuildViewDoneInBackground(); |
| 1252 |
} catch (\Throwable $e) { |
| 1253 |
error_log('404 Solution cron (view table rebuild): ' . $e->getMessage()); |
| 1254 |
} |
| 1255 |
} |
| 1256 |
} |
| 1257 |
add_action('abj404_cleanupCronAction', 'abj404_dailyMaintenanceCronJobListener'); |
| 1258 |
add_action('abj404_updateLogsHitsTableAction', 'abj404_updateLogsHitsTableListener'); |
| 1259 |
add_action('abj404_logsv2_canonical_backfill', 'abj404_logsv2CanonicalUrlBackfillListener'); |
| 1260 |
add_action('abj404_updatePermalinkCacheAction', 'abj404_updatePermalinkCacheListener', 10, 2); |
| 1261 |
add_action('abj404_rebuildViewDone', 'abj404_rebuildViewDoneListener'); |
| 1262 |
add_action('abj404_send_digest', 'abj404_sendDigestCronListener'); |
| 1263 |
add_action('abj404_send_queued_report', 'abj404_sendQueuedReportListener', 10, 1); |
| 1264 |
if (!function_exists('abj404_sendQueuedReportListener')) { |
| 1265 |
/** |
| 1266 |
* Cron handler for FeedbackTransport queued sends. Loads Loader.php so the |
| 1267 |
* autoloader resolves ABJ_404_Solution_FeedbackTransport, then dispatches. |
| 1268 |
* |
| 1269 |
* @param string $uuid |
| 1270 |
* @return void |
| 1271 |
*/ |
| 1272 |
function abj404_sendQueuedReportListener($uuid) { |
| 1273 |
try { |
| 1274 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1275 |
ABJ_404_Solution_FeedbackTransport::handleQueuedSend(is_string($uuid) ? $uuid : ''); |
| 1276 |
} catch (\Throwable $e) { |
| 1277 |
error_log('404 Solution cron (feedback transport): ' . $e->getMessage()); |
| 1278 |
} |
| 1279 |
} |
| 1280 |
} |
| 1281 |
if (!function_exists('abj404_sendDigestCronListener')) { |
| 1282 |
/** @return void */ |
| 1283 |
function abj404_sendDigestCronListener() { |
| 1284 |
try { |
| 1285 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1286 |
$dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 1287 |
$logger = ABJ_404_Solution_Logging::getInstance(); |
| 1288 |
$emailDigest = new ABJ_404_Solution_EmailDigest($dao, $logger); |
| 1289 |
$emailDigest->onCronSendDigest(); |
| 1290 |
} catch (\Throwable $e) { |
| 1291 |
error_log('404 Solution cron (email digest): ' . $e->getMessage()); |
| 1292 |
} |
| 1293 |
} |
| 1294 |
} |
| 1295 |
add_action('abj404_rebuild_ngram_cache_hook', 'abj404_rebuildNGramCacheListener', 10, 1); |
| 1296 |
add_action('abj404_network_activation_hook', 'abj404_networkActivationListener'); |
| 1297 |
add_action('abj404_network_activation_background', 'abj404_networkActivationBackgroundListener'); |
| 1298 |
add_action('abj404_network_upgrade_background', 'abj404_networkUpgradeBackgroundListener'); |
| 1299 |
add_action('abj404_gsc_fetch_cron', 'abj404_gscFetchCronListener'); |
| 1300 |
add_action('abj404_gsc_background_refresh', 'abj404_gscBackgroundRefreshListener'); |
| 1301 |
|
| 1302 |
if (!function_exists('abj404_gscFetchCronListener')) { |
| 1303 |
/** Nightly cron: fetch GSC data and cache it. @return void */ |
| 1304 |
function abj404_gscFetchCronListener(): void { |
| 1305 |
try { |
| 1306 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1307 |
$gscLogger = ABJ_404_Solution_Logging::getInstance(); |
| 1308 |
$gsc = new ABJ_404_Solution_GoogleSearchConsole($gscLogger); |
| 1309 |
$gsc->fetchAndCacheGscData(); |
| 1310 |
} catch (\Throwable $e) { |
| 1311 |
error_log('404 Solution cron (GSC fetch): ' . $e->getMessage()); |
| 1312 |
} |
| 1313 |
} |
| 1314 |
} |
| 1315 |
|
| 1316 |
if (!function_exists('abj404_gscBackgroundRefreshListener')) { |
| 1317 |
/** On-demand background refresh triggered when an admin views the Options tab with stale data. @return void */ |
| 1318 |
function abj404_gscBackgroundRefreshListener(): void { |
| 1319 |
try { |
| 1320 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1321 |
$gscLogger = ABJ_404_Solution_Logging::getInstance(); |
| 1322 |
$gsc = new ABJ_404_Solution_GoogleSearchConsole($gscLogger); |
| 1323 |
$gsc->fetchAndCacheGscData(); |
| 1324 |
} catch (\Throwable $e) { |
| 1325 |
error_log('404 Solution cron (GSC background refresh): ' . $e->getMessage()); |
| 1326 |
} |
| 1327 |
} |
| 1328 |
} |
| 1329 |
|
| 1330 |
/** |
| 1331 |
* Override the locale for this plugin if user has configured a language override. |
| 1332 |
* This allows users to use a different language for the 404 Solution plugin |
| 1333 |
* than their WordPress site language or user language preference. |
| 1334 |
* |
| 1335 |
* @param string $locale The current locale. |
| 1336 |
* @param string $domain The text domain. |
| 1337 |
* @return string The locale to use for translation loading. |
| 1338 |
*/ |
| 1339 |
if (!function_exists('abj404_override_plugin_locale')) { |
| 1340 |
/** |
| 1341 |
* @param string $locale |
| 1342 |
* @param string $domain |
| 1343 |
* @return string |
| 1344 |
*/ |
| 1345 |
function abj404_override_plugin_locale($locale, $domain) { |
| 1346 |
// Only override for our plugin's text domain. |
| 1347 |
// Use the value cached in $GLOBALS at plugin boot to avoid a redundant get_option() call. |
| 1348 |
if ($domain === '404-solution') { |
| 1349 |
$override = isset($GLOBALS['abj404_plugin_language_override']) && is_string($GLOBALS['abj404_plugin_language_override']) ? $GLOBALS['abj404_plugin_language_override'] : ''; |
| 1350 |
if ($override !== '') { |
| 1351 |
return $override; |
| 1352 |
} |
| 1353 |
} |
| 1354 |
return $locale; |
| 1355 |
} |
| 1356 |
} |
| 1357 |
add_filter('plugin_locale', 'abj404_override_plugin_locale', 999, 2); |
| 1358 |
|
| 1359 |
if (!function_exists('abj404_show_runtime_integrity_notice')) { |
| 1360 |
/** @return void */ |
| 1361 |
function abj404_show_runtime_integrity_notice() { |
| 1362 |
if (!is_admin() || !current_user_can('manage_options')) { |
| 1363 |
return; |
| 1364 |
} |
| 1365 |
$page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : ''; |
| 1366 |
if ($page !== ABJ404_PP) { |
| 1367 |
return; |
| 1368 |
} |
| 1369 |
$missing = get_transient('abj404_runtime_missing_files'); |
| 1370 |
if (!is_array($missing) || count($missing) === 0) { |
| 1371 |
return; |
| 1372 |
} |
| 1373 |
echo '<div class="notice notice-error"><p><strong>404 Solution:</strong> '; |
| 1374 |
echo esc_html(__('Some required plugin files are missing. Please reinstall the plugin package.', '404-solution')); |
| 1375 |
echo '</p><p><code>' . esc_html(implode(', ', array_map('basename', $missing))) . '</code></p></div>'; |
| 1376 |
} |
| 1377 |
} |
| 1378 |
add_action('admin_notices', 'abj404_show_runtime_integrity_notice'); |
| 1379 |
|
| 1380 |
if (!function_exists('abj404_show_plugin_db_notice')) { |
| 1381 |
/** @return void */ |
| 1382 |
function abj404_show_plugin_db_notice() { |
| 1383 |
if (!is_admin() || !current_user_can('manage_options')) { |
| 1384 |
return; |
| 1385 |
} |
| 1386 |
$page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : ''; |
| 1387 |
if ($page !== ABJ404_PP) { |
| 1388 |
return; |
| 1389 |
} |
| 1390 |
$notice = get_transient('abj404_plugin_db_notice'); |
| 1391 |
if (!is_array($notice) || empty($notice['message'])) { |
| 1392 |
return; |
| 1393 |
} |
| 1394 |
$type = isset($notice['type']) ? $notice['type'] : ''; |
| 1395 |
// Collation issues are developer-level; don't show them to the user. |
| 1396 |
if ($type === 'collation') { |
| 1397 |
return; |
| 1398 |
} |
| 1399 |
$guidance = ''; |
| 1400 |
if ($type === 'disk_full') { |
| 1401 |
$guidance = __('Contact your hosting provider. This is usually caused by a database quota, tablespace limit, or full /tmp partition — not necessarily a full disk.', '404-solution'); |
| 1402 |
} elseif ($type === 'read_only') { |
| 1403 |
$guidance = __('Your database is currently in read-only mode. Contact your hosting provider.', '404-solution'); |
| 1404 |
} elseif ($type === 'query_quota') { |
| 1405 |
$guidance = __('Your database query quota was exceeded. This usually resets automatically.', '404-solution'); |
| 1406 |
} elseif ($type === 'corrupted_temp_table') { |
| 1407 |
$guidance = __('A temporary MySQL table was corrupted, usually caused by disk or hardware issues. The plugin cannot repair it. Please contact your hosting provider.', '404-solution'); |
| 1408 |
} elseif ($type === 'log_table_full') { |
| 1409 |
$guidance = __('The 404 Solution log table is full. The plugin automatically trimmed the oldest 1,000 log entries to free space, but logging may still be limited. Please contact your hosting provider about disk space.', '404-solution'); |
| 1410 |
} elseif ($type === 'stale_permalink_cache') { |
| 1411 |
$guidance = __('The permalink cache appears to be empty. Try rebuilding it from the Tools tab, or check that your site has enough disk space.', '404-solution'); |
| 1412 |
} elseif ($type === 'lock_timeout') { |
| 1413 |
$guidance = __('A database lock wait timeout occurred. This is usually caused by another process holding a table lock on your database. It may resolve itself automatically, or contact your hosting provider if it persists.', '404-solution'); |
| 1414 |
} |
| 1415 |
echo '<div class="notice notice-error"><p><strong>404 Solution:</strong> ' . esc_html($notice['message']) . '</p>'; |
| 1416 |
if ($guidance !== '') { |
| 1417 |
echo '<p>' . esc_html($guidance) . '</p>'; |
| 1418 |
} |
| 1419 |
if (!empty($notice['error_string'])) { |
| 1420 |
echo '<details><summary>' . esc_html(__('Show database error details', '404-solution')) . '</summary>'; |
| 1421 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;max-width:100%;margin:6px 0;">' . esc_html($notice['error_string']) . '</pre></details>'; |
| 1422 |
} |
| 1423 |
echo '</div>'; |
| 1424 |
} |
| 1425 |
} |
| 1426 |
add_action('admin_notices', 'abj404_show_plugin_db_notice'); |
| 1427 |
|
| 1428 |
if (!function_exists('abj404_show_view_build_cron_notices')) { |
| 1429 |
/** |
| 1430 |
* Render the staged-view-build cron-stuck and schedule-failure notices. |
| 1431 |
* Set by DataAccess::scheduleViewDoneRebuild() when WordPress cron has |
| 1432 |
* stopped advancing (earliest overdue ready-job >= 24h old) or when |
| 1433 |
* wp_schedule_single_event itself fails. 24h dedup transients. |
| 1434 |
* |
| 1435 |
* @return void |
| 1436 |
*/ |
| 1437 |
function abj404_show_view_build_cron_notices() { |
| 1438 |
if (!is_admin() || !current_user_can('manage_options')) { |
| 1439 |
return; |
| 1440 |
} |
| 1441 |
$page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : ''; |
| 1442 |
if ($page !== ABJ404_PP) { |
| 1443 |
return; |
| 1444 |
} |
| 1445 |
$keys = array( |
| 1446 |
'abj404_view_build_stuck_wp_cron_disabled', |
| 1447 |
'abj404_view_build_cron_schedule_failed', |
| 1448 |
'abj404_view_done_hard_stale', |
| 1449 |
'abj404_logs_hits_rollup_stale', |
| 1450 |
); |
| 1451 |
foreach ($keys as $key) { |
| 1452 |
$notice = get_transient($key); |
| 1453 |
if (!is_array($notice) || empty($notice['message'])) { |
| 1454 |
continue; |
| 1455 |
} |
| 1456 |
$noticeMessage = is_string($notice['message']) ? $notice['message'] : ''; |
| 1457 |
echo '<div class="notice notice-warning"><p><strong>404 Solution:</strong> ' |
| 1458 |
. esc_html($noticeMessage) . '</p>'; |
| 1459 |
if (!empty($notice['error_string'])) { |
| 1460 |
$noticeErrorString = is_string($notice['error_string']) ? $notice['error_string'] : ''; |
| 1461 |
echo '<details><summary>' . esc_html(__('Show details', '404-solution')) |
| 1462 |
. '</summary><pre style="white-space:pre-wrap;word-break:break-all;max-width:100%;margin:6px 0;">' |
| 1463 |
. esc_html($noticeErrorString) . '</pre></details>'; |
| 1464 |
} |
| 1465 |
echo '</div>'; |
| 1466 |
} |
| 1467 |
} |
| 1468 |
} |
| 1469 |
add_action('admin_notices', 'abj404_show_view_build_cron_notices'); |
| 1470 |
|
| 1471 |
if (!function_exists('abj404_get_simulated_db_latency_ms')) { |
| 1472 |
/** @return bool */ |
| 1473 |
function abj404_is_local_debug_host() { |
| 1474 |
$serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : ''); |
| 1475 |
$serverName = strtolower(trim((string)$serverName)); |
| 1476 |
if ($serverName === '') { |
| 1477 |
return false; |
| 1478 |
} |
| 1479 |
|
| 1480 |
$normalizedHost = $serverName; |
| 1481 |
if (strpos($normalizedHost, '[') === 0) { |
| 1482 |
$endBracket = strpos($normalizedHost, ']'); |
| 1483 |
if ($endBracket !== false) { |
| 1484 |
$normalizedHost = substr($normalizedHost, 1, $endBracket - 1); |
| 1485 |
} |
| 1486 |
} else { |
| 1487 |
$colonCount = substr_count($normalizedHost, ':'); |
| 1488 |
if ($colonCount === 1 && preg_match('/:\d+$/', $normalizedHost)) { |
| 1489 |
$normalizedHost = preg_replace('/:\d+$/', '', $normalizedHost); |
| 1490 |
} |
| 1491 |
} |
| 1492 |
|
| 1493 |
$normalizedHost = rtrim((string)$normalizedHost, '.'); |
| 1494 |
return in_array($normalizedHost, array('127.0.0.1', '::1', 'localhost'), true); |
| 1495 |
} |
| 1496 |
|
| 1497 |
/** @return int */ |
| 1498 |
function abj404_get_simulated_db_latency_ms() { |
| 1499 |
if (!abj404_is_local_debug_host()) { |
| 1500 |
return 0; |
| 1501 |
} |
| 1502 |
if (defined('ABJ404_SIMULATED_DB_LATENCY_MS')) { |
| 1503 |
return max(0, min(5000, absint(ABJ404_SIMULATED_DB_LATENCY_MS))); |
| 1504 |
} |
| 1505 |
$value = get_option('abj404_simulated_db_latency_ms', 0); |
| 1506 |
return max(0, min(5000, absint(is_scalar($value) ? $value : 0))); |
| 1507 |
} |
| 1508 |
} |
| 1509 |
|
| 1510 |
if (!function_exists('abj404_show_diagnostic_latency_notice')) { |
| 1511 |
/** @return void */ |
| 1512 |
function abj404_show_diagnostic_latency_notice() { |
| 1513 |
// Intentionally no-op. Simulated latency status is shown in the plugin's |
| 1514 |
// Tools > Diagnostics card to avoid intrusive floating/global notices. |
| 1515 |
return; |
| 1516 |
} |
| 1517 |
} |
| 1518 |
|
| 1519 |
if (!function_exists('abj404_load_textdomain_if_needed')) { |
| 1520 |
/** |
| 1521 |
* Load plugin translations once, lazily. |
| 1522 |
* |
| 1523 |
* @return void |
| 1524 |
*/ |
| 1525 |
function abj404_load_textdomain_if_needed() { |
| 1526 |
static $loaded = false; |
| 1527 |
if ($loaded) { |
| 1528 |
return; |
| 1529 |
} |
| 1530 |
|
| 1531 |
$override_locale = ''; |
| 1532 |
if (!empty($GLOBALS['abj404_plugin_language_override'])) { |
| 1533 |
$override_locale = (string)$GLOBALS['abj404_plugin_language_override']; |
| 1534 |
} else { |
| 1535 |
$options = abj404_get_settings_options(); |
| 1536 |
$override_locale = (is_array($options) && !empty($options['plugin_language_override'])) |
| 1537 |
? $options['plugin_language_override'] : ''; |
| 1538 |
} |
| 1539 |
|
| 1540 |
if (!empty($override_locale)) { |
| 1541 |
$mo_file = ABJ404_PATH . 'languages/404-solution-' . $override_locale . '.mo'; |
| 1542 |
if (file_exists($mo_file)) { |
| 1543 |
load_textdomain('404-solution', $mo_file); |
| 1544 |
} |
| 1545 |
} else { |
| 1546 |
$lang_dir = dirname(plugin_basename(ABJ404_FILE)) . '/languages'; |
| 1547 |
load_plugin_textdomain('404-solution', false, $lang_dir); |
| 1548 |
} |
| 1549 |
|
| 1550 |
$loaded = true; |
| 1551 |
} |
| 1552 |
} |
| 1553 |
|
| 1554 |
if (!function_exists('abj404_maybe_refresh_runtime_integrity_cache')) { |
| 1555 |
/** |
| 1556 |
* Refresh runtime integrity cache at most once per TTL window. |
| 1557 |
* |
| 1558 |
* @param int $ttlSeconds |
| 1559 |
* @return void |
| 1560 |
*/ |
| 1561 |
function abj404_maybe_refresh_runtime_integrity_cache($ttlSeconds = 43200) { |
| 1562 |
if (!is_admin()) { |
| 1563 |
return; |
| 1564 |
} |
| 1565 |
|
| 1566 |
$checkedRecently = get_transient('abj404_runtime_integrity_checked'); |
| 1567 |
if ($checkedRecently) { |
| 1568 |
return; |
| 1569 |
} |
| 1570 |
|
| 1571 |
$missingRuntimeFiles = abj404_verify_runtime_integrity(); |
| 1572 |
if (count($missingRuntimeFiles) > 0) { |
| 1573 |
set_transient('abj404_runtime_missing_files', $missingRuntimeFiles, $ttlSeconds); |
| 1574 |
} else { |
| 1575 |
delete_transient('abj404_runtime_missing_files'); |
| 1576 |
} |
| 1577 |
|
| 1578 |
set_transient('abj404_runtime_integrity_checked', 1, $ttlSeconds); |
| 1579 |
} |
| 1580 |
} |
| 1581 |
|
| 1582 |
/** This only runs after WordPress is done enqueuing scripts. */ |
| 1583 |
if (!function_exists('abj404_loadSomethingWhenWordPressIsReady')) { |
| 1584 |
/** @return void */ |
| 1585 |
function abj404_loadSomethingWhenWordPressIsReady() { |
| 1586 |
// If boot failed (missing files), skip all init that depends on plugin classes. |
| 1587 |
if (!$GLOBALS['abj404_boot_ok']) { |
| 1588 |
return; |
| 1589 |
} |
| 1590 |
|
| 1591 |
$isAdminRequest = is_admin(); |
| 1592 |
if ($isAdminRequest) { |
| 1593 |
abj404_load_textdomain_if_needed(); |
| 1594 |
} |
| 1595 |
|
| 1596 |
// make debugging easier on localhost etc |
| 1597 |
if ($isAdminRequest) { |
| 1598 |
$serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : '(not found)'); |
| 1599 |
$serverNameIsInTheWhiteList = in_array($serverName, $GLOBALS['abj404_whitelist']); |
| 1600 |
|
| 1601 |
// Keep localhost debug helper on admin screens only; frontend requests stay lean. |
| 1602 |
if ($serverNameIsInTheWhiteList && function_exists('wp_get_current_user')) { |
| 1603 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1604 |
$abj404logic = ABJ_404_Solution_PluginLogic::getInstance(); |
| 1605 |
if ($abj404logic->userIsPluginAdmin()) { |
| 1606 |
$GLOBALS['abj404_display_errors'] = true; |
| 1607 |
} |
| 1608 |
} |
| 1609 |
} |
| 1610 |
|
| 1611 |
$action = null; |
| 1612 |
if ($isAdminRequest) { |
| 1613 |
$actionGet = isset($_GET['action']) && is_string($_GET['action']) ? $_GET['action'] : ''; |
| 1614 |
$actionPost = isset($_POST['action']) && is_string($_POST['action']) ? $_POST['action'] : ''; |
| 1615 |
if ($actionGet !== '') { |
| 1616 |
$action = sanitize_text_field($actionGet); |
| 1617 |
} else if ($actionPost !== '') { |
| 1618 |
$action = sanitize_text_field($actionPost); |
| 1619 |
} else { |
| 1620 |
$action = null; |
| 1621 |
} |
| 1622 |
} |
| 1623 |
if ($isAdminRequest && abj404_is_local_debug_host() && current_user_can('manage_options') && isset($_GET['abj404_set_sim_db_ms'])) { |
| 1624 |
$nonceOk = isset($_GET['_wpnonce']) ? wp_verify_nonce($_GET['_wpnonce'], 'abj404_set_sim_db_ms') : false; |
| 1625 |
if ($nonceOk) { |
| 1626 |
$newMs = max(0, min(5000, absint($_GET['abj404_set_sim_db_ms']))); |
| 1627 |
update_option('abj404_simulated_db_latency_ms', $newMs, false); |
| 1628 |
} |
| 1629 |
} |
| 1630 |
|
| 1631 |
$ttl = defined('HOUR_IN_SECONDS') ? (12 * HOUR_IN_SECONDS) : 43200; |
| 1632 |
abj404_maybe_refresh_runtime_integrity_cache($ttl); |
| 1633 |
|
| 1634 |
if ($isAdminRequest && $action === 'exportRedirects') { |
| 1635 |
require_once(plugin_dir_path( __FILE__ ) . "includes/Loader.php"); |
| 1636 |
$abj404logic = ABJ_404_Solution_PluginLogic::getInstance(); |
| 1637 |
$abj404logic->handleActionExport(); |
| 1638 |
} |
| 1639 |
} |
| 1640 |
} |
| 1641 |
add_action('admin_init', 'abj404_loadSomethingWhenWordPressIsReady'); |
| 1642 |
|
| 1643 |
if (!function_exists('abj404_maybePageLoadFallbackAdvance')) { |
| 1644 |
/** |
| 1645 |
* Admin-only, plugin-page-only synchronous fallback that advances the |
| 1646 |
* staged view-build by one tick (about 2s) when WP-Cron is broken. |
| 1647 |
* |
| 1648 |
* Pairs with the cron-stuck admin notice (c374): the notice tells the |
| 1649 |
* admin their cron is broken; this fallback unblocks the page in the |
| 1650 |
* meantime so they can fix cron without staring at the loading |
| 1651 |
* indicator forever. The actual gate logic and budget compression live |
| 1652 |
* in ABJ_404_Solution_DataAccess::runPageLoadFallbackAdvance() so they |
| 1653 |
* can be unit-tested directly; this wrapper is the admin_init hook |
| 1654 |
* that wires the DAO method into the request lifecycle. |
| 1655 |
* |
| 1656 |
* Guards (in order, all required): |
| 1657 |
* - boot succeeded (plugin class loadable); |
| 1658 |
* - is_admin() (frontend / REST / heartbeat requests are not in scope); |
| 1659 |
* - not AJAX or cron (those have their own advance paths); |
| 1660 |
* - request is for the plugin admin page (abj404_solution); other |
| 1661 |
* wp-admin pages are unrelated and should not be taxed with build |
| 1662 |
* work on every navigation; |
| 1663 |
* - current user has the plugin admin capability (manage_options) so |
| 1664 |
* an unauthenticated request cannot trigger build work; |
| 1665 |
* - DataAccess exposes runPageLoadFallbackAdvance (defense for older |
| 1666 |
* in-place upgrades whose DAO class predates this method). |
| 1667 |
* |
| 1668 |
* The DAO method itself owns the cron-stuck check, the transient gate, |
| 1669 |
* the per-stage budget compression, and the build-lock semantics. |
| 1670 |
* |
| 1671 |
* @return void |
| 1672 |
*/ |
| 1673 |
function abj404_maybePageLoadFallbackAdvance() { |
| 1674 |
if (!$GLOBALS['abj404_boot_ok']) { |
| 1675 |
return; |
| 1676 |
} |
| 1677 |
if (!is_admin()) { |
| 1678 |
return; |
| 1679 |
} |
| 1680 |
if (function_exists('wp_doing_ajax') && wp_doing_ajax()) { |
| 1681 |
return; |
| 1682 |
} |
| 1683 |
if (function_exists('wp_doing_cron') && wp_doing_cron()) { |
| 1684 |
return; |
| 1685 |
} |
| 1686 |
$currentPage = isset($_GET['page']) && is_string($_GET['page']) |
| 1687 |
? sanitize_text_field((string)$_GET['page']) : ''; |
| 1688 |
if ($currentPage !== 'abj404_solution') { |
| 1689 |
return; |
| 1690 |
} |
| 1691 |
if (!function_exists('current_user_can') || !current_user_can('manage_options')) { |
| 1692 |
return; |
| 1693 |
} |
| 1694 |
try { |
| 1695 |
require_once(plugin_dir_path(__FILE__) . "includes/Loader.php"); |
| 1696 |
$dao = ABJ_404_Solution_DataAccess::getInstance(); |
| 1697 |
if (is_object($dao) && method_exists($dao, 'runPageLoadFallbackAdvance')) { |
| 1698 |
$dao->runPageLoadFallbackAdvance(); |
| 1699 |
} |
| 1700 |
} catch (\Throwable $e) { |
| 1701 |
// Page-load fallback is best-effort. A failure here must not |
| 1702 |
// break admin page rendering. Log at warning level (error_log |
| 1703 |
// suffices for this surface) so the failure is observable |
| 1704 |
// without triggering the plugin's dev-email-report path. Per |
| 1705 |
// CLAUDE.md self-healing rule #6: infrastructure failures are |
| 1706 |
// warnings, not errors, when the plugin still functions. |
| 1707 |
error_log('404 Solution: page-load fallback advance failed: ' . $e->getMessage()); |
| 1708 |
} |
| 1709 |
} |
| 1710 |
} |
| 1711 |
// Priority 20 runs after abj404_loadSomethingWhenWordPressIsReady (default |
| 1712 |
// priority 10), so the textdomain is loaded and any pending exportRedirects |
| 1713 |
// has run before we burn ~2s of stage budget. Inverting that order would |
| 1714 |
// risk an export action being preceded by inline staged-build work, which |
| 1715 |
// changes the apparent latency of the export. |
| 1716 |
add_action('admin_init', 'abj404_maybePageLoadFallbackAdvance', 20); |
| 1717 |
|