| 1 |
<?php |
| 2 |
if (!defined('ABSPATH')) { |
| 3 |
exit; |
| 4 |
} |
| 5 |
|
| 6 |
/** |
| 7 |
* Degraded-boot admin surface: support-request wiring and the degraded menu. |
| 8 |
* |
| 9 |
* abj404_degraded_register_support_request() wires the "send debug log" AJAX |
| 10 |
* flow on the corrupt-install path so a stuck admin can still reach the |
| 11 |
* developer. abj404_degraded_admin_menu() registers the degraded menu item that |
| 12 |
* points at the corrupt-install diagnostics page. |
| 13 |
* |
| 14 |
* abj404_degraded_admin_notice() and abj404_degraded_admin_page() render the |
| 15 |
* corrupt-install admin notice and diagnostics page (with reinstall guidance and |
| 16 |
* a support-request card). The add_action() registrations that schedule these on |
| 17 |
* the degraded boot path stay in 404-solution.php; this file only defines the |
| 18 |
* functions. |
| 19 |
*/ |
| 20 |
// allow-no-test-found: boot-time degraded-path global functions wired via add_action in 404-solution.php; no same-named unit file. The degraded support-request and admin-page behavior is exercised in BlankPagePreventionTest and OpcacheStaleAfterUpgradeTest (both reference abj404_degraded_*). |
| 21 |
|
| 22 |
if (!function_exists('abj404_degraded_register_support_request')) { |
| 23 |
/** |
| 24 |
* Wire the support-request flow on the degraded boot path so an admin |
| 25 |
* stuck on the corrupt-install screen can still send a debug log. The |
| 26 |
* normal registration runs inside WordPress_Connector::registerAdminHooks() |
| 27 |
* which only executes on a successful boot; without this helper, the |
| 28 |
* AJAX handler that the modal POSTs to does not exist on the degraded |
| 29 |
* path and the click silently 400s. |
| 30 |
* |
| 31 |
* Strictly best-effort. Every step is guarded with file_exists() and |
| 32 |
* class_exists() so a corrupted install that is missing any of the |
| 33 |
* support-request files just falls back to the mailto link in the |
| 34 |
* degraded admin page. We must not throw a fatal here, because we |
| 35 |
* already ARE on the degraded path. |
| 36 |
* |
| 37 |
* @return void |
| 38 |
*/ |
| 39 |
function abj404_degraded_register_support_request() { |
| 40 |
$inc = ABJ404_PATH . 'includes/'; |
| 41 |
$supportFiles = array( |
| 42 |
$inc . 'ajax/AjaxSecurityGate.php', |
| 43 |
$inc . 'feedback/FeedbackTransport.php', |
| 44 |
$inc . 'feedback/SupportRequestButton.php', |
| 45 |
$inc . 'ajax/Ajax_SupportRequest.php', |
| 46 |
$inc . 'ajax/Ajax_SupportRequestPreview.php', |
| 47 |
); |
| 48 |
foreach ($supportFiles as $file) { |
| 49 |
if (!file_exists($file)) { |
| 50 |
return; |
| 51 |
} |
| 52 |
} |
| 53 |
try { |
| 54 |
foreach ($supportFiles as $file) { |
| 55 |
require_once $file; |
| 56 |
} |
| 57 |
// 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. The exception is NOT dropped: abj404_logRuntimeWarning() records it (plugin logger if resolvable, else the PHP fallback logger) before we fall through. |
| 58 |
} catch (\Throwable $e) { |
| 59 |
if (function_exists('abj404_logRuntimeWarning')) { |
| 60 |
abj404_logRuntimeWarning('abj404_degraded_register_support_request: support-request file require failed', $e); |
| 61 |
} |
| 62 |
return; |
| 63 |
} |
| 64 |
if (!class_exists('ABJ_404_Solution_Ajax_SupportRequest') |
| 65 |
|| !class_exists('ABJ_404_Solution_Ajax_SupportRequestPreview')) { |
| 66 |
return; |
| 67 |
} |
| 68 |
// Register the AJAX handlers directly. The normal init() path goes |
| 69 |
// through ABJ_404_Solution_WPUtils::safeAddAction() but that helper |
| 70 |
// may itself be missing on a corrupt install; falling back to |
| 71 |
// add_action() avoids that dependency. |
| 72 |
$supportInstance = ABJ_404_Solution_Ajax_SupportRequest::getInstance(); |
| 73 |
$previewInstance = ABJ_404_Solution_Ajax_SupportRequestPreview::getInstance(); |
| 74 |
add_action('wp_ajax_abj404_support_request', array($supportInstance, 'handleRequest')); |
| 75 |
add_action('wp_ajax_abj404_support_request_preview', array($previewInstance, 'handleRequest')); |
| 76 |
// Enqueue the JS assets on the degraded admin page only. Using a |
| 77 |
// closure keeps this self-contained without registering a new |
| 78 |
// global function on the corrupted boot path. |
| 79 |
add_action('admin_enqueue_scripts', function($hook) use ($inc) { |
| 80 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 81 |
$isOurPage = is_string($hook) && ( |
| 82 |
strpos($hook, $ppSlug) !== false |
| 83 |
|| strpos($hook, 'abj404_solution') !== false |
| 84 |
); |
| 85 |
if (!$isOurPage) { |
| 86 |
return; |
| 87 |
} |
| 88 |
$clientJs = $inc . 'ajax/SupportRequest.js'; |
| 89 |
$buttonJs = $inc . 'js/support-request-button.js'; |
| 90 |
$transportJs = $inc . 'js/support-request-transport.js'; |
| 91 |
$modalViewJs = $inc . 'js/support-request-modal-view.js'; |
| 92 |
if (!file_exists($clientJs) || !file_exists($buttonJs) |
| 93 |
|| !file_exists($transportJs) || !file_exists($modalViewJs)) { |
| 94 |
return; |
| 95 |
} |
| 96 |
$baseUrl = plugin_dir_url(ABJ404_FILE) . 'includes/'; |
| 97 |
$ver = defined('ABJ404_VERSION') ? ABJ404_VERSION : (string)abj404_now(); |
| 98 |
wp_enqueue_script('abj404-support-request-client', |
| 99 |
$baseUrl . 'ajax/SupportRequest.js', array(), $ver, true); |
| 100 |
wp_enqueue_script('abj404-support-request-transport', |
| 101 |
$baseUrl . 'js/support-request-transport.js', |
| 102 |
array('abj404-support-request-client'), $ver, true); |
| 103 |
wp_enqueue_script('abj404-support-request-modal-view', |
| 104 |
$baseUrl . 'js/support-request-modal-view.js', array(), $ver, true); |
| 105 |
wp_enqueue_script('abj404-support-request-button', |
| 106 |
$baseUrl . 'js/support-request-button.js', |
| 107 |
array('abj404-support-request-client', 'abj404-support-request-transport', 'abj404-support-request-modal-view'), |
| 108 |
$ver, true); |
| 109 |
// The modal renders wp.i18n.__() strings, so it needs its JED file even on |
| 110 |
// the degraded page. Guarded because this boot path runs on a broken install |
| 111 |
// where the class file itself may be one of the missing ones. |
| 112 |
if (class_exists('ABJ_404_Solution_WPUtils')) { |
| 113 |
ABJ_404_Solution_WPUtils::registerScriptTranslations(); |
| 114 |
} |
| 115 |
$supportNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequest::NONCE_ACTION); |
| 116 |
$previewNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequestPreview::NONCE_ACTION); |
| 117 |
$ajaxUrl = admin_url('admin-ajax.php'); |
| 118 |
$payload = wp_json_encode(array( |
| 119 |
'ajaxurl' => $ajaxUrl, |
| 120 |
'nonces' => array( |
| 121 |
'support_request' => $supportNonce, |
| 122 |
'support_request_preview' => $previewNonce, |
| 123 |
), |
| 124 |
)); |
| 125 |
$bootstrap = 'window.ABJ404=window.ABJ404||{};Object.assign(window.ABJ404,' |
| 126 |
. (is_string($payload) ? $payload : '{}') . ');'; |
| 127 |
wp_add_inline_script('abj404-support-request-client', $bootstrap, 'before'); |
| 128 |
}); |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
if (!function_exists('abj404_degraded_admin_menu')) { |
| 133 |
/** @return void */ |
| 134 |
function abj404_degraded_admin_menu() { |
| 135 |
$options = abj404_get_settings_options(); |
| 136 |
|
| 137 |
$menuName = '404 Solution'; |
| 138 |
$badge = " <span class='update-plugins count-1'><span class='plugin-count'>!</span></span>"; |
| 139 |
|
| 140 |
if (isset($options['menuLocation']) && $options['menuLocation'] === 'settingsLevel') { |
| 141 |
add_menu_page('404 Solution', $menuName . $badge, 'manage_options', 'abj404_solution', 'abj404_degraded_admin_page'); |
| 142 |
} else { |
| 143 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 144 |
add_submenu_page('options-general.php', '404 Solution', $menuName . $badge, 'manage_options', $ppSlug, 'abj404_degraded_admin_page'); |
| 145 |
} |
| 146 |
} |
| 147 |
} |
| 148 |
|
| 149 |
if (!function_exists('abj404_degraded_admin_notice')) { |
| 150 |
/** @return void */ |
| 151 |
function abj404_degraded_admin_notice() { |
| 152 |
if (!current_user_can('manage_options')) { |
| 153 |
return; |
| 154 |
} |
| 155 |
echo '<div class="notice notice-error"><p><strong>404 Solution:</strong> '; |
| 156 |
echo 'Plugin files are missing or corrupt. '; |
| 157 |
$ppSlug = defined('ABJ404_PP') ? ABJ404_PP : 'abj404_solution'; |
| 158 |
echo '<a href="' . esc_url(admin_url('options-general.php?page=' . $ppSlug)) . '">View details</a>'; |
| 159 |
echo '</p></div>'; |
| 160 |
} |
| 161 |
} |
| 162 |
|
| 163 |
if (!function_exists('abj404_degraded_admin_page')) { |
| 164 |
/** @return void */ |
| 165 |
function abj404_degraded_admin_page() { |
| 166 |
if (!current_user_can('manage_options')) { |
| 167 |
echo '<div class="wrap">'; |
| 168 |
echo '<h1>404 Solution</h1>'; |
| 169 |
echo '<div class="notice notice-error"><p>'; |
| 170 |
echo '<strong>Permission denied.</strong> '; |
| 171 |
echo 'Your user account does not have permission to access this page.'; |
| 172 |
echo '</p><p>'; |
| 173 |
echo 'Please verify that your WordPress role has the <code>manage_options</code> capability.'; |
| 174 |
echo '</p></div></div>'; |
| 175 |
return; |
| 176 |
} |
| 177 |
|
| 178 |
$missingFiles = isset($GLOBALS['abj404_missing_files']) && is_array($GLOBALS['abj404_missing_files']) ? $GLOBALS['abj404_missing_files'] : array(); |
| 179 |
$bootError = isset($GLOBALS['abj404_boot_error']) ? $GLOBALS['abj404_boot_error'] : ''; |
| 180 |
$pluginDir = defined('ABJ404_PATH') ? ABJ404_PATH : dirname(dirname(__DIR__)) . '/'; |
| 181 |
|
| 182 |
// Check for a stored fatal from a previous request. |
| 183 |
$fatalInfo = function_exists('get_transient') ? get_transient('abj404_boot_fatal') : false; |
| 184 |
|
| 185 |
echo '<div class="wrap">'; |
| 186 |
// allow-em-dash: — entity is user-facing HTML output in the corrupt-install page heading, separating the plugin name from the page subtitle. |
| 187 |
echo '<h1>404 Solution — Plugin Files Missing</h1>'; |
| 188 |
|
| 189 |
echo '<div class="notice notice-error inline"><p>'; |
| 190 |
echo '<strong>The 404 Solution plugin cannot start</strong> because one or more required files are missing or corrupt. '; |
| 191 |
echo 'This usually happens after a failed plugin update or incomplete file upload.'; |
| 192 |
echo '</p></div>'; |
| 193 |
|
| 194 |
if (!empty($missingFiles)) { |
| 195 |
echo '<div class="card" style="max-width:800px;">'; |
| 196 |
echo '<h2>Missing Files</h2>'; |
| 197 |
echo '<ul style="list-style:disc;padding-left:20px;">'; |
| 198 |
foreach ($missingFiles as $file) { |
| 199 |
// Show relative path for readability. |
| 200 |
$relative = is_string($file) ? str_replace($pluginDir, '', $file) : ''; |
| 201 |
echo '<li><code>' . esc_html($relative) . '</code></li>'; |
| 202 |
} |
| 203 |
echo '</ul>'; |
| 204 |
echo '</div>'; |
| 205 |
} |
| 206 |
|
| 207 |
$bootErrorText = is_scalar($bootError) ? (string)$bootError : ''; |
| 208 |
if ($bootErrorText !== '') { |
| 209 |
echo '<div class="card" style="max-width:800px;">'; |
| 210 |
echo '<h2>Error Details</h2>'; |
| 211 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;">' . esc_html($bootErrorText) . '</pre>'; |
| 212 |
echo '</div>'; |
| 213 |
} |
| 214 |
|
| 215 |
if (is_array($fatalInfo) && !empty($fatalInfo['message'])) { |
| 216 |
echo '<div class="card" style="max-width:800px;">'; |
| 217 |
echo '<h2>Fatal Error (previous request)</h2>'; |
| 218 |
$fatalFileRaw = isset($fatalInfo['file']) && is_scalar($fatalInfo['file']) ? (string)$fatalInfo['file'] : ''; |
| 219 |
$fatalFile = $fatalFileRaw !== '' ? str_replace($pluginDir, '', $fatalFileRaw) : 'unknown'; |
| 220 |
$fatalLine = isset($fatalInfo['line']) && is_scalar($fatalInfo['line']) ? (string)$fatalInfo['line'] : '?'; |
| 221 |
$fatalMessage = isset($fatalInfo['message']) && is_scalar($fatalInfo['message']) ? (string)$fatalInfo['message'] : ''; |
| 222 |
echo '<pre style="white-space:pre-wrap;word-break:break-all;">' . esc_html($fatalMessage) . "\n" . esc_html($fatalFile) . ':' . esc_html($fatalLine) . '</pre>'; |
| 223 |
echo '</div>'; |
| 224 |
} |
| 225 |
|
| 226 |
echo '<div class="card" style="max-width:800px;">'; |
| 227 |
echo '<h2>How to Fix</h2>'; |
| 228 |
echo '<ol>'; |
| 229 |
echo '<li>Go to <strong>Plugins → Installed Plugins</strong>, deactivate <strong>404 Solution</strong>, then delete it.</li>'; |
| 230 |
echo '<li>Reinstall from the WordPress plugin directory: '; |
| 231 |
$installUrl = admin_url('plugin-install.php?s=404+solution&tab=search'); |
| 232 |
echo '<a href="' . esc_url($installUrl) . '" class="button button-primary">Search “404 Solution”</a>'; |
| 233 |
echo '</li>'; |
| 234 |
echo '<li>Activate the fresh copy. Your redirects and settings are stored in the database and will not be lost.</li>'; |
| 235 |
echo '</ol>'; |
| 236 |
echo '</div>'; |
| 237 |
|
| 238 |
// Support-request card. This is the most valuable placement for |
| 239 |
// the "Send debug log to developer" button: an admin who reached |
| 240 |
// this screen needs help and may not be able to navigate the |
| 241 |
// normal plugin UI. When the SupportRequestButton class is |
| 242 |
// present (most corruption is partial), render the mount div so |
| 243 |
// the JS component can take over. Always emit a mailto fallback |
| 244 |
// underneath in case the JS files are themselves among the |
| 245 |
// missing files. |
| 246 |
echo '<div class="card" style="max-width:800px;">'; |
| 247 |
echo '<h2>Need help? Contact the developer</h2>'; |
| 248 |
echo '<p>Send the developer a one-time diagnostic report including the missing-file list above. Your redirects and settings are not shared.</p>'; |
| 249 |
$missingCount = count($missingFiles); |
| 250 |
$contextSummary = 'Corrupt install: ' . $missingCount . ' missing file(s)'; |
| 251 |
$bootErrorForSummary = is_scalar($bootError) ? (string)$bootError : ''; |
| 252 |
if ($bootErrorForSummary !== '') { |
| 253 |
$contextSummary .= '. Boot error: ' . substr($bootErrorForSummary, 0, 200); |
| 254 |
} |
| 255 |
if (class_exists('ABJ_404_Solution_SupportRequestButton')) { |
| 256 |
echo ABJ_404_Solution_SupportRequestButton::render('system_corrupt_install', $contextSummary); |
| 257 |
} |
| 258 |
$mailEmail = defined('ABJ404_AUTHOR_EMAIL') ? (string)ABJ404_AUTHOR_EMAIL : '404solution@ajexperience.com'; |
| 259 |
$mailSubject = rawurlencode('404 Solution: corrupt install report'); |
| 260 |
$homeUrlText = '(unknown)'; |
| 261 |
if (function_exists('home_url')) { |
| 262 |
$homeUrlVal = home_url(); |
| 263 |
$homeUrlText = is_string($homeUrlVal) ? $homeUrlVal : '(unknown)'; |
| 264 |
} |
| 265 |
$stringMissingFiles = array(); |
| 266 |
foreach ($missingFiles as $entry) { |
| 267 |
$stringMissingFiles[] = is_scalar($entry) ? (string)$entry : ''; |
| 268 |
} |
| 269 |
$missingFileLines = implode("\n", $stringMissingFiles); |
| 270 |
$mailBody = rawurlencode("Site URL: " . $homeUrlText . "\n" |
| 271 |
. "Missing files (" . (int)$missingCount . "):\n" |
| 272 |
. $missingFileLines |
| 273 |
. "\n\nBoot error:\n" . $bootErrorText); |
| 274 |
echo '<p style="margin-top:8px;">Or email manually: '; |
| 275 |
echo '<a href="mailto:' . esc_attr($mailEmail) . '?subject=' . $mailSubject . '&body=' . $mailBody . '">'; |
| 276 |
echo esc_html($mailEmail) . '</a></p>'; |
| 277 |
echo '</div>'; |
| 278 |
|
| 279 |
echo '</div>'; // .wrap |
| 280 |
} |
| 281 |
} |
| 282 |
|