• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 7cd8af37a7bbbd7b964cfa8db91dfdc0d3c167de

27 Mar 2026 10:12AM UTC coverage: 52.787% (-1.1%) from 53.917%
7cd8af37a7bbbd7b964cfa8db91dfdc0d3c167de

Pull #23062

github

web-flow
Merge 77b1c5f2b into 70d176237
Pull Request #23062: Register Yoast SEO abilities about analysis scores

8535 of 16034 branches covered (53.23%)

Branch coverage included in aggregate %.

143 of 307 new or added lines in 5 files covered. (46.58%)

794 existing lines in 37 files now uncovered.

33679 of 63936 relevant lines covered (52.68%)

46793.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.89
/src/abilities/user-interface/abilities-integration.php
1
<?php
2

3
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4
namespace Yoast\WP\SEO\Abilities\User_Interface;
5

6
use Yoast\WP\SEO\Abilities\Application\Score_Retriever;
7
use Yoast\WP\SEO\Abilities\Infrastructure\Enabled_Analysis_Features_Checker;
8
use Yoast\WP\SEO\Conditionals\Abilities_API_Conditional;
9
use Yoast\WP\SEO\Helpers\Capability_Helper;
10
use Yoast\WP\SEO\Integrations\Integration_Interface;
11

12
/**
13
 * Integration that registers Yoast SEO abilities with the WordPress Abilities API.
14
 */
