PluginProbe
Accessibility by AllAccessible / trunk
Accessibility by AllAccessible vtrunk
2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.6 trunk 1.0 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2 1.2.1 1.2.10 1.2.2 1.2.3 1.2.4 1.2.5 All 43 releases
allaccessible / allaccessible.php

allaccessible.php in Accessibility by AllAccessible trunk, at allaccessible.php

487 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 Plugin Name: AllAccessible
4 Plugin URI: https://www.allaccessible.org/platform/wordpress/
5 Description: Unlock true digital accessibility with AllAccessible - a comprehensive WordPress plugin driving your website towards WCAG/ADA compliance. Empower your users with a fully customizable accessibility widget, plus agentic AI remediation that auto-suggests fixes for your team to approve.
6 Version: 2.1.6
7 Requires at least: 5.5
8 Tested up to: 7.1
9 Requires PHP: 7.4
10 Author: AllAccessible Team
11 Author URI: https://www.allaccessible.org/
12 Text Domain: allaccessible
13 Domain Path: /languages
14 */
15
16 /**
17 * Copyright (C) 2024 AllAccessible.
18 * This file is part of AllAccessible.
19 *
20 * AllAccessible is free software: you can redistribute it and/or modify
21 * it under the terms of the GNU General Public License as published by
22 * the Free Software Foundation, either version 2 of the License, or
23 * any later version.
24 *
25 * AllAccessible is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * You should have received a copy of the GNU General Public License
31 * along with AllAccessible. If not, see <http://www.gnu.org/licenses/>.
32 *
33 * @package AllAccessible
34 * @author AllAccessible Team
35 * @copyright 2024 AllAccessible
36 * @license GPL-2.0+
37 */
38
39 if (!defined('ABSPATH')) {
40 die('You are not allowed to call this page directly.');
41 }
42
43 // Duplicate-copy guard: if another copy of this plugin already loaded (e.g. a
44 // second plugin folder from a mis-named zip upload, or an old copy left behind
45 // during an update), bail out silently instead of fataling the whole site with
46 // "Cannot redeclare aacb_require_if_exists()" / duplicate class errors.
47 // NOTE: check ONLY runtime state (a constant defined by a require at runtime).
48 // Do NOT test function_exists() here — PHP hoists top-level function
49 // declarations at compile time, so every function in this file already exists
50 // before line 1 runs, and the guard would abort the plugin's own first load.
51 if (defined('AACB_VERSION')) {
52 if (function_exists('error_log')) {
53 error_log('[AllAccessible] Duplicate plugin copy detected at ' . __FILE__ . ' — skipped. Remove the extra copy in wp-content/plugins/.');
54 }
55 return;
56 }
57
58 // Core Components
59 require_once plugin_dir_path(__FILE__) . 'inc/constants.php';
60 require_once plugin_dir_path(__FILE__) . 'inc/Debug.php';
61 require_once plugin_dir_path(__FILE__) . 'inc/SentryClient.php';
62 require_once plugin_dir_path(__FILE__) . 'inc/SentryBrowser.php';
63 AllAccessible_Sentry::init();
64 AllAccessible_SentryBrowser::register();
65 /**
66 * Guarded require: load a plugin file only if it exists.
67 *
68 * A partial/failed plugin update (some files copied, others not) previously
69 * fataled the ENTIRE site, because an unguarded `require_once` on a missing
70 * file is a fatal error during plugin load — white-screening wp-admin and the
71 * front end (Sentry WORDPRESS-PLUGIN-R/S: missing inc/PostLinkBackfill.php).
72 *
73 * Now a missing non-core file is reported to Sentry once and skipped, so the
74 * rest of the plugin still loads. Sentry is already initialised above, so
75 * reporting is safe here. The four bootstrap files (constants, Debug,
76 * SentryClient, SentryBrowser) above intentionally stay unguarded — without
77 * them nothing, including this reporter, can function.
78 *
79 * @return bool true if the file was loaded.
80 */
81 function aacb_require_if_exists($relative_path) {
82 $full = plugin_dir_path(__FILE__) . $relative_path;
83 if (file_exists($full)) {
84 require_once $full;
85 return true;
86 }
87 if (class_exists('AllAccessible_Sentry')) {
88 AllAccessible_Sentry::capture_message(
89 'Plugin file missing (likely partial/failed update): ' . $relative_path,
90 'error',
91 array('version' => defined('AACB_VERSION') ? AACB_VERSION : 'unknown')
92 );
93 }
94 error_log('[AllAccessible] Missing plugin file, skipped: ' . $relative_path);
95 return false;
96 }
97
98 aacb_require_if_exists('inc/VersionManager.php');
99 aacb_require_if_exists('inc/UrlCanonicalizer.php');
100
101 // Widget & Frontend
102 aacb_require_if_exists('inc/ContextGuard.php');
103 aacb_require_if_exists('inc/WidgetLoader.php');
104
105 // Admin Interface
106 aacb_require_if_exists('inc/OnboardingWizard.php');
107 aacb_require_if_exists('inc/SettingsPage.php');
108 aacb_require_if_exists('inc/WidgetCustomizer.php');
109 aacb_require_if_exists('inc/UsageDashboard.php');
110 aacb_require_if_exists('inc/ConversionCTA.php');
111 aacb_require_if_exists('inc/FeatureComparison.php');
112 aacb_require_if_exists('inc/DashboardBanner.php');
113 aacb_require_if_exists('inc/DeactivationSurvey.php');
114 aacb_require_if_exists('inc/DashboardLayout.php');
115
116 // API Integration (Premium Features)
117 aacb_require_if_exists('inc/api/ApiClient.php');
118 aacb_require_if_exists('inc/TierGate.php');
119 aacb_require_if_exists('inc/ContextInjector.php');
120 aacb_require_if_exists('inc/AgenticFixes/Labels.php');
121 aacb_require_if_exists('inc/AgenticFixesDashboardWidget.php');
122 aacb_require_if_exists('inc/AgenticFixesPage.php');
123 aacb_require_if_exists('inc/ImageManagerPage.php');
124 aacb_require_if_exists('inc/SitemapDetector.php');
125 aacb_require_if_exists('inc/ScanTriggerPanel.php');
126 aacb_require_if_exists('inc/ConnectionStatusCard.php');
127 aacb_require_if_exists('inc/EditorMetaBox.php');
128 aacb_require_if_exists('inc/PostListColumn.php');
129 aacb_require_if_exists('inc/PostLinkBackfill.php');
130 aacb_require_if_exists('inc/AdminBar.php');
131 aacb_require_if_exists('inc/ReviewNudge.php');
132
133 // Guard registrations: a skipped file above means its class is absent, so
134 // class_exists prevents a fatal here too.
135 if (class_exists('AllAccessible_PostListColumn')) { AllAccessible_PostListColumn::register(); }
136 if (class_exists('AllAccessible_PostLinkBackfill')) { AllAccessible_PostLinkBackfill::register(); }
137 if (class_exists('AllAccessible_AdminBar')) { AllAccessible_AdminBar::register(); }
138 if (class_exists('AllAccessible_ReviewNudge')) { AllAccessible_ReviewNudge::register(); }
139
140
141 /**
142 * Load translations
143 */
144 function aacb_load_textdomain() {
145 load_plugin_textdomain('allaccessible', false, basename(dirname(__FILE__)) . '/languages/');
146 }
147 add_action('init', 'aacb_load_textdomain');
148
149 /**
150 * Plugin activation
151 */
152 function AllAccessible_Activation() {
153 $options = get_option('aacb_options');
154
155 if (!is_array($options) || !isset($options['aacb_installed']) || $options['aacb_installed'] != 1) {
156 $opt = array('aacb_installed' => 1);
157 update_option('aacb_options', $opt);
158 }
159
160 if (class_exists('AllAccessible_PostLinkBackfill')) {
161 AllAccessible_PostLinkBackfill::on_activate();
162 }
163
164 if (!get_option('aacb_accountID')) {
165 set_transient('aacb_activation_redirect', 1, 60);
166 }
167 }
168 register_activation_hook(__FILE__, 'AllAccessible_Activation');
169
170 /**
171 * One-shot redirect to the onboarding wizard right after activation.
172 */
173 function aacb_maybe_redirect_after_activation() {
174 if (!get_transient('aacb_activation_redirect')) return;
175 delete_transient('aacb_activation_redirect');
176
177 if (wp_doing_ajax() || !is_admin()) return;
178 if (isset($_GET['activate-multi'])) return;
179 if (!current_user_can('manage_options')) return;
180
181 wp_safe_redirect(admin_url('admin.php?page=allaccessible-wizard'));
182 exit;
183 }
184 add_action('admin_init', 'aacb_maybe_redirect_after_activation');
185
186 /**
187 * Plugin deactivation
188 */
189 function AllAccessible_Deactivation() {
190 // Clean up scheduled events (wp_unschedule_hook also clears single
191 // events scheduled with args, which wp_clear_scheduled_hook misses).
192 wp_unschedule_hook('aacb_post_link_backfill_run');
193 wp_unschedule_hook('aacb_post_link_single');
194 wp_unschedule_hook('aacb_fetch_plugin_secret_event');
195 wp_unschedule_hook('aacb_daily_analytics_calculation'); // pre-2.1 legacy
196 }
197 register_deactivation_hook(__FILE__, 'AllAccessible_Deactivation');
198
199 /**
200 * AJAX handler for saving account ID
201 * Used by wizard and legacy settings page
202 */
203 function AllAccessible_save_settings() {
204 // Verify capabilities
205 if (!current_user_can('manage_options')) {
206 wp_send_json_error('Unauthorized access');
207 return;
208 }
209
210 // Verify nonce (support both old and new nonce names)
211 $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field($_POST['_wpnonce']) : '';
212 if (empty($nonce) || !wp_verify_nonce($nonce, 'allaccessible_save_settings')) {
213 wp_send_json_error('Invalid security token');
214 return;
215 }
216
217 // Save account ID if provided
218 if (isset($_POST['aacb_accountID'])) {
219 $account_id = sanitize_text_field($_POST['aacb_accountID']);
220 update_option('aacb_accountID', $account_id);
221
222 wp_send_json_success(array('message' => __('Account settings saved successfully', 'allaccessible')));
223 }
224
225 wp_send_json_error('No data to save');
226 }
227 add_action('wp_ajax_AllAccessible_save_settings', 'AllAccessible_save_settings');
228
229 /**
230 * AJAX handler to clear API cache
231 */
232 function aacb_clear_cache_ajax() {
233 check_ajax_referer('aacb_clear_cache', '_wpnonce');
234
235 if (!current_user_can('manage_options')) {
236 wp_send_json_error('Unauthorized');
237 }
238
239 $api_client = AllAccessible_ApiClient::get_instance();
240 $api_client->clear_cache();
241
242 wp_send_json_success();
243 }
244 add_action('wp_ajax_aacb_clear_cache', 'aacb_clear_cache_ajax');
245
246 /**
247 * AJAX handler to reset all plugin data
248 * Allows users to start fresh without deleting the plugin
249 *
250 * @since 2.0.3
251 */
252 function aacb_reset_plugin_data() {
253 check_ajax_referer('aacb_reset_plugin', '_wpnonce');
254
255 if (!current_user_can('manage_options')) {
256 wp_send_json_error(__('Unauthorized access', 'allaccessible'));
257 }
258
259 // Delete ALL plugin options — including the HMAC plugin secret and
260 // tier/scan state. A reset that keeps the old secret leaves a
261 // half-zombie identity: the next account connect signs with stale
262 // credentials. Wildcard sweep mirrors uninstall.php.
263 global $wpdb;
264 $wpdb->query(
265 "DELETE FROM {$wpdb->options}
266 WHERE option_name LIKE 'aacb\\_%'
267 OR option_name LIKE '\\_transient\\_aacb\\_%'
268 OR option_name LIKE '\\_transient\\_timeout\\_aacb\\_%'"
269 );
270 wp_cache_flush();
271
272 AllAccessible_ApiClient::get_instance()->flush_all_caches();
273
274 // Clear scheduled events tied to the old identity
275 wp_unschedule_hook('aacb_post_link_backfill_run');
276 wp_unschedule_hook('aacb_post_link_single');
277 wp_unschedule_hook('aacb_fetch_plugin_secret_event');
278
279 // Re-initialize with default options
280 $opt = array('aacb_installed' => 1);
281 update_option('aacb_options', $opt);
282
283 wp_send_json_success(array(
284 'message' => __('Plugin data has been reset successfully', 'allaccessible')
285 ));
286 }
287 add_action('wp_ajax_aacb_reset_plugin_data', 'aacb_reset_plugin_data');
288
289 /* =====================================================================
290 * Agentic Fixes — AJAX handlers
291 * ===================================================================== */
292
293 const AACB_MANIFEST_NONCE = 'aacb_manifest_action';
294
295 function aacb_assert_manifest_caller() {
296 check_ajax_referer(AACB_MANIFEST_NONCE, '_wpnonce');
297 if (!current_user_can('manage_options')) {
298 wp_send_json_error(__('Unauthorized', 'allaccessible'), 403);
299 }
300 }
301
302 function aacb_approve_manifest_ajax() {
303 aacb_assert_manifest_caller();
304 $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0;
305 $result = AllAccessible_ApiClient::get_instance()->approve_manifest($manifest_id);
306 if (is_wp_error($result)) {
307 wp_send_json_error($result->get_error_message(), 400);
308 }
309 wp_send_json_success($result);
310 }
311 add_action('wp_ajax_aacb_approve_manifest', 'aacb_approve_manifest_ajax');
312
313 function aacb_revert_manifest_ajax() {
314 aacb_assert_manifest_caller();
315 $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0;
316 $reason = isset($_POST['reason']) ? sanitize_text_field(wp_unslash($_POST['reason'])) : '';
317 $result = AllAccessible_ApiClient::get_instance()->revert_manifest($manifest_id, $reason);
318 if (is_wp_error($result)) {
319 wp_send_json_error($result->get_error_message(), 400);
320 }
321 wp_send_json_success($result);
322 }
323 add_action('wp_ajax_aacb_revert_manifest', 'aacb_revert_manifest_ajax');
324
325 function aacb_edit_fix_ajax() {
326 aacb_assert_manifest_caller();
327 $manifest_id = isset($_POST['manifest_id']) ? (int) $_POST['manifest_id'] : 0;
328 $fix_index = isset($_POST['fix_index']) ? (int) $_POST['fix_index'] : -1;
329 $value = isset($_POST['value']) ? wp_kses_post(wp_unslash($_POST['value'])) : '';
330 $result = AllAccessible_ApiClient::get_instance()->edit_fix($manifest_id, $fix_index, $value);
331
332 if (is_wp_error($result)) {
333 $payload = array(
334 'message' => $result->get_error_message(),
335 'wp_code' => $result->get_error_code(),
336 'server_data' => $result->get_error_data(),
337 'request' => array(
338 'manifest_id' => $manifest_id,
339 'fix_index' => $fix_index,
340 'value_len' => strlen((string) $value),
341 ),
342 );
343 wp_send_json_error($payload, 400);
344 }
345 wp_send_json_success($result);
346 }
347 add_action('wp_ajax_aacb_edit_fix', 'aacb_edit_fix_ajax');
348
349 /**
350 * Bulk approve.
351 */
352 function aacb_bulk_approve_manifests_ajax() {
353 aacb_assert_manifest_caller();
354 $site_id = isset($_POST['site_id']) ? (int) $_POST['site_id'] : 0;
355 $ids_raw = isset($_POST['manifest_ids']) ? (array) $_POST['manifest_ids'] : array();
356 $ids = array_values(array_filter(array_map('intval', $ids_raw), function($v) { return $v > 0; }));
357 $result = AllAccessible_ApiClient::get_instance()->bulk_approve_manifests($site_id, $ids);
358
359 AllAccessible_Debug::api('bulk_approve_manifests', array(
360 'site_id' => $site_id,
361 'manifest_ids' => $ids,
362 ), $result);
363
364 if (is_wp_error($result)) {
365 $payload = array(
366 'message' => $result->get_error_message(),
367 'wp_code' => $result->get_error_code(),
368 'server_data' => $result->get_error_data(),
369 'request' => array('site_id' => $site_id, 'manifest_ids' => $ids),
370 );
371 wp_send_json_error($payload, 400);
372 }
373 wp_send_json_success($result);
374 }
375 add_action('wp_ajax_aacb_bulk_approve_manifests', 'aacb_bulk_approve_manifests_ajax');
376
377 /* =====================================================================
378 * Scan trigger
379 * ===================================================================== */
380
381 const AACB_SCAN_NONCE = 'aacb_scan_action';
382
383 function aacb_assert_scan_caller() {
384 check_ajax_referer(AACB_SCAN_NONCE, '_wpnonce');
385 if (!current_user_can('manage_options')) {
386 wp_send_json_error(__('Unauthorized', 'allaccessible'), 403);
387 }
388 }
389
390 /**
391 * Return detected sitemap candidates.
392 */
393 function aacb_detect_sitemap_ajax() {
394 aacb_assert_scan_caller();
395 if (!class_exists('AllAccessible_SitemapDetector')) {
396 wp_send_json_error('Detector not loaded', 500);
397 }
398 $candidates = AllAccessible_SitemapDetector::all_candidates();
399 $primary = AllAccessible_SitemapDetector::detect(true);
400 wp_send_json_success(array(
401 'primary' => $primary,
402 'candidates' => $candidates,
403 ));
404 }
405 add_action('wp_ajax_aacb_detect_sitemap', 'aacb_detect_sitemap_ajax');
406
407 /**
408 * Start a scan.
409 */
410 function aacb_start_scan_ajax() {
411 aacb_assert_scan_caller();
412 $sitemap_url = isset($_POST['sitemap_url']) ? esc_url_raw(wp_unslash($_POST['sitemap_url'])) : '';
413 $viewport = isset($_POST['viewport']) ? sanitize_key($_POST['viewport']) : 'both';
414
415 $client = AllAccessible_ApiClient::get_instance();
416 $dispatched = $client->start_scan_workflow_async($sitemap_url, $viewport);
417
418 if (!$dispatched) {
419 $secret = $client->get_plugin_secret();
420 if (!empty($secret)) {
421 $dispatched = $client->start_scan_workflow_async($sitemap_url, $viewport);
422 }
423 }
424
425 if (!$dispatched) {
426 wp_send_json_error(__('Still finishing setup — wait a few seconds and try again.', 'allaccessible'), 503);
427 }
428
429 wp_send_json_success(array(
430 'queued' => true,
431 'message' => __('Scan queued. Results appear in 2-5 minutes.', 'allaccessible'),
432 'triggeredAt' => time(),
433 'sitemapUrl' => $sitemap_url,
434 ));
435 }
436 add_action('wp_ajax_aacb_start_scan', 'aacb_start_scan_ajax');
437
438 /**
439 * Poll scan progress (ScanTriggerPanel).
440 *
441 * Action is aacb_scan_progress, NOT aacb_scan_status — that action belongs
442 * to AdminBar::ajax_scan_status (different nonce, richer response). The two
443 * were briefly registered on the same action, which made AdminBar's nonce
444 * check kill the panel's polling with a 403.
445 */
446 function aacb_scan_progress_ajax() {
447 aacb_assert_scan_caller();
448 $job_id = isset($_POST['job_id']) ? (int) $_POST['job_id'] : 0;
449 $result = AllAccessible_ApiClient::get_instance()->get_scan_status($job_id);
450 if (is_wp_error($result)) {
451 wp_send_json_error($result->get_error_message(), 400);
452 }
453 // The panel JS reads status/pagesDone/totalPages flat — unwrap the
454 // job envelope the API returns.
455 wp_send_json_success(isset($result['job']) && is_array($result['job']) ? $result['job'] : $result);
456 }
457 add_action('wp_ajax_aacb_scan_progress', 'aacb_scan_progress_ajax');
458
459 /**
460 * Background fetch of the plugin secret.
461 */
462 add_action('aacb_fetch_plugin_secret_event', function() {
463 if (class_exists('AllAccessible_ApiClient')) {
464 AllAccessible_ApiClient::get_instance()->fetch_plugin_secret();
465 }
466 });
467
468 /**
469 * Force-refresh.
470 */
471 function aacb_verify_connection_ajax() {
472 check_ajax_referer('aacb_verify_connection', '_wpnonce');
473 if (!current_user_can('manage_options')) {
474 wp_send_json_error(__('Unauthorized', 'allaccessible'), 403);
475 }
476 delete_transient('aacb_site_options_cache');
477 delete_transient('aacb_validation_cache');
478 delete_transient('aacb_cache_manifest_summary_v2');
479 $client = AllAccessible_ApiClient::get_instance();
480 $opts = $client->get_site_options(true);
481 if (is_wp_error($opts)) {
482 wp_send_json_error($opts->get_error_message(), 400);
483 }
484 wp_send_json_success(array('refreshed' => true));
485 }
486 add_action('wp_ajax_aacb_verify_connection', 'aacb_verify_connection_ajax');
487