PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
gutenberg / lib / compat / wordpress-7.1 / notes-mentions.php

notes-mentions.php in Gutenberg 24.1.0, at lib/compat/wordpress-7.1/notes-mentions.php

226 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Mention notifications for notes (block comments).
4 *
5 * Note content can carry `@` mentions, stored as chips of the form
6 * `<span class="wp-note-mention user-N">@Name</span>` where `N` is the
7 * mentioned user's ID (the markup contract lives in
8 * packages/editor/src/components/collab-sidebar/note-mention-completer.tsx,
9 * and the kses allowance for the classes in
10 * lib/compat/wordpress-7.1/block-comments.php). When a note is created through
11 * the REST API this file parses those mentions out of the saved content and
12 * emails the mentioned users.
13 *
14 * WordPress core already notifies the post author of every note via
15 * `wp_new_comment_via_rest_notify_postauthor()` on `rest_insert_comment`. This
16 * file adds the mentioned-user audience on the same hook and deliberately
17 * leaves the post author to core to avoid sending them a duplicate email.
18 *
19 * @package gutenberg
20 * @since 7.1.0
21 */
22
23 /**
24 * Extracts the mentioned user IDs from note content.
25 *
26 * Mentions are stored as chips carrying the `wp-note-mention` class plus a
27 * `user-N` class token holding the mentioned user's ID. Only elements that
28 * carry both are treated as mentions.
29 *
30 * @since 7.1.0
31 *
32 * @param string $content Note (comment) content, as stored.
33 * @return list<int> Unique, positive mentioned user IDs.
34 * @phpstan-return list<positive-int>
35 */
36 function gutenberg_get_note_mentioned_user_ids( string $content ): array {
37 if ( ! str_contains( $content, 'wp-note-mention' ) ) {
38 return array();
39 }
40
41 $user_ids = array();
42 $processor = new WP_HTML_Tag_Processor( $content );
43 while (
44 $processor->next_tag(
45 array(
46 'tag_name' => 'SPAN',
47 'class_name' => 'wp-note-mention',
48 )
49 )
50 ) {
51 foreach ( $processor->class_list() as $class_name ) {
52 if ( 1 === preg_match( '/^user-([1-9][0-9]*)$/', $class_name, $matches ) ) {
53 $user_ids[] = (int) $matches[1];
54 break;
55 }
56 }
57 }
58
59 return array_values( array_unique( $user_ids, SORT_NUMERIC ) );
60 }
61
62 /**
63 * Notifies mentioned users about a new note.
64 *
65 * Runs on `rest_insert_comment` alongside core's post-author notification.
66 * The recipient set is the users mentioned in this note, minus the note's
67 * own author (you are not notified about your own note) and the post
68 * author (core already notifies them about every note).
69 *
70 * Only fires when a note is created, not when an existing one is edited,
71 * so correcting a note does not re-notify everyone who already received it.
72 *
73 * @since 7.1.0
74 *
75 * @param WP_Comment|null $comment The note that was just inserted. Null only if it was deleted in the meantime.
76 * @param mixed $request The REST request. Unused.
77 * @param bool $creating Whether this is a create (true) or update (false).
78 */
79 function gutenberg_notify_note_mentions( ?WP_Comment $comment, $request = null, bool $creating = true ): void {
80 if ( ! $creating || ! $comment ) {
81 return;
82 }
83
84 if ( 'note' !== $comment->comment_type ) {
85 return;
86 }
87
88 // Share the single user-facing notes notification preference with core.
89 if ( ! get_option( 'wp_notes_notify', 1 ) ) {
90 return;
91 }
92
93 $mentioned = gutenberg_get_note_mentioned_user_ids( $comment->comment_content );
94
95 $author_id = (int) $comment->user_id;
96
97 // get_post() falls back to the global post when passed 0, which would compose the email about the wrong post.
98 $comment_post_id = (int) $comment->comment_post_ID;
99 $post = $comment_post_id ? get_post( $comment_post_id ) : null;
100 $post_author_id = $post ? (int) $post->post_author : 0;
101
102 /*
103 * The recipient set is bounded and small (one note's mentions), so
104 * emails are sent synchronously here. If notification volume ever
105 * warrants it, the right fix is to offload delivery to a background
106 * queue (wp_schedule_single_event() / Action Scheduler) rather than
107 * throttle within the request.
108 */
109 foreach ( $mentioned as $user_id ) {
110 $user_id = (int) $user_id;
111
112 // Never notify the author about their own note.
113 if ( $user_id === $author_id ) {
114 continue;
115 }
116
117 // Core already notifies the post author of every note.
118 if ( $user_id === $post_author_id ) {
119 continue;
120 }
121
122 $user = get_userdata( $user_id );
123 if ( ! $user || empty( $user->user_email ) ) {
124 continue;
125 }
126
127 /*
128 * Only notify users who can actually read the note. Notes are
129 * internal: core's WP_REST_Comments_Controller::check_read_permission()
130 * only exposes a note to its author or to users who can edit it, so
131 * the email audience is held to the same bar. A plain read_post
132 * check would leak note content to e.g. subscribers on a public
133 * post, who cannot see the note in the editor.
134 */
135 if ( ! user_can( $user_id, 'edit_comment', $comment->comment_ID ) ) {
136 continue;
137 }
138
139 gutenberg_send_note_notification( $user, $comment, $post );
140 }
141 }
142 /*
143 * Once the Core backport lands, WordPress registers its own
144 * wp_notify_note_mentions() on this same hook from default-filters.php. With
145 * both callbacks attached every mentioned user would be emailed twice, so the
146 * plugin's copy - the newer of the two - replaces Core's while Gutenberg is
147 * active. Core's post-author notification is deliberately left alone: this
148 * file never notifies the post author, so the two do not overlap.
149 */
150 $gutenberg_note_mentions_priority = has_action( 'rest_insert_comment', 'wp_notify_note_mentions' );
151 if ( false !== $gutenberg_note_mentions_priority ) {
152 remove_action( 'rest_insert_comment', 'wp_notify_note_mentions', $gutenberg_note_mentions_priority );
153 }
154 unset( $gutenberg_note_mentions_priority );
155
156 add_action( 'rest_insert_comment', 'gutenberg_notify_note_mentions', 10, 3 );
157
158 /**
159 * Sends a single note mention notification email.
160 *
161 * The email is composed in the recipient's locale, matching how core composes
162 * other user-directed notifications, and links to the post editor the same way
163 * core's own note notification does.
164 *
165 * @since 7.1.0
166 *
167 * @param WP_User $user The recipient.
168 * @param WP_Comment $comment The note that triggered the notification.
169 * @param WP_Post|null $post The post the note belongs to.
170 * @return bool Whether the email was accepted for delivery by wp_mail().
171 */
172 function gutenberg_send_note_notification( WP_User $user, WP_Comment $comment, ?WP_Post $post ): bool {
173 $switched_locale = switch_to_user_locale( $user->ID );
174
175 /*
176 * The site title and the post title are escaped on the way into the database,
177 * and note content is stored as HTML. Both are reversed once here for the
178 * plain text arena of emails. Decoding a second time would go too far and
179 * resolve entities the author meant to be read literally. Tags are stripped
180 * before decoding, so escaped text such as "&lt;code&gt;" survives as text
181 * rather than being read as a tag and dropped.
182 */
183 $blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
184 $post_title = $post ? wp_specialchars_decode( get_the_title( $post ), ENT_QUOTES ) : '';
185 $author_name = $comment->comment_author ? $comment->comment_author : __( 'Someone', 'gutenberg' );
186 $content = wp_specialchars_decode( wp_strip_all_tags( $comment->comment_content ) );
187
188 /*
189 * The rest of the message is composed for the recipient, and so is the editor
190 * link: get_edit_post_link() answers for whoever is current, which here is the
191 * note's author over REST and nobody at all under WP-Cron.
192 */
193 $edit_link = '';
194 if ( $post ) {
195 $previous_user_id = get_current_user_id();
196 wp_set_current_user( $user->ID );
197 $edit_link = (string) get_edit_post_link( $post->ID, 'url' );
198 wp_set_current_user( $previous_user_id );
199 }
200
201 /* translators: %1$s: commenter name, %2$s: post title. */
202 $message = sprintf( __( '%1$s mentioned you in a note on "%2$s".', 'gutenberg' ), $author_name, $post_title );
203 /* translators: %1$s: site name, %2$s: post title. */
204 $subject = sprintf( __( '[%1$s] You were mentioned in a note on "%2$s"', 'gutenberg' ), $blogname, $post_title );
205
206 $lines = array( $message, '' );
207 if ( '' !== $content ) {
208 $lines[] = $content;
209 }
210 if ( $edit_link ) {
211 $lines[] = '';
212 $lines[] = __( 'Edit This', 'gutenberg' ) . ': ' . $edit_link;
213 }
214
215 // Declared explicitly so a filtered default cannot turn the message into HTML.
216 $headers = 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . '"';
217
218 $sent = wp_mail( $user->user_email, $subject, implode( "\n", $lines ), $headers );
219
220 if ( $switched_locale ) {
221 restore_previous_locale();
222 }
223
224 return $sent;
225 }
226