PluginProbe
ActivityPub / 9.2.0
ActivityPub v9.2.0
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 / handler / class-announce.php

class-announce.php in ActivityPub 9.2.0, at includes/handler/class-announce.php

164 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Announce handler file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Handler;
9
10 use Activitypub\Collection\Actors;
11 use Activitypub\Collection\Interactions;
12 use Activitypub\Comment;
13 use Activitypub\Http;
14
15 use function Activitypub\is_activity;
16 use function Activitypub\is_activity_public;
17 use function Activitypub\object_to_uri;
18
19 /**
20 * Handle Create requests.
21 */
22 class Announce {
23 /**
24 * Initialize the class, registering WordPress hooks.
25 */
26 public static function init() {
27 \add_action( 'activitypub_inbox_announce', array( self::class, 'handle_announce' ), 10, 3 );
28 }
29
30 /**
31 * Handles "Announce" requests.
32 *
33 * @param array $announcement The activity-object.
34 * @param int|int[] $user_ids The id(s) of the local blog-user(s).
35 * @param \Activitypub\Activity\Activity $activity The activity object.
36 */
37 public static function handle_announce( $announcement, $user_ids, $activity = null ) {
38 // Check if Activity is public or not.
39 if ( ! is_activity_public( $announcement ) ) {
40 // @todo maybe send email
41 return;
42 }
43
44 // Ignore announces from the blog actor.
45 if ( Actors::BLOG_USER_ID === Actors::get_id_by_resource( $announcement['actor'] ) ) {
46 return;
47 }
48
49 // Check if reposts are allowed.
50 if ( ! Comment::is_comment_type_enabled( 'repost' ) ) {
51 return;
52 }
53
54 self::maybe_save_announce( $announcement, $user_ids );
55
56 $object_url = object_to_uri( $announcement['object'] );
57
58 // Force no redirects for this object's request only, so the requested host stays the authoritative origin.
59 $no_redirects = static function ( $args, $url ) use ( $object_url ) {
60 if ( $url === $object_url ) {
61 $args['redirection'] = 0;
62 }
63 return $args;
64 };
65
66 /*
67 * Fetch the activity from its own id rather than the inline copy the Announce
68 * carries: that copy is the announcer's, who is not necessarily the activity's
69 * author. Redirects are forbidden (above) and the cache is bypassed so the
70 * requested host is the authoritative origin — otherwise a redirect, or a
71 * response cached from an earlier redirect-following fetch, could resolve to
72 * attacker content while the host check below still saw the trusted host.
73 */
74 \add_filter( 'http_request_args', $no_redirects, 10, 2 );
75 $object = Http::get_remote_object( $object_url, false );
76 \remove_filter( 'http_request_args', $no_redirects, 10 );
77
78 if ( ! $object || \is_wp_error( $object ) || ! \is_array( $object ) ) {
79 return;
80 }
81
82 if ( ! is_activity( $object ) ) {
83 return;
84 }
85
86 $origin_host = \strtolower( (string) \wp_parse_url( (string) $object_url, \PHP_URL_HOST ) );
87 $actor_host = \strtolower( (string) \wp_parse_url( (string) object_to_uri( $object['actor'] ?? '' ), \PHP_URL_HOST ) );
88
89 /*
90 * Only an actor's own server may vouch for an activity attributed to it, so the
91 * host it was fetched from must equal its actor's host — the same key-host ==
92 * actor-host binding verify_key_id() enforces for signed requests, generalised
93 * to every relayed activity type.
94 */
95 if ( '' === $origin_host || '' === $actor_host || $origin_host !== $actor_host ) {
96 return;
97 }
98
99 $type = \strtolower( $object['type'] );
100
101 /**
102 * Fires after an Announce has been received.
103 *
104 * @param array $object The object.
105 * @param int[] $user_ids The ids of the local blog-users.
106 * @param string $type The type of the activity.
107 * @param \Activitypub\Activity\Activity|null $activity The activity object.
108 */
109 \do_action( 'activitypub_inbox', $object, (array) $user_ids, $type, $activity );
110
111 /**
112 * Fires after an Announce of a specific type has been received.
113 *
114 * @param array $object The object.
115 * @param int[] $user_ids The ids of the local blog-users.
116 * @param \Activitypub\Activity\Activity|null $activity The activity object.
117 */
118 \do_action( "activitypub_inbox_{$type}", $object, (array) $user_ids, $activity );
119 }
120
121 /**
122 * Try to save the Announce.
123 *
124 * @param array $activity The activity-object.
125 * @param int|int[] $user_ids The id of the local blog-user.
126 */
127 public static function maybe_save_announce( $activity, $user_ids ) {
128 $url = object_to_uri( $activity );
129
130 if ( empty( $url ) ) {
131 return;
132 }
133
134 // Match any status, so a repost that was marked as spam or trashed still counts as seen.
135 $exists = Comment::object_id_to_comment( \esc_url_raw( $url ), array( 'status' => 'any' ) );
136 if ( $exists ) {
137 return;
138 }
139
140 // If the object is a Create activity, extract the actual object from it.
141 if ( isset( $activity['object']['type'] ) && 'Create' === $activity['object']['type'] ) {
142 $activity['object'] = object_to_uri( $activity['object']['object'] );
143 }
144
145 $success = false;
146 $result = Interactions::add_reaction( $activity );
147
148 if ( $result && ! \is_wp_error( $result ) ) {
149 $success = true;
150 $result = \get_comment( $result );
151 }
152
153 /**
154 * Fires after an ActivityPub Announce activity has been handled.
155 *
156 * @param array $activity The ActivityPub activity data.
157 * @param int[] $user_ids The local user IDs.
158 * @param bool $success True on success, false otherwise.
159 * @param array|string|int|\WP_Error|false $result The WP_Comment object of the created announce/repost comment, or null if creation failed.
160 */
161 \do_action( 'activitypub_handled_announce', $activity, (array) $user_ids, $success, $result );
162 }
163 }
164