Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Registrar
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
4 / 4
15
100.00% covered (success)
100.00%
1 / 1
 get_category_slug
n/a
0 / 0
n/a
0 / 0
0
 get_category_definition
n/a
0 / 0
n/a
0 / 0
0
 get_abilities
n/a
0 / 0
n/a
0 / 0
0
 init
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 register_category
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 register_abilities
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 should_register
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Abstract base class for registering a category and its abilities with the
4 * WordPress Abilities API.
5 *
6 * @package automattic/jetpack-wp-abilities
7 */
8
9// @phan-file-suppress PhanAbstractStaticMethodCallInStatic -- static:: dispatches to the concrete subclass for the three abstract getters; callers must not instantiate Registrar itself.
10
11namespace Automattic\Jetpack\WP_Abilities;
12
13/**
14 * Abstract base class that owns the boilerplate every Jetpack abilities
15 * registrar needs: hooking into the Abilities API lifecycle actions (or
16 * calling the registration methods directly when those actions have already
17 * fired), and guarding the Abilities API function calls so the class is safe
18 * to load on WP < 6.9.
19 *
20 * Consumers extend this class and override `get_category_slug()`,
21 * `get_category_definition()`, and `get_abilities()`.
22 */
23abstract class Registrar {
24
25    const PACKAGE_VERSION = '0.2.0';
26
27    /**
28     * Action fired by the Abilities API when ability categories should register.
29     */
30    const CATEGORIES_INIT_ACTION = 'wp_abilities_api_categories_init';
31
32    /**
33     * Action fired by the Abilities API when abilities should register.
34     */
35    const ABILITIES_INIT_ACTION = 'wp_abilities_api_init';
36
37    /**
38     * Return the category slug this registrar owns (e.g. "jetpack-forms").
39     *
40     * @return string
41     */
42    abstract public static function get_category_slug(): string;
43
44    /**
45     * Return the category definition passed to `wp_register_ability_category()`.
46     *
47     * Expected shape: [ 'label' => string, 'description' => string ].
48     *
49     * @return array
50     */
51    abstract public static function get_category_definition(): array;
52
53    /**
54     * Return the abilities this registrar owns as a `[ slug => spec ]` map.
55     *
56     * Each spec is passed as-is to `wp_register_ability()`. If a spec omits
57     * `category`, the registrar auto-injects `get_category_slug()`. If the spec
58     * sets `category` explicitly, it is preserved unchanged.
59     *
60     * @return array<string, array>
61     */
62    abstract public static function get_abilities(): array;
63
64    /**
65     * Wire up the Abilities API registrations.
66     *
67     * Gated behind the `jetpack_wp_abilities_enabled` filter, which defaults
68     * to `true`. Return `false` from it to turn registration off for a site
69     * or a single request.
70     *
71     * When the filter returns `true`, each of the two Abilities API lifecycle
72     * actions either gets a registration callback hooked, or — if the action
73     * has already fired — dispatches immediately so late-loading plugins
74     * still register on time.
75     *
76     * @return void
77     */
78    public static function init() {
79        /**
80         * Filters whether Jetpack Abilities API registration should run.
81         *
82         * Default `true`. Return `false` to skip registration for this
83         * request, for example to turn Jetpack's abilities off on a site.
84         *
85         * @since 0.1.0
86         * @since $$next-version$$ Defaults to `true`.
87         *
88         * @param bool $enabled Whether to register abilities. Default true.
89         */
90        if ( ! apply_filters( 'jetpack_wp_abilities_enabled', true ) ) {
91            return;
92        }
93
94        if ( did_action( self::CATEGORIES_INIT_ACTION ) ) {
95            static::register_category();
96        } else {
97            add_action( self::CATEGORIES_INIT_ACTION, array( static::class, 'register_category' ) );
98        }
99
100        if ( did_action( self::ABILITIES_INIT_ACTION ) ) {
101            static::register_abilities();
102        } else {
103            add_action( self::ABILITIES_INIT_ACTION, array( static::class, 'register_abilities' ) );
104        }
105    }
106
107    /**
108     * Register the category with the WordPress Abilities API.
109     *
110     * Safe to call directly or as a hook callback. Passes the category slug
111     * through the `jetpack_wp_abilities_should_register` filter so consumers
112     * can gate registration per-site, per-user, or per-feature-flag.
113     *
114     * @return void
115     */
116    public static function register_category() {
117        if ( ! function_exists( 'wp_register_ability_category' ) ) {
118            return;
119        }
120
121        $slug = static::get_category_slug();
122        if ( ! self::should_register( 'category', $slug ) ) {
123            return;
124        }
125
126        wp_register_ability_category( $slug, static::get_category_definition() );
127    }
128
129    /**
130     * Register every ability returned by `get_abilities()`.
131     *
132     * Safe to call directly or as a hook callback. Each ability slug is
133     * passed through the `jetpack_wp_abilities_should_register` filter
134     * individually, so consumers can enable a subset of abilities on a
135     * subset of sites or users. An ability whose category isn't registered
136     * is skipped, since WordPress would reject it.
137     *
138     * @return void
139     */
140    public static function register_abilities() {
141        if ( ! function_exists( 'wp_register_ability' ) ) {
142            return;
143        }
144
145        $category_slug = static::get_category_slug();
146
147        foreach ( static::get_abilities() as $slug => $spec ) {
148            if ( ! self::should_register( 'ability', $slug ) ) {
149                continue;
150            }
151            if ( ! array_key_exists( 'category', $spec ) ) {
152                $spec['category'] = $category_slug;
153            }
154            // For example a category the should-register filter blocked, or `site` in core's own tests, which unhook it.
155            if ( function_exists( 'wp_has_ability_category' ) && ! wp_has_ability_category( $spec['category'] ) ) {
156                continue;
157            }
158            wp_register_ability( $slug, $spec );
159        }
160    }
161
162    /**
163     * Apply the shared registration filter.
164     *
165     * @param string $type One of 'category' or 'ability'.
166     * @param string $slug The slug being registered.
167     * @return bool True when registration should proceed, false to skip.
168     */
169    private static function should_register( string $type, string $slug ): bool {
170        /**
171         * Filters whether an Abilities API category or ability should be registered.
172         *
173         * Returning false from any filter callback skips the registration,
174         * which is how consumers gate rollout per-site, per-user, or per
175         * feature flag. The filter fires once per category and once per
176         * individual ability, so callbacks can allow-list or deny-list by
177         * `$slug`.
178         *
179         * @since 0.1.0
180         *
181         * @param bool   $enabled Whether to register. Default true.
182         * @param string $type    Either 'category' or 'ability'.
183         * @param string $slug    The category or ability slug being registered.
184         */
185        return (bool) apply_filters( 'jetpack_wp_abilities_should_register', true, $type, $slug );
186    }
187}