PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / 3rd-party / activitypub.php

activitypub.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at 3rd-party/activitypub.php

208 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Compatibility shim so Jetpack-signed admin requests can reach the
4 * ActivityPub plugin's auth-gated client-to-server endpoints, used by the
5 * Jetpack-connected site's wp.com Reader to read the timeline and publish
6 * notes.
7 *
8 * Scope:
9 * - Three routes, with method affinity (inbox GET, proxy POST, outbox POST).
10 * - Blog-mode AP sites only; user-mode is out of scope.
11 * - Real OAuth flows are never overridden — when a Bearer is present we
12 * defer to the plugin's normal verification.
13 *
14 * @package automattic/jetpack
15 */
16
17 declare( strict_types = 1 );
18
19 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
20 use Automattic\Jetpack\Connection\Rest_Authentication;
21 use Automattic\Jetpack\Status;
22 use Automattic\Jetpack\Status\Host;
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit( 0 );
26 }
27
28 // The upstream filter passes a third `$scope` arg; the shim deliberately
29 // drops it (`accepted_args = 2`) because a Jetpack-signed admin grants full
30 // client-to-server access by design.
31 add_filter( 'activitypub_oauth_check_permission', 'jetpack_activitypub_reader_auth_check_permission', 10, 2 );
32
33 /**
34 * Filter callback for `activitypub_oauth_check_permission`.
35 *
36 * Returns `true` to authorise the request without an AP OAuth bearer when
37 * every scope predicate holds. Returns the incoming `$result` (typically
38 * null) otherwise, letting the plugin's normal OAuth check run.
39 *
40 * `$request` is typed `mixed` rather than `\WP_REST_Request` because the
41 * WordPress filter ABI provides no guarantee — `is_target_route()` performs
42 * the shape check before any method is dispatched on the argument.
43 *
44 * @since 15.9
45 *
46 * @param mixed $result Result from a previous filter, or null.
47 * @param mixed $request The REST request being checked, expected to be a `\WP_REST_Request`.
48 * @return mixed `true` when authorised; `$result` otherwise.
49 */
50 function jetpack_activitypub_reader_auth_check_permission( $result, $request ) {
51 if ( null !== $result ) {
52 return $result;
53 }
54
55 // Only run on sites where the wp.com Reader actually needs the bridge:
56 // connected, non-offline Jetpack sites that aren't wpcom Simple. Simple
57 // sites already share the AP OAuth datastore with the plugin and pass the
58 // standard verify_authentication path.
59 if (
60 ( new Host() )->is_wpcom_simple()
61 || ! ( new Connection_Manager() )->is_connected()
62 || ( new Status() )->is_offline_mode()
63 ) {
64 return $result;
65 }
66
67 // A real OAuth client beat us here. Let the plugin handle it normally.
68 if ( jetpack_activitypub_reader_auth_is_oauth_request() ) {
69 return $result;
70 }
71
72 if ( ! jetpack_activitypub_reader_auth_is_target_route( $request ) ) {
73 return $result;
74 }
75
76 if ( ! jetpack_activitypub_reader_auth_is_jetpack_signed() ) {
77 return $result;
78 }
79
80 // Must follow the signing check: Rest_Authentication installs the wpcom
81 // user on user-token signed requests, so the current user is only trustworthy
82 // after that gate has passed.
83 if ( ! current_user_can( 'manage_options' ) ) {
84 return $result;
85 }
86
87 if ( ! jetpack_activitypub_reader_auth_is_blog_mode() ) {
88 return $result;
89 }
90
91 return true;
92 }
93
94 /**
95 * Whether the current request carries a verified AP OAuth bearer.
96 *
97 * Wrapped so the `Server` class absence in non-AP environments is a clean
98 * `false` rather than a fatal.
99 *
100 * @since 15.9
101 *
102 * @return bool
103 */
104 function jetpack_activitypub_reader_auth_is_oauth_request(): bool {
105 if ( ! class_exists( 'Activitypub\OAuth\Server' ) ) {
106 return false;
107 }
108 return \Activitypub\OAuth\Server::is_oauth_request();
109 }
110
111 /**
112 * Whether the current request was Jetpack-signed (blog or user token).
113 *
114 * Both signing flavours are accepted: the wpcom bridge signs outbound calls
115 * with the user's Jetpack token when one is available and falls back to the
116 * blog token otherwise. Either is sufficient evidence the call originated
117 * from a wpcom shadow request the destination already trusts.
118 *
119 * @since 15.9
120 *
121 * @return bool
122 */
123 function jetpack_activitypub_reader_auth_is_jetpack_signed(): bool {
124 if ( ! class_exists( Rest_Authentication::class ) ) {
125 return false;
126 }
127 return Rest_Authentication::is_signed_with_user_token()
128 || Rest_Authentication::is_signed_with_blog_token();
129 }
130
131 /**
132 * Whether the destination AP plugin is configured to expose a blog actor.
133 *
134 * Accepts both `'blog'` (blog-only) and `'actor_blog'` (per-user + blog).
135 * On `'actor_blog'` sites the blog actor behaves identically to pure
136 * blog-mode and is the only actor the wpcom Reader operates on — the
137 * route patterns are pinned to `user_id=0`, so widening the grant to
138 * arbitrary user actors is not possible here.
139 *
140 * Pure user-mode (`'actor'`) is still rejected: the blog actor doesn't
141 * exist on those sites, so authorizing `user_id=0` routes would be
142 * nonsensical.
143 *
144 * Uses a `null` sentinel default so an unset option is treated as
145 * "unknown, deny" rather than implicitly accepted — the AP plugin's own
146 * option default is `ACTIVITYPUB_ACTOR_MODE` (i.e. `'actor'`), so
147 * falling back to a blog-accepting mode here would silently widen the
148 * grant surface on fresh installs.
149 *
150 * @since 15.9
151 *
152 * @return bool
153 */
154 function jetpack_activitypub_reader_auth_is_blog_mode(): bool {
155 $mode = get_option( 'activitypub_actor_mode', null );
156 return 'blog' === $mode || 'actor_blog' === $mode;
157 }
158
159 /**
160 * Whether the request targets one of the three Reader auth-gated routes.
161 *
162 * Each pattern is anchored to the AP namespace and includes a method affinity,
163 * so callers can't widen the shim by sending an unexpected verb at an allowed
164 * path (e.g. POSTing to inbox).
165 *
166 * @since 15.9
167 *
168 * @param \WP_REST_Request $request The REST request.
169 * @return bool
170 */
171 function jetpack_activitypub_reader_auth_is_target_route( $request ): bool {
172 if ( ! is_object( $request )
173 || ! method_exists( $request, 'get_route' )
174 || ! method_exists( $request, 'get_method' )
175 ) {
176 return false;
177 }
178
179 $route = (string) $request->get_route();
180 $method = strtoupper( (string) $request->get_method() );
181
182 // Patterns are pinned to the blog actor (user_id 0) on purpose: the wpcom
183 // Reader only operates on the blog actor, and granting the OAuth bypass
184 // for arbitrary user ids would silently widen the surface if the AP
185 // plugin ever loosened its downstream `verify_owner` check.
186 static $patterns = array(
187 'GET' => array(
188 '#^/activitypub/\d+\.\d+/(?:users|actors)/0/inbox/?$#',
189 ),
190 'POST' => array(
191 '#^/activitypub/\d+\.\d+/proxy/?$#',
192 '#^/activitypub/\d+\.\d+/(?:users|actors)/0/outbox/?$#',
193 ),
194 );
195
196 if ( ! isset( $patterns[ $method ] ) ) {
197 return false;
198 }
199
200 foreach ( $patterns[ $method ] as $pattern ) {
201 if ( preg_match( $pattern, $route ) ) {
202 return true;
203 }
204 }
205
206 return false;
207 }
208