. * * @package AllAccessible * @author AllAccessible Team * @copyright 2024 AllAccessible * @license GPL-2.0+ */ if (!defined('ABSPATH')) { die('You are not allowed to call this page directly.'); } // Duplicate-copy guard: if another copy of this plugin already loaded (e.g. a // second plugin folder from a mis-named zip upload, or an old copy left behind // during an update), bail out silently instead of fataling the whole site with // "Cannot redeclare aacb_require_if_exists()" / duplicate class errors. // NOTE: check ONLY runtime state (a constant defined by a require at runtime). // Do NOT test function_exists() here — PHP hoists top-level function // declarations at compile time, so every function in this file already exists // before line 1 runs, and the guard would abort the plugin's own first load. if (defined('AACB_VERSION')) { if (function_exists('error_log')) { error_log('[AllAccessible] Duplicate plugin copy detected at ' . __FILE__ . ' — skipped. Remove the extra copy in wp-content/plugins/.'); } return; } // Core Components require_once plugin_dir_path(__FILE__) . 'inc/constants.php'; require_once plugin_dir_path(__FILE__) . 'inc/Debug.php'; require_once plugin_dir_path(__FILE__) . 'inc/SentryClient.php'; require_once plugin_dir_path(__FILE__) . 'inc/SentryBrowser.php'; AllAccessible_Sentry::init(); AllAccessible_SentryBrowser::register(); /** * Guarded require: load a plugin file only if it exists. * * A partial/failed plugin update (some files copied, others not) previously * fataled the ENTIRE site, because an unguarded `require_once` on a missing * file is a fatal error during plugin load — white-screening wp-admin and the * front end (Sentry WORDPRESS-PLUGIN-R/S: missing inc/PostLinkBackfill.php). * * Now a missing non-core file is reported to Sentry once and skipped, so the * rest of the plugin still loads. Sentry is already initialised above, so * reporting is safe here. The four bootstrap files (constants, Debug, * SentryClient, SentryBrowser) above intentionally stay unguarded — without * them nothing, including this reporter, can function. * * @return bool true if the file was loaded. */ function aacb_require_if_exists($relative_path) { $full = plugin_dir_path(__FILE__) . $relative_path; if (file_exists($full)) { require_once $full; return true; } if (class_exists('AllAccessible_Sentry')) { AllAccessible_Sentry::capture_message( 'Plugin file missing (likely partial/failed update): ' . $relative_path, 'error', array('version' => defined('AACB_VERSION') ? AACB_VERSION : 'unknown') ); } error_log('[AllAccessible] Missing plugin file, skipped: ' . $relative_path); return false; } aacb_require_if_exists('inc/VersionManager.php'); aacb_require_if_exists('inc/UrlCanonicalizer.php'); // Widget & Frontend aacb_require_if_exists('inc/ContextGuard.php'); aacb_require_if_exists('inc/WidgetLoader.php'); // Admin Interface aacb_require_if_exists('inc/OnboardingWizard.php'); aacb_require_if_exists('inc/SettingsPage.php'); aacb_require_if_exists('inc/WidgetLanguages.php'); aacb_require_if_exists('inc/WidgetLanguageCard.php'); aacb_require_if_exists('inc/WidgetCustomizer.php'); aacb_require_if_exists('inc/UsageDashboard.php'); aacb_require_if_exists('inc/ConversionCTA.php'); aacb_require_if_exists('inc/FeatureComparison.php'); aacb_require_if_exists('inc/DashboardBanner.php'); aacb_require_if_exists('inc/DeactivationSurvey.php'); aacb_require_if_exists('inc/DashboardLayout.php'); // API Integration (Premium Features) aacb_require_if_exists('inc/api/SummaryCache.php'); aacb_require_if_exists('inc/api/ApiClient.php'); aacb_require_if_exists('inc/api/CredentialChallenge.php'); aacb_require_if_exists('inc/TierGate.php'); aacb_require_if_exists('inc/ContextInjector.php'); aacb_require_if_exists('inc/AgenticFixes/Labels.php'); aacb_require_if_exists('inc/AgenticFixesDashboardWidget.php'); aacb_require_if_exists('inc/AgenticFixesPage.php'); aacb_require_if_exists('inc/ImageManagerPage.php'); aacb_require_if_exists('inc/SitemapDetector.php'); aacb_require_if_exists('inc/ScanTriggerPanel.php'); aacb_require_if_exists('inc/ConnectionStatusCard.php'); aacb_require_if_exists('inc/EditorMetaBox.php'); aacb_require_if_exists('inc/PostListColumn.php'); aacb_require_if_exists('inc/PostLinkBackfill.php'); aacb_require_if_exists('inc/AdminBar.php'); aacb_require_if_exists('inc/ReviewNudge.php'); aacb_require_if_exists('inc/StatementPage.php'); aacb_require_if_exists('inc/GettingStarted.php'); aacb_require_if_exists('inc/StatementNudge.php'); aacb_require_if_exists('inc/WhatsNew.php'); // Guard registrations: a skipped file above means its class is absent, so // class_exists prevents a fatal here too. if (class_exists('AllAccessible_PostListColumn')) { AllAccessible_PostListColumn::register(); } if (class_exists('AllAccessible_CredentialChallenge')) { AllAccessible_CredentialChallenge::register(); } if (class_exists('AllAccessible_PostLinkBackfill')) { AllAccessible_PostLinkBackfill::register(); } if (class_exists('AllAccessible_AdminBar')) { AllAccessible_AdminBar::register(); } if (class_exists('AllAccessible_ReviewNudge')) { AllAccessible_ReviewNudge::register(); } if (class_exists('AllAccessible_StatementPage')) { AllAccessible_StatementPage::register(); } if (class_exists('AllAccessible_GettingStarted')) { AllAccessible_GettingStarted::register(); } if (class_exists('AllAccessible_StatementNudge')) { AllAccessible_StatementNudge::register(); } if (class_exists('AllAccessible_WhatsNew')) { AllAccessible_WhatsNew::register(); } /** * Load translations */ function aacb_load_textdomain() { load_plugin_textdomain('allaccessible', false, basename(dirname(__FILE__)) . '/languages/'); } add_action('init', 'aacb_load_textdomain'); /** * Plugin activation */ function AllAccessible_Activation() { $options = get_option('aacb_options'); if (!is_array($options) || !isset($options['aacb_installed']) || $options['aacb_installed'] != 1) { $opt = array('aacb_installed' => 1); update_option('aacb_options', $opt); } if (class_exists('AllAccessible_PostLinkBackfill')) { AllAccessible_PostLinkBackfill::on_activate(); } if (!get_option('aacb_accountID')) { set_transient('aacb_activation_redirect', 1, 60); } } register_activation_hook(__FILE__, 'AllAccessible_Activation'); /** * One-shot redirect to the onboarding wizard right after activation. */ function aacb_maybe_redirect_after_activation() { if (!get_transient('aacb_activation_redirect')) return; delete_transient('aacb_activation_redirect'); if (wp_doing_ajax() || !is_admin()) return; if (isset($_GET['activate-multi'])) return; if (!current_user_can('manage_options')) return; wp_safe_redirect(admin_url('admin.php?page=allaccessible-wizard')); exit; } add_action('admin_init', 'aacb_maybe_redirect_after_activation'); /** * Plugin deactivation */ function AllAccessible_Deactivation() { // Clean up scheduled events (wp_unschedule_hook also clears single // events scheduled with args, which wp_clear_scheduled_hook misses). wp_unschedule_hook('aacb_post_link_backfill_run'); wp_unschedule_hook('aacb_post_link_single'); wp_unschedule_hook('aacb_fetch_plugin_secret_event'); wp_unschedule_hook('aacb_daily_analytics_calculation'); // pre-2.1 legacy } register_deactivation_hook(__FILE__, 'AllAccessible_Deactivation'); /** * AJAX handler for saving account ID * Used by wizard and legacy settings page */ function AllAccessible_save_settings() { // Verify capabilities if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); return; } // Verify nonce (support both old and new nonce names) $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field($_POST['_wpnonce']) : ''; if (empty($nonce) || !wp_verify_nonce($nonce, 'allaccessible_save_settings')) { wp_send_json_error('Invalid security token'); return; } // Save account ID if provided if (isset($_POST['aacb_accountID'])) { $account_id = sanitize_text_field($_POST['aacb_accountID']); update_option('aacb_accountID', $account_id); wp_send_json_success(array('message' => __('Account settings saved successfully', 'allaccessible'))); } wp_send_json_error('No data to save'); } add_action('wp_ajax_AllAccessible_save_settings', 'AllAccessible_save_settings'); /** * AJAX handler to clear API cache */ function aacb_clear_cache_ajax() { check_ajax_referer('aacb_clear_cache', '_wpnonce'); if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } $api_client = AllAccessible_ApiClient::get_instance(); $api_client->clear_cache(); wp_send_json_success(); } add_action('wp_ajax_aacb_clear_cache', 'aacb_clear_cache_ajax'); /** * AJAX handler to reset all plugin data * Allows users to start fresh without deleting the plugin * * @since 2.0.3 */ function aacb_reset_plugin_data() { check_ajax_referer('aacb_reset_plugin', '_wpnonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized access', 'allaccessible')); } // Delete ALL plugin options — including the HMAC plugin secret and // tier/scan state. A reset that keeps the old secret leaves a // half-zombie identity: the next account connect signs with stale // credentials. Wildcard sweep mirrors uninstall.php. global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'aacb\\_%' OR option_name LIKE '\\_transient\\_aacb\\_%' OR option_name LIKE '\\_transient\\_timeout\\_aacb\\_%'" ); wp_cache_flush(); AllAccessible_ApiClient::get_instance()->flush_all_caches(); // Clear scheduled events tied to the old identity wp_unschedule_hook('aacb_post_link_backfill_run'); wp_unschedule_hook('aacb_post_link_single'); wp_unschedule_hook('aacb_fetch_plugin_secret_event'); // Re-initialize with default options $opt = array('aacb_installed' => 1); update_option('aacb_options', $opt); wp_send_json_success(array( 'message' => __('Plugin data has been reset successfully', 'allaccessible') )); } add_action('wp_ajax_aacb_reset_plugin_data', 'aacb_reset_plugin_data'); /* ===================================================================== * Agentic Fixes — AJAX handlers * ===================================================================== */ const AACB_MANIFEST_NONCE = 'aacb_manifest_action'; function aacb_assert_manifest_caller() { check_ajax_referer(AACB_MANIFEST_NONCE, '_wpnonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized', 'allaccessible'), 403); } } function aacb_approve_manifest_ajax() { aacb_assert_manifest_caller(); $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0; $result = AllAccessible_ApiClient::get_instance()->approve_manifest($manifest_id); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message(), 400); } if (class_exists('AllAccessible_ReviewNudge')) { AllAccessible_ReviewNudge::mark_moment('first_fix_approved'); } wp_send_json_success($result); } add_action('wp_ajax_aacb_approve_manifest', 'aacb_approve_manifest_ajax'); function aacb_revert_manifest_ajax() { aacb_assert_manifest_caller(); $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0; $reason = isset($_POST['reason']) ? sanitize_text_field(wp_unslash($_POST['reason'])) : ''; $result = AllAccessible_ApiClient::get_instance()->revert_manifest($manifest_id, $reason); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message(), 400); } wp_send_json_success($result); } add_action('wp_ajax_aacb_revert_manifest', 'aacb_revert_manifest_ajax'); function aacb_edit_fix_ajax() { aacb_assert_manifest_caller(); $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0; $fix_index = isset($_POST['fix_index']) ? (int) $_POST['fix_index'] : -1; $value = isset($_POST['value']) ? wp_kses_post(wp_unslash($_POST['value'])) : ''; $result = AllAccessible_ApiClient::get_instance()->edit_fix($manifest_id, $fix_index, $value); if (is_wp_error($result)) { $payload = array( 'message' => $result->get_error_message(), 'wp_code' => $result->get_error_code(), 'server_data' => $result->get_error_data(), 'request' => array( 'manifest_id' => $manifest_id, 'fix_index' => $fix_index, 'value_len' => strlen((string) $value), ), ); wp_send_json_error($payload, 400); } wp_send_json_success($result); } add_action('wp_ajax_aacb_edit_fix', 'aacb_edit_fix_ajax'); /** * Bulk approve. */ function aacb_bulk_approve_manifests_ajax() { aacb_assert_manifest_caller(); $site_id = isset($_POST['site_id']) ? (int) $_POST['site_id'] : 0; $ids_raw = isset($_POST['manifest_ids']) ? (array) $_POST['manifest_ids'] : array(); $ids = array_values(array_filter(array_map('intval', $ids_raw), function($v) { return $v > 0; })); $result = AllAccessible_ApiClient::get_instance()->bulk_approve_manifests($site_id, $ids); AllAccessible_Debug::api('bulk_approve_manifests', array( 'site_id' => $site_id, 'manifest_ids' => $ids, ), $result); if (is_wp_error($result)) { $payload = array( 'message' => $result->get_error_message(), 'wp_code' => $result->get_error_code(), 'server_data' => $result->get_error_data(), 'request' => array('site_id' => $site_id, 'manifest_ids' => $ids), ); wp_send_json_error($payload, 400); } // Partial success still counts: the server reports approvedCount for // the rows it actually approved (others land in `skipped`). if (is_array($result) && (int) ($result['approvedCount'] ?? 0) > 0 && class_exists('AllAccessible_ReviewNudge')) { AllAccessible_ReviewNudge::mark_moment('first_fix_approved'); } wp_send_json_success($result); } add_action('wp_ajax_aacb_bulk_approve_manifests', 'aacb_bulk_approve_manifests_ajax'); /* ===================================================================== * Scan trigger * ===================================================================== */ const AACB_SCAN_NONCE = 'aacb_scan_action'; function aacb_assert_scan_caller() { check_ajax_referer(AACB_SCAN_NONCE, '_wpnonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized', 'allaccessible'), 403); } } /** * Return detected sitemap candidates. */ function aacb_detect_sitemap_ajax() { aacb_assert_scan_caller(); if (!class_exists('AllAccessible_SitemapDetector')) { wp_send_json_error('Detector not loaded', 500); } $candidates = AllAccessible_SitemapDetector::all_candidates(); $primary = AllAccessible_SitemapDetector::detect(true); wp_send_json_success(array( 'primary' => $primary, 'candidates' => $candidates, )); } add_action('wp_ajax_aacb_detect_sitemap', 'aacb_detect_sitemap_ajax'); /** * Start a scan. */ function aacb_start_scan_ajax() { aacb_assert_scan_caller(); $sitemap_url = isset($_POST['sitemap_url']) ? esc_url_raw(wp_unslash($_POST['sitemap_url'])) : ''; $viewport = isset($_POST['viewport']) ? sanitize_key($_POST['viewport']) : 'both'; $client = AllAccessible_ApiClient::get_instance(); $outcome = $client->start_scan_workflow($sitemap_url, $viewport); if ($outcome === false) { $secret = $client->get_plugin_secret(); if (!empty($secret)) { $outcome = $client->start_scan_workflow($sitemap_url, $viewport); } } if ($outcome === false) { wp_send_json_error(__('Still finishing setup — wait a few seconds and try again.', 'allaccessible'), 503); } if ($outcome['state'] === 'blocked') { // 2.2: the server refused the scan. Say why instead of "Scan started". wp_send_json_error(array('reason' => $outcome['reason'], 'message' => $outcome['message']), 409); } $confirmed = ($outcome['state'] === 'queued'); wp_send_json_success(array( 'queued' => true, 'confirmed' => $confirmed, 'jobId' => $outcome['jobId'], // 'unconfirmed' = the request timed out on our side; the server may // still be running it. Don't claim it started. 'message' => $confirmed ? __('Scan started. Results appear on the Agentic Fixes page in 2-5 minutes.', 'allaccessible') : __('Scan requested. The server is still confirming it; check back in a few minutes.', 'allaccessible'), 'statusLine' => $confirmed ? __('Scan started just now', 'allaccessible') : __('Scan requested just now', 'allaccessible'), 'triggeredAt' => time(), 'sitemapUrl' => $sitemap_url, )); } add_action('wp_ajax_aacb_start_scan', 'aacb_start_scan_ajax'); /** * Poll scan progress (ScanTriggerPanel). * * Action is aacb_scan_progress, NOT aacb_scan_status — that action belongs * to AdminBar::ajax_scan_status (different nonce, richer response). The two * were briefly registered on the same action, which made AdminBar's nonce * check kill the panel's polling with a 403. */ function aacb_scan_progress_ajax() { aacb_assert_scan_caller(); $job_id = isset($_POST['job_id']) ? (int) $_POST['job_id'] : 0; $result = AllAccessible_ApiClient::get_instance()->get_scan_status($job_id); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message(), 400); } // The panel JS reads status/pagesDone/totalPages flat — unwrap the // job envelope the API returns. wp_send_json_success(isset($result['job']) && is_array($result['job']) ? $result['job'] : $result); } add_action('wp_ajax_aacb_scan_progress', 'aacb_scan_progress_ajax'); /** * Background fetch of the plugin secret. */ add_action('aacb_fetch_plugin_secret_event', function() { if (class_exists('AllAccessible_ApiClient')) { AllAccessible_ApiClient::get_instance()->fetch_plugin_secret(); } }); /** * Force-refresh. */ function aacb_verify_connection_ajax() { check_ajax_referer('aacb_verify_connection', '_wpnonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized', 'allaccessible'), 403); } delete_transient('aacb_site_options_cache'); delete_transient('aacb_validation_cache'); delete_transient('aacb_cache_manifest_summary_v2'); $client = AllAccessible_ApiClient::get_instance(); $opts = $client->get_site_options(true); if (is_wp_error($opts)) { wp_send_json_error($opts->get_error_message(), 400); } wp_send_json_success(array('refreshed' => true)); } add_action('wp_ajax_aacb_verify_connection', 'aacb_verify_connection_ajax');