PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
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 trunk, at includes/handler/class-announce.php

179 lines 6.2 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\is_same_host;
18 use function Activitypub\object_to_uri;
19
20 /**
21 * Handle Create requests.
22 */
23 class Announce {
24 /**
25 * Initialize the class, registering WordPress hooks.
26 */
27 public static function init() {
28 \add_action( 'activitypub_inbox_announce', array( self::class, 'handle_announce' ), 10, 3 );
29 }
30
31 /**
32 * Handles "Announce" requests.
33 *
34 * @param array $announcement The activity-object.
35 * @param int|int[] $user_ids The id(s) of the local blog-user(s).
36 * @param \Activitypub\Activity\Activity $activity The activity object.
37 */
38 public static function handle_announce( $announcement, $user_ids, $activity = null ) {
39 // Check if Activity is public or not.
40 if ( ! is_activity_public( $announcement ) ) {
41 // @todo maybe send email
42 return;
43 }
44
45 // Ignore announces from the blog actor.
46 if ( Actors::BLOG_USER_ID === Actors::get_id_by_resource( $announcement['actor'] ) ) {
47 return;
48 }
49
50 // Check if reposts are allowed.
51 if ( ! Comment::is_comment_type_enabled( 'repost' ) ) {
52 return;
53 }
54
55 self::maybe_save_announce( $announcement, $user_ids );
56
57 $object_url = object_to_uri( $announcement['object'] );
58
59 // Force no redirects for this object's request only, so the requested host stays the authoritative origin.
60 $no_redirects = static function ( $args, $url ) use ( $object_url ) {
61 if ( $url === $object_url ) {
62 $args['redirection'] = 0;
63 }
64 return $args;
65 };
66
67 /*
68 * Fetch the activity from its own id rather than the inline copy the Announce
69 * carries: that copy is the announcer's, who is not necessarily the activity's
70 * author. Redirects are forbidden (above) and the cache is bypassed so the
71 * requested host is the authoritative origin — otherwise a redirect, or a
72 * response cached from an earlier redirect-following fetch, could resolve to
73 * attacker content while the host check below still saw the trusted host.
74 */
75 \add_filter( 'http_request_args', $no_redirects, 10, 2 );
76 $object = Http::get_remote_object( $object_url, false );
77 \remove_filter( 'http_request_args', $no_redirects, 10 );
78
79 if ( ! $object || \is_wp_error( $object ) || ! \is_array( $object ) ) {
80 return;
81 }
82
83 if ( ! is_activity( $object ) ) {
84 return;
85 }
86
87 /*
88 * Only an actor's own server may vouch for an activity attributed to it, so the
89 * host it was fetched from must equal its actor's host — the same key-host ==
90 * actor-host binding verify_key_id() enforces for signed requests, generalised
91 * to every relayed activity type.
92 */
93 if ( ! is_same_host( $object_url, $object['actor'] ?? '' ) ) {
94 return;
95 }
96
97 /*
98 * The requested URL is not always the host that answered: get_remote_object() re-fetches a
99 * document from the id it declares when the two disagree, and returns the re-fetched copy.
100 * Bind the actor to that id as well, which an authentic activity shares a host with.
101 *
102 * Only when the document declares one. The id is derived exactly as get_remote_object()
103 * derives it, so the two cannot disagree about what counts as declared: whatever it treats
104 * as id-less it returns as served, without re-fetching, and the origin check above is
105 * already authoritative for those. Binding them here would drop relayed activities that
106 * legitimately omit an id.
107 */
108 $declared_id = isset( $object['id'] ) && \is_string( $object['id'] ) ? $object['id'] : '';
109
110 if ( '' !== $declared_id && ! is_same_host( $declared_id, $object['actor'] ?? '' ) ) {
111 return;
112 }
113
114 $type = \strtolower( $object['type'] );
115
116 /**
117 * Fires after an Announce has been received.
118 *
119 * @param array $object The object.
120 * @param int[] $user_ids The ids of the local blog-users.
121 * @param string $type The type of the activity.
122 * @param \Activitypub\Activity\Activity|null $activity The activity object.
123 */
124 \do_action( 'activitypub_inbox', $object, (array) $user_ids, $type, $activity );
125
126 /**
127 * Fires after an Announce of a specific type has been received.
128 *
129 * @param array $object The object.
130 * @param int[] $user_ids The ids of the local blog-users.
131 * @param \Activitypub\Activity\Activity|null $activity The activity object.
132 */
133 \do_action( "activitypub_inbox_{$type}", $object, (array) $user_ids, $activity );
134 }
135
136 /**
137 * Try to save the Announce.
138 *
139 * @param array $activity The activity-object.
140 * @param int|int[] $user_ids The id of the local blog-user.
141 */
142 public static function maybe_save_announce( $activity, $user_ids ) {
143 $url = object_to_uri( $activity );
144
145 if ( empty( $url ) ) {
146 return;
147 }
148
149 // Match any status, so a repost that was marked as spam or trashed still counts as seen.
150 $exists = Comment::object_id_to_comment( \esc_url_raw( $url ), array( 'status' => 'any' ) );
151 if ( $exists ) {
152 return;
153 }
154
155 // If the object is a Create activity, extract the actual object from it.
156 if ( isset( $activity['object']['type'] ) && 'Create' === $activity['object']['type'] ) {
157 $activity['object'] = object_to_uri( $activity['object']['object'] );
158 }
159
160 $success = false;
161 $result = Interactions::add_reaction( $activity );
162
163 if ( $result && ! \is_wp_error( $result ) ) {
164 $success = true;
165 $result = \get_comment( $result );
166 }
167
168 /**
169 * Fires after an ActivityPub Announce activity has been handled.
170 *
171 * @param array $activity The ActivityPub activity data.
172 * @param int[] $user_ids The local user IDs.
173 * @param bool $success True on success, false otherwise.
174 * @param array|string|int|\WP_Error|false $result The WP_Comment object of the created announce/repost comment, or null if creation failed.
175 */
176 \do_action( 'activitypub_handled_announce', $activity, (array) $user_ids, $success, $result );
177 }
178 }
179