PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.7
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.7
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / myyoast-client / user-interface / auth-command.php

auth-command.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.7, at src/myyoast-client/user-interface/auth-command.php

756 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 Yoast\WP\SEO\Commands\Command_Interface;
9 use Yoast\WP\SEO\Conditionals\MyYoast_Connection_Conditional;
10 use Yoast\WP\SEO\General\User_Interface\General_Page_Integration;
11 use Yoast\WP\SEO\Loadable_Interface;
12 use Yoast\WP\SEO\Main;
13 use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client;
14 use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client_Cleanup;
15 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface;
16 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Token_Storage_Interface;
17 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\User_Token_Storage_Interface;
18 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
19 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config;
20
21 /**
22 * Manages the MyYoast OAuth client registration, tokens, and authorization.
23 *
24 * These commands are intended to be used with the global --user flag to set the
25 * WordPress user context. For example: wp yoast auth status --user=admin
26 */
27 final class Auth_Command implements Command_Interface, Loadable_Interface {
28
29 /**
30 * The MyYoast client facade.
31 *
32 * @var MyYoast_Client
33 */
34 private $myyoast_client;
35
36 /**
37 * The client registration port.
38 *
39 * @var Client_Registration_Interface
40 */
41 private $client_registration;
42
43 /**
44 * The issuer configuration.
45 *
46 * @var Issuer_Config
47 */
48 private $issuer_config;
49
50 /**
51 * The site-level token storage port.
52 *
53 * @var Token_Storage_Interface
54 */
55 private $token_storage;
56
57 /**
58 * The user-level token storage port.
59 *
60 * @var User_Token_Storage_Interface
61 */
62 private $user_token_storage;
63
64 /**
65 * The cleanup service.
66 *
67 * @var MyYoast_Client_Cleanup
68 */
69 private $cleanup;
70
71 /**
72 * Auth_Command constructor.
73 *
74 * @param MyYoast_Client $myyoast_client The MyYoast client facade.
75 * @param Client_Registration_Interface $client_registration The client registration port.
76 * @param Issuer_Config $issuer_config The issuer configuration.
77 * @param Token_Storage_Interface $token_storage The site-level token storage port.
78 * @param User_Token_Storage_Interface $user_token_storage The user-level token storage port.
79 * @param MyYoast_Client_Cleanup $cleanup The cleanup service.
80 */
81 public function __construct(
82 MyYoast_Client $myyoast_client,
83 Client_Registration_Interface $client_registration,
84 Issuer_Config $issuer_config,
85 Token_Storage_Interface $token_storage,
86 User_Token_Storage_Interface $user_token_storage,
87 MyYoast_Client_Cleanup $cleanup
88 ) {
89 $this->myyoast_client = $myyoast_client;
90 $this->client_registration = $client_registration;
91 $this->issuer_config = $issuer_config;
92 $this->token_storage = $token_storage;
93 $this->user_token_storage = $user_token_storage;
94 $this->cleanup = $cleanup;
95 }
96
97 /**
98 * Returns the namespace of this command.
99 *
100 * @return string
101 */
102 public static function get_namespace() {
103 return Main::WP_CLI_NAMESPACE . ' auth';
104 }
105
106 /**
107 * Returns the conditionals based on which this command should be registered.
108 *
109 * @return array<string> The array of conditionals.
110 */
111 public static function get_conditionals() {
112 return [ MyYoast_Connection_Conditional::class ];
113 }
114
115 /**
116 * Shows the current MyYoast OAuth client status.
117 *
118 * Displays issuer configuration, registration state, and token status
119 * without making any network calls. Use the global --user flag to check
120 * a specific user's token status.
121 *
122 * ## OPTIONS
123 *
124 * [--format=<format>]
125 * : Output format.
126 * ---
127 * default: table
128 * options:
129 * - table
130 * - json
131 * ---
132 *
133 * ## EXAMPLES
134 *
135 * wp yoast auth status
136 * wp yoast auth status --user=admin
137 * wp yoast auth status --format=json
138 *
139 * @when after_wp_load
140 *
141 * @param array<int, string>|null $args The arguments.
142 * @param array<string, string>|null $assoc_args The associative arguments.
143 *
144 * @return void
145 */
146 public function status( $args = null, $assoc_args = null ): void {
147 $user_id = \get_current_user_id();
148
149 $issuer_url = $this->issuer_config->get_issuer_url();
150 $has_software = ( $this->issuer_config->get_software_statement() !== '' );
151 $has_iat = ( $this->issuer_config->get_initial_access_token() !== '' );
152 $is_registered = $this->myyoast_client->is_registered();
153 $client_id = null;
154 $registered_client = $this->client_registration->get_registered_client();
155
156 if ( $registered_client !== null ) {
157 $client_id = $registered_client->get_client_id();
158 }
159
160 $site_token = $this->token_storage->get();
161 $site_token_info = $this->build_token_info( $site_token );
162
163 $user_token = ( $user_id > 0 ) ? $this->user_token_storage->get( $user_id ) : null;
164 $user_token_info = $this->build_token_info( $user_token );
165
166 $data = [
167 'issuer_url' => $issuer_url,
168 'software_statement' => ( $has_software ) ? 'configured' : 'not configured',
169 'initial_access_token' => ( $has_iat ) ? 'configured' : 'not configured',
170 'registered' => ( $is_registered ) ? 'yes' : 'no',
171 'client_id' => ( $client_id ?? '-' ),
172 'site_token' => $site_token_info['status'],
173 'site_token_expires' => $site_token_info['expires'],
174 'site_token_scopes' => $site_token_info['scopes'],
175 'user_id' => ( $user_id > 0 ) ? $user_id : 'none (use --user flag)',
176 'user_token' => $user_token_info['status'],
177 'user_token_expires' => $user_token_info['expires'],
178 'user_token_scopes' => $user_token_info['scopes'],
179 'user_token_errors' => $user_token_info['error_count'],
180 ];
181
182 $this->output( $data, $assoc_args['format'] );
183 }
184
185 /**
186 * Registers the site as an OAuth client.
187 *
188 * Performs Dynamic Client Registration (RFC 7591) if the site is not
189 * already registered. Use --force to deregister and re-register.
190 *
191 * ## OPTIONS
192 *
193 * [--force]
194 * : Deregister first, then re-register.
195 *
196 * [--format=<format>]
197 * : Output format.
198 * ---
199 * default: table
200 * options:
201 * - table
202 * - json
203 * ---
204 *
205 * ## EXAMPLES
206 *
207 * wp yoast auth register
208 * wp yoast auth register --force
209 *
210 * @when after_wp_load
211 *
212 * @param array<int, string>|null $args The arguments.
213 * @param array<string, string>|null $assoc_args The associative arguments.
214 *
215 * @return void
216 *
217 * @throws ExitException When registration fails.
218 */
219 public function register( $args = null, $assoc_args = null ): void {
220 if ( isset( $assoc_args['force'] ) ) {
221 $this->myyoast_client->deregister();
222 WP_CLI::log( 'Deregistered existing client.' );
223 }
224
225 try {
226 $redirect_uri = \get_admin_url( null, 'admin.php?page=' . General_Page_Integration::PAGE . '&yoast_myyoast_oauth_callback=1' );
227 $client = $this->myyoast_client->ensure_registered( [ $redirect_uri ] );
228 } catch ( Exception $e ) {
229 WP_CLI::error( 'Registration failed: ' . $e->getMessage() );
230 return;
231 }
232
233 $this->output(
234 [
235 'client_id' => $client->get_client_id(),
236 'status' => 'registered',
237 ],
238 $assoc_args['format'],
239 );
240
241 WP_CLI::success( 'Client registered: ' . $client->get_client_id() );
242 }
243
244 /**
245 * Verifies the client registration with the server.
246 *
247 * Reads the current registration from the authorization server to
248 * confirm it is still valid and shows the registration metadata.
249 *
250 * ## OPTIONS
251 *
252 * [--format=<format>]
253 * : Output format.
254 * ---
255 * default: table
256 * options:
257 * - table
258 * - json
259 * ---
260 *
261 * ## EXAMPLES
262 *
263 * wp yoast auth verify
264 * wp yoast auth verify --format=json
265 *
266 * @when after_wp_load
267 *
268 * @param array<int, string>|null $args The arguments.
269 * @param array<string, string>|null $assoc_args The associative arguments.
270 *
271 * @return void
272 *
273 * @throws ExitException When verification fails.
274 */
275 public function verify( $args = null, $assoc_args = null ): void {
276 if ( ! $this->myyoast_client->is_registered() ) {
277 WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' );
278 }
279
280 try {
281 $metadata = $this->myyoast_client->verify_registration();
282 } catch ( Exception $e ) {
283 WP_CLI::error( 'Verification failed: ' . $e->getMessage() );
284 return;
285 }
286
287 // Redact sensitive fields.
288 unset( $metadata['registration_access_token'] );
289
290 $this->output( $this->flatten_for_display( $metadata ), $assoc_args['format'] );
291
292 WP_CLI::success( 'Registration is valid.' );
293 }
294
295 /**
296 * Removes the OAuth client registration.
297 *
298 * Deletes the client registration from the authorization server and
299 * clears all local registration data and cached tokens.
300 *
301 * ## OPTIONS
302 *
303 * [--local-only]
304 * : Only delete local data without contacting the server.
305 *
306 * [--yes]
307 * : Skip confirmation prompt.
308 *
309 * ## EXAMPLES
310 *
311 * wp yoast auth deregister
312 * wp yoast auth deregister --local-only
313 * wp yoast auth deregister --yes
314 *
315 * @when after_wp_load
316 *
317 * @param array<int, string>|null $args The arguments.
318 * @param array<string, string>|null $assoc_args The associative arguments.
319 *
320 * @return void
321 */
322 public function deregister( $args = null, $assoc_args = null ): void {
323 if ( ! $this->myyoast_client->is_registered() ) {
324 WP_CLI::warning( 'Not registered. Nothing to do.' );
325 return;
326 }
327
328 WP_CLI::confirm( 'This will deregister this site from MyYoast and clear all cached tokens. Proceed?', $assoc_args );
329
330 if ( isset( $assoc_args['local-only'] ) ) {
331 $this->client_registration->delete_local_data();
332 $this->myyoast_client->clear_site_token();
333 WP_CLI::success( 'Local registration data cleared.' );
334 return;
335 }
336
337 $result = $this->myyoast_client->deregister();
338 $this->myyoast_client->clear_site_token();
339
340 if ( $result ) {
341 WP_CLI::success( 'Client deregistered.' );
342 }
343 else {
344 WP_CLI::warning( 'Server-side deregistration failed (network error). Local token was cleared but client credentials remain.' );
345 }
346 }
347
348 /**
349 * Resets all MyYoast OAuth client state on this site.
350 *
351 * Performs the same cleanup as plugin uninstall: best-effort server-side
352 * deregistration, then deletes all site/user tokens, registered client
353 * credentials, key pairs, and OIDC/JWKS/DPoP caches. Intended for
354 * development environments that need to start from a clean slate without
355 * uninstalling the plugin.
356 *
357 * ## OPTIONS
358 *
359 * [--yes]
360 * : Skip confirmation prompt.
361 *
362 * ## EXAMPLES
363 *
364 * wp yoast auth reset
365 * wp yoast auth reset --yes
366 *
367 * @when after_wp_load
368 *
369 * @param array<int, string>|null $args The arguments.
370 * @param array<string, string>|null $assoc_args The associative arguments.
371 *
372 * @return void
373 */
374 public function reset( $args = null, $assoc_args = null ): void {
375 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 );
376
377 $this->cleanup->execute();
378
379 WP_CLI::success( 'MyYoast OAuth client state cleared.' );
380 }
381
382 /**
383 * Authorizes with MyYoast using the authorization code flow or client credentials.
384 *
385 * Without --site, starts the user authorization code flow:
386 * 1. Run without --code to get the authorization URL.
387 * 2. Visit the URL in a browser and authorize.
388 * 3. Copy the code and state from the callback URL.
389 * 4. Run again with --code and --state to exchange for tokens.
390 *
391 * With --site, performs a client_credentials grant for a site-level token.
392 *
393 * ## OPTIONS
394 *
395 * [--site]
396 * : Use client_credentials grant for a site-level token (no browser needed).
397 *
398 * [--scopes=<scopes>]
399 * : Comma-separated scopes to request.
400 *
401 * [--code=<code>]
402 * : Authorization code from the callback URL (user flow phase 2).
403 *
404 * [--state=<state>]
405 * : State parameter from the callback URL (user flow phase 2).
406 *
407 * [--url-only]
408 * : Only print the authorization URL without instructions (user flow phase 1).
409 *
410 * [--format=<format>]
411 * : Output format.
412 * ---
413 * default: table
414 * options:
415 * - table
416 * - json
417 * ---
418 *
419 * ## EXAMPLES
420 *
421 * # Site-level token (client_credentials):
422 * wp yoast auth authorize --site --scopes=service:analytics
423 *
424 * # User authorization code flow, phase 1 - get the URL:
425 * wp yoast auth authorize --user=admin --scopes=openid,profile
426 *
427 * # User authorization code flow, phase 2 - exchange the code:
428 * wp yoast auth authorize --user=admin --code=abc123 --state=xyz789
429 *
430 * @when after_wp_load
431 *
432 * @param array<int, string>|null $args The arguments.
433 * @param array<string, string>|null $assoc_args The associative arguments.
434 *
435 * @return void
436 *
437 * @throws ExitException When authorization fails.
438 */
439 public function authorize( $args = null, $assoc_args = null ): void {
440 $scopes = $this->parse_scopes( $assoc_args );
441
442 if ( isset( $assoc_args['site'] ) ) {
443 $this->authorize_site( $scopes, $assoc_args['format'] );
444 return;
445 }
446
447 $this->authorize_user( $assoc_args, $scopes, $assoc_args['format'] );
448 }
449
450 /**
451 * Revokes tokens for the current user and/or the site.
452 *
453 * Without --site, revokes the current user's tokens (requires --user flag).
454 * With --site, clears the cached site-level token.
455 * Both can be combined.
456 *
457 * ## OPTIONS
458 *
459 * [--site]
460 * : Clear the cached site-level token.
461 *
462 * [--yes]
463 * : Skip confirmation prompt.
464 *
465 * ## EXAMPLES
466 *
467 * wp yoast auth revoke --user=admin
468 * wp yoast auth revoke --site
469 * wp yoast auth revoke --user=admin --site --yes
470 *
471 * @when after_wp_load
472 *
473 * @param array<int, string>|null $args The arguments.
474 * @param array<string, string>|null $assoc_args The associative arguments.
475 *
476 * @return void
477 */
478 public function revoke( $args = null, $assoc_args = null ): void {
479 $user_id = \get_current_user_id();
480 $has_site = isset( $assoc_args['site'] );
481 $has_user = ( $user_id > 0 );
482
483 if ( ! $has_site && ! $has_user ) {
484 WP_CLI::error( 'Specify --site and/or use the global --user flag.' );
485 }
486
487 WP_CLI::confirm( 'This will revoke the specified tokens. Proceed?', $assoc_args );
488
489 if ( $has_user ) {
490 $this->myyoast_client->revoke_user_token( $user_id );
491 WP_CLI::log( \sprintf( 'User %d tokens revoked.', $user_id ) );
492 }
493
494 if ( $has_site ) {
495 $this->myyoast_client->clear_site_token();
496 WP_CLI::log( 'Site token cleared.' );
497 }
498
499 WP_CLI::success( 'Done.' );
500 }
501
502 /**
503 * Rotates cryptographic key pairs.
504 *
505 * Rotates the registration key pair (server roundtrip) and/or the DPoP
506 * key pair (local only).
507 *
508 * ## OPTIONS
509 *
510 * [--registration]
511 * : Rotate the registration (private_key_jwt) key pair. Requires a server roundtrip.
512 *
513 * [--dpop]
514 * : Rotate the DPoP proof key pair (local only).
515 *
516 * [--all]
517 * : Rotate all key pairs.
518 *
519 * [--yes]
520 * : Skip confirmation prompt.
521 *
522 * ## EXAMPLES
523 *
524 * wp yoast auth rotate-keys --registration
525 * wp yoast auth rotate-keys --dpop
526 * wp yoast auth rotate-keys --all
527 *
528 * @when after_wp_load
529 *
530 * @param array<int, string>|null $args The arguments.
531 * @param array<string, string>|null $assoc_args The associative arguments.
532 *
533 * @return void
534 *
535 * @throws ExitException When key rotation fails.
536 */
537 public function rotate_keys( $args = null, $assoc_args = null ): void {
538 $rotate_registration = isset( $assoc_args['registration'] ) || isset( $assoc_args['all'] );
539 $rotate_dpop = isset( $assoc_args['dpop'] ) || isset( $assoc_args['all'] );
540
541 if ( ! $rotate_registration && ! $rotate_dpop ) {
542 WP_CLI::error( 'Specify --registration, --dpop, or --all.' );
543 }
544
545 WP_CLI::confirm( 'This will rotate the specified key pairs. Existing tokens may be invalidated. Proceed?', $assoc_args );
546
547 if ( $rotate_registration ) {
548 if ( ! $this->myyoast_client->is_registered() ) {
549 WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' );
550 }
551
552 try {
553 $client = $this->myyoast_client->rotate_registration_keys();
554 WP_CLI::log( 'Registration keys rotated. Client ID: ' . $client->get_client_id() );
555 } catch ( Exception $e ) {
556 WP_CLI::error( 'Registration key rotation failed: ' . $e->getMessage() );
557 return;
558 }
559 }
560
561 if ( $rotate_dpop ) {
562 $this->myyoast_client->rotate_dpop_keys();
563 WP_CLI::log( 'DPoP keys rotated.' );
564 }
565
566 WP_CLI::success( 'Key rotation complete.' );
567 }
568
569 /**
570 * Performs a client_credentials grant for a site-level token.
571 *
572 * @param string[] $scopes The scopes to request.
573 * @param string $format The output format.
574 *
575 * @return void
576 *
577 * @throws ExitException When the token request fails.
578 */
579 private function authorize_site( array $scopes, string $format ): void {
580 try {
581 $token_set = $this->myyoast_client->get_site_token( $scopes );
582 } catch ( Exception $e ) {
583 WP_CLI::error( 'Site token request failed: ' . $e->getMessage() );
584 return;
585 }
586
587 $this->output( $this->build_token_info( $token_set ), $format );
588
589 WP_CLI::success( 'Site token obtained.' );
590 }
591
592 /**
593 * Handles the user authorization code flow.
594 *
595 * @param array<string, string> $assoc_args The associative arguments.
596 * @param string[] $scopes The scopes to request.
597 * @param string $format The output format.
598 *
599 * @return void
600 *
601 * @throws ExitException When authorization fails.
602 */
603 private function authorize_user( array $assoc_args, array $scopes, string $format ): void {
604 $user_id = \get_current_user_id();
605 if ( $user_id <= 0 ) {
606 WP_CLI::error( 'User authorization requires the global --user flag.' );
607 }
608
609 $has_code = isset( $assoc_args['code'] );
610 $has_state = isset( $assoc_args['state'] );
611
612 // Phase 2: exchange the code.
613 if ( $has_code && $has_state ) {
614 try {
615 $token_set = $this->myyoast_client->exchange_authorization_code(
616 $user_id,
617 $assoc_args['code'],
618 $assoc_args['state'],
619 );
620 } catch ( Exception $e ) {
621 WP_CLI::error( 'Code exchange failed: ' . $e->getMessage() );
622 return;
623 }
624
625 $this->output( $this->build_token_info( $token_set ), $format );
626
627 WP_CLI::success( 'User authorized.' );
628 return;
629 }
630
631 if ( $has_code || $has_state ) {
632 WP_CLI::error( 'Both --code and --state are required for code exchange.' );
633 }
634
635 // Phase 1: generate the authorization URL.
636 $redirect_uri = \get_admin_url( null, 'admin.php?page=' . General_Page_Integration::PAGE . '&yoast_myyoast_oauth_callback=1' );
637
638 try {
639 $url = $this->myyoast_client->get_authorization_url( $user_id, $redirect_uri, $scopes );
640 } catch ( Exception $e ) {
641 WP_CLI::error( 'Failed to generate authorization URL: ' . $e->getMessage() );
642 return;
643 }
644
645 if ( isset( $assoc_args['url-only'] ) ) {
646 WP_CLI::log( $url );
647 return;
648 }
649
650 WP_CLI::log( 'Visit this URL to authorize:' );
651 WP_CLI::log( '' );
652 WP_CLI::log( $url );
653 WP_CLI::log( '' );
654 WP_CLI::log( 'After authorizing, you will be redirected to a callback URL.' );
655 WP_CLI::log( 'If you need to manually complete authorization, copy the "code" and "state" parameters from the URL, then run:' );
656 WP_CLI::log( '' );
657 WP_CLI::log(
658 \sprintf(
659 ' wp yoast auth authorize --user=%s --code=<CODE> --state=<STATE>',
660 $user_id,
661 ),
662 );
663 }
664
665 /**
666 * Builds a display-safe token info array.
667 *
668 * @param Token_Set|null $token_set The token set, or null.
669 *
670 * @return array<string, string|int> The token info for display.
671 */
672 private function build_token_info( ?Token_Set $token_set ): array {
673 if ( $token_set === null ) {
674 return [
675 'status' => 'none',
676 'expires' => '-',
677 'scopes' => '-',
678 'error_count' => '-',
679 ];
680 }
681
682 return [
683 'status' => ( $token_set->is_expired() ) ? 'expired' : 'valid',
684 'expires' => \gmdate( 'Y-m-d H:i:s', $token_set->get_expires_at() ) . ' UTC',
685 'scopes' => ( $token_set->get_scope() ?? '-' ),
686 'error_count' => $token_set->get_error_count(),
687 ];
688 }
689
690 /**
691 * Parses the comma-separated scopes option.
692 *
693 * @param array<string, string>|null $assoc_args The associative arguments.
694 *
695 * @return string[] The parsed scopes.
696 */
697 private function parse_scopes( $assoc_args ): array {
698 if ( ! isset( $assoc_args['scopes'] ) || $assoc_args['scopes'] === '' ) {
699 return [];
700 }
701
702 return \array_values( \array_filter( \array_map( 'trim', \explode( ',', $assoc_args['scopes'] ) ) ) );
703 }
704
705 /**
706 * Flattens nested arrays for table display by JSON-encoding array values.
707 *
708 * @param array<string, string|string[]|bool> $data The data to flatten.
709 *
710 * @return array<string, string> The flattened data.
711 */
712 private function flatten_for_display( array $data ): array {
713 $result = [];
714 foreach ( $data as $key => $value ) {
715 if ( \is_array( $value ) ) {
716 // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- WP-CLI display output, not user-facing HTML.
717 $result[ $key ] = ( \wp_json_encode( $value ) ?? '[]' );
718 }
719 elseif ( \is_bool( $value ) ) {
720 $result[ $key ] = ( $value ) ? 'true' : 'false';
721 }
722 else {
723 $result[ $key ] = (string) $value;
724 }
725 }
726 return $result;
727 }
728
729 /**
730 * Outputs data in the requested format.
731 *
732 * @param array<string, string|int> $data The data to output.
733 * @param string $format The output format (table or json).
734 *
735 * @return void
736 */
737 private function output( array $data, string $format ): void {
738 if ( $format === 'json' ) {
739 // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.FoundWithAdditionalParams -- CLI output, not user-facing HTML.
740 $encoded = \wp_json_encode( $data, \JSON_PRETTY_PRINT );
741 WP_CLI::log( ( $encoded !== false ) ? $encoded : '{}' );
742 return;
743 }
744
745 $items = [];
746 foreach ( $data as $key => $value ) {
747 $items[] = [
748 'field' => $key,
749 'value' => (string) $value,
750 ];
751 }
752
753 WP_CLI\Utils\format_items( 'table', $items, [ 'field', 'value' ] );
754 }
755 }
756