PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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 / class-mailer.php

class-mailer.php in ActivityPub 7.7.0, at includes/class-mailer.php

489 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Mailer Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Collection\Actors;
11 use Activitypub\Collection\Posts;
12
13 /**
14 * Mailer Class.
15 */
16 class Mailer {
17 /**
18 * Initialize the Mailer.
19 */
20 public static function init() {
21 \add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
22 \add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
23
24 \add_action( 'activitypub_handled_follow', array( self::class, 'new_follower' ), 10, 3 );
25
26 \add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
27 \add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 20, 2 ); /** After @see \Activitypub\Handler\Create::handle_create() */
28
29 \add_filter( 'notify_post_author', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 );
30 \add_filter( 'notify_moderator', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 );
31 }
32
33 /**
34 * Filter the subject line for Like and Announce notifications.
35 *
36 * @param string $subject The default subject line.
37 * @param int|string $comment_id The comment ID.
38 *
39 * @return string The filtered subject line.
40 */
41 public static function comment_notification_subject( $subject, $comment_id ) {
42 $comment = \get_comment( $comment_id );
43
44 if ( ! $comment ) {
45 return $subject;
46 }
47
48 $type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
49
50 if ( 'activitypub' !== $type ) {
51 return $subject;
52 }
53
54 $singular = Comment::get_comment_type_attr( $comment->comment_type, 'singular' );
55
56 if ( ! $singular ) {
57 return $subject;
58 }
59
60 $post = \get_post( $comment->comment_post_ID );
61
62 /* translators: 1: Blog name, 2: Like or Repost, 3: Post title */
63 return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) );
64 }
65
66 /**
67 * Filter the notification text for Like and Announce notifications.
68 *
69 * @param string $message The default notification text.
70 * @param int|string $comment_id The comment ID.
71 *
72 * @return string The filtered notification text.
73 */
74 public static function comment_notification_text( $message, $comment_id ) {
75 $comment = \get_comment( $comment_id );
76
77 if ( ! $comment ) {
78 return $message;
79 }
80
81 $type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
82
83 if ( 'activitypub' !== $type ) {
84 return $message;
85 }
86
87 $comment_type = Comment::get_comment_type( $comment->comment_type );
88
89 if ( ! $comment_type ) {
90 return $message;
91 }
92
93 $post = \get_post( $comment->comment_post_ID );
94 $comment_author_domain = '';
95
96 // Only attempt to resolve hostname if we have a valid IP address.
97 if ( \filter_var( $comment->comment_author_IP, FILTER_VALIDATE_IP ) ) {
98 $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
99 }
100
101 // Check if this is a reaction to a post or a comment.
102 if ( 0 === (int) $comment->comment_parent ) {
103 $notify_message = \sprintf(
104 /* translators: 1: Comment type, 2: Post title */
105 \html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221;.', 'activitypub' ) ),
106 \esc_html( $comment_type['singular'] ),
107 \esc_html( $post->post_title )
108 ) . PHP_EOL . PHP_EOL;
109
110 } else {
111 $parent_comment = \get_comment( $comment->comment_parent );
112 $notify_message = \sprintf(
113 /* translators: 1: Comment type, 2: Post title, 3: Parent comment author */
114 \html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221; in reply to %3$s&#8217;s comment.', 'activitypub' ) ),
115 \esc_html( $comment_type['singular'] ),
116 \esc_html( $post->post_title ),
117 \esc_html( $parent_comment->comment_author )
118 ) . PHP_EOL . PHP_EOL;
119 }
120
121 /* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
122 $notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n";
123 /* translators: Reaction author URL. */
124 $notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
125 /* translators: Comment type label */
126 $notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
127 $notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";
128
129 return $notify_message;
130 }
131
132 /**
133 * Send a notification email for every new follower.
134 *
135 * @param array $activity The activity object.
136 * @param int|int[] $user_ids The id(s) of the local blog-user(s).
137 * @param bool $success True on success, false otherwise.
138 */
139 public static function new_follower( $activity, $user_ids, $success ) {
140 // Only send notification if the follow was successful.
141 if ( ! $success ) {
142 return;
143 }
144
145 // Extract the user ID (follows are always for a single user).
146 $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
147
148 // Do not send notifications to the Application user.
149 if ( Actors::APPLICATION_USER_ID === $user_id ) {
150 return;
151 }
152
153 if ( $user_id > Actors::BLOG_USER_ID ) {
154 if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) {
155 return;
156 }
157
158 $email = \get_userdata( $user_id )->user_email;
159 $admin_url = '/users.php?page=activitypub-followers-list';
160 } else {
161 if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) {
162 return;
163 }
164
165 $email = \get_option( 'admin_email' );
166 $admin_url = '/options-general.php?page=activitypub&tab=followers';
167 }
168
169 $actor = get_remote_metadata_by_actor( $activity['actor'] );
170 if ( ! $actor || \is_wp_error( $actor ) ) {
171 return;
172 }
173
174 $actor = self::normalize_actor( $actor );
175
176 $template_args = array_merge(
177 $actor,
178 array(
179 'admin_url' => $admin_url,
180 'user_id' => $user_id,
181 'stats' => array(
182 'outbox' => null,
183 'followers' => null,
184 'following' => null,
185 ),
186 )
187 );
188
189 foreach ( $template_args['stats'] as $field => $value ) {
190 if ( empty( $actor[ $field ] ) ) {
191 continue;
192 }
193
194 $result = Http::get( $actor[ $field ], true );
195 if ( 200 === \wp_remote_retrieve_response_code( $result ) ) {
196 $body = \json_decode( \wp_remote_retrieve_body( $result ), true );
197 if ( isset( $body['totalItems'] ) ) {
198 $template_args['stats'][ $field ] = $body['totalItems'];
199 }
200 }
201 }
202
203 /* translators: 1: Blog name, 2: Follower name */
204 $subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] );
205
206 \ob_start();
207 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args );
208 $html_message = \ob_get_clean();
209
210 $alt_function = function ( $mailer ) use ( $actor, $admin_url ) {
211 /* translators: 1: Follower name */
212 $message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n";
213 /* translators: Follower URL */
214 $message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
215 $message .= \__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
216 $message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";
217 $mailer->{'AltBody'} = $message;
218 };
219 \add_action( 'phpmailer_init', $alt_function );
220
221 \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
222
223 \remove_action( 'phpmailer_init', $alt_function );
224 }
225
226 /**
227 * Send a direct message.
228 *
229 * @param array $activity The activity object.
230 * @param int|int[] $user_ids The id(s) of the local blog-user(s).
231 */
232 public static function direct_message( $activity, $user_ids ) {
233 // Early return if activity is public or has no recipients.
234 if ( is_activity_public( $activity ) || empty( $activity['to'] ) ) {
235 return;
236 }
237
238 // Normalize to array.
239 $user_ids = (array) $user_ids;
240
241 // Build a map of user_id => actor_id and filter to only users in the "to" field.
242 $recipients = array();
243 foreach ( $user_ids as $user_id ) {
244 $actor = Actors::get_by_id( $user_id );
245 if ( \is_wp_error( $actor ) ) {
246 continue;
247 }
248
249 $actor_id = $actor->get_id();
250 if ( \in_array( $actor_id, (array) $activity['to'], true ) ) {
251 $recipients[ $user_id ] = $actor_id;
252 }
253 }
254
255 // No matching recipients.
256 if ( empty( $recipients ) ) {
257 return;
258 }
259
260 // Get actor metadata once (shared for all emails).
261 $actor = get_remote_metadata_by_actor( $activity['actor'] );
262 if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
263 return;
264 }
265
266 $actor = self::normalize_actor( $actor );
267
268 // Send email to each recipient.
269 foreach ( $recipients as $user_id => $actor_id ) {
270 // Check user preferences.
271 if ( $user_id > Actors::BLOG_USER_ID ) {
272 if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) {
273 continue;
274 }
275
276 $email = \get_userdata( $user_id )->user_email;
277 } else {
278 if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) {
279 continue;
280 }
281
282 $email = \get_option( 'admin_email' );
283 }
284
285 $template_args = array(
286 'activity' => $activity,
287 'actor' => $actor,
288 'user_id' => $user_id,
289 );
290
291 /* translators: 1: Blog name, 2 Actor name */
292 $subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
293
294 \ob_start();
295 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args );
296 $html_message = \ob_get_clean();
297
298 $alt_function = function ( $mailer ) use ( $actor, $activity ) {
299 $content = \html_entity_decode(
300 \wp_strip_all_tags(
301 str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
302 ),
303 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
304 );
305
306 /* translators: Actor name */
307 $message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n";
308 /* translators: Actor name */
309 $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
310 /* translators: Message URL */
311 $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
312
313 $mailer->{'AltBody'} = $message;
314 };
315 \add_action( 'phpmailer_init', $alt_function );
316
317 \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
318
319 \remove_action( 'phpmailer_init', $alt_function );
320 }
321 }
322
323 /**
324 * Send a mention notification.
325 *
326 * @param array $activity The activity object.
327 * @param int|int[] $user_ids The id(s) of the local blog-user(s).
328 */
329 public static function mention( $activity, $user_ids ) {
330 // Early return if activity has no mentions.
331 if ( empty( $activity['object']['tag'] ) ) {
332 return;
333 }
334
335 // Do not send a mention notification if the activity is a reply to a local post or comment.
336 if ( is_activity_reply( $activity ) && object_id_to_comment( $activity['object']['id'] ) ) {
337 return;
338 }
339
340 $recipients = array();
341 $mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
342 $mentions = array_map( '\Activitypub\object_to_uri', $mentions );
343 foreach ( (array) $user_ids as $user_id ) {
344 $actor = Actors::get_by_id( $user_id );
345 if ( \is_wp_error( $actor ) ) {
346 continue;
347 }
348
349 $actor_id = $actor->get_id();
350 if ( \in_array( $actor_id, $mentions, true ) ) {
351 $recipients[ $user_id ] = $actor_id;
352 }
353 }
354
355 // No matching recipients.
356 if ( empty( $recipients ) ) {
357 return;
358 }
359
360 // Get actor metadata once (shared for all emails).
361 $actor = get_remote_metadata_by_actor( $activity['actor'] );
362 if ( \is_wp_error( $actor ) ) {
363 return;
364 }
365
366 $actor = self::normalize_actor( $actor );
367
368 // Send email to each recipient.
369 foreach ( $recipients as $user_id => $actor_id ) {
370 // Check user preferences.
371 if ( $user_id > Actors::BLOG_USER_ID ) {
372 if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) {
373 continue;
374 }
375
376 $email = \get_userdata( $user_id )->user_email;
377 } else {
378 if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) {
379 continue;
380 }
381
382 $email = \get_option( 'admin_email' );
383 }
384
385 $template_args = array(
386 'activity' => $activity,
387 'actor' => $actor,
388 'user_id' => $user_id,
389 );
390
391 /* translators: 1: Blog name, 2 Actor name */
392 $subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
393
394 \ob_start();
395 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args );
396 $html_message = \ob_get_clean();
397
398 $alt_function = function ( $mailer ) use ( $actor, $activity ) {
399 $content = \html_entity_decode(
400 \wp_strip_all_tags(
401 str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
402 ),
403 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
404 );
405
406 /* translators: Message content */
407 $message = \sprintf( \esc_html__( 'New Mention: %s', 'activitypub' ), $content ) . "\r\n\r\n";
408 /* translators: Actor name */
409 $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
410 /* translators: Message URL */
411 $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
412
413 $mailer->{'AltBody'} = $message;
414 };
415 \add_action( 'phpmailer_init', $alt_function );
416
417 \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
418
419 \remove_action( 'phpmailer_init', $alt_function );
420 }
421 }
422
423 /**
424 * Apply defaults to the actor object.
425 *
426 * Ensure that the actor object has a name, url, and webfinger.
427 *
428 * @param array $actor The actor object.
429 *
430 * @return array The inflated actor object.
431 */
432 private static function normalize_actor( $actor ) {
433 if ( empty( $actor['name'] ) ) {
434 $actor['name'] = $actor['preferredUsername'];
435 }
436
437 if ( empty( $actor['url'] ) ) {
438 $actor['url'] = $actor['id'];
439 }
440 $actor['url'] = object_to_uri( $actor['url'] );
441
442 if ( empty( $actor['webfinger'] ) ) {
443 $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
444 }
445
446 return $actor;
447 }
448
449 /**
450 * Maybe prevent email notifications for comments.
451 *
452 * This filter can prevent both post author and moderator notifications
453 * for comments on specific post types, such as ActivityPub custom post types.
454 *
455 * @param bool $maybe_notify Whether to send the notification.
456 * @param int $comment_id The comment ID.
457 *
458 * @return bool False to prevent notification, original value otherwise.
459 */
460 public static function maybe_prevent_comment_notification( $maybe_notify, $comment_id ) {
461 // If already disabled, respect that.
462 if ( ! $maybe_notify ) {
463 return $maybe_notify;
464 }
465
466 // Only prevent if the create_posts option is enabled.
467 if ( '1' !== \get_option( 'activitypub_create_posts', false ) ) {
468 return $maybe_notify;
469 }
470
471 $comment = \get_comment( $comment_id );
472 if ( ! $comment ) {
473 return $maybe_notify;
474 }
475
476 $post = \get_post( $comment->comment_post_ID );
477 if ( ! $post ) {
478 return $maybe_notify;
479 }
480
481 // Prevent notifications for comments on ap_post.
482 if ( Posts::POST_TYPE === $post->post_type ) {
483 return false;
484 }
485
486 return $maybe_notify;
487 }
488 }
489