| @@ -4,18 +4,21 @@ | ||
| 4 | 4 | |
| 5 | 5 | use Exception; |
| 6 | 6 | use WP_CLI; |
| 7 | 7 | use WP_CLI\ExitException; |
| 8 | +use WP_CLI\Utils; | |
| 8 | 9 | use Yoast\WP\SEO\Commands\Command_Interface; |
| 9 | 10 | use Yoast\WP\SEO\Conditionals\MyYoast_Connection_Conditional; |
| 10 | -use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; | |
| 11 | 11 | use Yoast\WP\SEO\Loadable_Interface; |
| 12 | 12 | use Yoast\WP\SEO\Main; |
| 13 | +use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Registration_Temporarily_Unavailable_Exception; | |
| 13 | 14 | use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client; |
| 14 | 15 | use Yoast\WP\SEO\MyYoast_Client\Application\MyYoast_Client_Cleanup; |
| 15 | 16 | use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface; |
| 16 | 17 | use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Token_Storage_Interface; |
| 17 | 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; | |
| 18 | 21 | use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set; |
| 19 | 22 | use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config; |
| 20 | 23 | |
| 21 | 24 | /** |
| @@ -120,8 +123,14 @@ | ||
| 120 | 123 | * a specific user's token status. |
| 121 | 124 | * |
| 122 | 125 | * ## OPTIONS |
| 123 | 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 | + * | |
| 124 | 133 | * [--format=<format>] |
| 125 | 134 | * : Output format. |
| 126 | 135 | * --- |
| 127 | 136 | * default: table |
| @@ -133,8 +142,10 @@ | ||
| 133 | 142 | * ## EXAMPLES |
| 134 | 143 | * |
| 135 | 144 | * wp yoast auth status |
| 136 | 145 | * wp yoast auth status --user=admin |
| 146 | + * wp yoast auth status --resource=https://ai.yoa.st | |
| 147 | + * wp yoast auth status --all-resources | |
| 137 | 148 | * wp yoast auth status --format=json |
| 138 | 149 | * |
| 139 | 150 | * @when after_wp_load |
| 140 | 151 | * |
| @@ -156,14 +167,34 @@ | ||
| 156 | 167 | if ( $registered_client !== null ) { |
| 157 | 168 | $client_id = $registered_client->get_client_id(); |
| 158 | 169 | } |
| 159 | 170 | |
| 160 | - $site_token = $this->token_storage->get(); | |
| 161 | - $site_token_info = $this->build_token_info( $site_token ); | |
| 171 | + $has_all = (bool) Utils\get_flag_value( $assoc_args, 'all-resources', false ); | |
| 172 | + $resource = Utils\get_flag_value( $assoc_args, 'resource' ); | |
| 162 | 173 | |
| 163 | - $user_token = ( $user_id > 0 ) ? $this->user_token_storage->get( $user_id ) : null; | |
| 164 | - $user_token_info = $this->build_token_info( $user_token ); | |
| 174 | + if ( $has_all && $resource !== null && $resource !== '' ) { | |
| 175 | + WP_CLI::error( '--all-resources and --resource cannot be combined.' ); | |
| 176 | + } | |
| 165 | 177 | |
| 178 | + if ( $has_all ) { | |
| 179 | + $user_tokens = ( $user_id > 0 ) ? $this->user_token_storage->get_all( $user_id ) : []; | |
| 180 | + $site_tokens = $this->token_storage->get_all(); | |
| 181 | + } | |
| 182 | + else { | |
| 183 | + try { | |
| 184 | + $resource_filter = new Resource_Indicator( ( $resource !== null && $resource !== '' ) ? (string) $resource : null ); | |
| 185 | + } | |
| 186 | + catch ( Invalid_Resource_Exception $e ) { | |
| 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 | + | |
| 195 | + $format = Utils\get_flag_value( $assoc_args, 'format', 'table' ); | |
| 196 | + | |
| 166 | 197 | $data = [ |
| 167 | 198 | 'issuer_url' => $issuer_url, |
| 168 | 199 | 'software_statement' => ( $has_software ) ? 'configured' : 'not configured', |
| 169 | 200 | 'initial_access_token' => ( $has_iat ) ? 'configured' : 'not configured', |
| @@ -168,19 +199,14 @@ | ||
| 168 | 199 | 'software_statement' => ( $has_software ) ? 'configured' : 'not configured', |
| 169 | 200 | 'initial_access_token' => ( $has_iat ) ? 'configured' : 'not configured', |
| 170 | 201 | 'registered' => ( $is_registered ) ? 'yes' : 'no', |
| 171 | 202 | '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 | 203 | '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'], | |
| 204 | + 'user_tokens' => $this->build_token_inventory( $user_tokens ), | |
| 205 | + 'site_tokens' => $this->build_token_inventory( $site_tokens ), | |
| 180 | 206 | ]; |
| 181 | 207 | |
| 182 | - $this->output( $data, $assoc_args['format'] ); | |
| 208 | + $this->output( $data, $format ); | |
| 183 | 209 | } |
| 184 | 210 | |
| 185 | 211 | /** |
| 186 | 212 | * Registers the site as an OAuth client. |
| @@ -216,16 +242,20 @@ | ||
| 216 | 242 | * |
| 217 | 243 | * @throws ExitException When registration fails. |
| 218 | 244 | */ |
| 219 | 245 | public function register( $args = null, $assoc_args = null ): void { |
| 220 | - if ( isset( $assoc_args['force'] ) ) { | |
| 246 | + if ( Utils\get_flag_value( $assoc_args, 'force', false ) ) { | |
| 221 | 247 | $this->myyoast_client->deregister(); |
| 222 | 248 | WP_CLI::log( 'Deregistered existing client.' ); |
| 223 | 249 | } |
| 224 | 250 | |
| 225 | 251 | 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 ] ); | |
| 252 | + $client = $this->myyoast_client->ensure_registered(); | |
| 253 | + } catch ( Registration_Temporarily_Unavailable_Exception $e ) { | |
| 254 | + $retry_after = $e->get_retry_after_seconds(); | |
| 255 | + $retry_hint = ( $retry_after !== null ) ? \sprintf( ' Try again in %d seconds.', $retry_after ) : ' Try again later.'; | |
| 256 | + WP_CLI::error( 'Registration is temporarily unavailable.' . $retry_hint ); | |
| 257 | + return; | |
| 228 | 258 | } catch ( Exception $e ) { |
| 229 | 259 | WP_CLI::error( 'Registration failed: ' . $e->getMessage() ); |
| 230 | 260 | return; |
| 231 | 261 | } |
| @@ -234,9 +264,9 @@ | ||
| 234 | 264 | [ |
| 235 | 265 | 'client_id' => $client->get_client_id(), |
| 236 | 266 | 'status' => 'registered', |
| 237 | 267 | ], |
| 238 | - $assoc_args['format'], | |
| 268 | + Utils\get_flag_value( $assoc_args, 'format', 'table' ), | |
| 239 | 269 | ); |
| 240 | 270 | |
| 241 | 271 | WP_CLI::success( 'Client registered: ' . $client->get_client_id() ); |
| 242 | 272 | } |
| @@ -241,9 +271,9 @@ | ||
| 241 | 271 | WP_CLI::success( 'Client registered: ' . $client->get_client_id() ); |
| 242 | 272 | } |
| 243 | 273 | |
| 244 | 274 | /** |
| 245 | - * Verifies the client registration with the server. | |
| 275 | + * Refreshes the client registration status against the server. | |
| 246 | 276 | * |
| 247 | 277 | * Reads the current registration from the authorization server to |
| 248 | 278 | * confirm it is still valid and shows the registration metadata. |
| 249 | 279 | * |
| @@ -259,11 +289,13 @@ | ||
| 259 | 289 | * --- |
| 260 | 290 | * |
| 261 | 291 | * ## EXAMPLES |
| 262 | 292 | * |
| 263 | - * wp yoast auth verify | |
| 264 | - * wp yoast auth verify --format=json | |
| 293 | + * wp yoast auth refresh-status | |
| 294 | + * wp yoast auth refresh-status --format=json | |
| 265 | 295 | * |
| 296 | + * @subcommand refresh-status | |
| 297 | + * | |
| 266 | 298 | * @when after_wp_load |
| 267 | 299 | * |
| 268 | 300 | * @param array<int, string>|null $args The arguments. |
| 269 | 301 | * @param array<string, string>|null $assoc_args The associative arguments. |
| @@ -269,19 +301,19 @@ | ||
| 269 | 301 | * @param array<string, string>|null $assoc_args The associative arguments. |
| 270 | 302 | * |
| 271 | 303 | * @return void |
| 272 | 304 | * |
| 273 | - * @throws ExitException When verification fails. | |
| 305 | + * @throws ExitException When the status refresh fails. | |
| 274 | 306 | */ |
| 275 | - public function verify( $args = null, $assoc_args = null ): void { | |
| 307 | + public function refresh_status( $args = null, $assoc_args = null ): void { | |
| 276 | 308 | if ( ! $this->myyoast_client->is_registered() ) { |
| 277 | 309 | WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' ); |
| 278 | 310 | } |
| 279 | 311 | |
| 280 | 312 | try { |
| 281 | - $metadata = $this->myyoast_client->verify_registration(); | |
| 313 | + $metadata = $this->myyoast_client->refresh_registration_status(); | |
| 282 | 314 | } catch ( Exception $e ) { |
| 283 | - WP_CLI::error( 'Verification failed: ' . $e->getMessage() ); | |
| 315 | + WP_CLI::error( 'Status refresh failed: ' . $e->getMessage() ); | |
| 284 | 316 | return; |
| 285 | 317 | } |
| 286 | 318 | |
| 287 | 319 | // Redact sensitive fields. |
| @@ -286,9 +318,9 @@ | ||
| 286 | 318 | |
| 287 | 319 | // Redact sensitive fields. |
| 288 | 320 | unset( $metadata['registration_access_token'] ); |
| 289 | 321 | |
| 290 | - $this->output( $this->flatten_for_display( $metadata ), $assoc_args['format'] ); | |
| 322 | + $this->output( $metadata, Utils\get_flag_value( $assoc_args, 'format', 'table' ) ); | |
| 291 | 323 | |
| 292 | 324 | WP_CLI::success( 'Registration is valid.' ); |
| 293 | 325 | } |
| 294 | 326 | |
| @@ -326,17 +358,17 @@ | ||
| 326 | 358 | } |
| 327 | 359 | |
| 328 | 360 | WP_CLI::confirm( 'This will deregister this site from MyYoast and clear all cached tokens. Proceed?', $assoc_args ); |
| 329 | 361 | |
| 330 | - if ( isset( $assoc_args['local-only'] ) ) { | |
| 362 | + if ( Utils\get_flag_value( $assoc_args, 'local-only', false ) ) { | |
| 331 | 363 | $this->client_registration->delete_local_data(); |
| 332 | - $this->myyoast_client->clear_site_token(); | |
| 364 | + $this->myyoast_client->clear_all_site_tokens(); | |
| 333 | 365 | WP_CLI::success( 'Local registration data cleared.' ); |
| 334 | 366 | return; |
| 335 | 367 | } |
| 336 | 368 | |
| 337 | 369 | $result = $this->myyoast_client->deregister(); |
| 338 | - $this->myyoast_client->clear_site_token(); | |
| 370 | + $this->myyoast_client->clear_all_site_tokens(); | |
| 339 | 371 | |
| 340 | 372 | if ( $result ) { |
| 341 | 373 | WP_CLI::success( 'Client deregistered.' ); |
| 342 | 374 | } |
| @@ -397,8 +429,11 @@ | ||
| 397 | 429 | * |
| 398 | 430 | * [--scopes=<scopes>] |
| 399 | 431 | * : Comma-separated scopes to request. |
| 400 | 432 | * |
| 433 | + * [--resource=<uri>] | |
| 434 | + * : RFC 8707 resource indicator to bind the token to (e.g. https://ai.yoa.st). Omit for the default resource. | |
| 435 | + * | |
| 401 | 436 | * [--code=<code>] |
| 402 | 437 | * : Authorization code from the callback URL (user flow phase 2). |
| 403 | 438 | * |
| 404 | 439 | * [--state=<state>] |
| @@ -420,8 +455,11 @@ | ||
| 420 | 455 | * |
| 421 | 456 | * # Site-level token (client_credentials): |
| 422 | 457 | * wp yoast auth authorize --site --scopes=service:analytics |
| 423 | 458 | * |
| 459 | + * # Site-level token for a non-default resource: | |
| 460 | + * wp yoast auth authorize --site --resource=https://ai.yoa.st --scopes=service:ai:consume | |
| 461 | + * | |
| 424 | 462 | * # User authorization code flow, phase 1 - get the URL: |
| 425 | 463 | * wp yoast auth authorize --user=admin --scopes=openid,profile |
| 426 | 464 | * |
| 427 | 465 | * # User authorization code flow, phase 2 - exchange the code: |
| @@ -436,16 +474,19 @@ | ||
| 436 | 474 | * |
| 437 | 475 | * @throws ExitException When authorization fails. |
| 438 | 476 | */ |
| 439 | 477 | public function authorize( $args = null, $assoc_args = null ): void { |
| 440 | - $scopes = $this->parse_scopes( $assoc_args ); | |
| 478 | + $scopes = $this->parse_scopes( $assoc_args ); | |
| 479 | + $format = Utils\get_flag_value( $assoc_args, 'format', 'table' ); | |
| 480 | + $resource = Utils\get_flag_value( $assoc_args, 'resource' ); | |
| 481 | + $resource_indicator = ( $resource !== null && $resource !== '' ) ? (string) $resource : null; | |
| 441 | 482 | |
| 442 | - if ( isset( $assoc_args['site'] ) ) { | |
| 443 | - $this->authorize_site( $scopes, $assoc_args['format'] ); | |
| 483 | + if ( Utils\get_flag_value( $assoc_args, 'site', false ) ) { | |
| 484 | + $this->authorize_site( $scopes, $resource_indicator, $format ); | |
| 444 | 485 | return; |
| 445 | 486 | } |
| 446 | 487 | |
| 447 | - $this->authorize_user( $assoc_args, $scopes, $assoc_args['format'] ); | |
| 488 | + $this->authorize_user( $assoc_args, $scopes, $resource_indicator, $format ); | |
| 448 | 489 | } |
| 449 | 490 | |
| 450 | 491 | /** |
| 451 | 492 | * Revokes tokens for the current user and/or the site. |
| @@ -458,8 +499,14 @@ | ||
| 458 | 499 | * |
| 459 | 500 | * [--site] |
| 460 | 501 | * : Clear the cached site-level token. |
| 461 | 502 | * |
| 503 | + * [--resource=<uri>] | |
| 504 | + * : Limit revocation to a single RFC 8707 resource indicator. Omit to target the default resource. | |
| 505 | + * | |
| 506 | + * [--all-resources] | |
| 507 | + * : Revoke every stored token across all resource indicators. Cannot be combined with --resource. | |
| 508 | + * | |
| 462 | 509 | * [--yes] |
| 463 | 510 | * : Skip confirmation prompt. |
| 464 | 511 | * |
| 465 | 512 | * ## EXAMPLES |
| @@ -466,8 +513,10 @@ | ||
| 466 | 513 | * |
| 467 | 514 | * wp yoast auth revoke --user=admin |
| 468 | 515 | * wp yoast auth revoke --site |
| 469 | 516 | * wp yoast auth revoke --user=admin --site --yes |
| 517 | + * wp yoast auth revoke --user=admin --resource=https://ai.yoa.st | |
| 518 | + * wp yoast auth revoke --user=admin --site --all-resources | |
| 470 | 519 | * |
| 471 | 520 | * @when after_wp_load |
| 472 | 521 | * |
| 473 | 522 | * @param array<int, string>|null $args The arguments. |
| @@ -475,26 +524,62 @@ | ||
| 475 | 524 | * |
| 476 | 525 | * @return void |
| 477 | 526 | */ |
| 478 | 527 | 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 ); | |
| 528 | + $user_id = \get_current_user_id(); | |
| 529 | + $has_user = ( $user_id > 0 ); | |
| 530 | + $has_site = (bool) Utils\get_flag_value( $assoc_args, 'site', false ); | |
| 531 | + $has_all_resources = (bool) Utils\get_flag_value( $assoc_args, 'all-resources', false ); | |
| 532 | + $resource = Utils\get_flag_value( $assoc_args, 'resource' ); | |
| 482 | 533 | |
| 483 | 534 | if ( ! $has_site && ! $has_user ) { |
| 484 | 535 | WP_CLI::error( 'Specify --site and/or use the global --user flag.' ); |
| 485 | 536 | } |
| 486 | 537 | |
| 538 | + if ( $has_all_resources && $resource !== null && $resource !== '' ) { | |
| 539 | + WP_CLI::error( '--all-resources and --resource cannot be combined.' ); | |
| 540 | + } | |
| 541 | + | |
| 542 | + $resource_indicator = null; | |
| 543 | + if ( $resource !== null && $resource !== '' ) { | |
| 544 | + $resource_indicator = (string) $resource; | |
| 545 | + try { | |
| 546 | + new Resource_Indicator( $resource_indicator ); | |
| 547 | + } | |
| 548 | + catch ( Invalid_Resource_Exception $e ) { | |
| 549 | + WP_CLI::error( 'Invalid resource indicator: ' . $e->getMessage() ); | |
| 550 | + return; | |
| 551 | + } | |
| 552 | + } | |
| 553 | + | |
| 487 | 554 | WP_CLI::confirm( 'This will revoke the specified tokens. Proceed?', $assoc_args ); |
| 488 | 555 | |
| 489 | - if ( $has_user ) { | |
| 490 | - $this->myyoast_client->revoke_user_token( $user_id ); | |
| 491 | - WP_CLI::log( \sprintf( 'User %d tokens revoked.', $user_id ) ); | |
| 556 | + try { | |
| 557 | + if ( $has_user ) { | |
| 558 | + if ( $has_all_resources ) { | |
| 559 | + $this->myyoast_client->revoke_all_user_tokens( $user_id ); | |
| 560 | + WP_CLI::log( \sprintf( 'User %d tokens revoked across all resources.', $user_id ) ); | |
| 561 | + } | |
| 562 | + else { | |
| 563 | + $this->myyoast_client->revoke_user_token( $user_id, $resource_indicator ); | |
| 564 | + WP_CLI::log( \sprintf( 'User %d tokens revoked.', $user_id ) ); | |
| 565 | + } | |
| 566 | + } | |
| 567 | + | |
| 568 | + if ( $has_site ) { | |
| 569 | + if ( $has_all_resources ) { | |
| 570 | + $this->myyoast_client->clear_all_site_tokens(); | |
| 571 | + WP_CLI::log( 'All site tokens cleared.' ); | |
| 572 | + } | |
| 573 | + else { | |
| 574 | + $this->myyoast_client->clear_site_token( $resource_indicator ); | |
| 575 | + WP_CLI::log( 'Site token cleared.' ); | |
| 576 | + } | |
| 577 | + } | |
| 492 | 578 | } |
| 493 | - | |
| 494 | - if ( $has_site ) { | |
| 495 | - $this->myyoast_client->clear_site_token(); | |
| 496 | - WP_CLI::log( 'Site token cleared.' ); | |
| 579 | + catch ( Exception $e ) { | |
| 580 | + WP_CLI::error( 'Revocation failed: ' . $e->getMessage() ); | |
| 581 | + return; | |
| 497 | 582 | } |
| 498 | 583 | |
| 499 | 584 | WP_CLI::success( 'Done.' ); |
| 500 | 585 | } |
| @@ -534,10 +619,11 @@ | ||
| 534 | 619 | * |
| 535 | 620 | * @throws ExitException When key rotation fails. |
| 536 | 621 | */ |
| 537 | 622 | 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'] ); | |
| 623 | + $rotate_all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); | |
| 624 | + $rotate_registration = ( $rotate_all || (bool) Utils\get_flag_value( $assoc_args, 'registration', false ) ); | |
| 625 | + $rotate_dpop = ( $rotate_all || (bool) Utils\get_flag_value( $assoc_args, 'dpop', false ) ); | |
| 540 | 626 | |
| 541 | 627 | if ( ! $rotate_registration && ! $rotate_dpop ) { |
| 542 | 628 | WP_CLI::error( 'Specify --registration, --dpop, or --all.' ); |
| 543 | 629 | } |
| @@ -568,18 +654,19 @@ | ||
| 568 | 654 | |
| 569 | 655 | /** |
| 570 | 656 | * Performs a client_credentials grant for a site-level token. |
| 571 | 657 | * |
| 572 | - * @param string[] $scopes The scopes to request. | |
| 573 | - * @param string $format The output format. | |
| 658 | + * @param string[] $scopes The scopes to request. | |
| 659 | + * @param string|null $resource_indicator The resource indicator (RFC 8707), or null for the default resource. | |
| 660 | + * @param string $format The output format. | |
| 574 | 661 | * |
| 575 | 662 | * @return void |
| 576 | 663 | * |
| 577 | 664 | * @throws ExitException When the token request fails. |
| 578 | 665 | */ |
| 579 | - private function authorize_site( array $scopes, string $format ): void { | |
| 666 | + private function authorize_site( array $scopes, ?string $resource_indicator, string $format ): void { | |
| 580 | 667 | try { |
| 581 | - $token_set = $this->myyoast_client->get_site_token( $scopes ); | |
| 668 | + $token_set = $this->myyoast_client->get_site_token( $scopes, $resource_indicator ); | |
| 582 | 669 | } catch ( Exception $e ) { |
| 583 | 670 | WP_CLI::error( 'Site token request failed: ' . $e->getMessage() ); |
| 584 | 671 | return; |
| 585 | 672 | } |
| @@ -591,32 +678,33 @@ | ||
| 591 | 678 | |
| 592 | 679 | /** |
| 593 | 680 | * Handles the user authorization code flow. |
| 594 | 681 | * |
| 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. | |
| 682 | + * @param array<string, string> $assoc_args The associative arguments. | |
| 683 | + * @param string[] $scopes The scopes to request. | |
| 684 | + * @param string|null $resource_indicator The resource indicator (RFC 8707), or null for the default resource. | |
| 685 | + * @param string $format The output format. | |
| 598 | 686 | * |
| 599 | 687 | * @return void |
| 600 | 688 | * |
| 601 | 689 | * @throws ExitException When authorization fails. |
| 602 | 690 | */ |
| 603 | - private function authorize_user( array $assoc_args, array $scopes, string $format ): void { | |
| 691 | + private function authorize_user( array $assoc_args, array $scopes, ?string $resource_indicator, string $format ): void { | |
| 604 | 692 | $user_id = \get_current_user_id(); |
| 605 | 693 | if ( $user_id <= 0 ) { |
| 606 | 694 | WP_CLI::error( 'User authorization requires the global --user flag.' ); |
| 607 | 695 | } |
| 608 | 696 | |
| 609 | - $has_code = isset( $assoc_args['code'] ); | |
| 610 | - $has_state = isset( $assoc_args['state'] ); | |
| 697 | + $code = Utils\get_flag_value( $assoc_args, 'code' ); | |
| 698 | + $state = Utils\get_flag_value( $assoc_args, 'state' ); | |
| 611 | 699 | |
| 612 | - // Phase 2: exchange the code. | |
| 613 | - if ( $has_code && $has_state ) { | |
| 700 | + // Phase 2: exchange the code. The resource was persisted in the flow state during phase 1. | |
| 701 | + if ( $code !== null && $state !== null ) { | |
| 614 | 702 | try { |
| 615 | 703 | $token_set = $this->myyoast_client->exchange_authorization_code( |
| 616 | 704 | $user_id, |
| 617 | - $assoc_args['code'], | |
| 618 | - $assoc_args['state'], | |
| 705 | + (string) $code, | |
| 706 | + (string) $state, | |
| 619 | 707 | ); |
| 620 | 708 | } catch ( Exception $e ) { |
| 621 | 709 | WP_CLI::error( 'Code exchange failed: ' . $e->getMessage() ); |
| 622 | 710 | return; |
| @@ -627,23 +715,25 @@ | ||
| 627 | 715 | WP_CLI::success( 'User authorized.' ); |
| 628 | 716 | return; |
| 629 | 717 | } |
| 630 | 718 | |
| 631 | - if ( $has_code || $has_state ) { | |
| 719 | + if ( $code !== null || $state !== null ) { | |
| 632 | 720 | WP_CLI::error( 'Both --code and --state are required for code exchange.' ); |
| 633 | 721 | } |
| 634 | 722 | |
| 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' ); | |
| 723 | + // Phase 1: generate the authorization URL. Registration is a prerequisite. | |
| 724 | + if ( ! $this->myyoast_client->is_registered() ) { | |
| 725 | + WP_CLI::error( 'Not registered. Run "wp yoast auth register" first.' ); | |
| 726 | + } | |
| 637 | 727 | |
| 638 | 728 | try { |
| 639 | - $url = $this->myyoast_client->get_authorization_url( $user_id, $redirect_uri, $scopes ); | |
| 729 | + $url = $this->myyoast_client->get_authorization_url( $user_id, $scopes, $resource_indicator ); | |
| 640 | 730 | } catch ( Exception $e ) { |
| 641 | 731 | WP_CLI::error( 'Failed to generate authorization URL: ' . $e->getMessage() ); |
| 642 | 732 | return; |
| 643 | 733 | } |
| 644 | 734 | |
| 645 | - if ( isset( $assoc_args['url-only'] ) ) { | |
| 735 | + if ( Utils\get_flag_value( $assoc_args, 'url-only', false ) ) { | |
| 646 | 736 | WP_CLI::log( $url ); |
| 647 | 737 | return; |
| 648 | 738 | } |
| 649 | 739 | |
| @@ -671,8 +761,9 @@ | ||
| 671 | 761 | */ |
| 672 | 762 | private function build_token_info( ?Token_Set $token_set ): array { |
| 673 | 763 | if ( $token_set === null ) { |
| 674 | 764 | return [ |
| 765 | + 'resource' => '-', | |
| 675 | 766 | 'status' => 'none', |
| 676 | 767 | 'expires' => '-', |
| 677 | 768 | 'scopes' => '-', |
| 678 | 769 | 'error_count' => '-', |
| @@ -679,8 +770,9 @@ | ||
| 679 | 770 | ]; |
| 680 | 771 | } |
| 681 | 772 | |
| 682 | 773 | return [ |
| 774 | + 'resource' => ( $token_set->get_resource_indicator()->is_default() ? '(default)' : $token_set->get_resource_indicator()->value() ), | |
| 683 | 775 | 'status' => ( $token_set->is_expired() ) ? 'expired' : 'valid', |
| 684 | 776 | 'expires' => \gmdate( 'Y-m-d H:i:s', $token_set->get_expires_at() ) . ' UTC', |
| 685 | 777 | 'scopes' => ( $token_set->get_scope() ?? '-' ), |
| 686 | 778 | 'error_count' => $token_set->get_error_count(), |
| @@ -687,8 +779,23 @@ | ||
| 687 | 779 | ]; |
| 688 | 780 | } |
| 689 | 781 | |
| 690 | 782 | /** |
| 783 | + * Builds a display-safe inventory of tokens across resource buckets. | |
| 784 | + * | |
| 785 | + * @param Token_Set[] $token_sets The token sets. | |
| 786 | + * | |
| 787 | + * @return array<int, array<string, string|int>> The inventory. | |
| 788 | + */ | |
| 789 | + private function build_token_inventory( array $token_sets ): array { | |
| 790 | + $inventory = []; | |
| 791 | + foreach ( $token_sets as $token_set ) { | |
| 792 | + $inventory[] = $this->build_token_info( $token_set ); | |
| 793 | + } | |
| 794 | + return $inventory; | |
| 795 | + } | |
| 796 | + | |
| 797 | + /** | |
| 691 | 798 | * Parses the comma-separated scopes option. |
| 692 | 799 | * |
| 693 | 800 | * @param array<string, string>|null $assoc_args The associative arguments. |
| 694 | 801 | * |
| @@ -694,13 +801,14 @@ | ||
| 694 | 801 | * |
| 695 | 802 | * @return string[] The parsed scopes. |
| 696 | 803 | */ |
| 697 | 804 | private function parse_scopes( $assoc_args ): array { |
| 698 | - if ( ! isset( $assoc_args['scopes'] ) || $assoc_args['scopes'] === '' ) { | |
| 805 | + $scopes = Utils\get_flag_value( $assoc_args, 'scopes', '' ); | |
| 806 | + if ( $scopes === '' ) { | |
| 699 | 807 | return []; |
| 700 | 808 | } |
| 701 | 809 | |
| 702 | - return \array_values( \array_filter( \array_map( 'trim', \explode( ',', $assoc_args['scopes'] ) ) ) ); | |
| 810 | + return \array_values( \array_filter( \array_map( 'trim', \explode( ',', (string) $scopes ) ) ) ); | |
| 703 | 811 | } |
| 704 | 812 | |
| 705 | 813 | /** |
| 706 | 814 | * Flattens nested arrays for table display by JSON-encoding array values. |
| @@ -713,9 +821,9 @@ | ||
| 713 | 821 | $result = []; |
| 714 | 822 | foreach ( $data as $key => $value ) { |
| 715 | 823 | if ( \is_array( $value ) ) { |
| 716 | 824 | // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- WP-CLI display output, not user-facing HTML. |
| 717 | - $result[ $key ] = ( \wp_json_encode( $value ) ?? '[]' ); | |
| 825 | + $result[ $key ] = ( \wp_json_encode( $value ) ?? 'err' ); | |
| 718 | 826 | } |
| 719 | 827 | elseif ( \is_bool( $value ) ) { |
| 720 | 828 | $result[ $key ] = ( $value ) ? 'true' : 'false'; |
| 721 | 829 | } |
| @@ -741,13 +849,14 @@ | ||
| 741 | 849 | WP_CLI::log( ( $encoded !== false ) ? $encoded : '{}' ); |
| 742 | 850 | return; |
| 743 | 851 | } |
| 744 | 852 | |
| 745 | - $items = []; | |
| 746 | - foreach ( $data as $key => $value ) { | |
| 853 | + $flat_data = $this->flatten_for_display( $data ); | |
| 854 | + $items = []; | |
| 855 | + foreach ( $flat_data as $key => $value ) { | |
| 747 | 856 | $items[] = [ |
| 748 | 857 | 'field' => $key, |
| 749 | - 'value' => (string) $value, | |
| 858 | + 'value' => $value, | |
| 750 | 859 | ]; |
| 751 | 860 | } |
| 752 | 861 | |
| 753 | 862 | WP_CLI\Utils\format_items( 'table', $items, [ 'field', 'value' ] ); |