15
class Abilities_Integration implements Integration_Interface {
16

17
        /**
18
         * The score retriever.
19
         *
20
         * @var Score_Retriever
21
         */
22
        private $score_retriever;
23

24
        /**
25
         * The capability helper.
26
         *
27
         * @var Capability_Helper
28
         */
29
        private $capability_helper;
30

31
        /**
32
         * The enabled analysis features checker.
33
         *
34
         * @var Enabled_Analysis_Features_Checker
35
         */
36
        private $enabled_analysis_features_checker;
37

38
        /**
39
         * Returns the conditionals based on which this loadable should be active.
40
         *
41
         * @return array<string> The conditionals.
42
         */
43
        public static function get_conditionals() {
2✔
44
                return [ Abilities_API_Conditional::class ];
2✔
45
        }
46

47
        /**
48
         * Constructor.
49
         *
50
         * @param Score_Retriever                   $score_retriever                   The score retriever.
51
         * @param Capability_Helper                 $capability_helper                 The capability helper.
52
         * @param Enabled_Analysis_Features_Checker $enabled_analysis_features_checker The enabled analysis features checker.
53
         */
NEW
54
        public function __construct(
×
55
                Score_Retriever $score_retriever,
56
                Capability_Helper $capability_helper,
57
                Enabled_Analysis_Features_Checker $enabled_analysis_features_checker
58
        ) {
NEW
59
                $this->score_retriever                   = $score_retriever;
×
NEW
60
                $this->capability_helper                 = $capability_helper;
×
NEW
61
                $this->enabled_analysis_features_checker = $enabled_analysis_features_checker;
×
62
        }
63

64
        /**
65
         * Registers hooks with WordPress.
66
         *
67
         * @return void
68
         */
69
        public function register_hooks() {
2✔
70
                \add_action( 'wp_abilities_api_categories_init', [ $this, 'register_categories' ] );
2✔
71
                \add_action( 'wp_abilities_api_init', [ $this, 'register_abilities' ] );
2✔
72
        }
73

74
        /**
75
         * Registers the Yoast SEO ability category.
76
         *
77
         * @return void
78
         */
79
        public function register_categories() {
2✔
80
                \wp_register_ability_category(
2✔
81
                        'yoast-seo',
2✔
82
                        [
2✔
83
                                'label'       => \__( 'Yoast SEO', 'wordpress-seo' ),
2✔
84
                                'description' => \__( 'SEO analysis capabilities provided by Yoast SEO.', 'wordpress-seo' ),
2✔
85
                        ],
2✔
86
                );
2✔
87
        }
88

89
        /**
90
         * Registers the Yoast SEO abilities.
91
         *
92
         * @return void
93
         */
94
        public function register_abilities() {
4✔
95
                $seo_score_item_schema                                  = $this->get_score_output_schema( [ 'na', 'bad', 'ok', 'good', 'noindex' ] );
4✔
96
                $seo_score_item_schema['properties']['focus_keyphrase'] = [
4✔
97
                        'type'        => [ 'string', 'null' ],
4✔
98
                        'description' => \__( 'The focus keyphrase for the post, or null if not set.', 'wordpress-seo' ),
4✔
99
                ];
4✔
100

101
                $readability_score_item_schema        = $this->get_score_output_schema( [ 'na', 'bad', 'ok', 'good' ] );
4✔
102
                $inclusive_language_score_item_schema = $this->get_score_output_schema( [ 'na', 'bad', 'ok', 'good' ] );
4✔
103

104
                if ( $this->enabled_analysis_features_checker->is_keyword_analysis_enabled() ) {
4✔
105
                        \wp_register_ability(
4✔
106
                                'yoast-seo/get-seo-scores',
4✔
107
                                $this->get_shared_ability_args(
4✔
108
                                        [
4✔
109
                                                'label'            => \__( 'Get SEO Scores', 'wordpress-seo' ),
4✔
110
                                                'description'      => \__( 'Get the SEO scores for the most recently modified posts.', 'wordpress-seo' ),
4✔
111
                                                'output_schema'    => $this->wrap_in_array_schema( $seo_score_item_schema ),
4✔
112
                                                'execute_callback' => [ $this->score_retriever, 'get_seo_scores' ],
4✔
113
                                        ],
4✔
114
                                ),
4✔
115
                        );
4✔
116
                }
117

118
                if ( $this->enabled_analysis_features_checker->is_content_analysis_enabled() ) {
4✔
119
                        \wp_register_ability(
4✔
120
                                'yoast-seo/get-readability-scores',
4✔
121
                                $this->get_shared_ability_args(
4✔
122
                                        [
4✔
123
                                                'label'            => \__( 'Get Readability Scores', 'wordpress-seo' ),
4✔
124
                                                'description'      => \__( 'Get the readability scores for the most recently modified posts.', 'wordpress-seo' ),
4✔
125
                                                'output_schema'    => $this->wrap_in_array_schema( $readability_score_item_schema ),
4✔
126
                                                'execute_callback' => [ $this->score_retriever, 'get_readability_scores' ],
4✔
127
                                        ],
4✔
128
                                ),
4✔
129
                        );
4✔
130
                }
131

132
                if ( $this->enabled_analysis_features_checker->is_inclusive_language_enabled() ) {
4✔
133
                        \wp_register_ability(
2✔
134
                                'yoast-seo/get-inclusive-language-scores',
2✔
135
                                $this->get_shared_ability_args(
2✔
136
                                        [
2✔
137
                                                'label'            => \__( 'Get Inclusive Language Scores', 'wordpress-seo' ),
2✔
138
                                                'description'      => \__( 'Get the inclusive language scores for the most recently modified posts.', 'wordpress-seo' ),
2✔
139
                                                'output_schema'    => $this->wrap_in_array_schema( $inclusive_language_score_item_schema ),
2✔
140
                                                'execute_callback' => [ $this->score_retriever, 'get_inclusive_language_scores' ],
2✔
141
                                        ],
2✔
142
                                ),
2✔
143
                        );
2✔
144
                }
145

146
                $nullable_schema = static function ( array $schema ): array {
4✔
147
                        return [
4✔
148
                                'oneOf' => [
4✔
149
                                        $schema,
4✔
150
                                        [ 'type' => 'null' ],
4✔
151
                                ],
4✔
152
                        ];
4✔
153
                };
4✔
154

155
                // For sub-scores inside get-all-scores, use schemas without the title property.
156
                $seo_sub_schema                                  = $this->get_score_sub_schema( [ 'na', 'bad', 'ok', 'good', 'noindex' ] );
4✔
157
                $seo_sub_schema['properties']['focus_keyphrase'] = [
4✔
158
                        'type'        => [ 'string', 'null' ],
4✔
159
                        'description' => \__( 'The focus keyphrase for the post, or null if not set.', 'wordpress-seo' ),
4✔
160
                ];
4✔
161

162
                $readability_sub_schema        = $this->get_score_sub_schema( [ 'na', 'bad', 'ok', 'good' ] );
4✔
163
                $inclusive_language_sub_schema = $this->get_score_sub_schema( [ 'na', 'bad', 'ok', 'good' ] );
4✔
164

165
                $all_scores_item_schema = [
4✔
166
                        'type'       => 'object',
4✔
167
                        'properties' => [
4✔
168
                                'title'              => [
4✔
169
                                        'type'        => 'string',
4✔
170
                                        'description' => \__( 'The post title.', 'wordpress-seo' ),
4✔
171
                                ],
4✔
172
                                'seo'                => $nullable_schema( $seo_sub_schema ),
4✔
173
                                'readability'        => $nullable_schema( $readability_sub_schema ),
4✔
174
                                'inclusive_language' => $nullable_schema( $inclusive_language_sub_schema ),
4✔
175
                        ],
4✔
176
                ];
4✔
177

178
                \wp_register_ability(
4✔
179
                        'yoast-seo/get-all-scores',
4✔
180
                        $this->get_shared_ability_args(
4✔
181
                                [
4✔
182
                                        'label'            => \__( 'Get All Analysis Scores', 'wordpress-seo' ),
4✔
183
                                        'description'      => \__( 'Get all analysis scores (SEO, readability, inclusive language) for the most recently modified posts.', 'wordpress-seo' ),
4✔
184
                                        'output_schema'    => $this->wrap_in_array_schema( $all_scores_item_schema ),
4✔
185
                                        'execute_callback' => [ $this->score_retriever, 'get_all_scores' ],
4✔
186
                                ],
4✔
187
                        ),
4✔
188
                );
4✔
189
        }
190

191
        /**
192
         * Checks whether the current user can read scores.
193
         *
194
         * @return bool Whether the current user can read scores.
195
         */
196
        public function can_read_scores(): bool {
4✔
197
                return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
4✔
198
        }
199

200
        /**
201
         * Returns the shared ability arguments merged with ability-specific arguments.
202
         *
203
         * @param array<string, array<string, string>> $ability_specific_args The ability-specific arguments.
204
         *
205
         * @return array<string, array<string, string>> The merged ability arguments.
206
         */
NEW
207
        private function get_shared_ability_args( array $ability_specific_args ): array {
×
NEW
208
                return \array_merge(
×
NEW
209
                        [
×
NEW
210
                                'category'            => 'yoast-seo',
×
NEW
211
                                'input_schema'        => $this->get_number_of_posts_schema(),
×
NEW
212
                                'permission_callback' => [ $this, 'can_read_scores' ],
×
NEW
213
                                'meta'                => [
×
NEW
214
                                        'show_in_rest' => true,
×
NEW
215
                                        'annotations'  => [
×
NEW
216
                                                'readonly'    => true,
×
NEW
217
                                                'destructive' => false,
×
NEW
218
                                                'idempotent'  => true,
×
NEW
219
                                        ],
×
NEW
220
                                        'mcp'          => [
×
NEW
221
                                                'public' => true,
×
NEW
222
                                        ],
×
NEW
223
                                ],
×
NEW
224
                        ],
×
NEW
225
                        $ability_specific_args,
×
NEW
226
                );
×
227
        }
228

229
        /**
230
         * Returns the number of posts input schema.
231
         *
232
         * @return array<string, array<string, string>> The number of posts schema.
233
         */
NEW
234
        private function get_number_of_posts_schema(): array {
×
NEW
235
                return [
×
NEW
236
                        'type'       => 'object',
×
NEW
237
                        'properties' => [
×
NEW
238
                                'number_of_posts' => [
×
NEW
239
                                        'type'        => 'integer',
×
NEW
240
                                        'description' => \__( 'The number of recently modified posts to retrieve scores for. Defaults to 10.', 'wordpress-seo' ),
×
NEW
241
                                        'minimum'     => 1,
×
NEW
242
                                        'default'     => 10,
×
NEW
243
                                ],
×
NEW
244
                        ],
×
NEW
245
                ];
×
246
        }
247

248
        /**
249
         * Wraps an item schema in an array schema.
250
         *
251
         * @param array<string, array<string, string>> $item_schema The item schema.
252
         *
253
         * @return array<string, array<string, string>> The array schema.
254
         */
NEW
255
        private function wrap_in_array_schema( array $item_schema ): array {
×
NEW
256
                return [
×
NEW
257
                        'type'  => 'array',
×
NEW
258
                        'items' => $item_schema,
×
NEW
259
                ];
×
260
        }
261

262
        /**
263
         * Returns the score output schema for a specific set of valid ratings, including the title property.
264
         *
265
         * @param array<string> $ratings The valid rating slugs for this score type.
266
         *
267
         * @return array<string, array<string, string>> The score output schema.
268
         */
NEW
269
        private function get_score_output_schema( array $ratings ): array {
×
NEW
270
                return [
×
NEW
271
                        'type'       => 'object',
×
NEW
272
                        'properties' => [
×
NEW
273
                                'title'  => [
×
NEW
274
                                        'type'        => 'string',
×
NEW
275
                                        'description' => \__( 'The post title.', 'wordpress-seo' ),
×
NEW
276
                                ],
×
NEW
277
                                'score'  => [
×
NEW
278
                                        'type'        => 'integer',
×
NEW
279
                                        'description' => \__( 'The numeric score from 0 to 100.', 'wordpress-seo' ),
×
NEW
280
                                ],
×
NEW
281
                                'rating' => [
×
NEW
282
                                        'type'        => 'string',
×
NEW
283
                                        'enum'        => $ratings,
×
NEW
284
                                        'description' => \__( 'The rating slug.', 'wordpress-seo' ),
×
NEW
285
                                ],
×
NEW
286
                                'label'  => [
×
NEW
287
                                        'type'        => 'string',
×
NEW
288
                                        'description' => \__( 'A human-readable label for the rating.', 'wordpress-seo' ),
×
NEW
289
                                ],
×
NEW
290
                        ],
×
NEW
291
                ];
×
292
        }
293

294
        /**
295
         * Returns the score sub-schema (without title) for use inside the all-scores ability.
296
         *
297
         * @param array<string> $ratings The valid rating slugs for this score type.
298
         *
299
         * @return array<string, array<string, string>> The score sub-schema.
300
         */
NEW
301
        private function get_score_sub_schema( array $ratings ): array {
×
NEW
302
                return [
×
NEW
303
                        'type'       => 'object',
×
NEW
304
                        'properties' => [
×
NEW
305
                                'score'  => [
×
NEW
306
                                        'type'        => 'integer',
×
NEW
307
                                        'description' => \__( 'The numeric score from 0 to 100.', 'wordpress-seo' ),
×
NEW
308
                                ],
×
NEW
309
                                'rating' => [
×
NEW
310
                                        'type'        => 'string',
×
NEW
311
                                        'enum'        => $ratings,
×
NEW
312
                                        'description' => \__( 'The rating slug.', 'wordpress-seo' ),
×
NEW
313
                                ],
×
NEW
314
                                'label'  => [
×
NEW
315
                                        'type'        => 'string',
×
NEW
316
                                        'description' => \__( 'A human-readable label for the rating.', 'wordpress-seo' ),
×
NEW
317
                                ],
×
NEW
318
                        ],
×
NEW
319
                ];
×
320
        }
321
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc