PluginProbe
Edit Flow / 0.11.0
Edit Flow v0.11.0
0.11.1 0.11.0 0.7.2 0.7.3 0.7.4 0.7.5 0.7.6 0.8 0.8.1 0.8.2 0.9 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 0.9.7 0.9.8 0.9.9 trunk 0.1.5 0.10.0 0.10.1 0.10.2 All 44 releases
edit-flow / modules / notifications / notifications.php

notifications.php in Edit Flow 0.11.0, at modules/notifications/notifications.php

1,637 lines 61.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Notifications module for Edit Flow.
4 *
5 * Email notifications for Edit Flow and more.
6 *
7 * @package EditFlow
8 */
9
10 if ( ! defined( 'EF_NOTIFICATION_USE_CRON' ) ) {
11 define( 'EF_NOTIFICATION_USE_CRON', false );
12 }
13
14 if ( ! class_exists( 'EF_Notifications' ) ) {
15
16 /**
17 * Notifications module for Edit Flow.
18 */
19 class EF_Notifications extends EF_Module {
20
21 /**
22 * Taxonomy name used to store users following posts.
23 *
24 * @var string
25 */
26 public $following_users_taxonomy = 'following_users';
27
28 /**
29 * Taxonomy name used to store user groups following posts.
30 *
31 * @var string
32 */
33 public $following_usergroups_taxonomy = EF_User_Groups::taxonomy_key;
34
35 /**
36 * The module instance.
37 *
38 * @var object
39 */
40 public $module;
41
42 /**
43 * Capability required to edit post subscriptions.
44 *
45 * @var string
46 */
47 public $edit_post_subscriptions_cap = 'edit_post_subscriptions';
48
49 /**
50 * Register the module with Edit Flow but don't do anything else.
51 */
52 public function __construct() {
53
54 // Register the module with Edit Flow.
55 $this->module_url = $this->get_module_url( __FILE__ );
56 $args = [
57 'title' => __( 'Notifications', 'edit-flow' ),
58 'short_description' => __( 'Update your team of important changes to your content.', 'edit-flow' ),
59 'extended_description' => __( 'With email notifications, you can keep everyone updated about what’s happening with a given content. Each status change or editorial comment sends out an email notification to users subscribed to a post. User groups can be used to manage who receives notifications on what. With webhook notifications, all notifications will also be sent to the specified webhook URL(i.e.: Slack incoming webhooks) but will ignore specific user or user groups subscription settings.', 'edit-flow' ),
60 'module_url' => $this->module_url,
61 'img_url' => $this->module_url . 'lib/notifications_s128.png',
62 'slug' => 'notifications',
63 'default_options' => [
64 'enabled' => 'on',
65 'post_types' => [
66 'post' => 'on',
67 'page' => 'on',
68 ],
69 'always_notify_admin' => 'off',
70 'send_to_webhook' => 'off',
71 'webhook_url' => '',
72 ],
73 'configure_page_cb' => 'print_configure_view',
74 'post_type_support' => 'ef_notification',
75 'autoload' => false,
76 'settings_help_tab' => [
77 'id' => 'ef-notifications-overview',
78 'title' => __( 'Overview', 'edit-flow' ),
79 'content' => __( '<p>Notifications ensure you keep up to date with progress your most important content. Users can be subscribed to notifications on a post one by one or by selecting user groups.</p><p>When enabled, email notifications can be sent when a post changes status or an editorial comment is left by a writer or an editor.</p>', 'edit-flow' ),
80 ],
81 'settings_help_sidebar' => __( '<p><strong>For more information:</strong></p><p><a href="https://editflow.org/features/notifications/">Notifications Documentation</a></p><p><a href="https://wordpress.org/support/plugin/edit-flow/">Edit Flow Forum</a></p><p><a href="https://github.com/Automattic/Edit-Flow">Edit Flow on GitHub</a></p>', 'edit-flow' ),
82 ];
83 $this->module = EditFlow()->register_module( 'notifications', $args );
84 }
85
86 /**
87 * Initialize the notifications class if the plugin is enabled.
88 */
89 public function init() {
90
91 // Register our taxonomies for managing relationships.
92 $this->register_taxonomies();
93
94 // Allow users to use a different user capability for editing post subscriptions.
95 $this->edit_post_subscriptions_cap = apply_filters( 'ef_edit_post_subscriptions_cap', $this->edit_post_subscriptions_cap );
96
97 // Set up metabox and related actions.
98 add_action( 'add_meta_boxes', [ $this, 'add_post_meta_box' ] );
99
100 // Add "access badge" to the subscribers list.
101 add_action( 'ef_user_subscribe_actions', [ $this, 'display_subscriber_warning_badges' ], 10, 2 );
102
103 // Saving post actions.
104 // self::save_post_subscriptions() is hooked into transition_post_status so we can ensure usergroup data
105 // is properly saved before sending notifs.
106 add_action( 'transition_post_status', [ $this, 'save_post_subscriptions' ], 0, 3 );
107 add_action( 'transition_post_status', [ $this, 'notification_status_change' ], 10, 3 );
108 add_action( 'ef_post_insert_editorial_comment', [ $this, 'notification_comment' ] );
109 add_action( 'delete_user', [ $this, 'delete_user_action' ] );
110 add_action( 'ef_send_scheduled_email', [ $this, 'send_single_email' ], 10, 4 );
111
112 add_action( 'admin_init', [ $this, 'register_settings' ] );
113
114 // Javascript and CSS if we need it.
115 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
116 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_styles' ] );
117
118 // Add a "Follow" link to posts.
119 if ( apply_filters( 'ef_notifications_show_follow_link', true ) ) {
120 // A little extra JS for the follow button.
121 add_action( 'admin_head', [ $this, 'action_admin_head_follow_js' ] );
122 // Manage Posts.
123 add_filter( 'post_row_actions', [ $this, 'filter_post_row_actions' ], 10, 2 );
124 add_filter( 'page_row_actions', [ $this, 'filter_post_row_actions' ], 10, 2 );
125 // Calendar and Story Budget.
126 add_filter( 'ef_calendar_item_actions', [ $this, 'filter_post_row_actions' ], 10, 2 );
127 add_filter( 'ef_story_budget_item_actions', [ $this, 'filter_post_row_actions' ], 10, 2 );
128 }
129
130 // Ajax for saving notification updates.
131 add_action( 'wp_ajax_save_notifications', [ $this, 'ajax_save_post_subscriptions' ] );
132 add_action( 'wp_ajax_ef_notifications_user_post_subscription', [ $this, 'handle_user_post_subscription' ] );
133 }
134
135 /**
136 * Load the capabilities onto users the first time the module is run
137 *
138 * @since 0.7
139 */
140 public function install() {
141
142 // Add necessary capabilities to allow management of notifications.
143 $notifications_roles = [
144 'administrator' => [ 'edit_post_subscriptions' ],
145 'editor' => [ 'edit_post_subscriptions' ],
146 'author' => [ 'edit_post_subscriptions' ],
147 ];
148
149 foreach ( $notifications_roles as $role => $caps ) {
150 $this->add_caps_to_role( $role, $caps );
151 }
152 }
153
154 /**
155 * Upgrade our data in case we need to.
156 *
157 * @since 0.7
158 *
159 * @param string $previous_version The previous plugin version.
160 */
161 public function upgrade( $previous_version ) {
162 global $edit_flow;
163
164 // Upgrade path to v0.7.
165 if ( version_compare( $previous_version, '0.7', '<' ) ) {
166 // Migrate whether notifications were enabled or not.
167 $enabled = get_option( 'edit_flow_notifications_enabled' );
168 if ( $enabled ) {
169 $enabled = 'on';
170 } else {
171 $enabled = 'off';
172 }
173 $edit_flow->update_module_option( $this->module->name, 'enabled', $enabled );
174 delete_option( 'edit_flow_notifications_enabled' );
175 // Migrate whether to always notify the admin.
176 $always_notify_admin = get_option( 'edit_flow_always_notify_admin' );
177 if ( $always_notify_admin ) {
178 $always_notify_admin = 'on';
179 } else {
180 $always_notify_admin = 'off';
181 }
182 $edit_flow->update_module_option( $this->module->name, 'always_notify_admin', $always_notify_admin );
183 delete_option( 'edit_flow_always_notify_admin' );
184
185 // Technically we've run this code before so we don't want to auto-install new data.
186 $edit_flow->update_module_option( $this->module->name, 'loaded_once', true );
187 }
188 }
189
190 /**
191 * Register the taxonomies we use to manage relationships.
192 *
193 * @since 0.7
194 *
195 * @uses register_taxonomy()
196 */
197 public function register_taxonomies() {
198
199 // Load the currently supported post types so we only register against those.
200 $supported_post_types = $this->get_post_types_for_module( $this->module );
201
202 $args = [
203 'hierarchical' => false,
204 'update_count_callback' => '_update_post_term_count',
205 'label' => false,
206 'query_var' => false,
207 'rewrite' => false,
208 'public' => false,
209 'show_ui' => false,
210 ];
211 register_taxonomy( $this->following_users_taxonomy, $supported_post_types, $args );
212 }
213
214 /**
215 * Enqueue necessary admin scripts.
216 *
217 * @since 0.7
218 *
219 * @uses wp_enqueue_script()
220 */
221 public function enqueue_admin_scripts() {
222 global $post;
223
224 if ( $this->is_post_management_page( $this->module->name ) ) {
225 wp_enqueue_script( 'jquery-listfilterizer' );
226 wp_enqueue_script( 'edit-flow-notifications-js', $this->module_url . 'lib/notifications.js', [ 'jquery', 'jquery-listfilterizer' ], EDIT_FLOW_VERSION, true );
227
228 $localization_data = [
229 'no_access' => esc_html__( 'No Access', 'edit-flow' ),
230 'no_email' => esc_html__( 'No Email', 'edit-flow' ),
231 'post_author' => esc_html__( 'Post Author', 'edit-flow' ),
232 'auto_subscribed' => esc_html__( 'Auto-subscribed', 'edit-flow' ),
233 ];
234
235 // Add post author info if we're on a post edit screen.
236 if ( $post ) {
237 $localization_data['post_author_id'] = (int) $post->post_author;
238 $localization_data['post_author_auto_subscribe'] = apply_filters( 'ef_notification_auto_subscribe_post_author', true, 'subscription_action' );
239
240 // Check if post author is currently a follower.
241 $followers = $this->get_following_users( $post->ID, 'id' );
242 $localization_data['post_author_is_following'] = in_array( (int) $post->post_author, $followers, true );
243 }
244
245 wp_localize_script(
246 'edit-flow-notifications-js',
247 'ef_notifications_localization',
248 $localization_data
249 );
250 }
251 }
252
253 /**
254 * Enqueue necessary admin styles, but only on the proper pages
255 *
256 * @since 0.7
257 *
258 * @uses wp_enqueue_style()
259 */
260 public function enqueue_admin_styles() {
261
262 if ( $this->is_post_management_page( $this->module->name ) || $this->is_whitelisted_settings_view( $this->module->name ) ) {
263 wp_enqueue_style( 'jquery-listfilterizer' );
264 wp_enqueue_style( 'edit-flow-notifications-css', $this->module->module_url . 'lib/notifications.css', false, EDIT_FLOW_VERSION );
265 }
266 }
267
268 /**
269 * JS required for the Follow link to work
270 *
271 * @since 0.8
272 */
273 public function action_admin_head_follow_js() {
274 ?>
275 <script type='text/Javascript'>
276 jQuery(document).ready(function($) {
277 /**
278 * Action to Follow / Unfollow posts on the manage posts screen
279 */
280 $('.wp-list-table, #ef-calendar-view, #ef-story-budget-wrap').on( 'click', '.ef_follow_link a', function(e){
281
282 e.preventDefault();
283
284 var link = $(this);
285
286 $.ajax({
287 type : 'GET',
288 url : link.attr( 'href' ),
289 success : function( data ) {
290 if ( 'success' == data.status ) {
291 link.attr( 'href', data.message.link );
292 link.attr( 'title', data.message.title );
293 link.text( data.message.text );
294 }
295 // @todo expose the error somehow
296 }
297 });
298 return false;
299 });
300 });
301 </script>
302 <?php
303 }
304
305 /**
306 * Add a "Follow" link to supported post types Manage Posts view.
307 *
308 * @since 0.8
309 *
310 * @param array $actions Any existing item actions.
311 * @param int|object $post Post id or object.
312 * @return array The follow link has been appended.
313 */
314 public function filter_post_row_actions( $actions, $post ) {
315
316 $post = get_post( $post );
317
318 if ( ! in_array( $post->post_type, $this->get_post_types_for_module( $this->module ) ) ) {
319 return $actions;
320 }
321
322 if ( ! current_user_can( $this->edit_post_subscriptions_cap ) || ! current_user_can( 'edit_post', $post->ID ) ) {
323 return $actions;
324 }
325
326 $parts = $this->get_follow_action_parts( $post );
327
328 $actions['ef_follow_link'] = '<a title="' . esc_attr( $parts['title'] ) . '" href="' . esc_url( $parts['link'] ) . '">' . $parts['text'] . '</a>';
329
330 return $actions;
331 }
332
333 /**
334 * Get an action parts for a user to follow or unfollow a post.
335 *
336 * @since 0.8
337 *
338 * @param WP_Post $post The post object.
339 * @return array The action parts array.
340 */
341 private function get_follow_action_parts( $post ) {
342 $args = [
343 'action' => 'ef_notifications_user_post_subscription',
344 'post_id' => $post->ID,
345 ];
346
347 $following_users = $this->get_following_users( $post->ID );
348 if ( in_array( wp_get_current_user()->user_login, $following_users ) ) {
349 $args['method'] = 'unfollow';
350 $title_text = __( 'Click to unfollow updates to this post', 'edit-flow' );
351 $follow_text = __( 'Following', 'edit-flow' );
352 } else {
353 $args['method'] = 'follow';
354 $title_text = __( 'Follow updates to this post', 'edit-flow' );
355 $follow_text = __( 'Follow', 'edit-flow' );
356 }
357
358 // wp_nonce_url() has encoding issues: http://core.trac.wordpress.org/ticket/20771.
359 $args['_wpnonce'] = wp_create_nonce( 'ef_notifications_user_post_subscription' );
360
361 return [
362 'title' => $title_text,
363 'text' => $follow_text,
364 'link' => add_query_arg( $args, admin_url( 'admin-ajax.php' ) ),
365 ];
366 }
367
368 /**
369 * Add the subscriptions meta box to relevant post types.
370 */
371 public function add_post_meta_box() {
372
373 if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) {
374 return;
375 }
376
377 $usergroup_post_types = $this->get_post_types_for_module( $this->module );
378 foreach ( $usergroup_post_types as $post_type ) {
379 add_meta_box( 'edit-flow-notifications', __( 'Notifications', 'edit-flow' ), [ $this, 'notifications_meta_box' ], $post_type, 'advanced' );
380 }
381 }
382
383 /**
384 * Outputs box used to subscribe users and usergroups to Posts
385 *
386 * @todo add_cap to set subscribers for posts; default to Admin and editors
387 */
388 public function notifications_meta_box() {
389 global $post, $post_ID, $edit_flow;
390 ?>
391 <div id="ef-post_following_box">
392 <a name="subscriptions"></a>
393
394 <p><?php _e( 'Select the users and user groups that should receive email notifications when the status of this post is updated or when an editorial comment is added.', 'edit-flow' ); ?></p>
395 <div id="ef-post_following_users_box">
396 <h4><?php _e( 'Users', 'edit-flow' ); ?></h4>
397 <?php
398 $followers = $this->get_following_users( $post->ID, 'id' );
399 $select_form_args = [
400 'list_class' => 'ef-post_following_list',
401 ];
402 $this->users_select_form( $followers, $select_form_args );
403 ?>
404 </div>
405
406 <?php if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) : ?>
407 <div id="ef-post_following_usergroups_box">
408 <h4><?php _e( 'User Groups', 'edit-flow' ); ?></h4>
409 <?php
410 $following_usergroups = $this->get_following_usergroups( $post->ID, 'ids' );
411 $edit_flow->user_groups->usergroups_select_form( $following_usergroups );
412 ?>
413 </div>
414 <?php endif; ?>
415 <div class="clear"></div>
416 <input type="hidden" name="ef-save_followers" value="1" /> <?php // Extra protection against autosaves. ?>
417 <?php wp_nonce_field( 'save_user_usergroups', 'ef_notifications_nonce', false ); ?>
418 </div>
419
420 <?php
421 }
422
423 /**
424 * Show badges next to a subscriber's name for status information
425 *
426 * Applies on initial loading of list via PHP. JS will set these spans based on AJAX response when box is ticked/unticked.
427 *
428 * @param int $user_id The user ID.
429 * @param bool $checked True if the user is subscribed already, false otherwise.
430 * @return void
431 */
432 public function display_subscriber_warning_badges( $user_id, $checked ) {
433 global $post;
434
435 if ( ! isset( $post ) ) {
436 return;
437 }
438
439 $is_post_author = ( (int) $post->post_author === (int) $user_id );
440 $auto_subscribe_on = apply_filters( 'ef_notification_auto_subscribe_post_author', true, 'subscription_action' );
441
442 // Show "Post Author" badge for the post author.
443 if ( $is_post_author ) {
444 echo '<span class="post_following_list-post_author">' . esc_html__( 'Post Author', 'edit-flow' ) . '</span>';
445 }
446
447 // Show "Auto-subscribed" badge if post author is auto-subscribed.
448 if ( $is_post_author && $auto_subscribe_on && $checked ) {
449 echo '<span class="post_following_list-auto_subscribed">' . esc_html__( 'Auto-subscribed', 'edit-flow' ) . '</span>';
450 }
451
452 // Only show warning badges if user is subscribed.
453 if ( ! $checked ) {
454 return;
455 }
456
457 // Add No Access span if they won't be notified.
458 if ( ! $this->user_can_be_notified( get_user_by( 'id', $user_id ), $post->ID ) ) {
459 // span.post_following_list-no_access is also added in notifications.js after AJAX that ticks/unticks a user.
460 echo '<span class="post_following_list-no_access">' . esc_html__( 'No Access', 'edit-flow' ) . '</span>';
461 }
462
463 // Add No Email span if they have no email.
464 $user_object = get_user_by( 'id', $user_id );
465 if ( ! is_a( $user_object, 'WP_User' ) || empty( $user_object->user_email ) ) {
466 // span.post_following_list-no_email is also added in notifications.js after AJAX that ticks/unticks a user.
467 echo '<span class="post_following_list-no_email">' . esc_html__( 'No Email', 'edit-flow' ) . '</span>';
468 }
469 }
470
471 /**
472 * Called when a notification editorial metadata checkbox is checked. Handles saving of a user/usergroup to a post.
473 */
474 public function ajax_save_post_subscriptions() {
475 global $edit_flow;
476
477 // Verify nonce.
478 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
479 if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( $_POST['_nonce'], 'save_user_usergroups' ) ) {
480 wp_die( esc_html__( 'Nonce check failed. Please ensure you can add users or user groups to a post.', 'edit-flow' ) );
481 }
482
483 $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
484 $post = get_post( $post_id );
485
486 $valid_post = ! is_null( $post ) && ! wp_is_post_revision( $post_id ) && ! wp_is_post_autosave( $post_id );
487 if ( ! isset( $_POST['ef_notifications_name'] ) || ! $valid_post || ! current_user_can( $this->edit_post_subscriptions_cap ) || ! current_user_can( 'edit_post', $post_id ) ) {
488 wp_die();
489 }
490
491 $user_group_ids = [];
492 if ( isset( $_POST['user_group_ids'] ) && is_array( $_POST['user_group_ids'] ) ) {
493 $user_group_ids = array_map( 'intval', $_POST['user_group_ids'] );
494 }
495
496 if ( 'ef-selected-users[]' === $_POST['ef_notifications_name'] ) {
497 // Only subscribe users offered by the subscription picker (the publish_posts
498 // set); arbitrary user IDs in the request must not be added as followers.
499 $user_group_ids = array_values( array_intersect( $user_group_ids, $this->get_subscribable_user_ids() ) );
500
501 // Prevent auto-subscribing users that have opted out of notifications.
502 add_filter( 'ef_notification_auto_subscribe_current_user', '__return_false', PHP_INT_MAX );
503 $this->save_post_following_users( $post, $user_group_ids );
504
505 if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['post_id'] ) ) {
506
507 // Determine if any of the selected users won't have notification access.
508 $subscribers_with_no_access = array_filter(
509 $user_group_ids,
510 function ( $user_id ) use ( $post_id ) {
511 return ! $this->user_can_be_notified( get_user_by( 'id', $user_id ), $post_id );
512 }
513 );
514
515 // Determine if any of the selected users are missing their emails.
516 $subscribers_with_no_email = [];
517 foreach ( $user_group_ids as $user_id ) {
518 $user_object = get_user_by( 'id', $user_id );
519 if ( ! is_a( $user_object, 'WP_User' ) || empty( $user_object->user_email ) ) {
520 $subscribers_with_no_email[] = $user_id;
521 }
522 }
523
524 // Assemble the JSON reply with various lists of problematic users.
525 $json_success = [
526 'subscribers_with_no_access' => array_values( $subscribers_with_no_access ),
527 'subscribers_with_no_email' => array_values( $subscribers_with_no_email ),
528 ];
529
530 wp_send_json_success( $json_success );
531 }
532 // Remove auto-subscribe prevention behavior from earlier.
533 remove_filter( 'ef_notification_auto_subscribe_current_user', '__return_false', PHP_INT_MAX );
534 }
535
536 $groups_enabled = $this->module_enabled( 'user_groups' ) && in_array( get_post_type( $post_id ), $this->get_post_types_for_module( $edit_flow->user_groups->module ) );
537 if ( 'following_usergroups[]' === $_POST['ef_notifications_name'] && $groups_enabled ) {
538 $this->save_post_following_usergroups( $post, $user_group_ids );
539
540 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
541 // User groups don't have individual warning badges like users do,
542 // but we still need to return a success response.
543 wp_send_json_success(
544 [
545 'subscribers_with_no_access' => [],
546 'subscribers_with_no_email' => [],
547 ]
548 );
549 }
550 }
551
552 wp_die();
553 }
554
555 /**
556 * Handle a request to update a user's post subscription.
557 *
558 * @since 0.8
559 */
560 public function handle_user_post_subscription() {
561 // Require a valid nonce for this AJAX request.
562 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
563 if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) {
564 $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] );
565 }
566
567 if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) {
568 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
569 }
570
571 $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
572 $post = get_post( $post_id );
573
574 if ( ! $post ) {
575 $this->print_ajax_response( 'error', $this->module->messages['missing-post'] );
576 }
577
578 if ( ! current_user_can( 'edit_post', $post_id ) ) {
579 $this->print_ajax_response( 'error', $this->module->messages['invalid-permissions'] );
580 return;
581 }
582
583 if ( isset( $_GET['method'] ) && 'follow' == $_GET['method'] ) {
584 $retval = $this->follow_post_user( $post, get_current_user_id() );
585 } else {
586 $retval = $this->unfollow_post_user( $post, get_current_user_id() );
587 }
588
589 if ( is_wp_error( $retval ) ) {
590 $this->print_ajax_response( 'error', $retval->get_error_message() );
591 }
592
593 $this->print_ajax_response( 'success', (object) $this->get_follow_action_parts( $post ) );
594 }
595
596
597 /**
598 * Called when post is saved. Handles saving of user/usergroup followers.
599 *
600 * @param string $new_status The new post status.
601 * @param string $old_status The old post status.
602 * @param WP_Post $post The post object.
603 */
604 public function save_post_subscriptions( $new_status, $old_status, $post ) {
605 global $edit_flow;
606
607 // Skip revisions and autosaves - they have different post IDs which would fail nonce verification.
608 if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
609 return;
610 }
611
612 // Only process if Edit Flow's followers form was submitted.
613 if ( ! isset( $_POST['ef-save_followers'] ) ) {
614 return;
615 }
616
617 // Check capability.
618 if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) {
619 return;
620 }
621
622 // Verify Edit Flow's own nonce. Return early if missing or invalid - don't die,
623 // as this hook fires on all post transitions including non-admin contexts.
624 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce().
625 if ( ! isset( $_POST['ef_notifications_nonce'] ) || ! wp_verify_nonce( $_POST['ef_notifications_nonce'], 'save_user_usergroups' ) ) {
626 return;
627 }
628
629 // Only subscribe users offered by the subscription picker (the publish_posts set);
630 // arbitrary user IDs in the request must not be added as followers.
631 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Cast to integers and intersected with the allowed set below.
632 $submitted_users = isset( $_POST['ef-selected-users'] ) ? (array) wp_unslash( $_POST['ef-selected-users'] ) : [];
633 $users = array_values( array_intersect( array_map( 'intval', $submitted_users ), $this->get_subscribable_user_ids() ) );
634 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved.
635 $usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : [];
636 $this->save_post_following_users( $post, $users );
637 if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) {
638 $this->save_post_following_usergroups( $post, $usergroups );
639 }
640 }
641
642 /**
643 * Sets users to follow specified post.
644 *
645 * @param int|WP_Post $post The post ID or object.
646 * @param array|null $users Array of user IDs to follow the post.
647 */
648 public function save_post_following_users( $post, $users = null ) {
649 if ( ! is_array( $users ) ) {
650 $users = [];
651 }
652
653 // Add current user to following users.
654 $user = wp_get_current_user();
655 if ( $user && apply_filters( 'ef_notification_auto_subscribe_current_user', true, 'subscription_action' ) ) {
656 $users[] = $user->ID;
657 }
658
659 // Add post author to following users.
660 if ( apply_filters( 'ef_notification_auto_subscribe_post_author', true, 'subscription_action' ) ) {
661 $users[] = $post->post_author;
662 }
663
664 $users = array_unique( array_map( 'intval', $users ) );
665
666 $follow = $this->follow_post_user( $post, $users, false );
667 }
668
669 /**
670 * Sets usergroups to follow specified post.
671 *
672 * @param int|WP_Post $post The post ID or object.
673 * @param array|null $usergroups Usergroups to follow posts.
674 */
675 public function save_post_following_usergroups( $post, $usergroups = null ) {
676
677 if ( ! is_array( $usergroups ) ) {
678 $usergroups = [];
679 }
680 $usergroups = array_map( 'intval', $usergroups );
681
682 $follow = $this->follow_post_usergroups( $post, $usergroups, false );
683 }
684
685 /**
686 * Set up and send post status change notification email.
687 *
688 * @param string $new_status The new post status.
689 * @param string $old_status The old post status.
690 * @param WP_Post $post The post object.
691 */
692 public function notification_status_change( $new_status, $old_status, $post ) {
693 global $edit_flow;
694
695 // Kill switch for notification.
696 if ( ! apply_filters( 'ef_notification_status_change', $new_status, $old_status, $post ) || ! apply_filters( "ef_notification_{$post->post_type}_status_change", $new_status, $old_status, $post ) ) {
697 return false;
698 }
699
700 $supported_post_types = $this->get_post_types_for_module( $this->module );
701 if ( ! in_array( $post->post_type, $supported_post_types ) ) {
702 return;
703 }
704
705 // No need to notify if it's a revision, auto-draft, or if post status wasn't changed.
706 $ignored_statuses = apply_filters( 'ef_notification_ignored_statuses', [ $old_status, 'inherit', 'auto-draft' ], $post->post_type );
707
708 if ( ! in_array( $new_status, $ignored_statuses ) ) {
709
710 // Get current user.
711 $current_user = wp_get_current_user();
712
713 $post_author = get_userdata( $post->post_author );
714
715 $blogname = get_option( 'blogname' );
716
717 $body = '';
718
719 $post_id = $post->ID;
720 $post_title = ef_draft_or_post_title( $post_id );
721 $post_type = get_post_type_object( $post->post_type )->labels->singular_name;
722
723 if ( 0 != $current_user->ID ) {
724 $current_user_display_name = $current_user->display_name;
725 $current_user_email = sprintf( '(%s)', $current_user->user_email );
726 } else {
727 $current_user_display_name = __( 'WordPress Scheduler', 'edit-flow' );
728 $current_user_email = '';
729 }
730
731 $old_status_post_obj = get_post_status_object( $old_status );
732 $new_status_post_obj = get_post_status_object( $new_status );
733 $old_status_friendly_name = '';
734 $new_status_friendly_name = '';
735
736 /*
737 * The get_post_status_object() function will return null for certain statuses (i.e., 'new').
738 * The mega if/else block below should catch all cases, but just in case, we
739 * make sure to at least set $old_status_friendly_name and $new_status_friendly_name
740 * to an empty string to ensure they're at least set.
741 *
742 * Then, we attempt to set them to a sensible default before we start the
743 * mega if/else block.
744 */
745 if ( ! is_null( $old_status_post_obj ) ) {
746 $old_status_friendly_name = $old_status_post_obj->label;
747 }
748
749 if ( ! is_null( $new_status_post_obj ) ) {
750 $new_status_friendly_name = $new_status_post_obj->label;
751 }
752
753 // Email subject and first line of body.
754 // Set message subjects according to what action is being taken on the Post.
755 if ( 'new' == $old_status || 'auto-draft' == $old_status ) {
756 $old_status_friendly_name = 'New';
757 /* translators: 1: site name, 2: post type, 3. post title */
758 $subject = sprintf( __( '[%1$s] New %2$s Created: "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
759 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
760 $body .= sprintf( __( 'A new %1$s (#%2$s "%3$s") was created by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user->display_name, $current_user->user_email ) . "\r\n";
761 } elseif ( 'trash' == $new_status ) {
762 /* translators: 1: site name, 2: post type, 3. post title */
763 $subject = sprintf( __( '[%1$s] %2$s Trashed: "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
764 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
765 $body .= sprintf( __( '%1$s #%2$s "%3$s" was moved to the trash by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email ) . "\r\n";
766 } elseif ( 'trash' == $old_status ) {
767 /* translators: 1: site name, 2: post type, 3. post title */
768 $subject = sprintf( __( '[%1$s] %2$s Restored (from Trash): "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
769 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
770 $body .= sprintf( __( '%1$s #%2$s "%3$s" was restored from trash by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email ) . "\r\n";
771 } elseif ( 'future' == $new_status ) {
772 /* translators: 1: site name, 2: post type, 3. post title */
773 $subject = sprintf( __( '[%1$s] %2$s Scheduled: "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
774 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email 6. scheduled date */
775 $body .= sprintf( __( '%1$s #%2$s "%3$s" was scheduled by %4$s %5$s. It will be published on %6$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email, $this->get_scheduled_datetime( $post ) ) . "\r\n";
776 } elseif ( 'publish' == $new_status ) {
777 /* translators: 1: site name, 2: post type, 3. post title */
778 $subject = sprintf( __( '[%1$s] %2$s Published: "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
779 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
780 $body .= sprintf( __( '%1$s #%2$s "%3$s" was published by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email ) . "\r\n";
781 } elseif ( 'publish' == $old_status ) {
782 /* translators: 1: site name, 2: post type, 3. post title */
783 $subject = sprintf( __( '[%1$s] %2$s Unpublished: "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
784 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
785 $body .= sprintf( __( '%1$s #%2$s "%3$s" was unpublished by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email ) . "\r\n";
786 } else {
787 /* translators: 1: site name, 2: post type, 3. post title */
788 $subject = sprintf( __( '[%1$s] %2$s Status Changed for "%3$s"', 'edit-flow' ), $blogname, $post_type, $post_title );
789 /* translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email */
790 $body .= sprintf( __( 'Status was changed for %1$s #%2$s "%3$s" by %4$s %5$s', 'edit-flow' ), $post_type, $post_id, $post_title, $current_user_display_name, $current_user_email ) . "\r\n";
791 }
792
793 /* translators: 1: date, 2: time, 3: timezone */
794 $body .= sprintf( __( 'This action was taken on %1$s at %2$s %3$s', 'edit-flow' ), date_i18n( get_option( 'date_format' ) ), date_i18n( get_option( 'time_format' ) ), get_option( 'timezone_string' ) ) . "\r\n";
795
796 // Email body.
797 $body .= "\r\n";
798 /* translators: 1: old status, 2: new status */
799 $body .= sprintf( __( '%1$s => %2$s', 'edit-flow' ), $old_status_friendly_name, $new_status_friendly_name );
800 $body .= "\r\n\r\n";
801
802 $body .= "--------------------\r\n\r\n";
803
804 /* translators: 1: post type */
805 $body .= sprintf( __( '== %s Details ==', 'edit-flow' ), $post_type ) . "\r\n";
806 /* translators: 1: post title */
807 $body .= sprintf( __( 'Title: %s', 'edit-flow' ), $post_title ) . "\r\n";
808 if ( ! empty( $post_author ) ) {
809 /* translators: 1: author name, 2: author email */
810 $body .= sprintf( __( 'Author: %1$s (%2$s)', 'edit-flow' ), $post_author->display_name, $post_author->user_email ) . "\r\n";
811 }
812
813 $edit_post_link = get_edit_post_link( $post_id );
814 if ( is_null( $edit_post_link ) ) {
815 return;
816 }
817
818 $edit_link = htmlspecialchars_decode( $edit_post_link );
819
820 if ( 'publish' != $new_status ) {
821 $view_link = add_query_arg( [ 'preview' => 'true' ], wp_get_shortlink( $post_id ) );
822 } else {
823 $permalink = get_permalink( $post_id );
824 if ( is_null( $permalink ) ) {
825 return;
826 }
827
828 $view_link = htmlspecialchars_decode( $permalink );
829 }
830
831 $body .= "\r\n";
832 $body .= __( '== Actions ==', 'edit-flow' ) . "\r\n";
833 /* translators: 1: edit link */
834 $body .= sprintf( __( 'Add editorial comment: %s', 'edit-flow' ), $edit_link . '#editorialcomments/add' ) . "\r\n";
835 /* translators: 1: edit link */
836 $body .= sprintf( __( 'Edit: %s', 'edit-flow' ), $edit_link ) . "\r\n";
837 /* translators: 1: view link */
838 $body .= sprintf( __( 'View: %s', 'edit-flow' ), $view_link ) . "\r\n";
839
840 $body .= $this->get_notification_footer( $post );
841
842 $this->send_email( 'status-change', $post, $subject, $body );
843
844 if ( 'on' === $this->module->options->send_to_webhook ) {
845 /* translators: 1: user name, 2: post type, 3: post id, 4: edit link, 5: post title, 6: old status, 7: new status */
846 $format = __( '*%1$s* changed the status of *%2$s #%3$s - <%4$s|%5$s>* from *%6$s* to *%7$s*', 'edit-flow' );
847 $text = sprintf( $format, $current_user->display_name, $post_type, $post_id, $edit_link, $post_title, $old_status_friendly_name, $new_status_friendly_name );
848
849 $this->send_to_webhook( $text, 'status-change', $current_user, $post );
850 }
851 }
852 }
853
854 /**
855 * Set up and set editorial comment notification email.
856 *
857 * @param WP_Comment $comment The editorial comment object.
858 * @return boolean|null|void False if notification is disabled, null/void otherwise.
859 */
860 public function notification_comment( $comment ) {
861
862 $post = get_post( $comment->comment_post_ID );
863
864 $supported_post_types = $this->get_post_types_for_module( $this->module );
865 if ( ! in_array( $post->post_type, $supported_post_types ) ) {
866 return;
867 }
868
869 // Kill switch for notification.
870 if ( ! apply_filters( 'ef_notification_editorial_comment', $comment, $post ) ) {
871 return false;
872 }
873
874 $user = get_userdata( $post->post_author );
875 $current_user = wp_get_current_user();
876
877 $post_id = $post->ID;
878 $post_type = get_post_type_object( $post->post_type )->labels->singular_name;
879 $post_title = ef_draft_or_post_title( $post_id );
880
881 // Fetch the text list of people who were notified from comment meta.
882 // @see EF_Editorial_Comments->maybe_output_comment_meta().
883 $notification_list = get_comment_meta( $comment->comment_ID, 'notification_list', true );
884
885 // Set user to follow post, but make it filterable.
886 if ( apply_filters( 'ef_notification_auto_subscribe_current_user', true, 'comment' ) ) {
887 $this->follow_post_user( $post, (int) $current_user->ID );
888 }
889
890 // Set the post author to follow the post but make it filterable.
891 if ( apply_filters( 'ef_notification_auto_subscribe_post_author', true, 'comment' ) ) {
892 $this->follow_post_user( $post, (int) $post->post_author );
893 }
894
895 $blogname = get_option( 'blogname' );
896
897 /* translators: 1: blog name, 2: post title */
898 $subject = sprintf( __( '[%1$s] New Editorial Comment: "%2$s"', 'edit-flow' ), $blogname, $post_title );
899
900 /* translators: 1: post id, 2: post title, 3. post type */
901 $body = sprintf( __( 'A new editorial comment was added to %3$s #%1$s "%2$s"', 'edit-flow' ), $post_id, $post_title, $post_type ) . "\r\n\r\n";
902 /* translators: 1: comment author, 2: author email, 3: date, 4: time */
903 $body .= sprintf( __( '%1$s (%2$s) said on %3$s at %4$s:', 'edit-flow' ), $current_user->display_name, $current_user->user_email, mysql2date( get_option( 'date_format' ), $comment->comment_date ), mysql2date( get_option( 'time_format' ), $comment->comment_date ) ) . "\r\n";
904 $body .= "\r\n" . $comment->comment_content . "\r\n";
905
906 $body .= "\r\n--------------------\r\n";
907 // Insert the notification list from comment meta.
908 // @see EF_Editorial_Comments->maybe_output_comment_meta().
909 if ( $notification_list ) {
910 $body .= esc_html__( 'Notified', 'edit-flow' ) . ': ' . esc_html( $notification_list ) . "\n";
911 }
912
913 $edit_post_link = get_edit_post_link( $post_id );
914 if ( is_null( $edit_post_link ) ) {
915 return;
916 }
917
918 $edit_link = htmlspecialchars_decode( $edit_post_link );
919
920 $permalink = get_permalink( $post_id );
921 if ( is_null( $permalink ) ) {
922 return;
923 }
924
925 $view_link = htmlspecialchars_decode( $permalink );
926
927 $body .= "\r\n";
928 $body .= __( '== Actions ==', 'edit-flow' ) . "\r\n";
929 /* translators: 1: edit link */
930 $body .= sprintf( __( 'Reply: %s', 'edit-flow' ), $edit_link . '#editorialcomments/reply/' . $comment->comment_ID ) . "\r\n";
931 /* translators: 1: edit link */
932 $body .= sprintf( __( 'Add editorial comment: %s', 'edit-flow' ), $edit_link . '#editorialcomments/add' ) . "\r\n";
933 /* translators: 1: edit link */
934 $body .= sprintf( __( 'Edit: %s', 'edit-flow' ), $edit_link ) . "\r\n";
935 /* translators: 1: view link */
936 $body .= sprintf( __( 'View: %s', 'edit-flow' ), $view_link ) . "\r\n";
937
938 /* translators: 1: post type */
939 $body .= "\r\n" . sprintf( __( 'You can see all editorial comments on this %s here: ', 'edit-flow' ), $post_type ) . "\r\n";
940 $body .= $edit_link . '#editorialcomments' . "\r\n\r\n";
941
942 $body .= $this->get_notification_footer( $post );
943
944 $this->send_email( 'comment', $post, $subject, $body );
945
946 if ( 'on' === $this->module->options->send_to_webhook ) {
947 /* translators: 1: comment author, 2: post type, 3: post id, 4: edit link, 5: post title, 6: comment content */
948 $format = __( '*%1$s* left a comment on *%2$s #%3$s - <%4$s|%5$s>*', 'edit-flow' ) . "\n\n";
949 $format .= '%6$s';
950 $text = sprintf( $format, $comment->comment_author, $post_type, $post_id, $edit_link, $post_title, $comment->comment_content );
951
952 $this->send_to_webhook( $text, 'comment', $current_user, $comment );
953 }
954 }
955
956 /**
957 * Get the notification email footer.
958 *
959 * @param WP_Post $post The post object.
960 * @return string The email footer content.
961 */
962 public function get_notification_footer( $post ) {
963 $body = '';
964 $body .= "\r\n--------------------\r\n";
965 /* translators: 1: post title */
966 $body .= sprintf( __( 'You are receiving this email because you are subscribed to "%s".', 'edit-flow' ), ef_draft_or_post_title( $post->ID ) );
967 $body .= "\r\n";
968 // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Intentional use for email timestamp.
969 /* translators: 1: date */
970 $body .= sprintf( __( 'This email was sent %s.', 'edit-flow' ), date( 'r' ) );
971 $body .= "\r\n \r\n";
972 $body .= get_option( 'blogname' ) . ' | ' . get_bloginfo( 'url' ) . ' | ' . admin_url( '/' ) . "\r\n";
973 return $body;
974 }
975
976 /**
977 * Send email notification.
978 *
979 * @param string $action The notification action type.
980 * @param WP_Post $post The post object.
981 * @param string $subject The email subject.
982 * @param string $message The email message body.
983 * @param string $message_headers Optional email headers.
984 */
985 public function send_email( $action, $post, $subject, $message, $message_headers = '' ) {
986
987 // Get list of email recipients -- set them CC.
988 $recipients = $this->_get_notification_recipients( $post, true );
989
990 if ( $recipients && ! is_array( $recipients ) ) {
991 $recipients = explode( ',', $recipients );
992 }
993
994 $subject = apply_filters( 'ef_notification_send_email_subject', $subject, $action, $post );
995 $message = apply_filters( 'ef_notification_send_email_message', $message, $action, $post );
996 $message_headers = apply_filters( 'ef_notification_send_email_message_headers', $message_headers, $action, $post );
997
998 if ( EF_NOTIFICATION_USE_CRON ) {
999 $this->schedule_emails( $recipients, $subject, $message, $message_headers );
1000 } elseif ( ! empty( $recipients ) ) {
1001 foreach ( $recipients as $recipient ) {
1002 $this->send_single_email( $recipient, $subject, $message, $message_headers );
1003 }
1004 }
1005 }
1006
1007 /**
1008 * Send notifications to Slack.
1009 *
1010 * @param string $message Message to be sent to webhook.
1011 * @param string $action Action being taken. Currently only `status-change` and `comment`.
1012 * @param WP_User $user User who is taking the action.
1013 * @param WP_Post|WP_Comment $post Post or comment that the action is being taken on.
1014 */
1015 public function send_to_webhook( $message, $action, $user, $post ) {
1016 $webhook_url = $this->module->options->webhook_url;
1017
1018 // Bail if the webhook URL is not set.
1019 if ( empty( $webhook_url ) ) {
1020 return;
1021 }
1022
1023 // Set up the payload. The message embeds user-controlled values (display name, post
1024 // title, comment text). It is delivered as a JSON-encoded body below, so it cannot
1025 // break out of the request structure; the only residual is that a destination which
1026 // renders Slack mrkdwn will format any markup characters within those values. That is
1027 // cosmetic and the feature is opt-in; a site that needs to neutralise it can rewrite
1028 // the text via the payload filter.
1029 $payload = [
1030 'text' => $message,
1031 ];
1032
1033 // Apply filters to the payload.
1034 $payload = apply_filters( 'ef_notification_send_to_webhook_payload', $payload, $action, $user, $post );
1035
1036 // The webhook is a side-effect fired from transition_post_status and editorial comment
1037 // insertion, both of which have already completed by the time this runs (in AJAX and
1038 // non-AJAX requests alike). A failure must therefore never die or abort, which would
1039 // misreport an already-successful save/comment. Send best-effort with the SSRF-safe
1040 // variant (private, loopback and link-local hosts are rejected) and expose any failure
1041 // via an action so sites can log it.
1042 $response = wp_safe_remote_post(
1043 $webhook_url,
1044 [
1045 'body' => wp_json_encode( $payload ),
1046 'headers' => [ 'Content-Type' => 'application/json' ],
1047 ]
1048 );
1049 if ( is_wp_error( $response ) ) {
1050 do_action( 'ef_notification_webhook_failed', $response, $action, $user, $post );
1051 }
1052 }
1053
1054 /**
1055 * Schedules emails to be sent in succession.
1056 *
1057 * @param mixed $recipients Individual email or array of emails.
1058 * @param string $subject Subject of the email.
1059 * @param string $message Body of the email.
1060 * @param string $message_headers Optional. Message headers.
1061 * @param int $time_offset Optional. Delay in seconds per email.
1062 */
1063 public function schedule_emails( $recipients, $subject, $message, $message_headers = '', $time_offset = 1 ) {
1064 $recipients = (array) $recipients;
1065
1066 $send_time = time();
1067
1068 foreach ( $recipients as $recipient ) {
1069 wp_schedule_single_event( $send_time, 'ef_send_scheduled_email', [ $recipient, $subject, $message, $message_headers ] );
1070 $send_time += $time_offset;
1071 }
1072 }
1073
1074 /**
1075 * Sends an individual email.
1076 *
1077 * @param mixed $to Email to send to.
1078 * @param string $subject Subject of the email.
1079 * @param string $message Body of the email.
1080 * @param string $message_headers Optional. Message headers.
1081 */
1082 public function send_single_email( $to, $subject, $message, $message_headers = '' ) {
1083 // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- Notification feature requires email.
1084 wp_mail( $to, $subject, $message, $message_headers );
1085 }
1086
1087 /**
1088 * Returns a list of recipients for a given post.
1089 *
1090 * @param WP_Post $post The post object.
1091 * @param bool $return_string Whether to return recipients as comma-delimited string or array.
1092 * @return string|array Recipients to receive notification.
1093 */
1094 private function _get_notification_recipients( $post, $return_string = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Legacy method name.
1095 global $edit_flow;
1096
1097 $post_id = $post->ID;
1098 if ( ! $post_id ) {
1099 return $return_string ? '' : [];
1100 }
1101
1102 // Email all admins if enabled.
1103 $admins = [];
1104 if ( 'on' === $this->module->options->always_notify_admin ) {
1105 $admins[] = get_option( 'admin_email' );
1106 }
1107
1108 $usergroup_recipients = [];
1109 if ( $this->module_enabled( 'user_groups' ) ) {
1110 $usergroups = $this->get_following_usergroups( $post_id, 'ids' );
1111 foreach ( (array) $usergroups as $usergroup_id ) {
1112 $usergroup = $edit_flow->user_groups->get_usergroup_by( 'id', $usergroup_id );
1113 foreach ( (array) $usergroup->user_ids as $user_id ) {
1114 $usergroup_user = get_user_by( 'id', $user_id );
1115 if ( $this->user_can_be_notified( $usergroup_user, $post_id ) ) {
1116 $usergroup_recipients[] = $usergroup_user->user_email;
1117 }
1118 }
1119 }
1120 }
1121
1122 $user_recipients = $this->get_following_users( $post_id, 'user_email' );
1123 foreach ( $user_recipients as $key => $user ) {
1124 $user_object = get_user_by( 'email', $user );
1125 if ( ! $this->user_can_be_notified( $user_object, $post_id ) ) {
1126 unset( $user_recipients[ $key ] );
1127 }
1128 }
1129
1130 // Merge arrays, filter any duplicates, and remove empty entries.
1131 $recipients = array_filter( array_unique( array_merge( $admins, $user_recipients, $usergroup_recipients ) ) );
1132
1133 // Process the recipients for this email to be sent.
1134 foreach ( $recipients as $key => $user_email ) {
1135 // Don't send the email to the current user unless we've explicitly indicated they should receive it.
1136 if ( false === apply_filters( 'ef_notification_email_current_user', false ) && wp_get_current_user()->user_email == $user_email ) {
1137 unset( $recipients[ $key ] );
1138 }
1139 }
1140
1141 /**
1142 * Filters the list of notification recipients.
1143 *
1144 * @param array $recipients List of recipient email addresses.
1145 * @param WP_Post $post The post object.
1146 * @param bool $return_string True if the recipients list will later be returned as a string.
1147 */
1148 $recipients = apply_filters( 'ef_notification_recipients', $recipients, $post, $return_string );
1149
1150 // If string set to true, return comma-delimited.
1151 if ( $return_string && is_array( $recipients ) ) {
1152 return implode( ',', $recipients );
1153 } else {
1154 return $recipients;
1155 }
1156 }
1157
1158 /**
1159 * Check if a user can be notified.
1160 *
1161 * This is based off of the ability to edit the post/page by default.
1162 *
1163 * @since 0.8.3
1164 *
1165 * @param WP_User $user The user object.
1166 * @param int $post_id The post ID.
1167 * @return bool True if the user can be notified, false otherwise.
1168 */
1169 public function user_can_be_notified( $user, $post_id ) {
1170 $can_be_notified = false;
1171
1172 if ( $user instanceof WP_User && is_user_member_of_blog( $user->ID ) && is_numeric( $post_id ) ) {
1173 // The 'edit_post' cap check also covers the undocumented 'edit_page' cap.
1174 $can_be_notified = $user->has_cap( 'edit_post', $post_id );
1175 }
1176
1177 /**
1178 * Filters if a user can be notified. Defaults to true if they can edit the post/page.
1179 *
1180 * @param bool $can_be_notified True if the user can be notified.
1181 * @param WP_User|bool $user The user object, otherwise false.
1182 * @param int $post_id The post the user will be notified about.
1183 */
1184 return (bool) apply_filters( 'ef_notification_user_can_be_notified', $can_be_notified, $user, $post_id );
1185 }
1186
1187 /**
1188 * The set of user IDs that may be subscribed to a post, matching the subscription
1189 * picker (users_select_form). Used to reject arbitrary user IDs submitted in a request.
1190 *
1191 * @return int[] User IDs offered by the subscription picker.
1192 */
1193 private function get_subscribable_user_ids() {
1194 $args = apply_filters(
1195 'ef_users_select_form_get_users_args',
1196 array(
1197 'capability' => 'publish_posts',
1198 'fields' => array( 'ID', 'display_name', 'user_nicename', 'user_email' ),
1199 'orderby' => 'display_name',
1200 )
1201 );
1202 return array_map( 'intval', wp_list_pluck( get_users( $args ), 'ID' ) );
1203 }
1204
1205 /**
1206 * Set a user or users to follow a post.
1207 *
1208 * @param int|object $post Post object or ID.
1209 * @param string|array $users User or users to subscribe to post updates.
1210 * @param bool $append Whether users should be added to following_users list or replace existing list.
1211 *
1212 * @return true|WP_Error True on success, WP_Error on failure.
1213 */
1214 public function follow_post_user( $post, $users, $append = true ) {
1215
1216 $post = get_post( $post );
1217 if ( ! $post ) {
1218 return new WP_Error( 'missing-post', $this->module->messages['missing-post'] );
1219 }
1220
1221 if ( ! is_array( $users ) ) {
1222 $users = [ $users ];
1223 }
1224
1225 $user_terms = [];
1226 foreach ( $users as $user ) {
1227 if ( is_int( $user ) ) {
1228 $user = get_user_by( 'id', $user );
1229 } elseif ( is_string( $user ) ) {
1230 $user = get_user_by( 'login', $user );
1231 }
1232
1233 if ( ! is_object( $user ) ) {
1234 continue;
1235 }
1236
1237 $name = $user->user_login;
1238
1239 // Add user as a term if they don't exist.
1240 $term = $this->add_term_if_not_exists( $name, $this->following_users_taxonomy );
1241
1242 if ( ! is_wp_error( $term ) ) {
1243 $user_terms[] = $name;
1244 }
1245 }
1246 $set = wp_set_object_terms( $post->ID, $user_terms, $this->following_users_taxonomy, $append );
1247
1248 if ( is_wp_error( $set ) ) {
1249 return $set;
1250 } else {
1251 return true;
1252 }
1253 }
1254
1255 /**
1256 * Removes user from following_users taxonomy for the given Post,
1257 * so they no longer receive future notifications.
1258 *
1259 * @param object $post Post object or ID.
1260 * @param int|string|array $users One or more users to unfollow from the post.
1261 * @return true|WP_Error True on success, WP_Error on failure.
1262 */
1263 public function unfollow_post_user( $post, $users ) {
1264
1265 $post = get_post( $post );
1266 if ( ! $post ) {
1267 return new WP_Error( 'missing-post', $this->module->messages['missing-post'] );
1268 }
1269
1270 if ( ! is_array( $users ) ) {
1271 $users = [ $users ];
1272 }
1273
1274 $terms = get_the_terms( $post->ID, $this->following_users_taxonomy );
1275 if ( is_wp_error( $terms ) ) {
1276 return $terms;
1277 }
1278
1279 $user_terms = wp_list_pluck( $terms, 'slug' );
1280 foreach ( $users as $user ) {
1281 if ( is_int( $user ) ) {
1282 $user = get_user_by( 'id', $user );
1283 } elseif ( is_string( $user ) ) {
1284 $user = get_user_by( 'login', $user );
1285 }
1286
1287 if ( ! is_object( $user ) ) {
1288 continue;
1289 }
1290
1291 $key = array_search( $user->user_login, $user_terms );
1292 if ( false !== $key ) {
1293 unset( $user_terms[ $key ] );
1294 }
1295 }
1296 $set = wp_set_object_terms( $post->ID, $user_terms, $this->following_users_taxonomy, false );
1297
1298 if ( is_wp_error( $set ) ) {
1299 return $set;
1300 } else {
1301 return true;
1302 }
1303 }
1304
1305 /**
1306 * Set usergroups to follow a post.
1307 *
1308 * @param int|WP_Post $post Post object or ID.
1309 * @param array|int $usergroups Usergroup IDs to follow the post.
1310 * @param bool $append Whether to append to or replace existing usergroups.
1311 */
1312 public function follow_post_usergroups( $post, $usergroups = 0, $append = true ) {
1313 if ( ! $this->module_enabled( 'user_groups' ) ) {
1314 return;
1315 }
1316
1317 $post_id = ( is_int( $post ) ) ? $post : $post->ID;
1318
1319 if ( ! is_array( $usergroups ) ) {
1320 $usergroups = [ $usergroups ];
1321 }
1322
1323 // Make sure each usergroup id is an integer and not a number stored as a string.
1324 foreach ( $usergroups as $key => $usergroup ) {
1325 $usergroups[ $key ] = intval( $usergroup );
1326 }
1327
1328 wp_set_object_terms( $post_id, $usergroups, $this->following_usergroups_taxonomy, $append );
1329 }
1330
1331 /**
1332 * Removes users that are deleted from receiving future notifications.
1333 *
1334 * Makes them unfollow posts FOREVER!
1335 *
1336 * @param int $id ID of the user.
1337 */
1338 public function delete_user_action( $id ) {
1339 if ( ! $id ) {
1340 return;
1341 }
1342
1343 // Get user data.
1344 $user = get_userdata( $id );
1345
1346 if ( $user ) {
1347 // Delete term from the following_users taxonomy.
1348 $user_following_term = get_term_by( 'name', $user->user_login, $this->following_users_taxonomy );
1349 if ( $user_following_term ) {
1350 wp_delete_term( $user_following_term->term_id, $this->following_users_taxonomy );
1351 }
1352 }
1353 }
1354
1355 /**
1356 * Add user as a term if they aren't already.
1357 *
1358 * @param string $term Term to be added.
1359 * @param string $taxonomy Taxonomy to add term to.
1360 * @return array|WP_Error|true WP_Error if insert fails, term array on insert, true if exists.
1361 */
1362 public function add_term_if_not_exists( $term, $taxonomy ) {
1363 if ( ! term_exists( $term, $taxonomy ) ) {
1364 $args = [ 'slug' => sanitize_title( $term ) ];
1365 return wp_insert_term( $term, $taxonomy, $args );
1366 }
1367 return true;
1368 }
1369
1370 /**
1371 * Gets a list of the users following the specified post.
1372 *
1373 * @param int $post_id The ID of the post.
1374 * @param string $return_field The field to return.
1375 * @return array Users following the specified posts.
1376 */
1377 public function get_following_users( $post_id, $return_field = 'user_login' ) {
1378
1379 // Get following_users terms for the post.
1380 $users = wp_get_object_terms( $post_id, $this->following_users_taxonomy, [ 'fields' => 'names' ] );
1381
1382 // Don't have any following users.
1383 if ( ! $users || is_wp_error( $users ) ) {
1384 return [];
1385 }
1386
1387 // If just want user_login, return as is.
1388 if ( 'user_login' == $return_field ) {
1389 return $users;
1390 }
1391
1392 foreach ( (array) $users as $key => $user ) {
1393 switch ( $user ) {
1394 case is_int( $user ):
1395 $search = 'id';
1396 break;
1397 case is_email( $user ):
1398 $search = 'email';
1399 break;
1400 default:
1401 $search = 'login';
1402 break;
1403 }
1404 $new_user = get_user_by( $search, $user );
1405 if ( ! $new_user || ! is_user_member_of_blog( $new_user->ID ) ) {
1406 unset( $users[ $key ] );
1407 continue;
1408 }
1409 switch ( $return_field ) {
1410 case 'user_login':
1411 $users[ $key ] = $new_user->user_login;
1412 break;
1413 case 'id':
1414 $users[ $key ] = $new_user->ID;
1415 break;
1416 case 'user_email':
1417 $users[ $key ] = $new_user->user_email;
1418 break;
1419 }
1420 }
1421 if ( ! $users || is_wp_error( $users ) ) {
1422 $users = [];
1423 }
1424 return $users;
1425 }
1426
1427 /**
1428 * Gets a list of the usergroups that are following specified post.
1429 *
1430 * @param int $post_id The ID of the post.
1431 * @param string $return_field The field to return.
1432 * @return array All of the usergroup slugs.
1433 */
1434 public function get_following_usergroups( $post_id, $return_field = 'all' ) {
1435 global $edit_flow;
1436
1437 // Workaround for the fact that get_object_terms doesn't return just slugs.
1438 if ( 'slugs' == $return_field ) {
1439 $fields = 'all';
1440 } else {
1441 $fields = $return_field;
1442 }
1443
1444 $usergroups = wp_get_object_terms( $post_id, $this->following_usergroups_taxonomy, [ 'fields' => $fields ] );
1445
1446 if ( 'slugs' == $return_field ) {
1447 $slugs = [];
1448 foreach ( $usergroups as $usergroup ) {
1449 $slugs[] = $usergroup->slug;
1450 }
1451 $usergroups = $slugs;
1452 }
1453 return $usergroups;
1454 }
1455
1456 /**
1457 * Gets a list of posts that a user is following.
1458 *
1459 * @param string|int $user User login or ID of user.
1460 * @param array $args Query arguments.
1461 * @return array Posts a user is following.
1462 */
1463 public function get_user_following_posts( $user = 0, $args = null ) {
1464 if ( ! $user ) {
1465 $user = (int) wp_get_current_user()->ID;
1466 }
1467
1468 if ( is_int( $user ) ) {
1469 $user = get_userdata( $user )->user_login;
1470 }
1471
1472 $post_args = [
1473 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Required for user following functionality.
1474 'tax_query' => [
1475 [
1476 'taxonomy' => $this->following_users_taxonomy,
1477 'field' => 'slug',
1478 'terms' => $user,
1479 ],
1480 ],
1481 'posts_per_page' => '10',
1482 'orderby' => 'modified',
1483 'order' => 'DESC',
1484 'post_status' => 'any',
1485 ];
1486 $post_args = apply_filters( 'ef_user_following_posts_query_args', $post_args );
1487 $posts = get_posts( $post_args );
1488 return $posts;
1489 }
1490
1491 /**
1492 * Register settings for notifications so we can partially use the Settings API
1493 * (We use the Settings API for form generation, but not saving)
1494 *
1495 * @since 0.7
1496 */
1497 public function register_settings() {
1498 add_settings_section( $this->module->options_group_name . '_general', false, '__return_false', $this->module->options_group_name );
1499 add_settings_field( 'post_types', __( 'Post types for notifications:', 'edit-flow' ), [ $this, 'settings_post_types_option' ], $this->module->options_group_name, $this->module->options_group_name . '_general' );
1500 add_settings_field( 'always_notify_admin', __( 'Always notify blog admin', 'edit-flow' ), [ $this, 'settings_always_notify_admin_option' ], $this->module->options_group_name, $this->module->options_group_name . '_general' );
1501 add_settings_field( 'send_to_webhook', __( 'Send to Webhook', 'edit-flow' ), [ $this, 'settings_send_to_webhook' ], $this->module->options_group_name, $this->module->options_group_name . '_general' );
1502 add_settings_field( 'webhook_url', __( 'Webhook URL', 'edit-flow' ), [ $this, 'settings_webhook_url' ], $this->module->options_group_name, $this->module->options_group_name . '_general' );
1503 }
1504
1505 /**
1506 * Chose the post types for notifications
1507 *
1508 * @since 0.7
1509 */
1510 public function settings_post_types_option() {
1511 global $edit_flow;
1512 $edit_flow->settings->helper_option_custom_post_type( $this->module );
1513 }
1514
1515 /**
1516 * Option for whether the blog admin email address should be always notified or not
1517 *
1518 * @since 0.7
1519 */
1520 public function settings_always_notify_admin_option() {
1521 $options = [
1522 'off' => __( 'Disabled', 'edit-flow' ),
1523 'on' => __( 'Enabled', 'edit-flow' ),
1524 ];
1525 echo '<select id="always_notify_admin" name="' . esc_attr( $this->module->options_group_name ) . '[always_notify_admin]">';
1526 foreach ( $options as $value => $label ) {
1527 echo '<option value="' . esc_attr( $value ) . '"';
1528 echo selected( $this->module->options->always_notify_admin, $value );
1529 echo '>' . esc_html( $label ) . '</option>';
1530 }
1531 echo '</select>';
1532 }
1533
1534 /**
1535 * Option to enable sending notifications to Slack
1536 *
1537 * @since 0.9.9
1538 */
1539 public function settings_send_to_webhook() {
1540 $options = [
1541 'off' => __( 'Disabled', 'edit-flow' ),
1542 'on' => __( 'Enabled', 'edit-flow' ),
1543 ];
1544 echo '<select id="send_to_webhook" name="' . esc_attr( $this->module->options_group_name ) . '[send_to_webhook]">';
1545 foreach ( $options as $value => $label ) {
1546 echo '<option value="' . esc_attr( $value ) . '"';
1547 echo selected( $this->module->options->send_to_webhook, $value );
1548 echo '>' . esc_html( $label ) . '</option>';
1549 }
1550 echo '</select>';
1551 }
1552
1553 /**
1554 * Option to set the Slack webhook URL.
1555 *
1556 * @since 0.9.9
1557 */
1558 public function settings_webhook_url() {
1559 echo '<input type="text" id="webhook_url" name="' . esc_attr( $this->module->options_group_name ) . '[webhook_url]" value="' . esc_attr( $this->module->options->webhook_url ) . '" />';
1560 }
1561
1562 /**
1563 * Validate our user input as the settings are being saved.
1564 *
1565 * @since 0.7
1566 *
1567 * @param array $new_options The new options to validate.
1568 * @return array The validated options.
1569 */
1570 public function settings_validate( $new_options ) {
1571
1572 // Whitelist validation for the post type options.
1573 if ( ! isset( $new_options['post_types'] ) ) {
1574 $new_options['post_types'] = [];
1575 }
1576 $new_options['post_types'] = $this->clean_post_type_options( $new_options['post_types'], $this->module->post_type_support );
1577
1578 // Whitelist validation for the 'always_notify_admin' options.
1579 if ( ! isset( $new_options['always_notify_admin'] ) || 'on' != $new_options['always_notify_admin'] ) {
1580 $new_options['always_notify_admin'] = 'off';
1581 }
1582
1583 // White list validation for the 'send_to_slack' option.
1584 if ( ! isset( $new_options['send_to_webhook'] ) || 'on' != $new_options['send_to_webhook'] ) {
1585 $new_options['send_to_webhook'] = 'off';
1586 }
1587
1588 // White list validation for the 'slack_webhook_url' option.
1589 if ( ! isset( $new_options['webhook_url'] ) || esc_url_raw( $new_options['webhook_url'] ) !== $new_options['webhook_url'] ) {
1590 $new_options['webhook_url'] = '';
1591 } else {
1592 $new_options['webhook_url'] = esc_url_raw( $new_options['webhook_url'] );
1593 }
1594
1595 return $new_options;
1596 }
1597
1598 /**
1599 * Settings page for notifications.
1600 *
1601 * @since 0.7
1602 */
1603 public function print_configure_view() {
1604 ?>
1605 <form class="basic-settings" action="<?php echo esc_url( menu_page_url( $this->module->settings_slug, false ) ); ?>" method="post">
1606 <?php settings_fields( $this->module->options_group_name ); ?>
1607 <?php do_settings_sections( $this->module->options_group_name ); ?>
1608 <?php
1609 echo '<input id="edit_flow_module_name" name="edit_flow_module_name" type="hidden" value="' . esc_attr( $this->module->name ) . '" />';
1610 ?>
1611 <?php submit_button(); ?>
1612 </form>
1613 <?php
1614 }
1615
1616 /**
1617 * Gets a simple phrase containing the formatted date and time that the post is scheduled for.
1618 *
1619 * @since 0.8
1620 *
1621 * @param WP_Post $post Post object.
1622 * @return string The scheduled datetime in human-readable format.
1623 */
1624 private function get_scheduled_datetime( $post ) {
1625
1626 $scheduled_ts = strtotime( $post->post_date );
1627
1628 $date = date_i18n( get_option( 'date_format' ), $scheduled_ts );
1629 $time = date_i18n( get_option( 'time_format' ), $scheduled_ts );
1630
1631 /* translators: 1: date, 2: time */
1632 return sprintf( __( '%1$s at %2$s', 'edit-flow' ), $date, $time );
1633 }
1634 }
1635
1636 }
1637