PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 12.5
Jetpack – WP Security, Backup, Speed, & Growth v12.5
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 12.5, at modules/subscriptions.php

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