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