PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / root-boot / DegradedSupportRequest.php

DegradedSupportRequest.php in 404 Solution trunk, at includes/root-boot/DegradedSupportRequest.php

282 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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: &mdash; 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 &mdash; 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 &rarr; 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 &ldquo;404 Solution&rdquo;</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