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

Yoast / wordpress-seo / 1919f0c903666bb0f071ce14ec484c94ca5a15c2

22 May 2026 02:28PM UTC coverage: 53.319% (-0.6%) from 53.945%
1919f0c903666bb0f071ce14ec484c94ca5a15c2

Pull #23287

github

web-flow
Merge 9e1b1932d into 290a54725
Pull Request #23287: feat(myyoast-client): add RFC 8707 resource indicator support

8912 of 16498 branches covered (54.02%)

Branch coverage included in aggregate %.

130 of 281 new or added lines in 10 files covered. (46.26%)

80 existing lines in 3 files now uncovered.

35926 of 67596 relevant lines covered (53.15%)

44501.21 hits per line

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

0.0
/src/myyoast-client/user-interface/auth-command.php
1
<?php
2

3
namespace Yoast\WP\SEO\MyYoast_Client\User_Interface;
4

5
use Exception;
6
use WP_CLI;
7
use WP_CLI\ExitException;
8
use WP_CLI\Utils;
9
use Yoast\WP\SEO\Commands\Command_Interface;
10
use Yoast\WP\SEO\Conditionals\MyYoast_Connection_Conditional;
11
use Yoast\WP\SEO\General\User_Interface\General_Page_Integration;
12
use Yoast\WP\SEO\Loadable_Interface;
13
use Yoast\WP\SEO\Main;
14
use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client;
15
use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client_Cleanup;
16
use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface;
17
use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Token_Storage_Interface;
18
use Yoast\WP\SEO\MyYoast_Client\Application\Ports\User_Token_Storage_Interface;
19
use Yoast\WP\SEO\MyYoast_Client\Domain\Exceptions\Invalid_Resource_Exception;
20
use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
21
use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
22
use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config;
23

24
/**
25
 * Manages the MyYoast OAuth client registration, tokens, and authorization.
26
 *
27
 * These commands are intended to be used with the global --user flag to set the
28
 * WordPress user context. For example: wp yoast auth status --user=admin
29
 */
