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