PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 13.2.2
Jetpack – WP Security, Backup, Speed, & Growth v13.2.2
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / subscriptions.php

subscriptions.php in Jetpack – WP Security, Backup, Speed, & Growth 13.2.2, at modules/subscriptions.php

1,151 lines 35.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName)
2 /**
3 * Module Name: Newsletter
4 * Module Description: Let visitors subscribe to new posts and comments via email
5 * Sort Order: 9
6 * Recommendation Order: 8
7 * First Introduced: 1.2
8 * Requires Connection: Yes
9 * Requires User Connection: Yes
10 * Auto Activate: No
11 * Module Tags: Social
12 * Feature: Engagement
13 * Additional Search Queries: subscriptions, subscription, email, follow, followers, subscribers, signup, newsletter, creator
14 */
15
16 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
17
18 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
19 use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
20 use Automattic\Jetpack\Redirect;
21 use Automattic\Jetpack\Status;
22 use Automattic\Jetpack\Status\Host;
23
24 add_action( 'jetpack_modules_loaded', 'jetpack_subscriptions_load' );
25
26 // Loads the User Content Link Redirection feature.
27 require_once __DIR__ . '/subscriptions/jetpack-user-content-link-redirection.php';
28
29 /**
30 * Loads the Subscriptions module.
31 */
32 function jetpack_subscriptions_load() {
33 Jetpack::enable_module_configurable( __FILE__ );
34 }
35
36 /**
37 * Cherry picks keys from `$_SERVER` array.
38 *
39 * @since 6.0.0
40 *
41 * @return array An array of server data.
42 */
43 function jetpack_subscriptions_cherry_pick_server_data() {
44 $data = array();
45
46 foreach ( $_SERVER as $key => $value ) {
47 if ( ! is_string( $value ) || str_starts_with( $key, 'HTTP_COOKIE' ) ) {
48 continue;
49 }
50
51 if ( str_starts_with( $key, 'HTTP_' ) || in_array( $key, array( 'REMOTE_ADDR', 'REQUEST_URI', 'DOCUMENT_URI' ), true ) ) {
52 $data[ $key ] = $value;
53 }
54 }
55
56 return $data;
57 }
58
59 /**
60 * Main class file for the Subscriptions module.
61 */
62 class Jetpack_Subscriptions {
63 /**
64 * Whether Jetpack has been instantiated or not.
65 *
66 * @var bool
67 */
68 public $jetpack = false;
69
70 /**
71 * Hash of the siteurl option.
72 *
73 * @var string
74 */
75 public static $hash;
76
77 /**
78 * Singleton
79 *
80 * @static
81 */
82 public static function init() {
83 static $instance = false;
84
85 if ( ! $instance ) {
86 $instance = new Jetpack_Subscriptions();
87 }
88
89 return $instance;
90 }
91
92 /**
93 * Jetpack_Subscriptions constructor.
94 */
95 public function __construct() {
96 $this->jetpack = Jetpack::init();
97
98 // Don't use COOKIEHASH as it could be shared across installs && is non-unique in multisite.
99 // @see: https://twitter.com/nacin/status/378246957451333632 .
100 self::$hash = md5( get_option( 'siteurl' ) );
101
102 add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
103
104 // @todo remove sync from subscriptions and move elsewhere...
105
106 // Add Configuration Page.
107 add_action( 'admin_init', array( $this, 'configure' ) );
108
109 // Catch subscription widget submits.
110 if ( isset( $_REQUEST['jetpack_subscriptions_widget'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce checked in widget_submit() for logged in users.
111 add_action( 'template_redirect', array( $this, 'widget_submit' ) );
112 }
113
114 // Set up the comment subscription checkboxes.
115 add_filter( 'comment_form_submit_field', array( $this, 'comment_subscribe_init' ), 10, 2 );
116
117 // Catch comment posts and check for subscriptions.
118 add_action( 'comment_post', array( $this, 'comment_subscribe_submit' ), 50, 2 );
119
120 // Adds post meta checkbox in the post submit metabox.
121 add_action( 'post_submitbox_misc_actions', array( $this, 'subscription_post_page_metabox' ) );
122
123 add_action( 'transition_post_status', array( $this, 'maybe_send_subscription_email' ), 10, 3 );
124
125 add_filter( 'jetpack_published_post_flags', array( $this, 'set_post_flags' ), 10, 2 );
126
127 add_filter( 'post_updated_messages', array( $this, 'update_published_message' ), 18, 1 );
128
129 // Set "social_notifications_subscribe" option during the first-time activation.
130 add_action( 'jetpack_activate_module_subscriptions', array( $this, 'set_social_notifications_subscribe' ) );
131
132 // Hide subscription messaging in Publish panel for posts that were published in the past
133 add_action( 'init', array( $this, 'register_post_meta' ), 20 );
134 add_action( 'transition_post_status', array( $this, 'maybe_set_first_published_status' ), 10, 3 );
135
136 // Add Subscribers menu to Jetpack navigation.
137 add_action( 'jetpack_admin_menu', array( $this, 'add_subscribers_menu' ) );
138 }
139
140 /**
141 * Jetpack_Subscriptions::xmlrpc_methods()
142 *
143 * Register subscriptions methods with the Jetpack XML-RPC server.
144 *
145 * @param array $methods Methods being registered.
146 */
147 public function xmlrpc_methods( $methods ) {
148 return array_merge(
149 $methods,
150 array(
151 'jetpack.subscriptions.subscribe' => array( $this, 'subscribe' ),
152 )
153 );
154 }
155
156 /**
157 * Disable Subscribe on Single Post
158 * Register post meta
159 */
160 public function subscription_post_page_metabox() {
161 if (
162 /**
163 * Filter whether or not to show the per-post subscription option.
164 *
165 * @module subscriptions
166 *
167 * @since 3.7.0
168 *
169 * @param bool true = show checkbox option on all new posts | false = hide the option.
170 */
171 ! apply_filters( 'jetpack_allow_per_post_subscriptions', false ) ) {
172 return;
173 }
174
175 if ( has_filter( 'jetpack_subscriptions_exclude_these_categories' ) || has_filter( 'jetpack_subscriptions_include_only_these_categories' ) ) {
176 return;
177 }
178
179 global $post;
180 $disable_subscribe_value = get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true );
181 // only show checkbox if post hasn't been published and is a 'post' post type.
182 if ( get_post_status( $post->ID ) !== 'publish' && get_post_type( $post->ID ) === 'post' ) :
183 // Nonce it.
184 wp_nonce_field( 'disable_subscribe', 'disable_subscribe_nonce' );
185 ?>
186 <div class="misc-pub-section">
187 <label for="_jetpack_dont_email_post_to_subs"><?php esc_html_e( 'Jetpack Subscriptions:', 'jetpack' ); ?></label><br>
188 <input type="checkbox" name="_jetpack_dont_email_post_to_subs" id="jetpack-per-post-subscribe" value="1" <?php checked( $disable_subscribe_value, 1, true ); ?> />
189 <?php esc_html_e( 'Don&#8217;t send this to subscribers', 'jetpack' ); ?>
190 </div>
191 <?php
192 endif;
193 }
194
195 /**
196 * Checks whether or not the post should be emailed to subscribers
197 *
198 * It checks for the following things in order:
199 * - Usage of filter jetpack_subscriptions_exclude_these_categories
200 * - Usage of filter jetpack_subscriptions_include_only_these_categories
201 * - Existence of the per-post checkbox option
202 *
203 * Only one of these can be used at any given time.
204 *
205 * @param string $new_status Tthe "new" post status of the transition when saved.
206 * @param string $old_status The "old" post status of the transition when saved.
207 * @param object $post obj The post object.
208 */
209 public function maybe_send_subscription_email( $new_status, $old_status, $post ) {
210
211 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
212 return;
213 }
214
215 // Make sure that the checkbox is preseved.
216 if ( ! empty( $_POST['disable_subscribe_nonce'] ) && wp_verify_nonce( $_POST['disable_subscribe_nonce'], 'disable_subscribe' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unslash or sanitize nonces either.
217 $set_checkbox = isset( $_POST['_jetpack_dont_email_post_to_subs'] ) ? 1 : 0;
218 update_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', $set_checkbox );
219 }
220 }
221
222 /**
223 * Message used when publishing a post.
224 *
225 * @param array $messages Message array for a post.
226 */
227 public function update_published_message( $messages ) {
228 global $post;
229 if ( ! $this->should_email_post_to_subscribers( $post ) ) {
230 return $messages;
231 }
232
233 $view_post_link_html = sprintf(
234 ' <a href="%1$s">%2$s</a>',
235 esc_url( get_permalink( $post ) ),
236 __( 'View post', 'jetpack' )
237 );
238
239 $messages['post'][6] = sprintf(
240 /* translators: Message shown after a post is published */
241 esc_html__( 'Post published and sending emails to subscribers.', 'jetpack' )
242 ) . $view_post_link_html;
243 return $messages;
244 }
245
246 /**
247 * Determine if a post should notifiy subscribers via email.
248 *
249 * @param object $post The post.
250 */
251 public function should_email_post_to_subscribers( $post ) {
252 $should_email = true;
253 if ( get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ) ) {
254 return false;
255 }
256
257 // Only posts are currently supported.
258 if ( 'post' !== $post->post_type ) {
259 return false;
260 }
261
262 // Private posts are not sent to subscribers.
263 if ( 'private' === $post->post_status ) {
264 return false;
265 }
266
267 /**
268 * Array of categories that will never trigger subscription emails.
269 *
270 * Will not send subscription emails from any post from within these categories.
271 *
272 * @module subscriptions
273 *
274 * @since 3.7.0
275 *
276 * @param array $args Array of category slugs or ID's.
277 */
278 $excluded_categories = apply_filters( 'jetpack_subscriptions_exclude_these_categories', array() );
279
280 // Never email posts from these categories.
281 if ( ! empty( $excluded_categories ) && in_category( $excluded_categories, $post->ID ) ) {
282 $should_email = false;
283 }
284
285 /**
286 * ONLY send subscription emails for these categories
287 *
288 * Will ONLY send subscription emails to these categories.
289 *
290 * @module subscriptions
291 *
292 * @since 3.7.0
293 *
294 * @param array $args Array of category slugs or ID's.
295 */
296 $only_these_categories = apply_filters( 'jetpack_subscriptions_exclude_all_categories_except', array() );
297
298 // Only emails posts from these categories.
299 if ( ! empty( $only_these_categories ) && ! in_category( $only_these_categories, $post->ID ) ) {
300 $should_email = false;
301 }
302
303 return $should_email;
304 }
305
306 /**
307 * Retrieve which flags should be added to a particular post.
308 *
309 * @param array $flags Flags to be added.
310 * @param object $post A post object.
311 */
312 public function set_post_flags( $flags, $post ) {
313 $flags['send_subscription'] = $this->should_email_post_to_subscribers( $post );
314 return $flags;
315 }
316
317 /**
318 * Jetpack_Subscriptions::configure()
319 *
320 * Jetpack Subscriptions configuration screen.
321 */
322 public function configure() {
323 // Create the section.
324 add_settings_section(
325 'jetpack_subscriptions',
326 __( 'Jetpack Subscriptions Settings', 'jetpack' ),
327 array( $this, 'subscriptions_settings_section' ),
328 'discussion'
329 );
330
331 /** Subscribe to Posts */
332
333 add_settings_field(
334 'jetpack_subscriptions_post_subscribe',
335 __( 'Follow Blog', 'jetpack' ),
336 array( $this, 'subscription_post_subscribe_setting' ),
337 'discussion',
338 'jetpack_subscriptions'
339 );
340
341 register_setting(
342 'discussion',
343 'stb_enabled'
344 );
345
346 /** Subscribe to Comments */
347
348 add_settings_field(
349 'jetpack_subscriptions_comment_subscribe',
350 __( 'Follow Comments', 'jetpack' ),
351 array( $this, 'subscription_comment_subscribe_setting' ),
352 'discussion',
353 'jetpack_subscriptions'
354 );
355
356 register_setting(
357 'discussion',
358 'stc_enabled'
359 );
360
361 /** Enable Subscribe Modal */
362
363 add_settings_field(
364 'jetpack_subscriptions_comment_subscribe',
365 __( 'Enable Subscribe Modal', 'jetpack' ),
366 array( $this, 'subscribe_modal_setting' ),
367 'discussion',
368 'jetpack_subscriptions'
369 );
370
371 register_setting(
372 'discussion',
373 'sm_enabled'
374 );
375
376 /** Email me whenever: Someone subscribes to my blog */
377 /* @since 8.1 */
378
379 add_settings_section(
380 'notifications_section',
381 __( 'Someone subscribes to my blog', 'jetpack' ),
382 array( $this, 'social_notifications_subscribe_section' ),
383 'discussion'
384 );
385
386 add_settings_field(
387 'jetpack_subscriptions_social_notifications_subscribe',
388 __( 'Email me whenever', 'jetpack' ),
389 array( $this, 'social_notifications_subscribe_field' ),
390 'discussion',
391 'notifications_section'
392 );
393
394 register_setting(
395 'discussion',
396 'social_notifications_subscribe',
397 array( $this, 'social_notifications_subscribe_validate' )
398 );
399
400 /** Subscription Messaging Options */
401
402 register_setting(
403 'reading',
404 'subscription_options',
405 array( $this, 'validate_settings' )
406 );
407
408 add_settings_section(
409 'email_settings',
410 __( 'Follower Settings', 'jetpack' ),
411 array( $this, 'reading_section' ),
412 'reading'
413 );
414
415 add_settings_field(
416 'invitation',
417 __( 'Blog follow email text', 'jetpack' ),
418 array( $this, 'setting_invitation' ),
419 'reading',
420 'email_settings'
421 );
422
423 add_settings_field(
424 'comment-follow',
425 __( 'Comment follow email text', 'jetpack' ),
426 array( $this, 'setting_comment_follow' ),
427 'reading',
428 'email_settings'
429 );
430
431 add_settings_field(
432 'welcome',
433 __( 'Welcome email text', 'jetpack' ),
434 array( $this, 'setting_welcome' ),
435 'reading',
436 'email_settings'
437 );
438 }
439
440 /**
441 * Discussions setting section blurb.
442 */
443 public function subscriptions_settings_section() {
444 ?>
445 <p id="jetpack-subscriptions-settings"><?php esc_html_e( 'Change whether your visitors can subscribe to your posts or comments or both.', 'jetpack' ); ?></p>
446
447 <?php
448 }
449
450 /**
451 * Post Subscriptions Toggle.
452 */
453 public function subscription_post_subscribe_setting() {
454
455 $stb_enabled = get_option( 'stb_enabled', 1 );
456 ?>
457
458 <p class="description">
459 <input type="checkbox" name="stb_enabled" id="jetpack-post-subscribe" value="1" <?php checked( $stb_enabled, 1 ); ?> />
460 <?php
461 echo wp_kses(
462 __(
463 "Show a <em>'follow blog'</em> option in the comment form",
464 'jetpack'
465 ),
466 array( 'em' => array() )
467 );
468 ?>
469 </p>
470 <?php
471 }
472
473 /**
474 * Comments Subscriptions Toggle.
475 */
476 public function subscription_comment_subscribe_setting() {
477
478 $stc_enabled = get_option( 'stc_enabled', 1 );
479 ?>
480
481 <p class="description">
482 <input type="checkbox" name="stc_enabled" id="jetpack-comment-subscribe" value="1" <?php checked( $stc_enabled, 1 ); ?> />
483 <?php
484 echo wp_kses(
485 __(
486 "Show a <em>'follow comments'</em> option in the comment form",
487 'jetpack'
488 ),
489 array( 'em' => array() )
490 );
491 ?>
492 </p>
493
494 <?php
495 }
496
497 /**
498 * Subscribe Modal Toggle.
499 */
500 public function subscribe_modal_setting() {
501
502 $sm_enabled = get_option( 'sm_enabled', 1 );
503 ?>
504
505 <p class="description">
506 <input type="checkbox" name="sm_enabled" id="jetpack-subscribe-modal" value="1" <?php checked( $sm_enabled, 1 ); ?> />
507 <?php esc_html_e( 'Show a popup subscribe modal to readers.', 'jetpack' ); ?>
508 </p>
509
510 <?php
511 }
512
513 /**
514 * Someone subscribes to my blog section
515 *
516 * @since 8.1
517 */
518 public function social_notifications_subscribe_section() {
519 // Atypical usage here. We emit jquery to move subscribe notification checkbox to be with the rest of the email notification settings.
520 ?>
521 <script type="text/javascript">
522 jQuery( function( $ ) {
523 var table = $( '#social_notifications_subscribe' ).parents( 'table:first' ),
524 header = table.prevAll( 'h2:first' ),
525 newParent = $( '#moderation_notify' ).parent( 'label' ).parent();
526
527 if ( ! table.length || ! header.length || ! newParent.length ) {
528 return;
529 }
530
531 newParent.append( '<br/>' ).append( table.end().parent( 'label' ).siblings().andSelf() );
532 header.remove();
533 table.remove();
534 } );
535 </script>
536 <?php
537 }
538
539 /**
540 * Someone subscribes to my blog Toggle
541 *
542 * @since 8.1
543 */
544 public function social_notifications_subscribe_field() {
545 $checked = (int) ( 'on' === get_option( 'social_notifications_subscribe', 'on' ) );
546 ?>
547
548 <label>
549 <input type="checkbox" name="social_notifications_subscribe" id="social_notifications_subscribe" value="1" <?php checked( $checked ); ?> />
550 <?php
551 /* translators: this is a label for a setting that starts with "Email me whenever" */
552 esc_html_e( 'Someone subscribes to my blog', 'jetpack' );
553 ?>
554 </label>
555 <?php
556 }
557
558 /**
559 * Validate "Someone subscribes to my blog" option
560 *
561 * @since 8.1
562 *
563 * @param String $input the input string to be validated.
564 * @return string on|off
565 */
566 public function social_notifications_subscribe_validate( $input ) {
567 // If it's not set (was unchecked during form submission) or was set to off (during option update), return 'off'.
568 if ( ! $input || 'off' === $input ) {
569 return 'off';
570 }
571
572 // Otherwise we return 'on'.
573 return 'on';
574 }
575
576 /**
577 * Validate settings for the Subscriptions module.
578 *
579 * @param array $settings Settings to be validated.
580 */
581 public function validate_settings( $settings ) {
582 global $allowedposttags;
583
584 $default = $this->get_default_settings();
585
586 // Blog Follow.
587 $settings['invitation'] = trim( wp_kses( $settings['invitation'], $allowedposttags ) );
588 if ( empty( $settings['invitation'] ) ) {
589 $settings['invitation'] = $default['invitation'];
590 }
591
592 // Comments Follow (single post).
593 $settings['comment_follow'] = trim( wp_kses( $settings['comment_follow'], $allowedposttags ) );
594 if ( empty( $settings['comment_follow'] ) ) {
595 $settings['comment_follow'] = $default['comment_follow'];
596 }
597
598 return $settings;
599 }
600
601 /**
602 * HTML output helper for Reading section.
603 */
604 public function reading_section() {
605 echo '<p id="follower-settings">';
606 esc_html_e( 'These settings change emails sent from your blog to followers.', 'jetpack' );
607 echo '</p>';
608 }
609
610 /**
611 * HTML output helper for Invitation section.
612 */
613 public function setting_invitation() {
614 $settings = $this->get_settings();
615 echo '<textarea name="subscription_options[invitation]" class="large-text" cols="50" rows="5">' . esc_textarea( $settings['invitation'] ) . '</textarea>';
616 echo '<p><span class="description">' . esc_html__( 'Introduction text sent when someone follows your blog. (Site and confirmation details will be automatically added for you.)', 'jetpack' ) . '</span></p>';
617 }
618
619 /**
620 * HTML output helper for Comment Follow section.
621 */
622 public function setting_comment_follow() {
623 $settings = $this->get_settings();
624 echo '<textarea name="subscription_options[comment_follow]" class="large-text" cols="50" rows="5">' . esc_textarea( $settings['comment_follow'] ) . '</textarea>';
625 echo '<p><span class="description">' . esc_html__( 'Introduction text sent when someone follows a post on your blog. (Site and confirmation details will be automatically added for you.)', 'jetpack' ) . '</span></p>';
626 }
627
628 /**
629 * HTML output helper for Welcome section.
630 */
631 public function setting_welcome() {
632 $settings = $this->get_settings();
633 echo '<textarea name="subscription_options[welcome]" class="large-text" cols="50" rows="5">' . esc_textarea( $settings['welcome'] ) . '</textarea>';
634 echo '<p><span class="description">' . esc_html__( 'Welcome text sent when someone follows your blog.', 'jetpack' ) . '</span></p>';
635 }
636
637 /**
638 * Get default settings for the Subscriptions module.
639 */
640 public function get_default_settings() {
641 $site_url = get_home_url();
642 $display_url = preg_replace( '(^https?://)', '', untrailingslashit( $site_url ) );
643
644 return array(
645 /* translators: Both %1$s and %2$s is site address */
646 'invitation' => sprintf( __( "Howdy,\nYou recently subscribed to <a href='%1\$s'>%2\$s</a> and we need to verify the email you provided. Once you confirm below, you'll be able to receive and read new posts.\n\nIf you believe this is an error, ignore this message and nothing more will happen.", 'jetpack' ), $site_url, $display_url ),
647 'comment_follow' => __( "Howdy.\n\nYou recently followed one of my posts. This means you will receive an email when new comments are posted.\n\nTo activate, click confirm below. If you believe this is an error, ignore this message and we'll never bother you again.", 'jetpack' ),
648 /* translators: %1$s is the site address */
649 'welcome' => sprintf( __( 'Cool, you are now subscribed to %1$s and will receive an email notification when a new post is published.', 'jetpack' ), $display_url ),
650 );
651 }
652
653 /**
654 * Reeturn merged `subscription_options` option with module default settings.
655 */
656 public function get_settings() {
657 return wp_parse_args( (array) get_option( 'subscription_options' ), $this->get_default_settings() );
658 }
659
660 /**
661 * Jetpack_Subscriptions::subscribe()
662 *
663 * Send a synchronous XML-RPC subscribe to blog posts or subscribe to post comments request.
664 *
665 * @param string $email being subscribed.
666 * @param array $post_ids (optional) defaults to 0 for blog posts only: array of post IDs to subscribe to blog's posts.
667 * @param bool $async (optional) Should the subscription be performed asynchronously? Defaults to true.
668 * @param array $extra_data Additional data passed to the `jetpack.subscribeToSite` call.
669 *
670 * @return true|WP_Error true on success
671 * invalid_email : not a valid email address
672 * invalid_post_id : not a valid post ID
673 * unknown_post_id : unknown post
674 * not_subscribed : strange error. Jetpack servers at WordPress.com could subscribe the email.
675 * disabled : Site owner has disabled subscriptions.
676 * active : Already subscribed.
677 * pending : Tried to subscribe before but the confirmation link is never clicked. No confirmation email is sent.
678 * unknown : strange error. Jetpack servers at WordPress.com returned something malformed.
679 * unknown_status : strange error. Jetpack servers at WordPress.com returned something I didn't understand.
680 */
681 public function subscribe( $email, $post_ids = 0, $async = true, $extra_data = array() ) {
682 if ( ! is_email( $email ) ) {
683 return new WP_Error( 'invalid_email' );
684 }
685
686 if ( ! $async ) {
687 $xml = new Jetpack_IXR_ClientMulticall();
688 }
689
690 foreach ( (array) $post_ids as $post_id ) {
691 $post_id = (int) $post_id;
692 if ( $post_id < 0 ) {
693 return new WP_Error( 'invalid_post_id' );
694 } elseif ( $post_id && ! get_post( $post_id ) ) {
695 return new WP_Error( 'unknown_post_id' );
696 }
697
698 if ( $async ) {
699 XMLRPC_Async_Call::add_call( 'jetpack.subscribeToSite', 0, $email, $post_id, serialize( $extra_data ) ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
700 } else {
701 $xml->addCall( 'jetpack.subscribeToSite', $email, $post_id, serialize( $extra_data ) ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
702 }
703 }
704
705 if ( $async ) {
706 return;
707 }
708
709 // Call.
710 $xml->query();
711
712 if ( $xml->isError() ) {
713 return $xml->get_jetpack_error();
714 }
715
716 $responses = $xml->getResponse();
717
718 $r = array();
719 foreach ( (array) $responses as $response ) {
720 if ( isset( $response['faultCode'] ) || isset( $response['faultString'] ) ) {
721 $r[] = $xml->get_jetpack_error( $response['faultCode'], $response['faultString'] );
722 continue;
723 }
724
725 if ( ! is_array( $response[0] ) || empty( $response[0]['status'] ) ) {
726 $r[] = new WP_Error( 'unknown' );
727 continue;
728 }
729
730 switch ( $response[0]['status'] ) {
731 case 'error':
732 $r[] = new WP_Error( 'not_subscribed' );
733 continue 2;
734 case 'disabled':
735 $r[] = new WP_Error( 'disabled' );
736 continue 2;
737 case 'active':
738 $r[] = new WP_Error( 'active' );
739 continue 2;
740 case 'confirming':
741 $r[] = true;
742 continue 2;
743 case 'pending':
744 $r[] = new WP_Error( 'pending' );
745 continue 2;
746 default:
747 $r[] = new WP_Error( 'unknown_status', (string) $response[0]['status'] );
748 continue 2;
749 }
750 }
751
752 return $r;
753 }
754
755 /**
756 * Jetpack_Subscriptions::widget_submit()
757 *
758 * When a user submits their email via the blog subscription widget, check the details and call the subsribe() method.
759 */
760 public function widget_submit() {
761 // Check the nonce.
762 if ( ! wp_verify_nonce( isset( $_REQUEST['_wpnonce'] ) ? sanitize_key( $_REQUEST['_wpnonce'] ) : '', 'blogsub_subscribe_' . \Jetpack_Options::get_option( 'id' ) ) ) {
763 return false;
764 }
765
766 if ( empty( $_REQUEST['email'] ) || ! is_string( $_REQUEST['email'] ) ) {
767 return false;
768 }
769
770 $redirect_fragment = false;
771 if ( isset( $_REQUEST['redirect_fragment'] ) ) {
772 $redirect_fragment = preg_replace( '/[^a-z0-9_-]/i', '', $_REQUEST['redirect_fragment'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is manually unslashing and sanitizing.
773 }
774 if ( ! $redirect_fragment || ! is_string( $redirect_fragment ) ) {
775 $redirect_fragment = 'subscribe-blog';
776 }
777
778 $subscribe = self::subscribe(
779 isset( $_REQUEST['email'] ) ? wp_unslash( $_REQUEST['email'] ) : null, // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated inside self::subscribe().
780 0,
781 false,
782 array(
783 'source' => 'widget',
784 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no',
785 'comment_status' => '',
786 'server_data' => jetpack_subscriptions_cherry_pick_server_data(),
787 )
788 );
789
790 if ( is_wp_error( $subscribe ) ) {
791 $error = $subscribe->get_error_code();
792 } else {
793 $error = false;
794 foreach ( $subscribe as $response ) {
795 if ( is_wp_error( $response ) ) {
796 $error = $response->get_error_code();
797 break;
798 }
799 }
800 }
801
802 switch ( $error ) {
803 case false:
804 $result = 'success';
805 break;
806 case 'invalid_email':
807 $result = $error;
808 break;
809 case 'blocked_email':
810 $result = 'opted_out';
811 break;
812 case 'active':
813 $result = 'already';
814 break;
815 case 'flooded_email':
816 $result = 'many_pending_subs';
817 break;
818 case 'pending':
819 $result = 'pending';
820 break;
821 default:
822 $result = 'error';
823 break;
824 }
825
826 $redirect = add_query_arg( 'subscribe', $result );
827
828 /**
829 * Fires on each subscription form submission.
830 *
831 * @module subscriptions
832 *
833 * @since 3.7.0
834 *
835 * @param string $result Result of form submission: success, invalid_email, already, error.
836 */
837 do_action( 'jetpack_subscriptions_form_submission', $result );
838
839 wp_safe_redirect( "$redirect#$redirect_fragment" );
840 exit;
841 }
842
843 /**
844 * Jetpack_Subscriptions::comment_subscribe_init()
845 *
846 * Set up and add the comment subscription checkbox to the comment form.
847 *
848 * @param string $submit_button HTML markup for the submit field.
849 */
850 public function comment_subscribe_init( $submit_button ) {
851 global $post;
852
853 // Subscriptions are only available for posts so far.
854 if ( ! $post || 'post' !== $post->post_type ) {
855 return $submit_button;
856 }
857
858 $comments_checked = '';
859 $blog_checked = '';
860
861 // Check for a comment / blog submission and set a cookie to retain the setting and check the boxes.
862 if ( isset( $_COOKIE[ 'jetpack_comments_subscribe_' . self::$hash . '_' . $post->ID ] ) ) {
863 $comments_checked = ' checked="checked"';
864 }
865
866 if ( isset( $_COOKIE[ 'jetpack_blog_subscribe_' . self::$hash ] ) ) {
867 $blog_checked = ' checked="checked"';
868 }
869
870 // Some themes call this function, don't show the checkbox again.
871 remove_action( 'comment_form', 'subscription_comment_form' );
872
873 // Check if Mark Jaquith's Subscribe to Comments plugin is active - if so, suppress Jetpack checkbox.
874
875 $str = '';
876
877 if ( false === has_filter( 'comment_form', 'show_subscription_checkbox' ) && 1 === (int) get_option( 'stc_enabled', 1 ) && empty( $post->post_password ) && 'post' === get_post_type() ) {
878 // Subscribe to comments checkbox.
879 $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_comments" id="subscribe_comments" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $comments_checked . ' /> ';
880 $comment_sub_text = __( 'Notify me of follow-up comments by email.', 'jetpack' );
881 $str .= '<label class="subscribe-label" id="subscribe-label" for="subscribe_comments">' . esc_html(
882 /**
883 * Filter the Subscribe to comments text appearing below the comment form.
884 *
885 * @module subscriptions
886 *
887 * @since 3.4.0
888 *
889 * @param string $comment_sub_text Subscribe to comments text.
890 */
891 apply_filters( 'jetpack_subscribe_comment_label', $comment_sub_text )
892 ) . '</label>';
893 $str .= '</p>';
894 }
895
896 if ( 1 === (int) get_option( 'stb_enabled', 1 ) ) {
897 // Subscribe to blog checkbox.
898 $str .= '<p class="comment-subscription-form"><input type="checkbox" name="subscribe_blog" id="subscribe_blog" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;"' . $blog_checked . ' /> ';
899 $blog_sub_text = __( 'Notify me of new posts by email.', 'jetpack' );
900 $str .= '<label class="subscribe-label" id="subscribe-blog-label" for="subscribe_blog">' . esc_html(
901 /**
902 * Filter the Subscribe to blog text appearing below the comment form.
903 *
904 * @module subscriptions
905 *
906 * @since 3.4.0
907 *
908 * @param string $comment_sub_text Subscribe to blog text.
909 */
910 apply_filters( 'jetpack_subscribe_blog_label', $blog_sub_text )
911 ) . '</label>';
912 $str .= '</p>';
913 }
914
915 /**
916 * Filter the output of the subscription options appearing below the comment form.
917 *
918 * @module subscriptions
919 *
920 * @since 1.2.0
921 *
922 * @param string $str Comment Subscription form HTML output.
923 */
924 $str = apply_filters( 'jetpack_comment_subscription_form', $str );
925
926 return $str . $submit_button;
927 }
928
929 /**
930 * Jetpack_Subscriptions::comment_subscribe_init()
931 *
932 * When a user checks the comment subscribe box and submits a comment, subscribe them to the comment thread.
933 *
934 * @param int|string $comment_id Comment thread being subscribed to.
935 * @param string $approved Comment status.
936 */
937 public function comment_subscribe_submit( $comment_id, $approved ) {
938 if ( 'spam' === $approved ) {
939 return;
940 }
941
942 $comment = get_comment( $comment_id );
943 if ( ! $comment ) {
944 return;
945 }
946
947 // Set cookies for this post/comment.
948 $this->set_cookies( isset( $_REQUEST['subscribe_comments'] ), $comment->comment_post_ID, isset( $_REQUEST['subscribe_blog'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
949
950 if ( ! isset( $_REQUEST['subscribe_comments'] ) && ! isset( $_REQUEST['subscribe_blog'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
951 return;
952 }
953
954 $post_ids = array();
955
956 if ( isset( $_REQUEST['subscribe_comments'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
957 $post_ids[] = $comment->comment_post_ID;
958 }
959
960 if ( isset( $_REQUEST['subscribe_blog'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
961 $post_ids[] = 0;
962 }
963
964 $result = self::subscribe(
965 $comment->comment_author_email,
966 $post_ids,
967 true,
968 array(
969 'source' => 'comment-form',
970 'widget-in-use' => is_active_widget( false, false, 'blog_subscription', true ) ? 'yes' : 'no',
971 'comment_status' => $approved,
972 'server_data' => jetpack_subscriptions_cherry_pick_server_data(),
973 )
974 );
975
976 /**
977 * Fires on each comment subscription form submission.
978 *
979 * @module subscriptions
980 *
981 * @since 5.5.0
982 *
983 * @param NULL|WP_Error $result Result of form submission: NULL on success, WP_Error otherwise.
984 * @param array $post_ids An array of post IDs that the user subscribed to, 0 means blog subscription.
985 */
986 do_action( 'jetpack_subscriptions_comment_form_submission', $result, $post_ids );
987 }
988
989 /**
990 * Jetpack_Subscriptions::set_cookies()
991 *
992 * Set a cookie to save state on the comment and post subscription checkboxes.
993 *
994 * @param bool $subscribe_to_post Whether the user chose to subscribe to subsequent comments on this post.
995 * @param int $post_id If $subscribe_to_post is true, the post ID they've subscribed to.
996 * @param bool $subscribe_to_blog Whether the user chose to subscribe to all new posts on the blog.
997 */
998 public function set_cookies( $subscribe_to_post = false, $post_id = null, $subscribe_to_blog = false ) {
999 $post_id = (int) $post_id;
1000
1001 /** This filter is already documented in core/wp-includes/comment-functions.php */
1002 $cookie_lifetime = apply_filters( 'comment_cookie_lifetime', 30000000 );
1003
1004 /**
1005 * Filter the Jetpack Comment cookie path.
1006 *
1007 * @module subscriptions
1008 *
1009 * @since 2.5.0
1010 *
1011 * @param string COOKIEPATH Cookie path.
1012 */
1013 $cookie_path = apply_filters( 'jetpack_comment_cookie_path', COOKIEPATH );
1014
1015 /**
1016 * Filter the Jetpack Comment cookie domain.
1017 *
1018 * @module subscriptions
1019 *
1020 * @since 2.5.0
1021 *
1022 * @param string COOKIE_DOMAIN Cookie domain.
1023 */
1024 $cookie_domain = apply_filters( 'jetpack_comment_cookie_domain', COOKIE_DOMAIN );
1025
1026 if ( $subscribe_to_post && $post_id >= 0 ) {
1027 setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, 1, time() + $cookie_lifetime, $cookie_path, $cookie_domain, is_ssl(), true );
1028 } else {
1029 setcookie( 'jetpack_comments_subscribe_' . self::$hash . '_' . $post_id, '', time() - 3600, $cookie_path, $cookie_domain, is_ssl(), true );
1030 }
1031
1032 if ( $subscribe_to_blog ) {
1033 setcookie( 'jetpack_blog_subscribe_' . self::$hash, 1, time() + $cookie_lifetime, $cookie_path, $cookie_domain, is_ssl(), true );
1034 } else {
1035 setcookie( 'jetpack_blog_subscribe_' . self::$hash, '', time() - 3600, $cookie_path, $cookie_domain, is_ssl(), true );
1036 }
1037 }
1038
1039 /**
1040 * Set the social_notifications_subscribe option to `off` when the Subscriptions module is activated in the first time.
1041 *
1042 * @since 8.1
1043 *
1044 * @return void
1045 */
1046 public function set_social_notifications_subscribe() {
1047 if ( false === get_option( 'social_notifications_subscribe' ) ) {
1048 add_option( 'social_notifications_subscribe', 'off' );
1049 }
1050 }
1051
1052 /**
1053 * Save a flag when a post was ever published.
1054 *
1055 * It saves the post meta when the post was published and becomes a draft.
1056 * Then this meta is used to hide subscription messaging in Publish panel.
1057 *
1058 * @param string $new_status Tthe "new" post status of the transition when saved.
1059 * @param string $old_status The "old" post status of the transition when saved.
1060 * @param object $post obj The post object.
1061 */
1062 public function maybe_set_first_published_status( $new_status, $old_status, $post ) {
1063 $was_post_ever_published = get_post_meta( $post->ID, '_jetpack_post_was_ever_published', true );
1064 if ( ! $was_post_ever_published && 'publish' === $old_status && 'draft' === $new_status ) {
1065 update_post_meta( $post->ID, '_jetpack_post_was_ever_published', true );
1066 }
1067 }
1068
1069 /**
1070 * Checks if the current user can publish posts.
1071 *
1072 * @return bool
1073 */
1074 public function first_published_status_meta_auth_callback() {
1075 if ( current_user_can( 'publish_posts' ) ) {
1076 return true;
1077 }
1078 return false;
1079 }
1080
1081 /**
1082 * Registers the 'post_was_ever_published' post meta for use in the REST API.
1083 */
1084 public function register_post_meta() {
1085 $jetpack_post_was_ever_published = array(
1086 'type' => 'boolean',
1087 'description' => __( 'Whether the post was ever published.', 'jetpack' ),
1088 'single' => true,
1089 'default' => false,
1090 'show_in_rest' => array(
1091 'name' => 'jetpack_post_was_ever_published',
1092 ),
1093 'auth_callback' => array( $this, 'first_published_status_meta_auth_callback' ),
1094 );
1095
1096 register_meta( 'post', '_jetpack_post_was_ever_published', $jetpack_post_was_ever_published );
1097 }
1098
1099 /**
1100 * Create a Subscribers menu displayed on self-hosted sites.
1101 *
1102 * - It is not displayed on WordPress.com sites.
1103 * - It directs you to Calypso to the existing Subscribers page.
1104 *
1105 * @return void
1106 */
1107 public function add_subscribers_menu() {
1108 /*
1109 * Do not display any menu on WoA and WordPress.com Simple sites.
1110 * They already get a menu item under Users via nav-unification.
1111 */
1112 if ( ( new Host() )->is_wpcom_platform() ) {
1113 return;
1114 }
1115
1116 $status = new Status();
1117
1118 /*
1119 * Do not display if we're in Offline mode,
1120 * or if the user is not connected.
1121 */
1122 if (
1123 $status->is_offline_mode()
1124 || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected()
1125 ) {
1126 return;
1127 }
1128
1129 $blog_id = Connection_Manager::get_site_id( true );
1130
1131 $link = Redirect::get_url(
1132 'jetpack-menu-calypso-subscribers',
1133 array( 'site' => $blog_id ? $blog_id : $status->get_site_suffix() )
1134 );
1135
1136 add_submenu_page(
1137 'jetpack',
1138 esc_attr__( 'Subscribers', 'jetpack' ),
1139 __( 'Subscribers', 'jetpack' ) . ' <span class="dashicons dashicons-external"></span>',
1140 'manage_options',
1141 esc_url( $link ),
1142 null
1143 );
1144 }
1145 }
1146
1147 Jetpack_Subscriptions::init();
1148
1149 require __DIR__ . '/subscriptions/views.php';
1150 require __DIR__ . '/subscriptions/subscribe-modal/class-jetpack-subscribe-modal.php';
1151