* : The URL to fetch. * * [--signature=] * : Signature mode: default (plugin-configured), draft-cavage, rfc9421, double-knock, or none. * --- * default: default * options: * - default * - draft-cavage * - rfc9421 * - double-knock * - none * --- * * [--raw] * : Output the raw response body without formatting. * * [--include-headers] * : Show response headers alongside the body. * * ## EXAMPLES * * # Fetch an actor profile with default signature * $ wp activitypub fetch https://mastodon.social/@Gargron * * # Fetch with RFC 9421 signature * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=rfc9421 * * # Fetch with Draft Cavage signature * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=draft-cavage * * # Fetch with double-knock (RFC 9421 first, Draft Cavage fallback on 4xx) * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=double-knock * * # Fetch without signature * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=none * * # Show response headers * $ wp activitypub fetch https://mastodon.social/@Gargron --include-headers * * # Output raw response body * $ wp activitypub fetch https://mastodon.social/@Gargron --raw * * @param array $args The positional arguments. * @param array $assoc_args The associative arguments. */ public function __invoke( $args, $assoc_args ) { $url = $args[0]; $signature_mode = \WP_CLI\Utils\get_flag_value( $assoc_args, 'signature', 'default' ); $raw = \WP_CLI\Utils\get_flag_value( $assoc_args, 'raw', false ); $include_headers = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-headers', false ); \WP_CLI::log( \sprintf( 'Fetching: %s', $url ) ); \WP_CLI::log( \sprintf( 'Signature mode: %s', $signature_mode ) ); $get_args = array(); $cleanup = $this->apply_signature_mode( $signature_mode, $get_args ); $response = Http::get( $url, $get_args, false ); $cleanup(); if ( \is_wp_error( $response ) ) { \WP_CLI::error( \sprintf( 'Request failed: %s (Error code: %s).', $response->get_error_message(), $response->get_error_code() ) ); } $code = \wp_remote_retrieve_response_code( $response ); \WP_CLI::log( \sprintf( 'Response code: %d', $code ) ); \WP_CLI::log( '' ); // Show response headers if requested. if ( $include_headers ) { $headers = \wp_remote_retrieve_headers( $response ); \WP_CLI::log( '--- Response Headers ---' ); foreach ( $headers as $name => $value ) { \WP_CLI::log( \sprintf( '%s: %s', $name, $value ) ); } \WP_CLI::log( '' ); } $body = \wp_remote_retrieve_body( $response ); // Output the body. if ( $raw ) { \WP_CLI::log( $body ); } else { $data = \json_decode( $body, true ); if ( \JSON_ERROR_NONE === \json_last_error() ) { \WP_CLI::log( \wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) ); } else { \WP_CLI::log( $body ); } } } /** * Apply signature mode overrides via filters. * * For rfc9421, replaces the default sign_request and disables double-knock * to avoid an infinite retry loop when the server returns 4xx. * * @param string $mode The signature mode. * @param array $args The request arguments, passed by reference. * * @return callable Cleanup callback to restore original filters. */ private function apply_signature_mode( $mode, &$args ) { $filters = array(); $restore = array(); switch ( $mode ) { case 'default': break; case 'none': $args['key_id'] = null; $args['private_key'] = null; break; case 'rfc9421': case 'double-knock': // Replace default signing to force RFC 9421. For rfc9421 mode, // also disable double-knock to prevent an infinite retry loop. // For double-knock mode, keep it active but skip re-signing on retry. $removed_sign_request = \remove_filter( 'http_request_args', array( Signature::class, 'sign_request' ), 0 ); $is_double_knock = 'double-knock' === $mode; $removed_double_knock = false; if ( ! $is_double_knock ) { $removed_double_knock = \remove_filter( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10 ); } $forced_signer = function ( $request_args, $url ) use ( $is_double_knock ) { if ( ! isset( $request_args['key_id'], $request_args['private_key'] ) ) { return $request_args; } // In double-knock mode, skip if already signed (retry from maybe_double_knock). if ( $is_double_knock && ! empty( $request_args['headers']['Signature'] ) ) { return $request_args; } return ( new Http_Message_Signature() )->sign( $request_args, $url ); }; \add_filter( 'http_request_args', $forced_signer, 0, 2 ); $filters[] = array( 'http_request_args', $forced_signer, 0 ); if ( $removed_sign_request ) { $restore[] = array( 'http_request_args', array( Signature::class, 'sign_request' ), 0, 2 ); } if ( $removed_double_knock ) { $restore[] = array( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10, 3 ); } break; case 'draft-cavage': $force_cavage = function () { return '0'; }; \add_filter( 'pre_option_activitypub_rfc9421_signature', $force_cavage ); $filters[] = array( 'pre_option_activitypub_rfc9421_signature', $force_cavage ); break; default: \WP_CLI::error( \sprintf( 'Invalid signature mode "%s". Allowed modes: default, draft-cavage, rfc9421, double-knock, none.', $mode ) ); } return function () use ( $filters, $restore ) { foreach ( $filters as $filter ) { \remove_filter( $filter[0], $filter[1], $filter[2] ?? 10 ); } foreach ( $restore as $filter ) { \add_filter( $filter[0], $filter[1], $filter[2], $filter[3] ); } }; } }