PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.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
← All changes | includes/class-mailer.php +57 -388 8.2.15.7.0 View file →
@@ -1,9 +1,9 @@
1 1 <?php
2 2 /**
3 3 * Mailer Class.
4 4 *
5 - * @package Activitypub
5 + * @package ActivityPub
6 6 */
7 7
8 8 namespace Activitypub;
9 9
@@ -19,15 +19,17 @@
19 19 public static function init() {
20 20 \add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
21 21 \add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
22 22
23 - \add_action( 'activitypub_handled_follow', array( self::class, 'new_follower' ), 10, 3 );
23 + // New follower notification.
24 + if ( '1' === \get_option( 'activitypub_mailer_new_follower', '0' ) ) {
25 + \add_action( 'activitypub_notification_follow', array( self::class, 'new_follower' ) );
26 + }
24 27
25 - \add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
26 - \add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 20, 2 ); /** After @see \Activitypub\Handler\Create::handle_create() */
27 -
28 - \add_filter( 'notify_post_author', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 );
29 - \add_filter( 'notify_moderator', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 );
28 + // Direct message notification.
29 + if ( '1' === \get_option( 'activitypub_mailer_new_dm', '0' ) ) {
30 + \add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
31 + }
30 32 }
31 33
32 34 /**
33 35 * Filter the subject line for Like and Announce notifications.
@@ -89,35 +91,12 @@
89 91 return $message;
90 92 }
91 93
92 94 $post = \get_post( $comment->comment_post_ID );
93 - $comment_author_domain = '';
95 + $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
94 96
95 - // Only attempt to resolve hostname if we have a valid IP address.
96 - if ( \filter_var( $comment->comment_author_IP, FILTER_VALIDATE_IP ) ) {
97 - $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
98 - }
99 -
100 - // Check if this is a reaction to a post or a comment.
101 - if ( 0 === (int) $comment->comment_parent ) {
102 - $notify_message = \sprintf(
103 - /* translators: 1: Comment type, 2: Post title */
104 - \html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221;.', 'activitypub' ) ),
105 - \esc_html( $comment_type['singular'] ),
106 - \esc_html( $post->post_title )
107 - ) . PHP_EOL . PHP_EOL;
108 -
109 - } else {
110 - $parent_comment = \get_comment( $comment->comment_parent );
111 - $notify_message = \sprintf(
112 - /* translators: 1: Comment type, 2: Post title, 3: Parent comment author */
113 - \html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221; in reply to %3$s&#8217;s comment.', 'activitypub' ) ),
114 - \esc_html( $comment_type['singular'] ),
115 - \esc_html( $post->post_title ),
116 - \esc_html( $parent_comment->comment_author )
117 - ) . PHP_EOL . PHP_EOL;
118 - }
119 -
97 + /* translators: 1: Comment type, 2: Post title */
98 + $notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221;.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n";
120 99 /* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
121 100 $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";
122 101 /* translators: Reaction author URL. */
123 102 $notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
@@ -130,92 +109,47 @@
130 109
131 110 /**
132 111 * Send a notification email for every new follower.
133 112 *
134 - * @param array $activity The activity object.
135 - * @param int|int[] $user_ids The id(s) of the local blog-user(s).
136 - * @param bool $success True on success, false otherwise.
113 + * @param Notification $notification The notification object.
137 114 */
138 - public static function new_follower( $activity, $user_ids, $success ) {
139 - // Only send notification if the follow was successful.
140 - if ( ! $success ) {
115 + public static function new_follower( $notification ) {
116 + $actor = get_remote_metadata_by_actor( $notification->actor );
117 +
118 + if ( ! $actor || \is_wp_error( $actor ) ) {
141 119 return;
142 120 }
143 121
144 - // Extract the user ID (follows are always for a single user).
145 - $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
122 + $email = \get_option( 'admin_email' );
123 + $admin_url = '/options-general.php?page=activitypub&tab=followers';
146 124
147 - // Do not send notifications to the Application user.
148 - if ( Actors::APPLICATION_USER_ID === $user_id ) {
149 - return;
150 - }
125 + if ( $notification->target > Actors::BLOG_USER_ID ) {
126 + $user = \get_user_by( 'id', $notification->target );
151 127
152 - if ( $user_id > Actors::BLOG_USER_ID ) {
153 - if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) {
128 + if ( ! $user ) {
154 129 return;
155 130 }
156 131
157 - $email = \get_userdata( $user_id )->user_email;
132 + $email = $user->user_email;
158 133 $admin_url = '/users.php?page=activitypub-followers-list';
159 - } else {
160 - if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) {
161 - return;
162 - }
163 -
164 - $email = \get_option( 'admin_email' );
165 - $admin_url = '/options-general.php?page=activitypub&tab=followers';
166 134 }
167 135
168 - $actor = get_remote_metadata_by_actor( $activity['actor'] );
169 - if ( ! $actor || \is_wp_error( $actor ) ) {
170 - return;
171 - }
172 -
173 - $actor = self::normalize_actor( $actor );
174 -
175 - // Replace emoji in actor name and summary.
176 - if ( ! empty( $actor['name'] ) ) {
177 - $actor['name'] = Emoji::replace_for_actor( $actor['name'], $actor['url'] );
178 - }
179 - if ( ! empty( $actor['summary'] ) ) {
180 - $actor['summary'] = Emoji::replace_for_actor( $actor['summary'], $actor['url'] );
181 - }
182 -
183 136 $template_args = array_merge(
184 137 $actor,
185 138 array(
186 139 'admin_url' => $admin_url,
187 - 'user_id' => $user_id,
188 - 'stats' => array(
189 - 'outbox' => null,
190 - 'followers' => null,
191 - 'following' => null,
192 - ),
140 + 'target' => $notification->target,
193 141 )
194 142 );
195 143
196 - foreach ( $template_args['stats'] as $field => $value ) {
197 - if ( empty( $actor[ $field ] ) ) {
198 - continue;
199 - }
200 -
201 - $result = Http::get( $actor[ $field ], array(), true );
202 - if ( 200 === \wp_remote_retrieve_response_code( $result ) ) {
203 - $body = \json_decode( \wp_remote_retrieve_body( $result ), true );
204 - if ( isset( $body['totalItems'] ) ) {
205 - $template_args['stats'][ $field ] = $body['totalItems'];
206 - }
207 - }
208 - }
209 -
210 144 /* translators: 1: Blog name, 2: Follower name */
211 - $subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] );
145 + $subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), get_option( 'blogname' ), $actor['name'] );
212 146
213 147 \ob_start();
214 - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args );
148 + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/new-follower-email.php', false, $template_args );
215 149 $html_message = \ob_get_clean();
216 150
217 - $alt_function = static function ( $mailer ) use ( $actor, $admin_url ) {
151 + $alt_function = function ( $mailer ) use ( $actor, $admin_url ) {
218 152 /* translators: 1: Follower name */
219 153 $message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n";
220 154 /* translators: Follower URL */
221 155 $message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
@@ -232,319 +166,54 @@
232 166
233 167 /**
234 168 * Send a direct message.
235 169 *
236 - * @param array $activity The activity object.
237 - * @param int|int[] $user_ids The id(s) of the local blog-user(s).
170 + * @param array $activity The activity object.
171 + * @param int $user_id The id of the local blog-user.
238 172 */
239 - public static function direct_message( $activity, $user_ids ) {
240 - // Early return if activity is public or has no recipients.
241 - if ( is_activity_public( $activity ) || empty( $activity['to'] ) ) {
173 + public static function direct_message( $activity, $user_id ) {
174 + if (
175 + is_activity_public( $activity ) ||
176 + // Only accept messages that have the user in the "to" field.
177 + empty( $activity['to'] ) ||
178 + ! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['to'], true )
179 + ) {
242 180 return;
243 181 }
244 182
245 - // Normalize to array.
246 - $user_ids = (array) $user_ids;
183 + $actor = get_remote_metadata_by_actor( $activity['actor'] );
247 184
248 - // Build a map of user_id => actor_id and filter to only users in the "to" field.
249 - $recipients = array();
250 - foreach ( $user_ids as $user_id ) {
251 - $actor = Actors::get_by_id( $user_id );
252 - if ( \is_wp_error( $actor ) ) {
253 - continue;
254 - }
255 -
256 - $actor_id = $actor->get_id();
257 - if ( \in_array( $actor_id, (array) $activity['to'], true ) ) {
258 - $recipients[ $user_id ] = $actor_id;
259 - }
260 - }
261 -
262 - // No matching recipients.
263 - if ( empty( $recipients ) ) {
264 - return;
265 - }
266 -
267 - // Get actor metadata once (shared for all emails).
268 - $actor = get_remote_metadata_by_actor( $activity['actor'] );
269 185 if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
270 186 return;
271 187 }
272 188
273 - $actor = self::normalize_actor( $actor );
189 + $email = \get_option( 'admin_email' );
274 190
275 - // Send email to each recipient.
276 - foreach ( $recipients as $user_id => $actor_id ) {
277 - // Check user preferences.
278 - if ( $user_id > Actors::BLOG_USER_ID ) {
279 - if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) {
280 - continue;
281 - }
191 + if ( (int) $user_id > Actors::BLOG_USER_ID ) {
192 + $user = \get_user_by( 'id', $user_id );
282 193
283 - $email = \get_userdata( $user_id )->user_email;
284 - } else {
285 - if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) {
286 - continue;
287 - }
288 -
289 - $email = \get_option( 'admin_email' );
194 + if ( ! $user ) {
195 + return;
290 196 }
291 197
292 - $template_args = array(
293 - 'activity' => $activity,
294 - 'actor' => $actor,
295 - 'user_id' => $user_id,
296 - );
297 -
298 - /* translators: 1: Blog name, 2 Actor name */
299 - $subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
300 -
301 - \ob_start();
302 - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args );
303 - $html_message = \ob_get_clean();
304 -
305 - $alt_function = static function ( $mailer ) use ( $actor, $activity ) {
306 - $content = \html_entity_decode(
307 - \wp_strip_all_tags(
308 - str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
309 - ),
310 - ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
311 - );
312 -
313 - /* translators: Actor name */
314 - $message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n";
315 - /* translators: Actor name */
316 - $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
317 - /* translators: Message URL */
318 - $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
319 -
320 - $mailer->{'AltBody'} = $message;
321 - };
322 - \add_action( 'phpmailer_init', $alt_function );
323 -
324 - \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
325 -
326 - \remove_action( 'phpmailer_init', $alt_function );
327 - }
328 - }
329 -
330 - /**
331 - * Send a mention notification.
332 - *
333 - * @param array $activity The activity object.
334 - * @param int|int[] $user_ids The id(s) of the local blog-user(s).
335 - */
336 - public static function mention( $activity, $user_ids ) {
337 - // Early return if activity has no mentions.
338 - if ( empty( $activity['object']['tag'] ) ) {
339 - return;
340 - }
341 -
342 - // Do not send a mention notification if the activity is a reply to a local post or comment.
343 - if ( is_activity_reply( $activity ) && object_id_to_comment( $activity['object']['id'] ) ) {
344 - return;
345 - }
346 -
347 - $recipients = array();
348 - $mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
349 - $mentions = array_map( '\Activitypub\object_to_uri', $mentions );
350 - foreach ( (array) $user_ids as $user_id ) {
351 - $actor = Actors::get_by_id( $user_id );
352 - if ( \is_wp_error( $actor ) ) {
353 - continue;
354 - }
355 -
356 - $actor_id = $actor->get_id();
357 - if ( \in_array( $actor_id, $mentions, true ) ) {
358 - $recipients[ $user_id ] = $actor_id;
359 - }
360 - }
361 -
362 - // No matching recipients.
363 - if ( empty( $recipients ) ) {
364 - return;
365 - }
366 -
367 - // Get actor metadata once (shared for all emails).
368 - $actor = get_remote_metadata_by_actor( $activity['actor'] );
369 - if ( \is_wp_error( $actor ) ) {
370 - return;
371 - }
372 -
373 - $actor = self::normalize_actor( $actor );
374 -
375 - // Send email to each recipient.
376 - foreach ( $recipients as $user_id => $actor_id ) {
377 - // Check user preferences.
378 - if ( $user_id > Actors::BLOG_USER_ID ) {
379 - if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) {
380 - continue;
381 - }
382 -
383 - $email = \get_userdata( $user_id )->user_email;
384 - } else {
385 - if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) {
386 - continue;
387 - }
388 -
389 - $email = \get_option( 'admin_email' );
390 - }
391 -
392 - $template_args = array(
393 - 'activity' => $activity,
394 - 'actor' => $actor,
395 - 'user_id' => $user_id,
396 - );
397 -
398 - /* translators: 1: Blog name, 2 Actor name */
399 - $subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
400 -
401 - \ob_start();
402 - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args );
403 - $html_message = \ob_get_clean();
404 -
405 - $alt_function = static function ( $mailer ) use ( $actor, $activity ) {
406 - $content = \html_entity_decode(
407 - \wp_strip_all_tags(
408 - str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
409 - ),
410 - ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
411 - );
412 -
413 - /* translators: Message content */
414 - $message = \sprintf( \esc_html__( 'New Mention: %s', 'activitypub' ), $content ) . "\r\n\r\n";
415 - /* translators: Actor name */
416 - $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
417 - /* translators: Message URL */
418 - $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
419 -
420 - $mailer->{'AltBody'} = $message;
421 - };
422 - \add_action( 'phpmailer_init', $alt_function );
423 -
424 - \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
425 -
426 - \remove_action( 'phpmailer_init', $alt_function );
427 - }
428 - }
429 -
430 - /**
431 - * Send a templated email to a user.
432 - *
433 - * @param int $user_id The user ID (or BLOG_USER_ID for blog actor).
434 - * @param string $subject The email subject.
435 - * @param string $template The template name (without path/extension).
436 - * @param array $args Template arguments.
437 - * @param string $alt_body Optional plain text alternative. Auto-generated from HTML if empty.
438 - *
439 - * @return bool True if email was sent, false otherwise.
440 - */
441 - public static function send( $user_id, $subject, $template, $args = array(), $alt_body = '' ) {
442 - // Get the recipient email address.
443 - if ( $user_id > Actors::BLOG_USER_ID ) {
444 - $user = \get_userdata( $user_id );
445 - if ( ! $user || empty( $user->user_email ) ) {
446 - return false;
447 - }
448 198 $email = $user->user_email;
449 - } else {
450 - $email = \get_option( 'admin_email' );
451 199 }
452 200
453 - // Load the HTML template.
454 - $template_file = ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/' . \sanitize_file_name( $template ) . '.php';
201 + $content = \html_entity_decode(
202 + \wp_strip_all_tags(
203 + str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
204 + ),
205 + ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
206 + );
455 207
456 - /**
457 - * Filter the email template file path.
458 - *
459 - * @param string $template_file The template file path.
460 - * @param string $template The template name.
461 - * @param int $user_id The user ID.
462 - * @param array $args Template arguments.
463 - */
464 - $template_file = \apply_filters( 'activitypub_email_template', $template_file, $template, $user_id, $args );
208 + /* translators: 1: Blog name, 2 Actor name */
209 + $subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
210 + /* translators: 1: Blog name, 2: Actor name */
211 + $message = \sprintf( \esc_html__( 'New Direct Message: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), $content ) . "\r\n\r\n";
212 + /* translators: Actor name */
213 + $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
214 + /* translators: Actor URL */
215 + $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
465 216
466 - if ( ! \file_exists( $template_file ) ) {
467 - return false;
468 - }
469 -
470 - \ob_start();
471 - \load_template( $template_file, false, $args );
472 - $html_message = \ob_get_clean();
473 -
474 - // Build plain text alternative from HTML if not provided.
475 - if ( empty( $alt_body ) ) {
476 - $alt_body = \wp_strip_all_tags( $html_message );
477 - }
478 - $alt_function = static function ( $mailer ) use ( $alt_body ) {
479 - $mailer->{'AltBody'} = $alt_body;
480 - };
481 - \add_action( 'phpmailer_init', $alt_function );
482 -
483 - $result = \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
484 -
485 - \remove_action( 'phpmailer_init', $alt_function );
486 -
487 - return $result;
488 - }
489 -
490 - /**
491 - * Apply defaults to the actor object.
492 - *
493 - * Ensure that the actor object has a name, url, and webfinger.
494 - *
495 - * @param array $actor The actor object.
496 - *
497 - * @return array The inflated actor object.
498 - */
499 - private static function normalize_actor( $actor ) {
500 - if ( empty( $actor['name'] ) ) {
501 - $actor['name'] = $actor['preferredUsername'];
502 - }
503 -
504 - if ( empty( $actor['url'] ) ) {
505 - $actor['url'] = $actor['id'];
506 - }
507 - $actor['url'] = object_to_uri( $actor['url'] );
508 -
509 - if ( empty( $actor['webfinger'] ) ) {
510 - $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
511 - }
512 -
513 - return $actor;
514 - }
515 -
516 - /**
517 - * Maybe prevent email notifications for comments.
518 - *
519 - * This filter can prevent both post author and moderator notifications
520 - * for comments on specific post types, such as ActivityPub custom post types.
521 - *
522 - * @param bool $maybe_notify Whether to send the notification.
523 - * @param int $comment_id The comment ID.
524 - *
525 - * @return bool False to prevent notification, original value otherwise.
526 - */
527 - public static function maybe_prevent_comment_notification( $maybe_notify, $comment_id ) {
528 - // If already disabled, respect that.
529 - if ( ! $maybe_notify ) {
530 - return $maybe_notify;
531 - }
532 -
533 - $comment = \get_comment( $comment_id );
534 - if ( ! $comment ) {
535 - return $maybe_notify;
536 - }
537 -
538 - $post = \get_post( $comment->comment_post_ID );
539 - if ( ! $post ) {
540 - return $maybe_notify;
541 - }
542 -
543 - // Prevent notifications for comments on ap_post.
544 - if ( is_ap_post( $post ) ) {
545 - return false;
546 - }
547 -
548 - return $maybe_notify;
217 + \wp_mail( $email, $subject, $message );
549 218 }
550 219 }