PluginProbe
Accessibility by AllAccessible / 2.1.5
Accessibility by AllAccessible v2.1.5
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 2.1.5, at allaccessible.php

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