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 / inc / ContextGuard.php

ContextGuard.php in Accessibility by AllAccessible trunk, at inc/ContextGuard.php

174 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Context Guard for AllAccessible
4 *
5 * Single decision point for "should the widget be printed on this request?".
6 * Used by WidgetLoader (widget <script>) and ContextInjector (context block)
7 * so both stay in lockstep, and surfaced in the admin bar for debugging.
8 *
9 * The widget must not run on requests that are not real, visitor-facing pages:
10 * - wp-admin
11 * - 404s (otherwise the API registers the 404 URL as a monitored page)
12 * - feeds (not HTML)
13 * - Customizer / post previews
14 * - page-builder editing sessions. Several builders (Oxygen, Divi, Elementor,
15 * Beaver Builder, WPBakery, Bricks, Brizy, Thrive, Cornerstone, Avada)
16 * render their editor as a FRONTEND request flagged only by a query param,
17 * so is_admin() is false and the widget + scanner would run inside the editor.
18 *
19 * Site owners can extend the decision with the `allaccessible_skip_widget` filter.
20 *
21 * WordPress conditionals are called only when they exist so this file can be
22 * exercised by tests/context-guard-test.php without a WordPress bootstrap.
23 *
24 * @package AllAccessible
25 * @since 2.1.6
26 */
27
28 if (!defined('ABSPATH')) {
29 die('You are not allowed to call this page directly.');
30 }
31
32 class AllAccessible_ContextGuard {
33
34 /**
35 * Query params that mark a page-builder editing/preview session.
36 *
37 * Presence alone is enough (Beaver Builder launches on a bare `?fl_builder`
38 * and Brizy on a bare `?brizy-edit-iframe`, so a value check would miss
39 * both). Params here MUST be vendor-namespaced: matching a bare English
40 * word would strip the widget from ordinary visitor URLs — see
41 * BUILDER_QUERY_PARAMS_WITH_VALUE for the ambiguous ones.
42 */
43 const BUILDER_QUERY_PARAMS = array(
44 'ct_builder', // Oxygen
45 'ct_inner', // Oxygen (inner content)
46 'et_fb', // Divi Visual Builder
47 'elementor-preview', // Elementor
48 'fl_builder', // Beaver Builder (often valueless)
49 'vc_editable', // WPBakery
50 'vc_action', // WPBakery
51 'brizy-edit', // Brizy (valueless)
52 'brizy-edit-iframe', // Brizy (valueless)
53 'fusion_load_nonce', // Avada Live
54 'fb-edit', // Avada Live (front-end editor)
55 );
56
57 /**
58 * Builder params whose NAME is an ordinary word, so presence alone is not
59 * evidence of a builder session: `/shop/?bricks=clay` on a masonry store is
60 * a real visitor URL. These only count when the value matches what the
61 * builder actually sends.
62 */
63 const BUILDER_QUERY_PARAMS_WITH_VALUE = array(
64 'bricks' => array('run'), // Bricks
65 'tve' => array('true', '1'), // Thrive Architect
66 'cornerstone' => array('1', 'true'), // Cornerstone
67 );
68
69 /**
70 * True when the widget must NOT be printed for the current request.
71 *
72 * @return bool
73 */
74 public static function should_skip_widget() {
75 return self::skip_reason() !== '';
76 }
77
78 /**
79 * Human-readable reason the widget is skipped (for the admin bar / debug),
80 * or '' when the widget should load.
81 *
82 * @return string
83 */
84 public static function skip_reason() {
85 if (self::wp_true('is_admin')) {
86 return 'admin';
87 }
88 if (self::wp_true('is_404')) {
89 return '404';
90 }
91 if (self::wp_true('is_feed')) {
92 return 'feed';
93 }
94 if (self::wp_true('is_customize_preview')) {
95 return 'customizer';
96 }
97 // `preview` is a PUBLIC query var: WP_Query sets is_preview() from it with
98 // no nonce and no login. An editor pasting a preview link into email or
99 // Slack would otherwise strip the widget for every anonymous visitor who
100 // clicks it. Only treat it as a preview for a user who can actually edit.
101 if (self::wp_true('is_preview') && self::current_user_can_edit()) {
102 return 'preview';
103 }
104 if (self::in_builder_session()) {
105 return 'page builder';
106 }
107 if (function_exists('apply_filters') && apply_filters('allaccessible_skip_widget', false)) {
108 return 'filter';
109 }
110 return '';
111 }
112
113 /**
114 * Is this request a page-builder editing session?
115 *
116 * A builder session is ALWAYS an authenticated request from someone who can
117 * edit content. Requiring that first is what makes the query-param check
118 * safe: without it any anonymous visitor whose URL happens to carry one of
119 * these params (a shop facet, a campaign tag, a shared link) silently loses
120 * the accessibility widget on a live page.
121 *
122 * @return bool
123 */
124 public static function in_builder_session() {
125 if (!self::current_user_can_edit()) {
126 return false;
127 }
128
129 foreach (self::BUILDER_QUERY_PARAMS as $param) {
130 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only detection, no state change.
131 // isset(), not !empty(): Beaver Builder and Brizy pass these bare.
132 if (isset($_GET[$param])) {
133 return true;
134 }
135 }
136
137 foreach (self::BUILDER_QUERY_PARAMS_WITH_VALUE as $param => $values) {
138 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only detection, no state change.
139 if (!isset($_GET[$param])) {
140 continue;
141 }
142 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only detection, no state change.
143 $value = is_scalar($_GET[$param]) ? strtolower((string) $_GET[$param]) : '';
144 if (in_array($value, $values, true)) {
145 return true;
146 }
147 }
148
149 return false;
150 }
151
152 /**
153 * Can the current user edit content?
154 *
155 * Outside WordPress (standalone test) both functions are absent and this
156 * returns false, so the guard fails OPEN — the widget renders. That is the
157 * safe direction: a missing check must never hide the product.
158 */
159 private static function current_user_can_edit() {
160 if (!function_exists('is_user_logged_in') || !function_exists('current_user_can')) {
161 return false;
162 }
163 return is_user_logged_in() && current_user_can('edit_posts');
164 }
165
166 /**
167 * Call a WordPress conditional only if it is defined (keeps this file
168 * loadable outside WordPress, e.g. in the standalone test).
169 */
170 private static function wp_true($fn) {
171 return function_exists($fn) && (bool) call_user_func($fn);
172 }
173 }
174