*/ private static $adminRuntimeErrors = array(); /** @var ABJ_404_Solution_PluginLogic */ private $logic; /** @var ABJ_404_Solution_DataAccess */ private $dao; /** @var ABJ_404_Solution_Logging */ private $logger; /** @var ABJ_404_Solution_Functions */ private $f; /** @var ABJ_404_Solution_SpellChecker */ private $spellChecker; /** @var ABJ_404_Solution_FrontendRequestPipeline|null */ private $frontendPipeline = null; /** * Constructor with dependency injection. * * @param ABJ_404_Solution_PluginLogic|null $pluginLogic Business logic service * @param ABJ_404_Solution_DataAccess|null $dataAccess Data access layer * @param ABJ_404_Solution_Logging|null $logging Logging service * @param ABJ_404_Solution_Functions|null $functions String utilities * @param ABJ_404_Solution_SpellChecker|null $spellChecker Spell checker service */ public function __construct($pluginLogic = null, $dataAccess = null, $logging = null, $functions = null, $spellChecker = null) { // Use injected dependencies or fall back to getInstance() for backward compatibility $this->logic = $pluginLogic !== null ? $pluginLogic : abj_service('plugin_logic'); $this->dao = $dataAccess !== null ? $dataAccess : abj_service('data_access'); $this->logger = $logging !== null ? $logging : abj_service('logging'); $this->f = $functions !== null ? $functions : abj_service('functions'); $this->spellChecker = $spellChecker !== null ? $spellChecker : abj_service('spell_checker'); } /** @return ABJ_404_Solution_FrontendRequestPipeline */ private function getFrontendPipeline() { if ($this->frontendPipeline !== null) { return $this->frontendPipeline; } if (!class_exists('ABJ_404_Solution_FrontendRequestPipeline')) { require_once dirname(__FILE__) . '/FrontendRequestPipeline.php'; } $matchingEngines = []; if (class_exists('ABJ_404_Solution_ServiceContainer')) { $engines = ABJ_404_Solution_ServiceContainer::safeGet('matching_engines'); if (is_array($engines)) { $matchingEngines = $engines; } } $this->frontendPipeline = new ABJ_404_Solution_FrontendRequestPipeline( $this->logic, $this->dao, $this->logger, $this->f, $this->spellChecker, $matchingEngines ); return $this->frontendPipeline; } /** @return self */ public static function getInstance() { if (self::$instance !== null) { return self::$instance; } // If the DI container is initialized, prefer it. if (class_exists('ABJ_404_Solution_ServiceContainer')) { $svc = ABJ_404_Solution_ServiceContainer::safeGet('wordpress_connector'); if ($svc instanceof self) { self::$instance = $svc; return self::$instance; } } self::$instance = new ABJ_404_Solution_WordPress_Connector(); return self::$instance; } /** * Persist and queue an admin runtime error so users see a notice instead of a blank page. * * @param string $hookName * @param Throwable $e * @return void */ private static function reportAdminRuntimeError(string $hookName, Throwable $e): void { $summary = sprintf('[%s] %s', $hookName, $e->getMessage()); self::$adminRuntimeErrors[] = $summary; try { $logger = abj_service('logging'); $logger->errorMessage('Admin runtime exception in ' . $hookName . ': ' . $e->getMessage()); } catch (Throwable $ignored) { // Last-resort logging fallback. @error_log('404 Solution admin runtime exception in ' . $hookName . ': ' . $e->getMessage()); } if (function_exists('set_transient')) { set_transient('abj404_admin_runtime_error', $summary, 300); } } /** * Echo one-time admin runtime errors captured from earlier hooks in this request (or previous request). * * @return void */ private static function echoAdminRuntimeErrorNotice(): void { $errors = self::$adminRuntimeErrors; self::$adminRuntimeErrors = array(); if (function_exists('get_transient')) { $saved = get_transient('abj404_admin_runtime_error'); if (is_string($saved) && $saved !== '') { $errors[] = $saved; delete_transient('abj404_admin_runtime_error'); } } if (empty($errors)) { return; } $message = implode("\n", array_unique(array_filter($errors))); echo '

404 Solution: '; echo esc_html__('An internal error occurred while loading this admin page.', '404-solution'); echo '

' . esc_html__('Show details', '404-solution') . '
';
        echo esc_html($message);
        echo '
'; } /** Setup. * @return void */ static function init() { self::registerLifecycleHooks(); self::registerAdminHooks(); self::registerAsyncSuggestionHooks(); ABJ_404_Solution_PluginLogic::doRegisterCrons(); } /** @return void */ private static function registerLifecycleHooks() { if (!is_admin()) { return; } register_deactivation_hook(ABJ404_NAME, 'ABJ_404_Solution_PluginLogic::runOnPluginDeactivation'); register_activation_hook(ABJ404_NAME, 'ABJ_404_Solution_PluginLogic::runOnPluginActivation'); if (is_multisite()) { add_action('wpmu_new_blog', 'ABJ_404_Solution_PluginLogic::activateNewSite', 10, 6); add_action('wp_initialize_site', 'ABJ_404_Solution_PluginLogic::activateNewSiteModern', 10, 2); add_action('delete_blog', 'ABJ_404_Solution_PluginLogic::deleteBlogData', 10, 2); } } /** @return void */ private static function registerAdminHooks() { if (!is_admin()) { return; } add_filter("plugin_action_links_" . ABJ404_NAME, 'ABJ_404_Solution_WordPress_Connector::addSettingsLinkToPluginPage'); add_filter('plugin_row_meta', 'ABJ_404_Solution_WordPress_Connector::addPluginRowMeta', 10, 2); add_action('admin_notices', 'ABJ_404_Solution_WordPress_Connector::echoDashboardNotification'); add_action('admin_init', 'ABJ_404_Solution_WordPress_Connector::handleReviewResponseRedirects'); add_action('admin_menu', 'ABJ_404_Solution_WordPress_Connector::addMainSettingsPageLink'); add_action('admin_enqueue_scripts', 'ABJ_404_Solution_WordPress_Connector::add_scripts', 11); add_action('admin_enqueue_scripts', 'ABJ_404_Solution_WordPress_Connector::enqueueSupportRequestAssetsOnPluginsPage', 11); add_action('admin_head', 'ABJ_404_Solution_WordPress_Connector::outputCriticalThemeCSS', 1); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_echoViewLogsFor', 'ABJ_404_Solution_Ajax_Php::echoViewLogsFor'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_trashLink', 'ABJ_404_Solution_Ajax_TrashLink::trashAction'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_echoRedirectToPages', 'ABJ_404_Solution_Ajax_Php::echoRedirectToPages'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_updateOptions', 'ABJ_404_Solution_Ajax_Php::updateOptions'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_load_gsc_section', 'ABJ_404_Solution_Ajax_Php::loadGscSection'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404getTrendData', 'ABJ_404_Solution_Ajax_TrendData::echoTrendData'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_crossPluginPreview', 'ABJ_404_Solution_Ajax_CrossPluginImporter::handlePreview'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_gsc_oauth_callback', 'ABJ_404_Solution_WordPress_Connector::handleGscOauthCallback'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_gsc_revoke', 'ABJ_404_Solution_WordPress_Connector::handleGscRevoke'); ABJ_404_Solution_Ajax_EngineProfiles::registerActions(); ABJ_404_Solution_Ajax_SettingsModeToggle::init(); ABJ_404_Solution_Ajax_RestoreDefaults::init(); ABJ_404_Solution_Ajax_SupportRequest::init(); ABJ_404_Solution_Ajax_SupportRequestPreview::init(); ABJ_404_Solution_UninstallModal::init(); ABJ_404_Solution_SetupWizard::init(); if (class_exists('ABJ_404_Solution_Privacy')) { ABJ_404_Solution_Privacy::init(); } } /** @return void */ private static function registerAsyncSuggestionHooks() { ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_compute_suggestions', 'ABJ_404_Solution_Ajax_SuggestionCompute::computeSuggestions'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_nopriv_abj404_compute_suggestions', 'ABJ_404_Solution_Ajax_SuggestionCompute::computeSuggestions'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_abj404_poll_suggestions', 'ABJ_404_Solution_Ajax_SuggestionPolling::pollSuggestions'); ABJ_404_Solution_WPUtils::safeAddAction('wp_ajax_nopriv_abj404_poll_suggestions', 'ABJ_404_Solution_Ajax_SuggestionPolling::pollSuggestions'); } /** Include things necessary for ajax. * @param string $hook * @return void */ static function add_scripts($hook) { // only load this stuff for this plugin. // thanks to https://pippinsplugins.com/loading-scripts-correctly-in-the-wordpress-admin/ if (!array_key_exists('abj404_settingsPageName', $GLOBALS) || $hook != $GLOBALS['abj404_settingsPageName']) { return; } try { $subpage = ''; if (array_key_exists('subpage', $_GET)) { $subpage = sanitize_text_field(self::normalizeRequestScalar($_GET['subpage'])); } // Default plugin landing is redirects when subpage is not specified. if ($subpage === '') { $subpage = 'abj404_redirects'; } $isOptionsPage = ($subpage === 'abj404_options'); $isStatsPage = ($subpage === 'abj404_stats'); $isToolsPage = ($subpage === 'abj404_tools'); $isCardAccordionPage = in_array($subpage, array('abj404_options', 'abj404_tools', 'abj404_stats'), true); $isLogsPage = ($subpage === 'abj404_logs'); $isListPage = in_array($subpage, array('abj404_redirects', 'abj404_captured', 'abj404_logs'), true); $isEditPage = ($subpage === 'abj404_edit'); $needsDestinationAutocomplete = in_array($subpage, array('abj404_redirects', 'abj404_captured', 'abj404_options', 'abj404_edit'), true); // remove the "thank you for creating with wordpress" message add_filter('admin_footer_text', 'ABJ_404_Solution_WordPress_Connector::remove_admin_footer_text'); // remove the version number message add_filter('update_footer', 'ABJ_404_Solution_WordPress_Connector::remove_admin_footer_text', 11); // jquery is used for the searchable dropdown list of pages for adding a redirect and other things. ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery'); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-ui-autocomplete'); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-effects-core'); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-effects-highlight'); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('jquery-color'); wp_register_script('abj404-redirect_to_ajax', plugin_dir_url(__FILE__) . 'ajax/redirect_to_ajax.js', array('jquery', 'jquery-ui-autocomplete')); wp_register_script('abj404-exclude_pages_ajax', plugin_dir_url(__FILE__) . 'ajax/exclude_pages_ajax.js', array('jquery', 'jquery-ui-autocomplete', 'abj404-redirect_to_ajax')); // Localize the script with new data $translation_array = array( 'type_a_page_name' => __('(Type a page name or an external URL)', '404-solution'), 'a_page_has_been_selected' => __('(A page has been selected.)', '404-solution'), 'an_external_url_will_be_used' => __('(An external URL will be used.)', '404-solution') ); wp_localize_script('abj404-redirect_to_ajax', 'abj404localization', $translation_array ); if ($needsDestinationAutocomplete) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-redirect_to_ajax'); wp_localize_script('abj404-exclude_pages_ajax', 'abj404localization', $translation_array ); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-exclude_pages_ajax'); } // make sure the "apply" button is only enabled if at least one checkbox is selected wp_register_script('abj404-enable_disable_apply_button_js', ABJ404_URL . 'includes/js/enableDisableApplyButton.js'); $translation_array = array('{altText}' => __('Choose at least one URL', '404-solution')); wp_localize_script('abj404-enable_disable_apply_button_js', 'abj404localization', $translation_array); if ($isListPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-enable_disable_apply_button_js'); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-trash_link_ajax', plugin_dir_url(__FILE__) . 'ajax/trash_link_ajax.js', array('jquery')); } // tableInteractions.js provides abj404ToggleRegexInfo() used on both list pages // and the Edit Redirect page (subpage=abj404_edit). if ($isListPage || $isEditPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-table-interactions', plugin_dir_url(__FILE__) . 'js/tableInteractions.js', array('jquery')); // Localized strings for time-ago display wp_localize_script('abj404-table-interactions', 'abj404_time_ago', array( 'second' => __('second', '404-solution'), 'seconds' => __('seconds', '404-solution'), 'minute' => __('minute', '404-solution'), 'minutes' => __('minutes', '404-solution'), 'hour' => __('hour', '404-solution'), 'hours' => __('hours', '404-solution'), 'day' => __('day', '404-solution'), 'days' => __('days', '404-solution'), 'ago' => __('ago', '404-solution'), )); } if ($isListPage || $isStatsPage) { self::enqueueViewUpdaterModules(plugin_dir_url(__FILE__) . 'ajax/'); } if ($isLogsPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-search_logs_ajax', plugin_dir_url(__FILE__) . 'ajax/search_logs_ajax.js', array('jquery', 'jquery-ui-autocomplete')); } if ($isOptionsPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-general-js', plugin_dir_url(__FILE__) . 'js/general.js', array('jquery')); // Localize general.js strings for translation wp_localize_script('abj404-general-js', 'abj404General', array( 'savingSettings' => __('Saving settings...', '404-solution'), )); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-theme-preview', plugin_dir_url(__FILE__) . 'js/themePreview.js', array('jquery')); // Settings mode toggle (Simple/Advanced) ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-settings-mode-toggle', plugin_dir_url(__FILE__) . 'ajax/SettingsModeToggle.js', array('jquery')); // Restore defaults (sticky save bar) ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-restore-defaults', plugin_dir_url(__FILE__) . 'ajax/RestoreDefaults.js', array('jquery')); // Behavior tiles (404 destination selector) ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-behavior-tiles', ABJ404_URL . 'includes/js/behaviorTiles.js', array()); ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-settings-deferred', ABJ404_URL . 'includes/js/settingsDeferred.js', array('jquery')); } if ($isCardAccordionPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-options-accordion', plugin_dir_url(__FILE__) . 'js/optionsAccordion.js', array('jquery')); // Localize accordion strings for translation wp_localize_script('abj404-options-accordion', 'abj404Accordion', array( 'expandAll' => __('Expand All', '404-solution'), 'collapseAll' => __('Collapse All', '404-solution'), )); } if ($isOptionsPage) { ABJ_404_Solution_WPUtils::my_wp_enq_scrpt('abj404-engine-profiles', plugin_dir_url(__FILE__) . 'ajax/ajax-engine-profiles.js', array('jquery')); wp_localize_script('abj404-engine-profiles', 'abj404EngineProfiles', array( 'nonce' => wp_create_nonce('abj404_engine_profiles_nonce'), 'ajaxUrl' => admin_url('admin-ajax.php'), 'i18n' => array( 'edit' => __('Edit', '404-solution'), 'delete' => __('Delete', '404-solution'), 'addProfile' => __('Add Engine Profile', '404-solution'), 'editProfile' => __('Edit Engine Profile', '404-solution'), 'nameRequired' => __('Profile name is required.', '404-solution'), 'patternRequired' => __('URL pattern is required.', '404-solution'), 'saved' => __('Profile saved.', '404-solution'), 'saveFailed' => __('Failed to save profile.', '404-solution'), 'confirmDelete' => __('Delete this engine profile?', '404-solution'), ), )); } ABJ_404_Solution_WPUtils::my_wp_enq_scrpt( 'abj404-review-feedback', plugin_dir_url(__FILE__) . 'js/reviewFeedback.js', array() ); self::registerSupportRequestAssets(); ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-styles', ABJ404_URL . 'includes/html/404solutionStyles.css', array()); ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-themes', ABJ404_URL . 'includes/html/adminThemes.css', array()); // Load RTL styles for Arabic, Hebrew, and other right-to-left languages if (is_rtl()) { ABJ_404_Solution_WPUtils::my_wp_enq_style('abj404solution-rtl', ABJ404_URL . 'includes/html/404solutionStyles-rtl.css', array('abj404solution-styles')); } } catch (Throwable $e) { self::reportAdminRuntimeError('admin_enqueue_scripts', $e); } } /** * Enqueue the reusable support-request button assets. Loaded on * every plugin admin page so any screen can drop a * SupportRequestButton::render() mount-point without a per-screen * enqueue checklist that drifts as new mount points are added. * * The inline bootstrap exposes window.ABJ404.ajaxurl plus * window.ABJ404.nonces.{support_request, support_request_preview} * so the JS client and the modal component can both reach the * nonces without wp_localize_script's per-handle binding. * * @return void */ /** * Enqueue the support-request button assets specifically for the * wp-admin/plugins.php screen so the row-meta link added by * `addPluginRowMeta()` can open its consent modal in-place. The * plugin's main `add_scripts()` enqueue is gated to the plugin's * settings page and would skip plugins.php otherwise. * * Scope: this hook runs on every admin page but no-ops unless the * current screen is plugins.php, keeping the asset footprint tight. * * @param string $hook the admin page slug WP passes to admin_enqueue_scripts * @return void */ static function enqueueSupportRequestAssetsOnPluginsPage($hook) { if ($hook !== 'plugins.php') { return; } try { self::registerSupportRequestAssets(); } catch (Throwable $e) { self::reportAdminRuntimeError('admin_enqueue_scripts:plugins.php', $e); } } /** * Enqueue the view-updater module bundle (the AJAX-driven admin table * orchestration). Split out of add_scripts() to keep that function under * the ModularityTest body-line cap. Enqueue order matters: every file * below uses globals defined by the modules listed before it; the * bootstrap (view_updater.js) declares the jQuery.ready entry point and * must load LAST so the helpers are defined when ready fires. * WordPress's $deps array enforces this ordering on the emitted *