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

WordPress_Connector.php in 404 Solution 4.2.0, at includes/WordPress_Connector.php

835 lines 37.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /* Functions in this class should only be for plugging into WordPress listeners (filters, actions, etc). */
9
10 class ABJ_404_Solution_WordPress_Connector {
11
12 /** @var self|null */
13 private static $instance = null;
14
15 /** @var array<int, string> */
16 private static $adminRuntimeErrors = array();
17
18 /** @var ABJ_404_Solution_PluginLogic */
19 private $logic;
20
21 /** @var ABJ_404_Solution_RedirectsRepository */ private $redirectsRepository;
22
23 /** @var mixed */ private $logsRepository;
24
25 /** @var mixed */ private $statsRepository;
26
27 /** @var ABJ_404_Solution_Logging */
28 private $logger;
29
30 /** @var ABJ_404_Solution_Functions */
31 private $f;
32
33 /** @var ABJ_404_Solution_SpellChecker */
34 private $spellChecker;
35
36 /** @var ABJ_404_Solution_FrontendRequestPipeline|null */
37 private $frontendPipeline = null;
38
39 /**
40 * Constructor with dependency injection.
41 *
42 * @param ABJ_404_Solution_PluginLogic|null $pluginLogic Business logic service
43 * @param ABJ_404_Solution_RedirectsRepository|null $redirectsRepository Redirects repository
44 * @param ABJ_404_Solution_Logging|null $logging Logging service
45 * @param ABJ_404_Solution_Functions|null $functions String utilities
46 * @param ABJ_404_Solution_SpellChecker|null $spellChecker Spell checker service
47 * @param mixed|null $logsRepository Log writer
48 * @param mixed|null $statsRepository Stats reader
49 */
50 public function __construct($pluginLogic = null, $redirectsRepository = null, $logging = null, $functions = null, $spellChecker = null, $logsRepository = null, $statsRepository = null) {
51 $this->logic = $pluginLogic !== null ? $pluginLogic : abj_service('plugin_logic');
52 $this->redirectsRepository = $redirectsRepository !== null ? $redirectsRepository : abj_service('redirects_repository');
53 $this->logger = $logging !== null ? $logging : abj_service('logging');
54 $this->f = $functions !== null ? $functions : abj_service('functions');
55 $this->spellChecker = $spellChecker !== null ? $spellChecker : abj_service('spell_checker');
56 $this->logsRepository = $logsRepository !== null ? $logsRepository :
57 (is_object($redirectsRepository) && method_exists($redirectsRepository, 'logRedirectHit') ? $redirectsRepository : abj_service('logs_repository'));
58 $this->statsRepository = $statsRepository !== null ? $statsRepository :
59 (is_object($redirectsRepository) && method_exists($redirectsRepository, 'getCapturedCountForNotification') ? $redirectsRepository : abj_service('stats_repository'));
60 }
61
62 /** @return ABJ_404_Solution_FrontendRequestPipeline */
63 private function getFrontendPipeline() {
64 if ($this->frontendPipeline !== null) {
65 return $this->frontendPipeline;
66 }
67
68 if (!class_exists('ABJ_404_Solution_FrontendRequestPipeline')) {
69 require_once dirname(__FILE__) . '/FrontendRequestPipeline.php';
70 }
71
72 $matchingEngines = [];
73 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
74 $engines = ABJ_404_Solution_ServiceContainer::safeGet('matching_engines');
75 if (is_array($engines)) {
76 $matchingEngines = $engines;
77 }
78 }
79
80 $this->frontendPipeline = new ABJ_404_Solution_FrontendRequestPipeline(
81 $this->logic,
82 $this->redirectsRepository,
83 $this->logger,
84 $this->f,
85 $this->spellChecker,
86 $matchingEngines,
87 $this->logsRepository
88 );
89 return $this->frontendPipeline;
90 }
91
92 public function getCapturedCountForNotification(): int {
93 if (!is_object($this->statsRepository) || !method_exists($this->statsRepository, 'getCapturedCountForNotification')) { return 0; } try { return (int)call_user_func(array($this->statsRepository, 'getCapturedCountForNotification')); } catch (Throwable $e) {
94 if (is_object($this->logger) && method_exists($this->logger, 'errorMessage')) { $this->logger->errorMessage('Captured-count notification lookup failed: ' . $e->getMessage(), $e instanceof Exception ? $e : null); } else { error_log('404 Solution: Captured-count notification lookup failed: ' . $e->getMessage()); } return 0; }
95 }
96
97 /** @return ABJ_404_Solution_PluginLogic */
98 public function getPluginLogic() {
99 return $this->logic;
100 }
101
102 /** @return ABJ_404_Solution_Logging */
103 public function getLogger() {
104 return $this->logger;
105 }
106
107 /** @return self */
108 public static function getInstance() {
109 if (self::$instance !== null) {
110 return self::$instance;
111 }
112
113 // If the DI container is initialized, prefer it.
114 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
115 $svc = ABJ_404_Solution_ServiceContainer::safeGet('wordpress_connector');
116 if ($svc instanceof self) {
117 self::$instance = $svc;
118 return self::$instance;
119 }
120 }
121
122 self::$instance = new ABJ_404_Solution_WordPress_Connector();
123
124 return self::$instance;
125 }
126
127 /**
128 * Persist and queue an admin runtime error so users see a notice instead of a blank page.
129 *
130 * @param string $hookName
131 * @param Throwable $e
132 * @return void
133 */
134 public static function reportAdminRuntimeError(string $hookName, Throwable $e): void {
135 $summary = sprintf('[%s] %s', $hookName, $e->getMessage());
136 self::$adminRuntimeErrors[] = $summary;
137
138 try {
139 $logger = abj_service('logging');
140 $logger->errorMessage('Admin runtime exception in ' . $hookName . ': ' . $e->getMessage());
141 } catch (Throwable $ignored) {
142 // Last-resort logging fallback.
143 @error_log('404 Solution admin runtime exception in ' . $hookName . ': ' . $e->getMessage());
144 }
145
146 if (function_exists('set_transient')) {
147 // allow-cache-empty: runtime-error notice summary is generated locally and intentionally persisted as-is.
148 set_transient('abj404_admin_runtime_error', $summary, 300);
149 }
150 }
151
152 /**
153 * Echo one-time admin runtime errors captured from earlier hooks in this request (or previous request).
154 *
155 * @return void
156 */
157 public static function echoAdminRuntimeErrorNotice(): void {
158 $errors = self::$adminRuntimeErrors;
159 self::$adminRuntimeErrors = array();
160
161 if (function_exists('get_transient')) {
162 $saved = get_transient('abj404_admin_runtime_error');
163 if (is_string($saved) && $saved !== '') {
164 $errors[] = $saved;
165 delete_transient('abj404_admin_runtime_error');
166 }
167 }
168
169 if (empty($errors)) {
170 return;
171 }
172
173 $message = implode("\n", array_unique(array_filter($errors)));
174 echo '<div class="notice notice-error"><p><strong>404 Solution:</strong> ';
175 echo esc_html__('An internal error occurred while loading this admin page.', '404-solution');
176 echo '</p><details><summary>' . esc_html__('Show details', '404-solution') . '</summary><pre style="white-space:pre-wrap;word-break:break-all;max-width:100%;margin:6px 0;">';
177 echo esc_html($message);
178 echo '</pre></details></div>';
179 }
180
181 /** Setup.
182 * @return void
183 */
184 static function init() {
185 self::registerLifecycleHooks();
186 self::registerAdminHooks();
187 self::registerAsyncSuggestionHooks();
188 ABJ_404_Solution_PluginLogic::doRegisterCrons();
189 }
190
191 /** @return void */
192 private static function registerLifecycleHooks() {
193 if (!is_admin()) {
194 return;
195 }
196
197 register_deactivation_hook(ABJ404_NAME, 'ABJ_404_Solution_PluginLogic::runOnPluginDeactivation');
198 register_activation_hook(ABJ404_NAME, 'ABJ_404_Solution_PluginLogic::runOnPluginActivation');
199
200 if (is_multisite()) {
201 add_action('wpmu_new_blog', 'ABJ_404_Solution_PluginLogic::activateNewSite', 10, 6);
202 add_action('wp_initialize_site', 'ABJ_404_Solution_PluginLogic::activateNewSiteModern', 10, 2);
203 add_action('delete_blog', 'ABJ_404_Solution_PluginLogic::deleteBlogData', 10, 2);
204 }
205 }
206
207 /** @return void */
208 private static function registerAdminHooks() {
209 if (!is_admin()) {
210 return;
211 }
212
213 add_filter("plugin_action_links_" . ABJ404_NAME,
214 'ABJ_404_Solution_WordPress_Connector::addSettingsLinkToPluginPage');
215 add_filter('plugin_row_meta',
216 'ABJ_404_Solution_WordPress_Connector::addPluginRowMeta', 10, 2);
217 add_action('admin_notices',
218 'ABJ_404_Solution_ReviewFeedback::echoDashboardNotification');
219 add_action('admin_init',
220 'ABJ_404_Solution_ReviewFeedback::handleResponseRedirects');
221 add_action('admin_menu',
222 'ABJ_404_Solution_WordPress_Connector::addMainSettingsPageLink');
223 add_action('admin_enqueue_scripts',
224 'ABJ_404_Solution_WordPress_Connector::add_scripts', 11);
225 add_action('admin_enqueue_scripts',
226 'ABJ_404_Solution_WordPress_Connector::enqueueSupportRequestAssetsOnPluginsPage', 11);
227 add_action('admin_head',
228 'ABJ_404_Solution_AdminThemeManager::outputCriticalThemeCSS', 1);
229
230 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_echoViewLogsFor', 'ABJ_404_Solution_Ajax_Php::echoViewLogsFor');
231 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_trashLink', 'ABJ_404_Solution_Ajax_TrashLink::trashAction');
232 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_echoRedirectToPages', 'ABJ_404_Solution_Ajax_Php::echoRedirectToPages');
233 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_updateOptions', 'ABJ_404_Solution_Ajax_Php::updateOptions');
234 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_load_gsc_section', 'ABJ_404_Solution_Ajax_Php::loadGscSection');
235 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404getTrendData', 'ABJ_404_Solution_Ajax_TrendData::echoTrendData');
236 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_crossPluginPreview', 'ABJ_404_Solution_Ajax_CrossPluginImporter::handlePreview');
237 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_gsc_oauth_callback', 'ABJ_404_Solution_GscOAuthHandler::handleCallback');
238 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_gsc_revoke', 'ABJ_404_Solution_GscOAuthHandler::handleRevoke');
239
240 ABJ_404_Solution_Ajax_EngineProfiles::registerActions();
241 ABJ_404_Solution_Ajax_SettingsModeToggle::init();
242 ABJ_404_Solution_Ajax_RestoreDefaults::init();
243 ABJ_404_Solution_Ajax_SupportRequest::init();
244 ABJ_404_Solution_Ajax_SupportRequestPreview::init();
245 ABJ_404_Solution_UninstallModal::init();
246 ABJ_404_Solution_SetupWizard::init();
247 if (class_exists('ABJ_404_Solution_Privacy')) {
248 ABJ_404_Solution_Privacy::init();
249 }
250 }
251
252 /** @return void */
253 private static function registerAsyncSuggestionHooks() {
254 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_compute_suggestions', 'ABJ_404_Solution_Ajax_SuggestionCompute::computeSuggestions');
255 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_nopriv_abj404_compute_suggestions', 'ABJ_404_Solution_Ajax_SuggestionCompute::computeSuggestions');
256 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_poll_suggestions', 'ABJ_404_Solution_Ajax_SuggestionPolling::pollSuggestions');
257 ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_nopriv_abj404_poll_suggestions', 'ABJ_404_Solution_Ajax_SuggestionPolling::pollSuggestions');
258 }
259
260 /** Include things necessary for ajax.
261 * @param string $hook
262 * @return void
263 */
264 static function add_scripts($hook) {
265 // only load this stuff for this plugin.
266 // thanks to https://pippinsplugins.com/loading-scripts-correctly-in-the-wordpress-admin/
267 if (!array_key_exists('abj404_settingsPageName', $GLOBALS) ||
268 $hook != $GLOBALS['abj404_settingsPageName']) {
269 return;
270 }
271
272 try {
273 $subpage = '';
274 if (array_key_exists('subpage', $_GET)) {
275 $subpage = sanitize_text_field(self::normalizeRequestScalar($_GET['subpage']));
276 }
277 // Default plugin landing is redirects when subpage is not specified.
278 if ($subpage === '') {
279 $subpage = 'abj404_redirects';
280 }
281
282 $isOptionsPage = ($subpage === 'abj404_options');
283 $isStatsPage = ($subpage === 'abj404_stats');
284 $isToolsPage = ($subpage === 'abj404_tools');
285 $isCardAccordionPage = in_array($subpage, array('abj404_options', 'abj404_tools', 'abj404_stats'), true);
286 $isLogsPage = ($subpage === 'abj404_logs');
287 $isListPage = in_array($subpage, array('abj404_redirects', 'abj404_captured', 'abj404_logs'), true);
288 $isEditPage = ($subpage === 'abj404_edit');
289 $needsDestinationAutocomplete = in_array($subpage, array('abj404_redirects', 'abj404_captured', 'abj404_options', 'abj404_edit'), true);
290
291 // remove the "thank you for creating with wordpress" message
292 add_filter('admin_footer_text',
293 'ABJ_404_Solution_WordPress_Connector::remove_admin_footer_text');
294 // remove the version number message
295 add_filter('update_footer',
296 'ABJ_404_Solution_WordPress_Connector::remove_admin_footer_text', 11);
297
298 // jquery is used for the searchable dropdown list of pages for adding a redirect and other things.
299 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery');
300 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-ui-autocomplete');
301 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-effects-core');
302 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-effects-highlight');
303 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-color');
304
305 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-admin-ajax',
306 ABJ404_URL . 'includes/js/abj404-admin-ajax.js', array('jquery'));
307
308 wp_register_script('abj404-redirect_to_ajax', plugin_dir_url(__FILE__) . 'ajax/redirect_to_ajax.js',
309 array('jquery', 'jquery-ui-autocomplete'));
310 wp_register_script('abj404-exclude_pages_ajax', plugin_dir_url(__FILE__) . 'ajax/exclude_pages_ajax.js',
311 array('jquery', 'jquery-ui-autocomplete', 'abj404-redirect_to_ajax'));
312 // Localize the script with new data
313 $translation_array = array(
314 'type_a_page_name' => __('(Type a page name or an external URL)', '404-solution'),
315 'a_page_has_been_selected' => __('(A page has been selected.)', '404-solution'),
316 'an_external_url_will_be_used' => __('(An external URL will be used.)', '404-solution')
317 );
318 wp_localize_script('abj404-redirect_to_ajax', 'abj404localization', $translation_array );
319 if ($needsDestinationAutocomplete) {
320 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-redirect_to_ajax');
321 wp_localize_script('abj404-exclude_pages_ajax', 'abj404localization', $translation_array );
322 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-exclude_pages_ajax');
323 }
324
325 // make sure the "apply" button is only enabled if at least one checkbox is selected
326 wp_register_script('abj404-enable_disable_apply_button_js',
327 ABJ404_URL . 'includes/js/enableDisableApplyButton.js');
328 $translation_array = array('{altText}' => __('Choose at least one URL', '404-solution'));
329 wp_localize_script('abj404-enable_disable_apply_button_js', 'abj404localization', $translation_array);
330 if ($isListPage) {
331 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-enable_disable_apply_button_js');
332 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-trash_link_ajax', plugin_dir_url(__FILE__) . 'ajax/trash_link_ajax.js',
333 array('jquery'));
334 }
335 // tableInteractions.js provides abj404ToggleRegexInfo() used on both list pages
336 // and the Edit Redirect page (subpage=abj404_edit).
337 if ($isListPage || $isEditPage) {
338 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-table-interactions', plugin_dir_url(__FILE__) . 'js/tableInteractions.js',
339 array('jquery'));
340
341 // Localized strings for time-ago display
342 wp_localize_script('abj404-table-interactions', 'abj404_time_ago', array(
343 'second' => __('second', '404-solution'),
344 'seconds' => __('seconds', '404-solution'),
345 'minute' => __('minute', '404-solution'),
346 'minutes' => __('minutes', '404-solution'),
347 'hour' => __('hour', '404-solution'),
348 'hours' => __('hours', '404-solution'),
349 'day' => __('day', '404-solution'),
350 'days' => __('days', '404-solution'),
351 'ago' => __('ago', '404-solution'),
352 ));
353 }
354
355 if ($isListPage || $isStatsPage) {
356 self::enqueueViewUpdaterModules(plugin_dir_url(__FILE__) . 'ajax/');
357 }
358
359 if ($isLogsPage) {
360 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-search_logs_ajax', plugin_dir_url(__FILE__) . 'ajax/search_logs_ajax.js',
361 array('jquery', 'jquery-ui-autocomplete'));
362 }
363
364 if ($isOptionsPage) {
365 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-general-js', plugin_dir_url(__FILE__) . 'js/general.js',
366 array('jquery'));
367
368 // Localize general.js strings for translation
369 wp_localize_script('abj404-general-js', 'abj404General', array(
370 'savingSettings' => __('Saving settings...', '404-solution'),
371 ));
372
373 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-theme-preview', plugin_dir_url(__FILE__) . 'js/themePreview.js',
374 array('jquery'));
375
376 // Settings mode toggle (Simple/Advanced)
377 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-settings-mode-toggle', plugin_dir_url(__FILE__) . 'ajax/SettingsModeToggle.js',
378 array('jquery'));
379
380 // Restore defaults (sticky save bar)
381 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-restore-defaults', plugin_dir_url(__FILE__) . 'ajax/RestoreDefaults.js',
382 array('jquery'));
383
384 // Behavior tiles (404 destination selector)
385 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-behavior-tiles', ABJ404_URL . 'includes/js/behaviorTiles.js',
386 array());
387 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-settings-deferred', ABJ404_URL . 'includes/js/settingsDeferred.js',
388 array('jquery'));
389 }
390
391 if ($isCardAccordionPage) {
392 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-options-accordion', plugin_dir_url(__FILE__) . 'js/optionsAccordion.js',
393 array('jquery'));
394
395 // Localize accordion strings for translation
396 wp_localize_script('abj404-options-accordion', 'abj404Accordion', array(
397 'expandAll' => __('Expand All', '404-solution'),
398 'collapseAll' => __('Collapse All', '404-solution'),
399 ));
400 }
401
402 if ($isOptionsPage) {
403 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-engine-profiles',
404 plugin_dir_url(__FILE__) . 'ajax/ajax-engine-profiles.js',
405 array('jquery'));
406 wp_localize_script('abj404-engine-profiles', 'abj404EngineProfiles', array(
407 'nonce' => wp_create_nonce('abj404_engine_profiles_nonce'),
408 'ajaxUrl' => admin_url('admin-ajax.php'),
409 'i18n' => array(
410 'edit' => __('Edit', '404-solution'),
411 'delete' => __('Delete', '404-solution'),
412 'addProfile' => __('Add Engine Profile', '404-solution'),
413 'editProfile' => __('Edit Engine Profile', '404-solution'),
414 'nameRequired' => __('Profile name is required.', '404-solution'),
415 'patternRequired' => __('URL pattern is required.', '404-solution'),
416 'saved' => __('Profile saved.', '404-solution'),
417 'saveFailed' => __('Failed to save profile.', '404-solution'),
418 'confirmDelete' => __('Delete this engine profile?', '404-solution'),
419 ),
420 ));
421 }
422
423 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt(
424 'abj404-review-feedback',
425 plugin_dir_url(__FILE__) . 'js/reviewFeedback.js',
426 array()
427 );
428
429 self::registerSupportRequestAssets();
430
431 ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-styles', ABJ404_URL . 'includes/html/404solutionStyles.css',
432 array());
433 ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-themes', ABJ404_URL . 'includes/html/adminThemes.css',
434 array());
435
436 // Load RTL styles for Arabic, Hebrew, and other right-to-left languages
437 if (is_rtl()) {
438 ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-rtl', ABJ404_URL . 'includes/html/404solutionStyles-rtl.css',
439 array('abj404solution-styles'));
440 }
441 } catch (Throwable $e) {
442 self::reportAdminRuntimeError('admin_enqueue_scripts', $e);
443 }
444 }
445
446 /**
447 * Enqueue the reusable support-request button assets. Loaded on
448 * every plugin admin page so any screen can drop a
449 * SupportRequestButton::render() mount-point without a per-screen
450 * enqueue checklist that drifts as new mount points are added.
451 *
452 * The inline bootstrap exposes window.ABJ404.ajaxurl plus
453 * window.ABJ404.nonces.{support_request, support_request_preview}
454 * so the JS client and the modal component can both reach the
455 * nonces without wp_localize_script's per-handle binding.
456 *
457 * @return void
458 */
459 /**
460 * Enqueue the support-request button assets specifically for the
461 * wp-admin/plugins.php screen so the row-meta link added by
462 * `addPluginRowMeta()` can open its consent modal in-place. The
463 * plugin's main `add_scripts()` enqueue is gated to the plugin's
464 * settings page and would skip plugins.php otherwise.
465 *
466 * Scope: this hook runs on every admin page but no-ops unless the
467 * current screen is plugins.php, keeping the asset footprint tight.
468 *
469 * @param string $hook the admin page slug WP passes to admin_enqueue_scripts
470 * @return void
471 */
472 static function enqueueSupportRequestAssetsOnPluginsPage($hook) {
473 if ($hook !== 'plugins.php') {
474 return;
475 }
476 try {
477 self::registerSupportRequestAssets();
478 } catch (Throwable $e) {
479 self::reportAdminRuntimeError('admin_enqueue_scripts:plugins.php', $e);
480 }
481 }
482
483 /**
484 * Enqueue the view-updater module bundle (the AJAX-driven admin table
485 * orchestration). Split out of add_scripts() to keep that function under
486 * the ModularityTest body-line cap. Enqueue order matters: every file
487 * below uses globals defined by the modules listed before it; the
488 * bootstrap (view_updater.js) declares the jQuery.ready entry point and
489 * must load LAST so the helpers are defined when ready fires.
490 * WordPress's $deps array enforces this ordering on the emitted
491 * <script> tags. The B20 nonce-refresh helper exposes
492 * abj404AjaxWithNonceRetry which every sibling uses via a soft typeof
493 * reference, so its enqueue must precede them.
494 *
495 * @param string $vuBase URL prefix for the ajax/ assets directory.
496 * @return void
497 */
498 private static function enqueueViewUpdaterModules(string $vuBase): void {
499 $enq = array('ABJ_404_Solution_WPUtils', 'my_wp_enq_scrpt');
500 $enq('abj404-view-updater-nonce-refresh',
501 $vuBase . 'view_updater_nonce_refresh.js', array('jquery'));
502 $enq('abj404-view-updater-stage-diagnostics',
503 $vuBase . 'view_updater_stage_diagnostics.js', array('jquery'));
504 $enq('abj404-view-updater-compare',
505 $vuBase . 'view_updater_compare.js', array('jquery'));
506 $enq('abj404-view-updater-toast',
507 $vuBase . 'view_updater_toast.js', array('jquery'));
508 $enq('abj404-view-updater-stats', $vuBase . 'view_updater_stats.js',
509 array('jquery', 'abj404-view-updater-toast', 'abj404-view-updater-nonce-refresh'));
510 $enq('abj404-view-updater-build-advance', $vuBase . 'view_updater_build_advance.js',
511 array('jquery', 'abj404-view-updater-stage-diagnostics', 'abj404-view-updater-nonce-refresh'));
512 $enq('abj404-view-updater-table-init', $vuBase . 'view_updater_table_init.js',
513 array('jquery', 'abj404-view-updater-toast', 'abj404-view-updater-stats',
514 'abj404-view-updater-nonce-refresh'));
515 $enq('abj404-view-updater-table-warmup', $vuBase . 'view_updater_table_warmup.js',
516 array('jquery', 'abj404-view-updater-stage-diagnostics',
517 'abj404-view-updater-build-advance', 'abj404-view-updater-table-init',
518 'abj404-view-updater-nonce-refresh'));
519 $enq('abj404-view-updater-pagination', $vuBase . 'view_updater_pagination.js',
520 array('jquery', 'abj404-view-updater-compare', 'abj404-view-updater-stage-diagnostics',
521 'abj404-view-updater-build-advance', 'abj404-view-updater-table-init',
522 'abj404-view-updater-table-warmup', 'abj404-view-updater-toast',
523 'abj404-view-updater-nonce-refresh'));
524 $enq('abj404-view-updater', $vuBase . 'view_updater.js',
525 array('jquery', 'jquery-ui-autocomplete',
526 'abj404-view-updater-stage-diagnostics', 'abj404-view-updater-compare',
527 'abj404-view-updater-toast', 'abj404-view-updater-stats',
528 'abj404-view-updater-build-advance', 'abj404-view-updater-table-init',
529 'abj404-view-updater-table-warmup', 'abj404-view-updater-pagination',
530 'abj404-view-updater-nonce-refresh'));
531 }
532
533 private static function registerSupportRequestAssets(): void {
534 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-support-request-client',
535 plugin_dir_url(__FILE__) . 'ajax/SupportRequest.js', array());
536 ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-support-request-button',
537 ABJ404_URL . 'includes/js/support-request-button.js',
538 array('abj404-support-request-client'));
539 if (!function_exists('wp_add_inline_script')) {
540 return;
541 }
542 $supportNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequest::NONCE_ACTION);
543 $previewNonce = wp_create_nonce(ABJ_404_Solution_Ajax_SupportRequestPreview::NONCE_ACTION);
544 $ajaxUrl = function_exists('admin_url') ? admin_url('admin-ajax.php') : '/wp-admin/admin-ajax.php';
545 $payload = wp_json_encode(array(
546 'ajaxurl' => $ajaxUrl,
547 'nonces' => array(
548 'support_request' => $supportNonce,
549 'support_request_preview' => $previewNonce,
550 ),
551 ));
552 $bootstrap = 'window.ABJ404=window.ABJ404||{};Object.assign(window.ABJ404,'
553 . (is_string($payload) ? $payload : '{}') . ');';
554 wp_add_inline_script('abj404-support-request-client', $bootstrap, 'before');
555 }
556
557 /** @deprecated Use ABJ_404_Solution_AdminThemeManager::isDarkModeDetected() */
558 static function isDarkModeDetected(): bool {
559 return ABJ_404_Solution_AdminThemeManager::isDarkModeDetected();
560 }
561
562 /** @deprecated Use ABJ_404_Solution_AdminThemeManager::getAutoSelectedTheme() */
563 static function getAutoSelectedTheme(): string {
564 return ABJ_404_Solution_AdminThemeManager::getAutoSelectedTheme();
565 }
566
567 /**
568 * @param string $content
569 * @return string
570 */
571 static function remove_admin_footer_text($content) {
572 return '';
573 }
574
575 /** Add the "Settings" link to the WordPress plugins page (next to activate/deactivate and edit).
576 * @param array<int|string, string> $links
577 * @return array<int|string, string>
578 */
579 static function addSettingsLinkToPluginPage($links) {
580 $instance = self::getInstance();
581
582 if (!is_array($links)) {
583 $instance->logger->infoMessage("The settings links variable was not an array. " .
584 "Please verify the validity of other plugins. " . print_r($links, true));
585 $links = array();
586 }
587
588 if (!is_admin() || !$instance->logic->userIsPluginAdmin()) {
589 $instance->logger->logUserCapabilities("addSettingsLinkToPluginPage");
590
591 return $links;
592 }
593
594 $settings_link = '<a href="options-general.php?page=' . ABJ404_PP . '&subpage=abj404_options">' .
595 __('Settings', '404-solution') . '</a>';
596 array_unshift($links, $settings_link);
597
598 $debugExplanation = __('Debug Log', '404-solution');
599 $debugLogLink = $instance->logic->getDebugLogFileLink();
600 $debugExplanation = '<a href="options-general.php' . $debugLogLink . '" target="_blank" >'
601 . $debugExplanation . '</a>';
602 array_push($links, $debugExplanation);
603
604 return $links;
605 }
606
607 /**
608 * Adds a "Send debug log to developer" link to the plugin row on
609 * the Plugins page. The link opens the support-request consent
610 * modal in-place on wp-admin/plugins.php (handled by
611 * support-request-button.js, which attaches to elements matching
612 * .abj404-support-request-link). Opening in-place is deliberate:
613 * the Plugins listing is the screen an admin reaches when the
614 * plugin's own Settings page is broken, so the modal must not
615 * depend on Settings rendering correctly.
616 *
617 * The href falls back to the same-page anchor `#abj404-support-request`
618 * so the link is still well-formed if support-request-button.js
619 * fails to load. The modal itself is the only path that transmits
620 * the support-request payload; clicking the link never POSTs.
621 *
622 * @param array<int|string, string> $links
623 * @param string $file
624 * @return array<int|string, string>
625 */
626 static function addPluginRowMeta($links, $file) {
627 if ($file !== ABJ404_NAME) {
628 return $links;
629 }
630 $links[] = '<a href="#abj404-support-request"'
631 . ' class="abj404-support-request-link"'
632 . ' data-triggered-from="plugins_row_action">'
633 . esc_html__('Send debug log to developer', '404-solution') . '</a>';
634 return $links;
635 }
636
637 /** This is called directly by php code inserted into the page by the user.
638 * Code: <?php if (!empty($abj404connector)) {$abj404connector->suggestions(); } ?>
639 * @global type $abj404shortCode
640 */
641 /** @return void */
642 function suggestions() {
643 $abj404shortCode = abj_service('shortcode');
644
645 if (is_404()) {
646 $content = $abj404shortCode->shortcodePageSuggestions(array());
647
648 echo $content;
649 }
650 }
651
652 /** @return void */
653 function processRedirectAllRequests() {
654 $this->getFrontendPipeline()->processRedirectAllRequests();
655 }
656 /**
657 * Process the 404s
658 */
659 /** @return void */
660 function process404() {
661 $this->getFrontendPipeline()->process404();
662 }
663
664 /**
665 * @param array<string, mixed> $options
666 * @param string $requestedURL
667 * @return bool true if the user is sent to the default 404 page.
668 */
669 function tryRegexRedirect($options, $requestedURL) {
670 return $this->getFrontendPipeline()->tryRegexRedirect($options, $requestedURL);
671 }
672
673 /**
674 * @param array<string, mixed> $options
675 * @param string $requestedURL
676 * @param array<string, mixed> $redirect
677 * @return void
678 */
679 function logAReallyLongDebugMessage($options, $requestedURL, $redirect) {
680 $this->getFrontendPipeline()->logAReallyLongDebugMessage($options, $requestedURL, $redirect);
681 }
682
683 /** Redirect to the page specified.
684 * @param string $requestedURL
685 * @param array<string, mixed> $redirect
686 * @param string $matchReason
687 * @return bool true if the user is sent to the default 404 page.
688 */
689 function processRedirect($requestedURL, $redirect, $matchReason) {
690 return $this->getFrontendPipeline()->processRedirect($requestedURL, $redirect, $matchReason);
691 }
692
693 /** @deprecated Use ABJ_404_Solution_ReviewFeedback::echoDashboardNotification() */
694 static function echoDashboardNotification(): void {
695 ABJ_404_Solution_ReviewFeedback::echoDashboardNotification();
696 }
697
698 /** @deprecated Use ABJ_404_Solution_ReviewFeedback::handleResponseRedirects() */
699 static function handleReviewResponseRedirects(): void {
700 ABJ_404_Solution_ReviewFeedback::handleResponseRedirects();
701 }
702
703 /**
704 * Safely unslash request data when wp_unslash exists and is callable.
705 * Some test environments report wp_unslash as existing but throw when called.
706 *
707 * @param mixed $value
708 * @return mixed
709 */
710 public static function safeWpUnslash($value) {
711 if (!function_exists('wp_unslash')) {
712 return $value;
713 }
714
715 try {
716 return wp_unslash($value);
717 } catch (Throwable $e) { // allow-silent-catch: wp_unslash() failure; pass-through preserves the original value which is always usable
718 return $value;
719 }
720 }
721
722 /**
723 * Normalize request input to a scalar string to avoid warnings when arrays/objects are passed.
724 *
725 * @param mixed $value
726 * @return string
727 */
728 public static function normalizeRequestScalar($value) {
729 $value = self::safeWpUnslash($value);
730 if (!is_scalar($value)) {
731 return '';
732 }
733 return (string)$value;
734 }
735
736 /**
737 * Normalize and sanitize feedback issue selections from request data.
738 *
739 * @param mixed $issuesRaw
740 * @return array<int, string>
741 */
742 public static function sanitizeFeedbackIssues($issuesRaw) {
743 $issuesRaw = self::safeWpUnslash($issuesRaw);
744 if (!is_array($issuesRaw)) {
745 $issuesRaw = array($issuesRaw);
746 }
747
748 $issues = array();
749 foreach ($issuesRaw as $issue) {
750 if (is_array($issue) || is_object($issue)) {
751 continue;
752 }
753 $clean = sanitize_text_field((string)$issue);
754 if ($clean !== '') {
755 $issues[] = $clean;
756 }
757 }
758 return $issues;
759 }
760
761 /** Adds a link under the "Settings" link to the plugin page.
762 * @global string $menu
763 * @global type $abj404dao
764 * @global type $abj404logic
765 * @global type $abj404logging
766 */
767 /** @return void */
768 static function addMainSettingsPageLink() {
769 global $menu;
770
771 // The menu must ALWAYS be registered so the admin page is accessible.
772 // Wrap all pre-registration logic in try/catch — if anything fails
773 // (missing tables, broken service container, etc.), fall through to
774 // register the menu with safe defaults.
775 $pageName = "404 Solution";
776 $menuLocation = '';
777
778 try {
779 $instance = self::getInstance();
780
781 if (!is_admin() || !$instance->logic->userIsPluginAdmin()) {
782 $instance->logger->logUserCapabilities("addMainSettingsPageLink");
783 return;
784 }
785
786 // Use skip_db_check=true so menu registration never triggers
787 // updateToNewVersion() — that can hang on slow database upgrades
788 // and block the entire admin page from rendering.
789 $options = $instance->logic->getOptions(true);
790 $menuLocation = isset($options['menuLocation']) ? $options['menuLocation'] : '';
791
792 // Admin notice badge
793 if (isset($options['admin_notification']) && $options['admin_notification'] != '0') {
794 $captured = $instance->getCapturedCountForNotification();
795 if ($captured >= $options['admin_notification']) {
796 $pageName .= " <span class='update-plugins count-1'><span class='update-count'>" . esc_html((string)$captured) . "</span></span>";
797 if (isset($menu[80][0])) {
798 $pos = $instance->f->strpos($menu[80][0], 'update-plugins');
799 if ($pos === false) {
800 $menu[80][0] = $menu[80][0] . " <span class='update-plugins count-1'><span class='update-count'>1</span></span>";
801 }
802 }
803 }
804 }
805 } catch (\Throwable $e) {
806 // Something failed before menu registration. Continue with defaults
807 // so the admin page is still accessible for debugging. Surface the
808 // failure to PHP's error log so it isn't completely invisible.
809 error_log('404 Solution: addMainSettingsPageLink pre-registration failed: ' . $e->getMessage());
810 }
811
812 if ($menuLocation === 'settingsLevel') {
813 // this adds the settings link at the same level as the "Tools" and "Settings" menu items.
814 $GLOBALS['abj404_settingsPageName'] = add_menu_page(PLUGIN_NAME, PLUGIN_NAME, 'manage_options', 'abj404_solution',
815 'abj404_admin_page_callback');
816
817 } else {
818 // this adds the settings link at Settings->404 Solution.
819 $GLOBALS['abj404_settingsPageName'] = add_submenu_page('options-general.php', PLUGIN_NAME, $pageName, 'manage_options', ABJ404_PP,
820 'abj404_admin_page_callback');
821 }
822 }
823
824 /** @deprecated Use ABJ_404_Solution_GscOAuthHandler::handleCallback() */
825 public static function handleGscOauthCallback(): void {
826 ABJ_404_Solution_GscOAuthHandler::handleCallback();
827 }
828
829 /** @deprecated Use ABJ_404_Solution_GscOAuthHandler::handleRevoke() */
830 public static function handleGscRevoke(): void {
831 ABJ_404_Solution_GscOAuthHandler::handleRevoke();
832 }
833
834 }
835