30
final class Auth_Command implements Command_Interface, Loadable_Interface {
31

32
        /**
33
         * The MyYoast client facade.
34
         *
35
         * @var MyYoast_Client
36
         */
37
        private $myyoast_client;
38

39
        /**
40
         * The client registration port.
41
         *
42
         * @var Client_Registration_Interface
43
         */
44
        private $client_registration;
45

46
        /**
47
         * The issuer configuration.
48
         *
49
         * @var Issuer_Config
50
         */
51
        private $issuer_config;
52

53
        /**
54
         * The site-level token storage port.
55
         *
56
         * @var Token_Storage_Interface
57
         */
58
        private $token_storage;
59

60
        /**
61
         * The user-level token storage port.
62
         *
63
         * @var User_Token_Storage_Interface
64
         */
65
        private $user_token_storage;
66

67
        /**
68
         * The cleanup service.
69
         *
70
         * @var MyYoast_Client_Cleanup
71
         */
72
        private $cleanup;
73

74
        /**
75
         * Auth_Command constructor.
76
         *
77
         * @param MyYoast_Client                $myyoast_client      The MyYoast client facade.
78
         * @param Client_Registration_Interface $client_registration The client registration port.
79
         * @param Issuer_Config                 $issuer_config       The issuer configuration.
80
         * @param Token_Storage_Interface       $token_storage       The site-level token storage port.
81
         * @param User_Token_Storage_Interface  $user_token_storage  The user-level token storage port.
82
         * @param MyYoast_Client_Cleanup        $cleanup             The cleanup service.
83
         */
84
        public function __construct(
×
85
                MyYoast_Client $myyoast_client,
86
                Client_Registration_Interface $client_registration,
87
                Issuer_Config $issuer_config,
88
                Token_Storage_Interface $token_storage,
89
                User_Token_Storage_Interface $user_token_storage,
90
                MyYoast_Client_Cleanup $cleanup
91
        ) {
92
                $this->myyoast_client      = $myyoast_client;
×
93
                $this->client_registration = $client_registration;
×
94
                $this->issuer_config       = $issuer_config;
×
95
                $this->token_storage       = $token_storage;
×
96
                $this->user_token_storage  = $user_token_storage;
×
97
                $this->cleanup             = $cleanup;
×
98
        }
99

100
        /**
101
         * Returns the namespace of this command.
102
         *
103
         * @return string
104
         */
105
        public static function get_namespace() {
×
106
                return Main::WP_CLI_NAMESPACE . ' auth';
×
107
        }
108

109
        /**
110
         * Returns the conditionals based on which this command should be registered.
111
         *
112
         * @return array<string> The array of conditionals.
113
         */
114
        public static function get_conditionals() {
×
115
                return [ MyYoast_Connection_Conditional::class ];
×
116
        }
117

118
        /**
119
         * Shows the current MyYoast OAuth client status.
120
         *
121
         * Displays issuer configuration, registration state, and token status
122
         * without making any network calls. Use the global --user flag to check
123
         * a specific user's token status.
124
         *
125
         * ## OPTIONS
126
         *
127
         * [--resource=<uri>]
128
         * : Show status for a specific RFC 8707 resource indicator. Omit to target the default resource. Cannot be combined with --all-resources.
129
         *
130
         * [--all-resources]
131
         * : Show status for every stored resource bucket. Cannot be combined with --resource.
132
         *
133
         * [--format=<format>]
134
         * : Output format.
135
         * ---
136
         * default: table
137
         * options:
138
         *   - table
139
         *   - json
140
         * ---
141
         *
142
         * ## EXAMPLES
143
         *
144
         *     wp yoast auth status
145
         *     wp yoast auth status --user=admin
146
         *     wp yoast auth status --resource=https://ai.yoa.st
147
         *     wp yoast auth status --all-resources
148
         *     wp yoast auth status --format=json
149
         *
150
         * @when after_wp_load
151
         *
152
         * @param array<int, string>|null    $args       The arguments.
153
         * @param array<string, string>|null $assoc_args The associative arguments.
154
         *
155
         * @return void
156
         */
157
        public function status( $args = null, $assoc_args = null ): void {
×
158
                $user_id = \get_current_user_id();
×
159

160
                $issuer_url        = $this->issuer_config->get_issuer_url();
×
161
                $has_software      = ( $this->issuer_config->get_software_statement() !== '' );
×
NEW
162
                $has_iat           = ( $this->issuer_config->get_initial_access_token() !== '' );
×
NEW
163
                $is_registered     = $this->myyoast_client->is_registered();
×
NEW
164
                $client_id         = null;
×
NEW
165
                $registered_client = $this->client_registration->get_registered_client();
×
166

NEW
167
                if ( $registered_client !== null ) {
×
NEW
168
                        $client_id = $registered_client->get_client_id();
×
169
                }
170

NEW
171
                $has_all  = (bool) Utils\get_flag_value( $assoc_args, 'all-resources', false );
×
NEW
172
                $resource = Utils\get_flag_value( $assoc_args, 'resource' );
×
173

NEW
174
                if ( $has_all && $resource !== null && $resource !== '' ) {
×
NEW
175
                        WP_CLI::error( '--all-resources and --resource cannot be combined.' );
×
176
                }
177

NEW
178
                if ( $has_all ) {
×
NEW
179
                        $user_tokens = ( $user_id > 0 ) ? $this->user_token_storage->get_all( $user_id ) : [];
×
NEW
180
                        $site_tokens = $this->token_storage->get_all();
×
181
                }
182
                else {
183
                        try {
NEW
184
                                $resource_filter = new Resource_Indicator( ( $resource !== null && $resource !== '' ) ? (string) $resource : null );
×
185
                        }
NEW
186
                        catch ( Invalid_Resource_Exception $e ) {
×
UNCOV
187
                                WP_CLI::error( 'Invalid resource indicator: ' . $e->getMessage() );
×
188
                                return;
×
189
                        }
190

191
                        $user_tokens = ( $user_id > 0 ) ? \array_filter( [ $this->user_token_storage->get( $user_id, $resource_filter ) ] ) : [];
×
192
                        $site_tokens = \array_filter( [ $this->token_storage->get( $resource_filter ) ] );
×
193
                }
194

NEW
195
                $format = Utils\get_flag_value( $assoc_args, 'format', 'table' );
×
196

197
                $data = [
×
198
                        'issuer_url'           => $issuer_url,
×
NEW
199
                        'software_statement'   => ( $has_software ) ? 'configured' : 'not configured',
×
200
                        'initial_access_token' => ( $has_iat ) ? 'configured' : 'not configured',
×
201
                        'registered'           => ( $is_registered ) ? 'yes' : 'no',
×
202
                        'client_id'            => ( $client_id ?? '-' ),
×
203
                        'user_id'              => ( $user_id > 0 ) ? $user_id : 'none (use --user flag)',
×
204
                        'user_tokens'          => $this->build_token_inventory( $user_tokens ),
×
205
                        'site_tokens'          => $this->build_token_inventory( $site_tokens ),
×
206
                ];
×
207

208
                $this->output( $data, $format );
×
209
        }
210

211
        /**
212
         * Registers the site as an OAuth client.
213
         *
214
         * Performs Dynamic Client Registration (RFC 7591) if the site is not
215
         * already registered. Use --force to deregister and re-register.
216
         *
217
         * ## OPTIONS
218
         *
219
         * [--force]
220
         * : Deregister first, then re-register.
221
         *
222
         * [--format=<format>]
223
         * : Output format.
224
         * ---
225
         * default: table
226
         * options:
227
         *   - table
228
         *   - json
229
         * ---
230
         *
231
         * ## EXAMPLES
232
         *
233
         *     wp yoast auth register
234
         *     wp yoast auth register --force
235
         *
236
         * @when after_wp_load
237
         *
238
         * @param array<int, string>|null    $args       The arguments.
239
         * @param array<string, string>|null $assoc_args The associative arguments.
240
         *
241
         * @return void
242
         *
243
         * @throws ExitException When registration fails.
244
         */
245
        public function register( $args = null, $assoc_args = null ): void {
×
246
                if ( Utils\get_flag_value( $assoc_args, 'force', false ) ) {
×
247
                        $this->myyoast_client->deregister();
×
248
                        WP_CLI::log( 'Deregistered existing client.' );
×
249
                }
250

251
                try {
252
                        $redirect_uri = \get_admin_url( null, 'admin.php?page=' . General_Page_Integration::PAGE . '&yoast_myyoast_oauth_callback=1' );
×
253
                        $client       = $this->myyoast_client->ensure_registered( [ $redirect_uri ] );
×
254
                } catch ( Exception $e ) {
×
NEW
255
                        WP_CLI::error( 'Registration failed: ' . $e->getMessage() );
×
256
                        return;
×
257
                }
258

259
                $this->output(
×
260
                        [
×
261
                                'client_id' => $client->get_client_id(),
×
262
                                'status'    => 'registered',
×
263
                        ],
×
264
                        Utils\get_flag_value( $assoc_args, 'format', 'table' ),
×
265
                );
×
266

267
                WP_CLI::success( 'Client registered: ' . $client->get_client_id() );
×
268
        }
269

270
        /**
271
         * Verifies the client registration with the server.
272
         *
273
         * Reads the current registration from the authorization server to
274
         * confirm it is still valid and shows the registration metadata.
275
         *
276
         * ## OPTIONS
277
         *
278
         * [--format=<format>]
279
         * : Output format.
280
         * ---
281
         * default: table
282
         * options:
283
         *   - table
284
         *   - json
285
         * ---
286
         *
287
         * ## EXAMPLES
288
         *
289
         *     wp yoast auth verify
290
         *     wp yoast auth verify --format=json
291
         *
292
         * @when after_wp_load
293
         *
294
         * @param array<int, string>|null    $args       The arguments.
295
         * @param array<string, string>|null $assoc_args The associative arguments.
296
         *
297
         * @return void
298
         *
299
         * @throws ExitException When verification fails.
300
         */
301
        public function verify( $args = null, $assoc_args = null ): void {
×
302
                if ( ! $this->myyoast_client->is_registered() ) {
×
303
                        WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' );
×
304
                }
305

306
                try {
NEW
307
                        $metadata = $this->myyoast_client->verify_registration();
×
308
                } catch ( Exception $e ) {
×
309
                        WP_CLI::error( 'Verification failed: ' . $e->getMessage() );
×
310
                        return;
×
311
                }
312

313
                // Redact sensitive fields.
314
                unset( $metadata['registration_access_token'] );
×
315

316
                $this->output( $metadata, Utils\get_flag_value( $assoc_args, 'format', 'table' ) );
×
317

318
                WP_CLI::success( 'Registration is valid.' );
×
319
        }
320

321
        /**
322
         * Removes the OAuth client registration.
323
         *
324
         * Deletes the client registration from the authorization server and
325
         * clears all local registration data and cached tokens.
326
         *
327
         * ## OPTIONS
328
         *
329
         * [--local-only]
330
         * : Only delete local data without contacting the server.
331
         *
332
         * [--yes]
333
         * : Skip confirmation prompt.
334
         *
335
         * ## EXAMPLES
336
         *
337
         *     wp yoast auth deregister
338
         *     wp yoast auth deregister --local-only
339
         *     wp yoast auth deregister --yes
340
         *
341
         * @when after_wp_load
342
         *
343
         * @param array<int, string>|null    $args       The arguments.
344
         * @param array<string, string>|null $assoc_args The associative arguments.
345
         *
346
         * @return void
347
         */
348
        public function deregister( $args = null, $assoc_args = null ): void {
×
NEW
349
                if ( ! $this->myyoast_client->is_registered() ) {
×
350
                        WP_CLI::warning( 'Not registered. Nothing to do.' );
×
351
                        return;
×
352
                }
353

354
                WP_CLI::confirm( 'This will deregister this site from MyYoast and clear all cached tokens. Proceed?', $assoc_args );
×
355

356
                if ( Utils\get_flag_value( $assoc_args, 'local-only', false ) ) {
×
357
                        $this->client_registration->delete_local_data();
×
358
                        $this->myyoast_client->clear_all_site_tokens();
×
359
                        WP_CLI::success( 'Local registration data cleared.' );
×
360
                        return;
×
361
                }
362

363
                $result = $this->myyoast_client->deregister();
×
364
                $this->myyoast_client->clear_all_site_tokens();
×
365

366
                if ( $result ) {
×
367
                        WP_CLI::success( 'Client deregistered.' );
×
368
                }
369
                else {
370
                        WP_CLI::warning( 'Server-side deregistration failed (network error). Local token was cleared but client credentials remain.' );
×
371
                }
372
        }
373

374
        /**
375
         * Resets all MyYoast OAuth client state on this site.
376
         *
377
         * Performs the same cleanup as plugin uninstall: best-effort server-side
378
         * deregistration, then deletes all site/user tokens, registered client
379
         * credentials, key pairs, and OIDC/JWKS/DPoP caches. Intended for
380
         * development environments that need to start from a clean slate without
381
         * uninstalling the plugin.
382
         *
383
         * ## OPTIONS
384
         *
385
         * [--yes]
386
         * : Skip confirmation prompt.
387
         *
388
         * ## EXAMPLES
389
         *
390
         *     wp yoast auth reset
391
         *     wp yoast auth reset --yes
392
         *
393
         * @when after_wp_load
394
         *
395
         * @param array<int, string>|null    $args       The arguments.
396
         * @param array<string, string>|null $assoc_args The associative arguments.
397
         *
398
         * @return void
399
         */
UNCOV
400
        public function reset( $args = null, $assoc_args = null ): void {
×
UNCOV
401
                WP_CLI::confirm( 'This will wipe all MyYoast OAuth client state on this site (registered client, site/user tokens, key pairs, OIDC/JWKS/DPoP caches). Proceed?', $assoc_args );
×
402

403
                $this->cleanup->execute();
×
404

UNCOV
405
                WP_CLI::success( 'MyYoast OAuth client state cleared.' );
×
406
        }
407

408
        /**
409
         * Authorizes with MyYoast using the authorization code flow or client credentials.
410
         *
411
         * Without --site, starts the user authorization code flow:
412
         * 1. Run without --code to get the authorization URL.
413
         * 2. Visit the URL in a browser and authorize.
414
         * 3. Copy the code and state from the callback URL.
415
         * 4. Run again with --code and --state to exchange for tokens.
416
         *
417
         * With --site, performs a client_credentials grant for a site-level token.
418
         *
419
         * ## OPTIONS
420
         *
421
         * [--site]
422
         * : Use client_credentials grant for a site-level token (no browser needed).
423
         *
424
         * [--scopes=<scopes>]
425
         * : Comma-separated scopes to request.
426
         *
427
         * [--resource=<uri>]
428
         * : RFC 8707 resource indicator to bind the token to (e.g. https://ai.yoa.st). Omit for the default resource.
429
         *
430
         * [--code=<code>]
431
         * : Authorization code from the callback URL (user flow phase 2).
432
         *
433
         * [--state=<state>]
434
         * : State parameter from the callback URL (user flow phase 2).
435
         *
436
         * [--url-only]
437
         * : Only print the authorization URL without instructions (user flow phase 1).
438
         *
439
         * [--format=<format>]
440
         * : Output format.
441
         * ---
442
         * default: table
443
         * options:
444
         *   - table
445
         *   - json
446
         * ---
447
         *
448
         * ## EXAMPLES
449
         *
450
         *     # Site-level token (client_credentials):
451
         *     wp yoast auth authorize --site --scopes=service:analytics
452
         *
453
         *     # Site-level token for a non-default resource:
454
         *     wp yoast auth authorize --site --resource=https://ai.yoa.st --scopes=service:ai:consume
455
         *
456
         *     # User authorization code flow, phase 1 - get the URL:
457
         *     wp yoast auth authorize --user=admin --scopes=openid,profile
458
         *
459
         *     # User authorization code flow, phase 2 - exchange the code:
460
         *     wp yoast auth authorize --user=admin --code=abc123 --state=xyz789
461
         *
462
         * @when after_wp_load
463
         *
464
         * @param array<int, string>|null    $args       The arguments.
465
         * @param array<string, string>|null $assoc_args The associative arguments.
466
         *
467
         * @return void
468
         *
469
         * @throws ExitException When authorization fails.
470
         */
UNCOV
471
        public function authorize( $args = null, $assoc_args = null ): void {
×
UNCOV
472
                $scopes             = $this->parse_scopes( $assoc_args );
×
UNCOV
473
                $format             = Utils\get_flag_value( $assoc_args, 'format', 'table' );
×
UNCOV
474
                $resource           = Utils\get_flag_value( $assoc_args, 'resource' );
×
UNCOV
475
                $resource_indicator = ( $resource !== null && $resource !== '' ) ? (string) $resource : null;
×
476

UNCOV
477
                if ( Utils\get_flag_value( $assoc_args, 'site', false ) ) {
×
UNCOV
478
                        $this->authorize_site( $scopes, $resource_indicator, $format );
×
NEW
479
                        return;
×
480
                }
481

NEW
482
                $this->authorize_user( $assoc_args, $scopes, $resource_indicator, $format );
×
483
        }
484

485
        /**
486
         * Revokes tokens for the current user and/or the site.
487
         *
488
         * Without --site, revokes the current user's tokens (requires --user flag).
489
         * With --site, clears the cached site-level token.
490
         * Both can be combined.
491
         *
492
         * ## OPTIONS
493
         *
494
         * [--site]
495
         * : Clear the cached site-level token.
496
         *
497
         * [--resource=<uri>]
498
         * : Limit revocation to a single RFC 8707 resource indicator. Omit to target the default resource.
499
         *
500
         * [--all-resources]
501
         * : Revoke every stored token across all resource indicators. Cannot be combined with --resource.
502
         *
503
         * [--yes]
504
         * : Skip confirmation prompt.
505
         *
506
         * ## EXAMPLES
507
         *
508
         *     wp yoast auth revoke --user=admin
509
         *     wp yoast auth revoke --site
510
         *     wp yoast auth revoke --user=admin --site --yes
511
         *     wp yoast auth revoke --user=admin --resource=https://ai.yoa.st
512
         *     wp yoast auth revoke --user=admin --site --all-resources
513
         *
514
         * @when after_wp_load
515
         *
516
         * @param array<int, string>|null    $args       The arguments.
517
         * @param array<string, string>|null $assoc_args The associative arguments.
518
         *
519
         * @return void
520
         */
NEW
521
        public function revoke( $args = null, $assoc_args = null ): void {
×
NEW
522
                $user_id           = \get_current_user_id();
×
NEW
523
                $has_user          = ( $user_id > 0 );
×
NEW
524
                $has_site          = (bool) Utils\get_flag_value( $assoc_args, 'site', false );
×
NEW
525
                $has_all_resources = (bool) Utils\get_flag_value( $assoc_args, 'all-resources', false );
×
NEW
526
                $resource          = Utils\get_flag_value( $assoc_args, 'resource' );
×
527

NEW
528
                if ( ! $has_site && ! $has_user ) {
×
NEW
529
                        WP_CLI::error( 'Specify --site and/or use the global --user flag.' );
×
530
                }
531

NEW
532
                if ( $has_all_resources && $resource !== null && $resource !== '' ) {
×
UNCOV
533
                        WP_CLI::error( '--all-resources and --resource cannot be combined.' );
×
534
                }
535

UNCOV
536
                $resource_indicator = null;
×
UNCOV
537
                if ( $resource !== null && $resource !== '' ) {
×
UNCOV
538
                        $resource_indicator = (string) $resource;
×
539
                        try {
UNCOV
540
                                new Resource_Indicator( $resource_indicator );
×
541
                        }
UNCOV
542
                        catch ( Invalid_Resource_Exception $e ) {
×
UNCOV
543
                                WP_CLI::error( 'Invalid resource indicator: ' . $e->getMessage() );
×
UNCOV
544
                                return;
×
545
                        }
546
                }
547

UNCOV
548
                WP_CLI::confirm( 'This will revoke the specified tokens. Proceed?', $assoc_args );
×
549

550
                try {
UNCOV
551
                        if ( $has_user ) {
×
UNCOV
552
                                if ( $has_all_resources ) {
×
UNCOV
553
                                        $this->myyoast_client->revoke_all_user_tokens( $user_id );
×
UNCOV
554
                                        WP_CLI::log( \sprintf( 'User %d tokens revoked across all resources.', $user_id ) );
×
555
                                }
556
                                else {
557
                                        $this->myyoast_client->revoke_user_token( $user_id, $resource_indicator );
×
558
                                        WP_CLI::log( \sprintf( 'User %d tokens revoked.', $user_id ) );
×
559
                                }
560
                        }
561

562
                        if ( $has_site ) {
×
563
                                if ( $has_all_resources ) {
×
UNCOV
564
                                        $this->myyoast_client->clear_all_site_tokens();
×
UNCOV
565
                                        WP_CLI::log( 'All site tokens cleared.' );
×
566
                                }
567
                                else {
568
                                        $this->myyoast_client->clear_site_token( $resource_indicator );
×
569
                                        WP_CLI::log( 'Site token cleared.' );
×
570
                                }
571
                        }
572
                }
573
                catch ( Exception $e ) {
×
NEW
574
                        WP_CLI::error( 'Revocation failed: ' . $e->getMessage() );
×
NEW
575
                        return;
×
576
                }
577

UNCOV
578
                WP_CLI::success( 'Done.' );
×
579
        }
580

581
        /**
582
         * Rotates cryptographic key pairs.
583
         *
584
         * Rotates the registration key pair (server roundtrip) and/or the DPoP
585
         * key pair (local only).
586
         *
587
         * ## OPTIONS
588
         *
589
         * [--registration]
590
         * : Rotate the registration (private_key_jwt) key pair. Requires a server roundtrip.
591
         *
592
         * [--dpop]
593
         * : Rotate the DPoP proof key pair (local only).
594
         *
595
         * [--all]
596
         * : Rotate all key pairs.
597
         *
598
         * [--yes]
599
         * : Skip confirmation prompt.
600
         *
601
         * ## EXAMPLES
602
         *
603
         *     wp yoast auth rotate-keys --registration
604
         *     wp yoast auth rotate-keys --dpop
605
         *     wp yoast auth rotate-keys --all
606
         *
607
         * @when after_wp_load
608
         *
609
         * @param array<int, string>|null    $args       The arguments.
610
         * @param array<string, string>|null $assoc_args The associative arguments.
611
         *
612
         * @return void
613
         *
614
         * @throws ExitException When key rotation fails.
615
         */
UNCOV
616
        public function rotate_keys( $args = null, $assoc_args = null ): void {
×
NEW
617
                $rotate_all          = (bool) Utils\get_flag_value( $assoc_args, 'all', false );
×
618
                $rotate_registration = ( $rotate_all || (bool) Utils\get_flag_value( $assoc_args, 'registration', false ) );
×
NEW
619
                $rotate_dpop         = ( $rotate_all || (bool) Utils\get_flag_value( $assoc_args, 'dpop', false ) );
×
620

UNCOV
621
                if ( ! $rotate_registration && ! $rotate_dpop ) {
×
622
                        WP_CLI::error( 'Specify --registration, --dpop, or --all.' );
×
623
                }
624

UNCOV
625
                WP_CLI::confirm( 'This will rotate the specified key pairs. Existing tokens may be invalidated. Proceed?', $assoc_args );
×
626

UNCOV
627
                if ( $rotate_registration ) {
×
628
                        if ( ! $this->myyoast_client->is_registered() ) {
×
629
                                WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' );
×
630
                        }
631

632
                        try {
NEW
633
                                $client = $this->myyoast_client->rotate_registration_keys();
×
NEW
634
                                WP_CLI::log( 'Registration keys rotated. Client ID: ' . $client->get_client_id() );
×
NEW
635
                        } catch ( Exception $e ) {
×
NEW
636
                                WP_CLI::error( 'Registration key rotation failed: ' . $e->getMessage() );
×
637
                                return;
×
638
                        }
639
                }
640

UNCOV
641
                if ( $rotate_dpop ) {
×
NEW
642
                        $this->myyoast_client->rotate_dpop_keys();
×
643
                        WP_CLI::log( 'DPoP keys rotated.' );
×
644
                }
645

UNCOV
646
                WP_CLI::success( 'Key rotation complete.' );
×
647
        }
648

649
        /**
650
         * Performs a client_credentials grant for a site-level token.
651
         *
652
         * @param string[]    $scopes             The scopes to request.
653
         * @param string|null $resource_indicator The resource indicator (RFC 8707), or null for the default resource.
654
         * @param string      $format             The output format.
655
         *
656
         * @return void
657
         *
658
         * @throws ExitException When the token request fails.
659
         */
UNCOV
660
        private function authorize_site( array $scopes, ?string $resource_indicator, string $format ): void {
×
661
                try {
UNCOV
662
                        $token_set = $this->myyoast_client->get_site_token( $scopes, $resource_indicator );
×
663
                } catch ( Exception $e ) {
×
664
                        WP_CLI::error( 'Site token request failed: ' . $e->getMessage() );
×
665
                        return;
×
666
                }
667

UNCOV
668
                $this->output( $this->build_token_info( $token_set ), $format );
×
669

NEW
670
                WP_CLI::success( 'Site token obtained.' );
×
671
        }
672

673
        /**
674
         * Handles the user authorization code flow.
675
         *
676
         * @param array<string, string> $assoc_args         The associative arguments.
677
         * @param string[]              $scopes             The scopes to request.
678
         * @param string|null           $resource_indicator The resource indicator (RFC 8707), or null for the default resource.
679
         * @param string                $format             The output format.
680
         *
681
         * @return void
682
         *
683
         * @throws ExitException When authorization fails.
684
         */
685
        private function authorize_user( array $assoc_args, array $scopes, ?string $resource_indicator, string $format ): void {
×
686
                $user_id = \get_current_user_id();
×
687
                if ( $user_id <= 0 ) {
×
688
                        WP_CLI::error( 'User authorization requires the global --user flag.' );
×
689
                }
690

691
                $code  = Utils\get_flag_value( $assoc_args, 'code' );
×
692
                $state = Utils\get_flag_value( $assoc_args, 'state' );
×
693

694
                // Phase 2: exchange the code. The resource was persisted in the flow state during phase 1.
695
                if ( $code !== null && $state !== null ) {
×
696
                        try {
697
                                $token_set = $this->myyoast_client->exchange_authorization_code(
×
698
                                        $user_id,
×
699
                                        (string) $code,
×
700
                                        (string) $state,
×
701
                                );
×
702
                        } catch ( Exception $e ) {
×
703
                                WP_CLI::error( 'Code exchange failed: ' . $e->getMessage() );
×
704
                                return;
×
705
                        }
706

707
                        $this->output( $this->build_token_info( $token_set ), $format );
×
708

709
                        WP_CLI::success( 'User authorized.' );
×
710
                        return;
×
711
                }
712

713
                if ( $code !== null || $state !== null ) {
×
NEW
714
                        WP_CLI::error( 'Both --code and --state are required for code exchange.' );
×
715
                }
716

717
                // Phase 1: generate the authorization URL.
UNCOV
718
                $redirect_uri = \get_admin_url( null, 'admin.php?page=' . General_Page_Integration::PAGE . '&yoast_myyoast_oauth_callback=1' );
×
719

720
                try {
UNCOV
721
                        $url = $this->myyoast_client->get_authorization_url( $user_id, $redirect_uri, $scopes, $resource_indicator );
×
722
                } catch ( Exception $e ) {
×
NEW
723
                        WP_CLI::error( 'Failed to generate authorization URL: ' . $e->getMessage() );
×
724
                        return;
×
725
                }
726

UNCOV
727
                if ( Utils\get_flag_value( $assoc_args, 'url-only', false ) ) {
×
UNCOV
728
                        WP_CLI::log( $url );
×
729
                        return;
×
730
                }
731

NEW
732
                WP_CLI::log( 'Visit this URL to authorize:' );
×
NEW
733
                WP_CLI::log( '' );
×
NEW
734
                WP_CLI::log( $url );
×
NEW
735
                WP_CLI::log( '' );
×
NEW
736
                WP_CLI::log( 'After authorizing, you will be redirected to a callback URL.' );
×
NEW
737
                WP_CLI::log( 'If you need to manually complete authorization, copy the "code" and "state" parameters from the URL, then run:' );
×
NEW
738
                WP_CLI::log( '' );
×
NEW
739
                WP_CLI::log(
×
NEW
740
                        \sprintf(
×
NEW
741
                                '  wp yoast auth authorize --user=%s --code=<CODE> --state=<STATE>',
×
NEW
742
                                $user_id,
×
NEW
743
                        ),
×
NEW
744
                );
×
745
        }
746

747
        /**
748
         * Builds a display-safe token info array.
749
         *
750
         * @param Token_Set|null $token_set The token set, or null.
751
         *
752
         * @return array<string, string|int> The token info for display.
753
         */
NEW
754
        private function build_token_info( ?Token_Set $token_set ): array {
×
NEW
755
                if ( $token_set === null ) {
×
756
                        return [
×
757
                                'resource'    => '-',
×
758
                                'status'      => 'none',
×
NEW
759
                                'expires'     => '-',
×
760
                                'scopes'      => '-',
×
761
                                'error_count' => '-',
×
762
                        ];
×
763
                }
764

UNCOV
765
                return [
×
UNCOV
766
                        'resource'    => ( $token_set->get_resource_indicator()->is_default() ? '(default)' : $token_set->get_resource_indicator()->value() ),
×
UNCOV
767
                        'status'      => ( $token_set->is_expired() ) ? 'expired' : 'valid',
×
UNCOV
768
                        'expires'     => \gmdate( 'Y-m-d H:i:s', $token_set->get_expires_at() ) . ' UTC',
×
UNCOV
769
                        'scopes'      => ( $token_set->get_scope() ?? '-' ),
×
UNCOV
770
                        'error_count' => $token_set->get_error_count(),
×
UNCOV
771
                ];
×
772
        }
773

774
        /**
775
         * Builds a display-safe inventory of tokens across resource buckets.
776
         *
777
         * @param Token_Set[] $token_sets The token sets.
778
         *
779
         * @return array<int, array<string, string|int>>|string The inventory, or a placeholder when empty.
780
         */
UNCOV
781
        private function build_token_inventory( array $token_sets ) {
×
782
                $inventory = [];
×
783
                foreach ( $token_sets as $token_set ) {
×
784
                        $inventory[] = $this->build_token_info( $token_set );
×
785
                }
786
                return $inventory;
×
787
        }
788

789
        /**
790
         * Parses the comma-separated scopes option.
791
         *
792
         * @param array<string, string>|null $assoc_args The associative arguments.
793
         *
794
         * @return string[] The parsed scopes.
795
         */
UNCOV
796
        private function parse_scopes( $assoc_args ): array {
×
797
                $scopes = Utils\get_flag_value( $assoc_args, 'scopes', '' );
×
798
                if ( $scopes === '' ) {
×
799
                        return [];
×
800
                }
801

NEW
802
                return \array_values( \array_filter( \array_map( 'trim', \explode( ',', (string) $scopes ) ) ) );
×
803
        }
804

805
        /**
806
         * Flattens nested arrays for table display by JSON-encoding array values.
807
         *
808
         * @param array<string, string|string[]|bool> $data The data to flatten.
809
         *
810
         * @return array<string, string> The flattened data.
811
         */
UNCOV
812
        private function flatten_for_display( array $data ): array {
×
813
                $result = [];
×
814
                foreach ( $data as $key => $value ) {
×
815
                        if ( \is_array( $value ) ) {
×
816
                                // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- WP-CLI display output, not user-facing HTML.
UNCOV
817
                                $result[ $key ] = ( \wp_json_encode( $value ) ?? 'err' );
×
818
                        }
UNCOV
819
                        elseif ( \is_bool( $value ) ) {
×
820
                                $result[ $key ] = ( $value ) ? 'true' : 'false';
×
821
                        }
822
                        else {
UNCOV
823
                                $result[ $key ] = (string) $value;
×
824
                        }
825
                }
UNCOV
826
                return $result;
×
827
        }
828

829
        /**
830
         * Outputs data in the requested format.
831
         *
832
         * @param array<string, string|int> $data   The data to output.
833
         * @param string                    $format The output format (table or json).
834
         *
835
         * @return void
836
         */
UNCOV
837
        private function output( array $data, string $format ): void {
×
838
                if ( $format === 'json' ) {
×
839
                        // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.FoundWithAdditionalParams -- CLI output, not user-facing HTML.
UNCOV
840
                        $encoded = \wp_json_encode( $data, \JSON_PRETTY_PRINT );
×
841
                        WP_CLI::log( ( $encoded !== false ) ? $encoded : '{}' );
×
842
                        return;
×
843
                }
844

UNCOV
845
                $flat_data = $this->flatten_for_display( $data );
×
846
                $items     = [];
×
847
                foreach ( $flat_data as $key => $value ) {
×
848
                        $items[] = [
×
849
                                'field' => $key,
×
850
                                'value' => $value,
×
851
                        ];
×
852
                }
853

854
                WP_CLI\Utils\format_items( 'table', $items, [ 'field', 'value' ] );
×
855
        }
856
}
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