PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-status / src / class-feature-policy.php

class-feature-policy.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-status/src/class-feature-policy.php

321 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A single place for hosts to force, default, or hide Jetpack features.
4 *
5 * @package automattic/jetpack-status
6 */
7
8 namespace Automattic\Jetpack;
9
10 /**
11 * Reads `jetpack_feature_policy` and feeds it into the filters that already own each decision.
12 *
13 * Activation goes through `jetpack_active_modules`, defaults through `jetpack_get_default_modules`,
14 * and visibility through both `jetpack_my_jetpack_feature_visibility` and
15 * `jetpack_admin_menu_visibility`, so every existing reader, including forced-module detection,
16 * sees the policy without knowing it exists.
17 */
18 class Feature_Policy {
19
20 /**
21 * The policy filter's name.
22 *
23 * @var string
24 */
25 const FILTER = 'jetpack_feature_policy';
26
27 const ACTIVATION_DEFAULT = 'default';
28 const ACTIVATION_FORCED_ON = 'forced-on';
29 const ACTIVATION_FORCED_OFF = 'forced-off';
30 const ACTIVATION_DEFAULT_ON = 'default-on';
31 const ACTIVATION_DEFAULT_OFF = 'default-off';
32
33 const VISIBILITY_VISIBLE = 'visible';
34 const VISIBILITY_HIDDEN = 'hidden';
35
36 /**
37 * Runs after other callbacks so the policy has the last word.
38 *
39 * @var int
40 */
41 const PRIORITY = PHP_INT_MAX;
42
43 /**
44 * Bridged filters, mapped to the callback and argument count each takes.
45 *
46 * @var array
47 */
48 const BRIDGES = array(
49 'jetpack_active_modules' => array( 'filter_active_modules', 1 ),
50 'jetpack_get_default_modules' => array( 'filter_default_modules', 5 ),
51 'jetpack_my_jetpack_feature_visibility' => array( 'filter_visibility', 1 ),
52 'jetpack_admin_menu_visibility' => array( 'filter_menu_visibility', 2 ),
53 );
54
55 /**
56 * Forced-on slugs already reported through `_doing_it_wrong()` this request.
57 *
58 * @var string[]
59 */
60 private static $warned = array();
61
62 /**
63 * Registers the bridge callbacks once something uses the policy filter.
64 *
65 * Called by each reader rather than at load, because the status package has no bootstrap.
66 * Waiting for a policy keeps `has_filter( 'jetpack_active_modules' )` false on sites without one.
67 *
68 * @return void
69 */
70 public static function ensure_hooks() {
71 if ( ! has_filter( self::FILTER ) ) {
72 return;
73 }
74
75 foreach ( self::BRIDGES as $hook => list( $method, $accepted_args ) ) {
76 if ( false === has_filter( $hook, array( __CLASS__, $method ) ) ) {
77 add_filter( $hook, array( __CLASS__, $method ), self::PRIORITY, $accepted_args );
78 }
79 }
80 }
81
82 /**
83 * Removes the bridge callbacks. For tests.
84 *
85 * @return void
86 */
87 public static function reset() {
88 foreach ( self::BRIDGES as $hook => list( $method ) ) {
89 remove_filter( $hook, array( __CLASS__, $method ), self::PRIORITY );
90 }
91
92 self::$warned = array();
93 }
94
95 /**
96 * The validated policy.
97 *
98 * @return array Map of slug to an array with `activation` and `visibility`, either of which may be null.
99 */
100 public static function get_policy() {
101 /**
102 * Filters how Jetpack treats each feature on this site.
103 *
104 * Keys are module slugs for `activation`. For `visibility`, keys are anything the My Jetpack
105 * Features page answers to: product, module, or feature slugs. Each value is an array with
106 * either or both of:
107 *
108 * - `activation`: 'forced-on' or 'forced-off' pin the module on every request, the same as
109 * `jetpack_active_modules`. 'default-on' or 'default-off' change what Jetpack turns on when
110 * it activates its default modules, which happens at connection and upgrade, not on an
111 * existing site. 'default' leaves it alone. A 'forced-on' slug this site has no module for
112 * still reads as active, but calls `_doing_it_wrong()` since nothing will load it.
113 * - `visibility`: 'hidden' keeps the item off the My Jetpack Features page and out of the
114 * wp-admin sidebar, 'visible' shows it in both. A sidebar entry matches on its item key or
115 * on the product or module gate it declares. The policy runs last, so 'visible' overrides a
116 * 'hidden' another callback set.
117 *
118 * Standalone plugin products (Akismet, Boost, CRM, Protect) take `visibility` only: WordPress
119 * decides which plugins load before Jetpack runs, so forcing one needs `option_active_plugins`
120 * in an mu-plugin.
121 *
122 * Register the policy before `after_setup_theme`, from an mu-plugin, plugin, or theme:
123 * `Jetpack::load_modules()` runs there at priority -2, so a later policy (on `init`, say) misses
124 * module loading while My Jetpack still honors it — the module runs on a page saying it is off.
125 *
126 * @since 7.1.0
127 *
128 * @param array $policy Map of slug to policy, empty until a host adds to it.
129 */
130 $policy = apply_filters( 'jetpack_feature_policy', array() );
131
132 if ( ! is_array( $policy ) ) {
133 return array();
134 }
135
136 $activations = array( self::ACTIVATION_DEFAULT, self::ACTIVATION_FORCED_ON, self::ACTIVATION_FORCED_OFF, self::ACTIVATION_DEFAULT_ON, self::ACTIVATION_DEFAULT_OFF );
137 $visibilities = array( self::VISIBILITY_VISIBLE, self::VISIBILITY_HIDDEN );
138 $valid = array();
139
140 foreach ( $policy as $slug => $entry ) {
141 if ( ! is_string( $slug ) || '' === $slug || ! is_array( $entry ) ) {
142 continue;
143 }
144
145 $activation = $entry['activation'] ?? null;
146 $visibility = $entry['visibility'] ?? null;
147
148 $valid[ $slug ] = array(
149 'activation' => in_array( $activation, $activations, true ) ? $activation : null,
150 'visibility' => in_array( $visibility, $visibilities, true ) ? $visibility : null,
151 );
152 }
153
154 return $valid;
155 }
156
157 /**
158 * Adds forced-on modules to the active list and drops forced-off ones.
159 *
160 * @param array $active Active module slugs.
161 * @return array
162 */
163 public static function filter_active_modules( $active ) {
164 if ( ! is_array( $active ) ) {
165 return $active;
166 }
167
168 $policy = self::get_policy();
169 $forced_on = self::get_slugs( $policy, 'activation', self::ACTIVATION_FORCED_ON );
170
171 self::warn_about_slugs_with_no_module( $forced_on );
172
173 $active = array_merge( $active, $forced_on );
174
175 return array_values( array_unique( array_diff( $active, self::get_slugs( $policy, 'activation', self::ACTIVATION_FORCED_OFF ) ) ) );
176 }
177
178 /**
179 * Reports a forced-on slug this site has no module for.
180 *
181 * The slug is kept, because discarding a host's instruction silently is worse than honoring a
182 * typo, so it reads as active everywhere while `load_modules()` never loads it.
183 *
184 * @param string[] $forced_on Slugs the policy forces on.
185 * @return void
186 */
187 private static function warn_about_slugs_with_no_module( $forced_on ) {
188 $unwarned = array_diff( $forced_on, self::$warned );
189
190 // This runs on every get_active() call, so skip the module scan unless there is news.
191 if ( ! $unwarned || ! function_exists( '_doing_it_wrong' ) ) {
192 return;
193 }
194
195 // No arguments: every slug this site has, so only a typo is flagged.
196 $available = ( new Modules() )->get_available();
197 self::$warned = array_merge( self::$warned, $unwarned );
198
199 foreach ( $unwarned as $slug ) {
200 if ( in_array( $slug, $available, true ) ) {
201 continue;
202 }
203
204 $message = sprintf( 'Forced on "%s", which is not a Jetpack module on this site. It will report as active, but nothing will load it.', $slug );
205
206 // The version token is only replaced at release time when this call is on one line.
207 _doing_it_wrong( 'jetpack_feature_policy', esc_html( $message ), '7.1.0' );
208 }
209 }
210
211 /**
212 * Adds default-on modules to the defaults and drops default-off and forced-off ones.
213 *
214 * Forced-off has to go too: the activation path writes whatever it activates to
215 * `jetpack_active_modules`, so a forced-off default would be saved as active.
216 *
217 * @param array $modules Default module slugs.
218 * @param bool|string $min_version Minimum module version, passed through from the caller.
219 * @param bool|string $max_version Maximum module version, passed through from the caller.
220 * @param bool|null $requires_connection Connection requirement, passed through from the caller.
221 * @param bool|null $requires_user_connection User connection requirement, passed through from the caller.
222 * @return array
223 */
224 public static function filter_default_modules( $modules, $min_version = false, $max_version = false, $requires_connection = null, $requires_user_connection = null ) {
225 if ( ! is_array( $modules ) ) {
226 return $modules;
227 }
228
229 $policy = self::get_policy();
230 $default_on = self::get_slugs( $policy, 'activation', self::ACTIVATION_DEFAULT_ON );
231
232 if ( $default_on ) {
233 // Honor the caller's constraints: an offline activation must not pick up a module that needs a connection.
234 $available = ( new Modules() )->get_available( $min_version, $max_version, $requires_connection, $requires_user_connection );
235 $modules = array_merge( $modules, array_intersect( $default_on, $available ) );
236 }
237
238 return array_values( array_unique( array_diff( $modules, self::get_slugs( $policy, 'activation', self::ACTIVATION_DEFAULT_OFF ), self::get_slugs( $policy, 'activation', self::ACTIVATION_FORCED_OFF ) ) ) );
239 }
240
241 /**
242 * Sets each slug's My Jetpack visibility from the policy.
243 *
244 * @param array $states Map of slug to visibility state.
245 * @return array
246 */
247 public static function filter_visibility( $states ) {
248 if ( ! is_array( $states ) ) {
249 $states = array();
250 }
251
252 foreach ( self::get_policy() as $slug => $entry ) {
253 if ( null !== $entry['visibility'] ) {
254 $states[ $slug ] = $entry['visibility'];
255 }
256 }
257
258 return $states;
259 }
260
261 /**
262 * Sets each wp-admin sidebar item's state from the policy entry that names it.
263 *
264 * Hosts name features, not menu slugs, so an item is matched by its key and then by the
265 * product or module gate it declared. A policy slug matching no item changes nothing.
266 *
267 * @param array $states Map of menu item key to visibility state.
268 * @param array $items The registered menu items.
269 * @return array
270 */
271 public static function filter_menu_visibility( $states, $items = array() ) {
272 if ( ! is_array( $states ) || ! is_array( $items ) ) {
273 return $states;
274 }
275
276 $policy = self::get_policy();
277
278 foreach ( $items as $item ) {
279 if ( ! is_array( $item ) ) {
280 continue;
281 }
282
283 $args = isset( $item['args'] ) && is_array( $item['args'] ) ? $item['args'] : array();
284 $key = empty( $args['key'] ) ? ( $item['menu_slug'] ?? null ) : $args['key'];
285
286 if ( ! is_string( $key ) || '' === $key ) {
287 continue;
288 }
289
290 foreach ( array( $key, $args['product'] ?? null, $args['module'] ?? null ) as $slug ) {
291 if ( is_string( $slug ) && ! empty( $policy[ $slug ]['visibility'] ) ) {
292 $states[ $key ] = $policy[ $slug ]['visibility'];
293 break;
294 }
295 }
296 }
297
298 return $states;
299 }
300
301 /**
302 * Slugs whose policy sets `$key` to `$value`.
303 *
304 * @param array $policy The validated policy.
305 * @param string $key 'activation' or 'visibility'.
306 * @param string $value The value to match.
307 * @return string[]
308 */
309 private static function get_slugs( $policy, $key, $value ) {
310 $slugs = array();
311
312 foreach ( $policy as $slug => $entry ) {
313 if ( $value === $entry[ $key ] ) {
314 $slugs[] = $slug;
315 }
316 }
317
318 return $slugs;
319 }
320 }
321