PluginProbe
ActivityPub / 9.2.1
ActivityPub v9.2.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / cli / class-fetch-command.php

class-fetch-command.php in ActivityPub 9.2.1, at includes/cli/class-fetch-command.php

222 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fetch CLI Command.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Cli;
9
10 use Activitypub\Http;
11 use Activitypub\Signature;
12 use Activitypub\Signature\Http_Message_Signature;
13
14 /**
15 * Fetch a remote ActivityPub URL with signed HTTP requests.
16 *
17 * Useful for debugging HTTP Signatures and federation issues.
18 * Signs requests as the application actor by default.
19 *
20 * @package Activitypub
21 */
22 class Fetch_Command extends \WP_CLI_Command {
23
24 /**
25 * Fetch a remote ActivityPub URL with a signed HTTP request.
26 *
27 * Signs the request as the application actor and displays the response.
28 * Supports switching between signature modes for debugging.
29 *
30 * ## OPTIONS
31 *
32 * <url>
33 * : The URL to fetch.
34 *
35 * [--signature=<mode>]
36 * : Signature mode: default (plugin-configured), draft-cavage, rfc9421, double-knock, or none.
37 * ---
38 * default: default
39 * options:
40 * - default
41 * - draft-cavage
42 * - rfc9421
43 * - double-knock
44 * - none
45 * ---
46 *
47 * [--raw]
48 * : Output the raw response body without formatting.
49 *
50 * [--include-headers]
51 * : Show response headers alongside the body.
52 *
53 * ## EXAMPLES
54 *
55 * # Fetch an actor profile with default signature
56 * $ wp activitypub fetch https://mastodon.social/@Gargron
57 *
58 * # Fetch with RFC 9421 signature
59 * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=rfc9421
60 *
61 * # Fetch with Draft Cavage signature
62 * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=draft-cavage
63 *
64 * # Fetch with double-knock (RFC 9421 first, Draft Cavage fallback on 4xx)
65 * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=double-knock
66 *
67 * # Fetch without signature
68 * $ wp activitypub fetch https://mastodon.social/@Gargron --signature=none
69 *
70 * # Show response headers
71 * $ wp activitypub fetch https://mastodon.social/@Gargron --include-headers
72 *
73 * # Output raw response body
74 * $ wp activitypub fetch https://mastodon.social/@Gargron --raw
75 *
76 * @param array $args The positional arguments.
77 * @param array $assoc_args The associative arguments.
78 */
79 public function __invoke( $args, $assoc_args ) {
80 $url = $args[0];
81 $signature_mode = \WP_CLI\Utils\get_flag_value( $assoc_args, 'signature', 'default' );
82 $raw = \WP_CLI\Utils\get_flag_value( $assoc_args, 'raw', false );
83 $include_headers = \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-headers', false );
84
85 \WP_CLI::log( \sprintf( 'Fetching: %s', $url ) );
86 \WP_CLI::log( \sprintf( 'Signature mode: %s', $signature_mode ) );
87
88 $get_args = array();
89 $cleanup = $this->apply_signature_mode( $signature_mode, $get_args );
90 $response = Http::get( $url, $get_args, false );
91
92 $cleanup();
93
94 if ( \is_wp_error( $response ) ) {
95 \WP_CLI::error( \sprintf( 'Request failed: %s (Error code: %s).', $response->get_error_message(), $response->get_error_code() ) );
96 }
97
98 $code = \wp_remote_retrieve_response_code( $response );
99
100 \WP_CLI::log( \sprintf( 'Response code: %d', $code ) );
101 \WP_CLI::log( '' );
102
103 // Show response headers if requested.
104 if ( $include_headers ) {
105 $headers = \wp_remote_retrieve_headers( $response );
106
107 \WP_CLI::log( '--- Response Headers ---' );
108
109 foreach ( $headers as $name => $value ) {
110 \WP_CLI::log( \sprintf( '%s: %s', $name, $value ) );
111 }
112
113 \WP_CLI::log( '' );
114 }
115
116 $body = \wp_remote_retrieve_body( $response );
117
118 // Output the body.
119 if ( $raw ) {
120 \WP_CLI::log( $body );
121 } else {
122 $data = \json_decode( $body, true );
123
124 if ( \JSON_ERROR_NONE === \json_last_error() ) {
125 \WP_CLI::log( \wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
126 } else {
127 \WP_CLI::log( $body );
128 }
129 }
130 }
131
132 /**
133 * Apply signature mode overrides via filters.
134 *
135 * For rfc9421, replaces the default sign_request and disables double-knock
136 * to avoid an infinite retry loop when the server returns 4xx.
137 *
138 * @param string $mode The signature mode.
139 * @param array $args The request arguments, passed by reference.
140 *
141 * @return callable Cleanup callback to restore original filters.
142 */
143 private function apply_signature_mode( $mode, &$args ) {
144 $filters = array();
145 $restore = array();
146
147 switch ( $mode ) {
148 case 'default':
149 break;
150
151 case 'none':
152 $args['key_id'] = null;
153 $args['private_key'] = null;
154 break;
155
156 case 'rfc9421':
157 case 'double-knock':
158 // Replace default signing to force RFC 9421. For rfc9421 mode,
159 // also disable double-knock to prevent an infinite retry loop.
160 // For double-knock mode, keep it active but skip re-signing on retry.
161 $removed_sign_request = \remove_filter( 'http_request_args', array( Signature::class, 'sign_request' ), 0 );
162
163 $is_double_knock = 'double-knock' === $mode;
164 $removed_double_knock = false;
165
166 if ( ! $is_double_knock ) {
167 $removed_double_knock = \remove_filter( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10 );
168 }
169
170 $forced_signer = function ( $request_args, $url ) use ( $is_double_knock ) {
171 if ( ! isset( $request_args['key_id'], $request_args['private_key'] ) ) {
172 return $request_args;
173 }
174 // In double-knock mode, skip if already signed (retry from maybe_double_knock).
175 if ( $is_double_knock && ! empty( $request_args['headers']['Signature'] ) ) {
176 return $request_args;
177 }
178 return ( new Http_Message_Signature() )->sign( $request_args, $url );
179 };
180 \add_filter( 'http_request_args', $forced_signer, 0, 2 );
181
182 $filters[] = array( 'http_request_args', $forced_signer, 0 );
183
184 if ( $removed_sign_request ) {
185 $restore[] = array( 'http_request_args', array( Signature::class, 'sign_request' ), 0, 2 );
186 }
187
188 if ( $removed_double_knock ) {
189 $restore[] = array( 'http_response', array( Signature::class, 'maybe_double_knock' ), 10, 3 );
190 }
191 break;
192
193 case 'draft-cavage':
194 $force_cavage = function () {
195 return '0';
196 };
197
198 \add_filter( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
199
200 $filters[] = array( 'pre_option_activitypub_rfc9421_signature', $force_cavage );
201 break;
202
203 default:
204 \WP_CLI::error(
205 \sprintf(
206 'Invalid signature mode "%s". Allowed modes: default, draft-cavage, rfc9421, double-knock, none.',
207 $mode
208 )
209 );
210 }
211
212 return function () use ( $filters, $restore ) {
213 foreach ( $filters as $filter ) {
214 \remove_filter( $filter[0], $filter[1], $filter[2] ?? 10 );
215 }
216 foreach ( $restore as $filter ) {
217 \add_filter( $filter[0], $filter[1], $filter[2], $filter[3] );
218 }
219 };
220 }
221 }
222