PluginProbe
Accessibility by AllAccessible / 2.1.6
Accessibility by AllAccessible v2.1.6
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.6, at allaccessible.php

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