File: /var/www/test/wp-content/plugins/jetpack/3rd-party/activitypub.php
<?php
/**
* Compatibility shim so Jetpack-signed admin requests can reach the
* ActivityPub plugin's auth-gated client-to-server endpoints, used by the
* Jetpack-connected site's wp.com Reader to read the timeline and publish
* notes.
*
* Scope:
* - Three routes, with method affinity (inbox GET, proxy POST, outbox POST).
* - Blog-mode AP sites only; user-mode is out of scope.
* - Real OAuth flows are never overridden — when a Bearer is present we
* defer to the plugin's normal verification.
*
* @package automattic/jetpack
*/
declare( strict_types = 1 );
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Rest_Authentication;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
// The upstream filter passes a third `$scope` arg; the shim deliberately
// drops it (`accepted_args = 2`) because a Jetpack-signed admin grants full
// client-to-server access by design.
add_filter( 'activitypub_oauth_check_permission', 'jetpack_activitypub_reader_auth_check_permission', 10, 2 );
/**
* Filter callback for `activitypub_oauth_check_permission`.
*
* Returns `true` to authorise the request without an AP OAuth bearer when
* every scope predicate holds. Returns the incoming `$result` (typically
* null) otherwise, letting the plugin's normal OAuth check run.
*
* `$request` is typed `mixed` rather than `\WP_REST_Request` because the
* WordPress filter ABI provides no guarantee — `is_target_route()` performs
* the shape check before any method is dispatched on the argument.
*
* @since 15.9
*
* @param mixed $result Result from a previous filter, or null.
* @param mixed $request The REST request being checked, expected to be a `\WP_REST_Request`.
* @return mixed `true` when authorised; `$result` otherwise.
*/
function jetpack_activitypub_reader_auth_check_permission( $result, $request ) {
if ( null !== $result ) {
return $result;
}
// Only run on sites where the wp.com Reader actually needs the bridge:
// connected, non-offline Jetpack sites that aren't wpcom Simple. Simple
// sites already share the AP OAuth datastore with the plugin and pass the
// standard verify_authentication path.
if (
( new Host() )->is_wpcom_simple()
|| ! ( new Connection_Manager() )->is_connected()
|| ( new Status() )->is_offline_mode()
) {
return $result;
}
// A real OAuth client beat us here. Let the plugin handle it normally.
if ( jetpack_activitypub_reader_auth_is_oauth_request() ) {
return $result;
}
if ( ! jetpack_activitypub_reader_auth_is_target_route( $request ) ) {
return $result;
}
if ( ! jetpack_activitypub_reader_auth_is_jetpack_signed() ) {
return $result;
}
// Must follow the signing check: Rest_Authentication installs the wpcom
// user on user-token signed requests, so the current user is only trustworthy
// after that gate has passed.
if ( ! current_user_can( 'manage_options' ) ) {
return $result;
}
if ( ! jetpack_activitypub_reader_auth_is_blog_mode() ) {
return $result;
}
return true;
}
/**
* Whether the current request carries a verified AP OAuth bearer.
*
* Wrapped so the `Server` class absence in non-AP environments is a clean
* `false` rather than a fatal.
*
* @since 15.9
*
* @return bool
*/
function jetpack_activitypub_reader_auth_is_oauth_request(): bool {
if ( ! class_exists( 'Activitypub\OAuth\Server' ) ) {
return false;
}
return \Activitypub\OAuth\Server::is_oauth_request();
}
/**
* Whether the current request was Jetpack-signed (blog or user token).
*
* Both signing flavours are accepted: the wpcom bridge signs outbound calls
* with the user's Jetpack token when one is available and falls back to the
* blog token otherwise. Either is sufficient evidence the call originated
* from a wpcom shadow request the destination already trusts.
*
* @since 15.9
*
* @return bool
*/
function jetpack_activitypub_reader_auth_is_jetpack_signed(): bool {
if ( ! class_exists( Rest_Authentication::class ) ) {
return false;
}
return Rest_Authentication::is_signed_with_user_token()
|| Rest_Authentication::is_signed_with_blog_token();
}
/**
* Whether the destination AP plugin is configured to expose a blog actor.
*
* Accepts both `'blog'` (blog-only) and `'actor_blog'` (per-user + blog).
* On `'actor_blog'` sites the blog actor behaves identically to pure
* blog-mode and is the only actor the wpcom Reader operates on — the
* route patterns are pinned to `user_id=0`, so widening the grant to
* arbitrary user actors is not possible here.
*
* Pure user-mode (`'actor'`) is still rejected: the blog actor doesn't
* exist on those sites, so authorizing `user_id=0` routes would be
* nonsensical.
*
* Uses a `null` sentinel default so an unset option is treated as
* "unknown, deny" rather than implicitly accepted — the AP plugin's own
* option default is `ACTIVITYPUB_ACTOR_MODE` (i.e. `'actor'`), so
* falling back to a blog-accepting mode here would silently widen the
* grant surface on fresh installs.
*
* @since 15.9
*
* @return bool
*/
function jetpack_activitypub_reader_auth_is_blog_mode(): bool {
$mode = get_option( 'activitypub_actor_mode', null );
return 'blog' === $mode || 'actor_blog' === $mode;
}
/**
* Whether the request targets one of the three Reader auth-gated routes.
*
* Each pattern is anchored to the AP namespace and includes a method affinity,
* so callers can't widen the shim by sending an unexpected verb at an allowed
* path (e.g. POSTing to inbox).
*
* @since 15.9
*
* @param \WP_REST_Request $request The REST request.
* @return bool
*/
function jetpack_activitypub_reader_auth_is_target_route( $request ): bool {
if ( ! is_object( $request )
|| ! method_exists( $request, 'get_route' )
|| ! method_exists( $request, 'get_method' )
) {
return false;
}
$route = (string) $request->get_route();
$method = strtoupper( (string) $request->get_method() );
// Patterns are pinned to the blog actor (user_id 0) on purpose: the wpcom
// Reader only operates on the blog actor, and granting the OAuth bypass
// for arbitrary user ids would silently widen the surface if the AP
// plugin ever loosened its downstream `verify_owner` check.
static $patterns = array(
'GET' => array(
'#^/activitypub/\d+\.\d+/(?:users|actors)/0/inbox/?$#',
),
'POST' => array(
'#^/activitypub/\d+\.\d+/proxy/?$#',
'#^/activitypub/\d+\.\d+/(?:users|actors)/0/outbox/?$#',
),
);
if ( ! isset( $patterns[ $method ] ) ) {
return false;
}
foreach ( $patterns[ $method ] as $pattern ) {
if ( preg_match( $pattern, $route ) ) {
return true;
}
}
return false;
}