PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 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 All 508 releases
← All changes | 3rd-party/activitypub.php +207 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,207 @@
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 +}