Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.84% covered (danger)
23.84%
164 / 688
7.69% covered (danger)
7.69%
3 / 39
CRAP
n/a
0 / 0
Automattic\Jetpack\Extensions\Subscriptions\register_block
85.83% covered (warning)
85.83%
103 / 120
0.00% covered (danger)
0.00%
0 / 1
11.34
Automattic\Jetpack\Extensions\Subscriptions\is_wpcom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\register_newsletter_access_column
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\add_paywalled_content_post_meta
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
Automattic\Jetpack\Extensions\Subscriptions\render_newsletter_access_rows
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
7.77
Automattic\Jetpack\Extensions\Subscriptions\newsletter_access_column_styles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\fetch_subscriber_counts
22.22% covered (danger)
22.22%
6 / 27
0.00% covered (danger)
0.00%
0 / 1
16.76
Automattic\Jetpack\Extensions\Subscriptions\get_subscriber_count
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\has_attribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\get_attribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\get_setting_class_name
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\get_element_class_names_from_attributes
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\is_button_only_style
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\get_element_styles_from_attributes
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
182
Automattic\Jetpack\Extensions\Subscriptions\get_attribute_color
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\get_global_style_color
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\get_color_from_slug
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
Automattic\Jetpack\Extensions\Subscriptions\is_jetpack_memberships_loaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\render_block
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
462
Automattic\Jetpack\Extensions\Subscriptions\get_post_access_level_for_current_post
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\render_for_website
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 1
702
Automattic\Jetpack\Extensions\Subscriptions\render_for_email
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\render_email
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
6
Automattic\Jetpack\Extensions\Subscriptions\add_paywall
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
Automattic\Jetpack\Extensions\Subscriptions\maybe_close_comments
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
Automattic\Jetpack\Extensions\Subscriptions\maybe_gate_existing_comments
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\is_jetpack_token_subscription_service_loaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\maybe_prevent_super_cache_caching
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
Automattic\Jetpack\Extensions\Subscriptions\get_paywall_content
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\get_current_url
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
110
Automattic\Jetpack\Extensions\Subscriptions\get_submit_button_text
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\is_top_subscription
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\sanitize_submit_text
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
Automattic\Jetpack\Extensions\Subscriptions\get_paywall_blocks
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
110
Automattic\Jetpack\Extensions\Subscriptions\is_user_auth
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
Automattic\Jetpack\Extensions\Subscriptions\is_paid_post
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\is_subscribers_post
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
Automattic\Jetpack\Extensions\Subscriptions\get_paywall_blocks_subscribe_pending
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
Automattic\Jetpack\Extensions\Subscriptions\get_paywall_simple
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Subscriptions Block.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Extensions\Subscriptions;
9
10use Automattic\Jetpack\Blocks;
11use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
12use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Jetpack_Token_Subscription_Service;
13use Automattic\Jetpack\Modules;
14use Automattic\Jetpack\Status\Host;
15use Automattic\Jetpack\Status\Request;
16use Jetpack_Gutenberg;
17use Jetpack_Memberships;
18use Jetpack_Subscriptions_Widget;
19
20if ( ! defined( 'ABSPATH' ) ) {
21    exit( 0 );
22}
23
24require_once __DIR__ . '/class-jetpack-subscription-site.php';
25require_once __DIR__ . '/constants.php';
26require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
27
28/**
29 * These block defaults should match ./constants.js
30 */
31const DEFAULT_BORDER_RADIUS_VALUE = 0;
32const DEFAULT_BORDER_WEIGHT_VALUE = 1;
33const DEFAULT_FONTSIZE_VALUE      = '16px';
34const DEFAULT_PADDING_VALUE       = 15;
35const DEFAULT_SPACING_VALUE       = 10;
36const DEFAULT_BUTTON_WIDTH        = 'auto';
37
38/**
39 * Registers the block for use in Gutenberg
40 * This is done via an action so that we can disable
41 * registration if we need to.
42 */
43function register_block() {
44    /*
45     * Disable the feature on P2 blogs
46     */
47    if ( function_exists( '\WPForTeams\is_wpforteams_site' ) &&
48        \WPForTeams\is_wpforteams_site( get_current_blog_id() ) ) {
49        return;
50    }
51
52    /*
53     * Do not proceed if the newsletter feature (Subscriptions module) is not enabled
54     */
55    if ( ! ( new Modules() )->is_active( 'subscriptions' ) ) {
56        Jetpack_Gutenberg::set_extension_unavailable( 'subscriptions', 'missing_module', array( 'module' => 'subscriptions' ) );
57        return;
58    }
59
60    require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
61
62    if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) {
63        Blocks::jetpack_register_block(
64            __DIR__,
65            array(
66                'render_callback'       => __NAMESPACE__ . '\render_block',
67                'render_email_callback' => __NAMESPACE__ . '\render_email',
68                'supports'              => array(
69                    'spacing' => array(
70                        'margin'  => true,
71                        'padding' => true,
72                    ),
73                    'align'   => array( 'wide', 'full' ),
74                ),
75            )
76        );
77    }
78
79    register_post_meta(
80        'post',
81        META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS,
82        array(
83            'show_in_rest'      => true,
84            'single'            => true,
85            'type'              => 'string',
86            /* translators: Keep the quoted values, block names and meta keys in English. */
87            'description'       => __( 'Who can read the post on the site: "everybody" (the default, also when empty), "subscribers" (free and paid) or "paid_subscribers" (paid only). Anyone without access sees a subscribe prompt instead. "everybody" and "subscribers" posts are emailed to all subscribers, "paid_subscribers" posts only to paid ones; with newsletter categories on, only to those subscribed to one of its categories or to all content. To show a free preview, add a Paywall block (jetpack/paywall) where it should end: everything above it stays public, the post is emailed to all subscribers and cut off at the paywall for anyone without access, and an "everybody" post becomes subscribers-only below the block. "paid_subscribers" works only on sites that already have a paid newsletter tier. Without one, the owner first connects Stripe and adds a tier under Monetize > Payment Settings in the WordPress.com dashboard (Jetpack Cloud on self-hosted sites). If unsure a tier exists, check with the site owner first.', 'jetpack' ),
88            // The REST schema already rejects non-strings, but non-REST writers
89            // (importers, XML-RPC, WP-CLI, direct update_post_meta, sync) only pass
90            // through sanitize_meta(). Coerce anything that isn't a string to ''
91            // ("everybody") so a corrupt value can't be persisted and later fatal the
92            // strict string-typed access checks.
93            'sanitize_callback' => function ( $value ) {
94                return is_string( $value ) ? $value : '';
95            },
96            'auth_callback'     => function () {
97                return wp_get_current_user()->has_cap( 'edit_posts' );
98            },
99        )
100    );
101
102    // The meta is a "don't send" flag, so invert the blog option (which defaults to true = "send").
103    $dont_email_default = ! get_option( 'wpcom_newsletter_send_default', true );
104
105    register_post_meta(
106        'post',
107        META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS,
108        array(
109            'default'       => $dont_email_default,
110            'show_in_rest'  => true,
111            'single'        => true,
112            'type'          => 'boolean',
113            /* translators: Keep the quoted values, block names and meta keys in English. */
114            'description'   => __( 'Set to true to publish without emailing the post to subscribers. The email is sent once, when the post goes live, so this has to be set in the request that publishes it, or any time before the date of a scheduled post. Once the post is published, changing it neither sends nor recalls the email. The default comes from the site\'s newsletter settings and can be either value, so send it explicitly when the email matters.', 'jetpack' ),
115            'auth_callback' => function () {
116                return wp_get_current_user()->has_cap( 'edit_posts' );
117            },
118        )
119    );
120
121    register_post_meta(
122        'post',
123        META_NAME_FOR_POST_TIER_ID_SETTINGS,
124        array(
125            'show_in_rest'  => true,
126            'single'        => true,
127            'type'          => 'integer',
128            /* translators: Keep the quoted values, block names and meta keys in English. */
129            'description'   => __( 'Only applies when _jetpack_newsletter_access is "paid_subscribers": the ID of the monthly paid newsletter tier a reader needs. Subscribers on that tier, its yearly plan or a plan priced the same or higher in the same currency can read the post and get the email. Unset or 0 lets every paid subscriber in, which is the right value unless the site owner names a specific tier. An ID that matches no tier is ignored.', 'jetpack' ),
130            'auth_callback' => function () {
131                return wp_get_current_user()->has_cap( 'edit_posts' );
132            },
133        )
134    );
135
136    register_post_meta(
137        'post',
138        META_NAME_CONTAINS_PAYWALLED_CONTENT,
139        array(
140            'show_in_rest'  => true,
141            'single'        => true,
142            'type'          => 'boolean',
143            /* translators: Keep the quoted values, block names and meta keys in English. */
144            'description'   => __( 'Computed on every save: true when the post has a Paywall block or its _jetpack_newsletter_access is "subscribers" or "paid_subscribers". Any value sent here is overwritten; change _jetpack_newsletter_access or the content instead.', 'jetpack' ),
145            'auth_callback' => function () {
146                return wp_get_current_user()->has_cap( 'edit_posts' );
147            },
148        )
149    );
150
151    // This ensures Jetpack will sync this post meta to WPCOM.
152    add_filter(
153        'jetpack_sync_post_meta_whitelist',
154        function ( $allowed_meta ) {
155            return array_merge(
156                $allowed_meta,
157                array(
158                    META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS,
159                    META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS,
160                    META_NAME_CONTAINS_PAYWALLED_CONTENT,
161                    META_NAME_FOR_POST_TIER_ID_SETTINGS,
162                    META_NAME_CONTAINS_PAID_CONTENT,
163                )
164            );
165        }
166    );
167
168    // Hide the content â€“ Priority 8 makes it run before do_blocks gets called for the content
169    add_filter( 'the_content', __NAMESPACE__ . '\add_paywall', 8 );
170
171    // Close comments on the front-end
172    add_filter( 'comments_open', __NAMESPACE__ . '\maybe_close_comments', 10, 2 );
173    add_filter( 'pings_open', __NAMESPACE__ . '\maybe_close_comments', 10, 2 );
174
175    // Hide existing comments
176    add_filter( 'get_comment', __NAMESPACE__ . '\maybe_gate_existing_comments' );
177
178    // Add a 'Newsletter' column to the Edit posts page
179    // We only display the "Newsletter" column if we have configured the paid newsletter plan
180    if ( defined( 'WP_ADMIN' ) && WP_ADMIN && Jetpack_Memberships::has_configured_plans_jetpack_recurring_payments( 'newsletter' ) ) {
181        add_action( 'manage_post_posts_columns', __NAMESPACE__ . '\register_newsletter_access_column' );
182        add_action( 'manage_post_posts_custom_column', __NAMESPACE__ . '\render_newsletter_access_rows', 10, 2 );
183        add_action( 'admin_head', __NAMESPACE__ . '\newsletter_access_column_styles' );
184    }
185
186    add_action( 'init', __NAMESPACE__ . '\maybe_prevent_super_cache_caching' );
187
188    add_action( 'wp_after_insert_post', __NAMESPACE__ . '\add_paywalled_content_post_meta', 99, 2 );
189
190    add_filter(
191        'jetpack_options_whitelist',
192        function ( $options ) {
193            $options[] = 'jetpack_subscriptions_subscribe_post_end_enabled';
194            $options[] = 'jetpack_subscriptions_subscribe_navigation_enabled';
195
196            return $options;
197        }
198    );
199
200    // If called via REST API, we need to register later in the lifecycle
201    if ( ( new Host() )->is_wpcom_platform() && ! Request::is_frontend() ) {
202        add_action(
203            'restapi_theme_init',
204            function () {
205                Jetpack_Subscription_Site::init()->handle_subscribe_block_placements();
206            }
207        );
208    } else {
209        Jetpack_Subscription_Site::init()->handle_subscribe_block_placements();
210    }
211}
212add_action( 'init', __NAMESPACE__ . '\register_block', 9 );
213
214/**
215 * Returns true when in a WP.com environment.
216 *
217 * @return boolean
218 */
219function is_wpcom() {
220    return defined( 'IS_WPCOM' ) && IS_WPCOM;
221}
222
223/**
224 * Adds a 'Newsletter' column after the 'Title' column in the post list
225 *
226 * @param array $columns An array of column names.
227 * @return array An array of column names.
228 */
229function register_newsletter_access_column( $columns ) {
230    $position   = array_search( 'title', array_keys( $columns ), true );
231    $new_column = array( NEWSLETTER_COLUMN_ID => __( 'Newsletter', 'jetpack' ) );
232    return array_merge(
233        array_slice( $columns, 0, $position + 1, true ),
234        $new_column,
235        array_slice( $columns, $position, null, true )
236    );
237}
238
239/**
240 * Add a meta to prevent publication on firehose, ES AI or Reader
241 *
242 * @param int      $post_id Post id being saved.
243 * @param \WP_Post $post Post being saved.
244 * @return void
245 */
246function add_paywalled_content_post_meta( int $post_id, \WP_Post $post ) {
247    if ( $post->post_type !== 'post' ) {
248        return;
249    }
250
251    $access_level = get_post_meta( $post_id, META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS, true );
252
253    // A Paywall block gates the post whatever the stored access level; see Jetpack_Memberships::get_post_access_level().
254    $is_paywalled = has_block( \Automattic\Jetpack\Extensions\Paywall\BLOCK_NAME, $post );
255    switch ( $access_level ) {
256        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS:
257        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS:
258        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS:
259            $is_paywalled = true;
260    }
261    if ( $is_paywalled ) {
262        update_post_meta( $post_id, META_NAME_CONTAINS_PAYWALLED_CONTENT, $is_paywalled );
263    }
264    if ( ! $is_paywalled ) {
265        delete_post_meta( $post_id, META_NAME_CONTAINS_PAYWALLED_CONTENT );
266    }
267}
268
269/**
270 * Displays the newsletter access level.
271 *
272 * @param string $column_id The ID of the column to display.
273 * @param int    $post_id The current post ID.
274 */
275function render_newsletter_access_rows( $column_id, $post_id ) {
276    if ( NEWSLETTER_COLUMN_ID !== $column_id ) {
277        return;
278    }
279
280    $access_level = Jetpack_Memberships::get_post_access_level( $post_id );
281
282    switch ( $access_level ) {
283        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS:
284            echo esc_html__( 'Paid Subscribers (all plans)', 'jetpack' );
285            break;
286        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS:
287            echo esc_html__( 'Paid Subscribers', 'jetpack' );
288            break;
289        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS:
290            echo esc_html__( 'Subscribers', 'jetpack' );
291            break;
292        case Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY:
293            echo esc_html__( 'Everybody', 'jetpack' );
294            break;
295        default:
296            echo '';
297    }
298}
299
300/**
301 * Adds the Newsletter column styles
302 */
303function newsletter_access_column_styles() {
304    echo '<style id="jetpack-newsletter-newsletter-access-column"> table.fixed .column-newsletter_access { width: 10%; } </style>';
305}
306
307/**
308 * Determine the amount of folks currently subscribed to the blog, splitted out in total_subscribers, email_subscribers, social_followers & paid_subscribers.
309 *
310 * @return array containing ['value' => ['total_subscribers' => 0, 'email_subscribers' => 0, 'paid_subscribers' => 0, 'social_followers' => 0]]
311 */
312function fetch_subscriber_counts() {
313    $subs_count = 0;
314    if ( is_wpcom() ) {
315        $subs_count = array(
316            'value' => \wpcom_fetch_subs_counts( true ),
317        );
318    } else {
319        $cache_key  = 'wpcom_subscribers_totals';
320        $subs_count = get_transient( $cache_key );
321        if ( false === $subs_count || 'failed' === $subs_count['status'] ) {
322            $xml = new \Jetpack_IXR_Client();
323            $xml->query( 'jetpack.fetchSubscriberCounts' );
324
325            if ( $xml->isError() ) { // If we get an error from .com, set the status to failed so that we will try again next time the data is requested.
326                $subs_count = array(
327                    'status'  => 'failed',
328                    'code'    => $xml->getErrorCode(),
329                    'message' => $xml->getErrorMessage(),
330                    'value'   => $subs_count['value'] ?? array(
331                        'total_subscribers' => 0,
332                        'email_subscribers' => 0,
333                        'social_followers'  => 0,
334                        'paid_subscribers'  => 0,
335                    ),
336                );
337            } else {
338                $subs_count = array(
339                    'status' => 'success',
340                    'value'  => $xml->getResponse(),
341                );
342            }
343            set_transient( $cache_key, $subs_count, 3600 ); // Try to cache the result for at least 1 hour.
344        }
345    }
346    return $subs_count;
347}
348
349/**
350 * Returns subscriber count based on include_social_followers attribute
351 *
352 * @param bool $include_social_followers Whether to include social followers in the count.
353 * @return int
354 */
355function get_subscriber_count( $include_social_followers ) {
356    $counts = fetch_subscriber_counts();
357
358    if ( $include_social_followers ) {
359        $subscriber_count = $counts['value']['total_subscribers'] + $counts['value']['social_followers'];
360    } else {
361        $subscriber_count = $counts['value']['total_subscribers'];
362    }
363    return $subscriber_count;
364}
365
366/**
367 * Returns true if the block attributes contain a value for the given key.
368 *
369 * @param array  $attributes Array containing the block attributes.
370 * @param string $key        Block attribute key.
371 *
372 * @return boolean
373 */
374function has_attribute( $attributes, $key ) {
375    return isset( $attributes[ $key ] ) && $attributes[ $key ] !== 'undefined';
376}
377
378/**
379 * Returns the value for the given attribute key, with the option of providing a default fallback value.
380 *
381 * @param array  $attributes Array containing the block attributes.
382 * @param string $key        Block attribute key.
383 * @param mixed  $default    Optional fallback value in case the key doesn't exist.
384 *
385 * @return mixed
386 */
387function get_attribute( $attributes, $key, $default = null ) {
388    return has_attribute( $attributes, $key ) ? $attributes[ $key ] : $default;
389}
390
391/**
392 * Mimics getColorClassName, getFontSizeClass and getGradientClass from @wordpress/block-editor js package.
393 *
394 * @param string $setting Setting name.
395 * @param string $value   Setting value.
396 *
397 * @return string
398 */
399function get_setting_class_name( $setting, $value ) {
400    if ( ! $setting || ! $value ) {
401        return '';
402    }
403
404    return sprintf( 'has-%s-%s', $value, $setting );
405}
406
407/**
408 * Uses block attributes to generate an array containing the classes for various block elements.
409 * Based on Jetpack_Subscriptions_Widget::do_subscription_form() which the block was originally using.
410 *
411 * @param array $attributes Array containing the block attributes.
412 *
413 * @return array
414 */
415function get_element_class_names_from_attributes( $attributes ) {
416    $text_color_class = get_setting_class_name( 'color', get_attribute( $attributes, 'textColor' ) );
417    $font_size_class  = get_setting_class_name( 'font-size', get_attribute( $attributes, 'fontSize' ) );
418    $border_class     = get_setting_class_name( 'border-color', get_attribute( $attributes, 'borderColor' ) );
419
420    $button_background_class = get_setting_class_name( 'background-color', get_attribute( $attributes, 'buttonBackgroundColor' ) );
421    $button_gradient_class   = get_setting_class_name( 'gradient-background', get_attribute( $attributes, 'buttonGradient' ) );
422
423    $email_field_background_class = get_setting_class_name( 'background-color', get_attribute( $attributes, 'emailFieldBackgroundColor' ) );
424    $email_field_gradient_class   = get_setting_class_name( 'gradient-background', get_attribute( $attributes, 'emailFieldGradient' ) );
425
426    $submit_button_classes = array_filter(
427        array(
428            'wp-block-button__link'  => true,
429            'no-border-radius'       => 0 === get_attribute( $attributes, 'borderRadius', 0 ),
430            $font_size_class         => true,
431            $border_class            => true,
432            'has-text-color'         => ! empty( $text_color_class ),
433            $text_color_class        => true,
434            'has-background'         => ! empty( $button_background_class ) || ! empty( $button_gradient_class ),
435            $button_background_class => ! empty( $button_background_class ),
436            $button_gradient_class   => ! empty( $button_gradient_class ),
437        )
438    );
439
440    $email_field_classes = array_filter(
441        array(
442            'no-border-radius'            => 0 === get_attribute( $attributes, 'borderRadius', 0 ),
443            $font_size_class              => true,
444            $border_class                 => true,
445            $email_field_background_class => true,
446            $email_field_gradient_class   => true,
447        )
448    );
449
450    $block_wrapper_classes = array_filter(
451        array(
452            'wp-block-jetpack-subscriptions__supports-newline' => true,
453            'wp-block-jetpack-subscriptions__use-newline' => (bool) get_attribute( $attributes, 'buttonOnNewLine' ),
454            'wp-block-jetpack-subscriptions__show-subs'   => (bool) get_attribute( $attributes, 'showSubscribersTotal' ),
455        )
456    );
457
458    return array(
459        'block_wrapper' => implode( ' ', array_keys( $block_wrapper_classes ) ),
460        'email_field'   => implode( ' ', array_keys( $email_field_classes ) ),
461        'submit_button' => implode( ' ', array_keys( $submit_button_classes ) ),
462    );
463}
464
465/**
466 * Checks if block style is "button only"
467 *
468 * @param string $class_name Block attribute className; multiple names are spearated by space.
469 *
470 * @return bool
471 */
472function is_button_only_style( $class_name ) {
473    if ( empty( $class_name ) ) {
474        return false;
475    }
476
477    $class_names = explode( ' ', $class_name );
478    return in_array( 'is-style-button', $class_names, true );
479}
480
481/**
482 * Uses block attributes to generate an array containing the styles for various block elements.
483 * Based on Jetpack_Subscriptions_Widget::do_subscription_form() which the block was originally using.
484 *
485 * @param array $attributes Array containing the block attributes.
486 *
487 * @return array
488 */
489function get_element_styles_from_attributes( $attributes ) {
490    $is_button_only_style = is_button_only_style( get_attribute( $attributes, 'className', '' ) );
491
492    $button_background_style = ! has_attribute( $attributes, 'buttonBackgroundColor' ) && has_attribute( $attributes, 'customButtonGradient' )
493        ? get_attribute( $attributes, 'customButtonGradient' )
494        : get_attribute( $attributes, 'customButtonBackgroundColor' );
495
496    $email_field_styles           = '';
497    $submit_button_wrapper_styles = '';
498    $submit_button_styles         = '';
499
500    if ( ! empty( $button_background_style ) ) {
501        $submit_button_styles .= sprintf( 'background: %s;', $button_background_style );
502    }
503
504    if ( has_attribute( $attributes, 'customTextColor' ) ) {
505        $submit_button_styles .= sprintf( 'color: %s;', get_attribute( $attributes, 'customTextColor' ) );
506    }
507
508    if ( has_attribute( $attributes, 'buttonWidth' ) ) {
509        $submit_button_wrapper_styles .= sprintf( 'width: %s;', get_attribute( $attributes, 'buttonWidth', DEFAULT_BUTTON_WIDTH ) );
510        $submit_button_wrapper_styles .= 'max-width: 100%;';
511
512        // Account for custom margins on inline forms.
513        $submit_button_styles .= true === get_attribute( $attributes, 'buttonOnNewLine' )
514            ? 'width: 100%;'
515            : sprintf( 'width: calc(100%% - %dpx);', get_attribute( $attributes, 'spacing', DEFAULT_SPACING_VALUE ) );
516    }
517
518    $font_size = get_attribute( $attributes, 'customFontSize', DEFAULT_FONTSIZE_VALUE );
519    $style     = sprintf( 'font-size: %s%s;', $font_size, is_numeric( $font_size ) ? 'px' : '' );
520
521    $submit_button_styles .= $style;
522    $email_field_styles   .= $style;
523
524    $padding = get_attribute( $attributes, 'padding', DEFAULT_PADDING_VALUE );
525    $style   = sprintf( 'padding: %1$dpx %2$dpx %1$dpx %2$dpx;', $padding, round( $padding * 1.5 ) );
526
527    $submit_button_styles .= $style;
528    $email_field_styles   .= $style;
529
530    if ( ! $is_button_only_style ) {
531        $button_spacing = get_attribute( $attributes, 'spacing', DEFAULT_SPACING_VALUE );
532        if ( true === get_attribute( $attributes, 'buttonOnNewLine' ) ) {
533            $submit_button_styles .= sprintf( 'margin-top: %dpx;', $button_spacing );
534        } else {
535            $submit_button_styles .= 'margin: 0; '; // Reset Safari's 2px default margin for buttons affecting input and button union
536            $submit_button_styles .= sprintf( 'margin-left: %dpx;', $button_spacing );
537        }
538    }
539
540    if ( has_attribute( $attributes, 'borderColor' ) ) {
541        $style                 = sprintf( 'border-color: %s;', get_attribute( $attributes, 'borderColor', '' ) );
542        $submit_button_styles .= $style;
543        $email_field_styles   .= $style;
544    }
545
546    $style                 = sprintf( 'border-radius: %dpx;', get_attribute( $attributes, 'borderRadius', DEFAULT_BORDER_RADIUS_VALUE ) );
547    $submit_button_styles .= $style;
548    $email_field_styles   .= $style;
549
550    $style                 = sprintf( 'border-width: %dpx;', get_attribute( $attributes, 'borderWeight', DEFAULT_BORDER_WEIGHT_VALUE ) );
551    $submit_button_styles .= $style;
552    $email_field_styles   .= $style;
553
554    if ( has_attribute( $attributes, 'customBorderColor' ) ) {
555        $style = sprintf( 'border-color: %s; border-style: solid;', get_attribute( $attributes, 'customBorderColor' ) );
556
557        $submit_button_styles .= $style;
558        $email_field_styles   .= $style;
559    }
560
561    if ( ! Request::is_frontend() ) {
562        $background_color_style = get_attribute_color( 'buttonBackgroundColor', $attributes, '#113AF5' /* default lettre theme color */ );
563        $text_color_style       = get_attribute_color( 'textColor', $attributes, '#FFFFFF' );
564        $submit_button_styles  .= sprintf( ' background-color: %s; color: %s;', $background_color_style, $text_color_style );
565    }
566
567    return array(
568        'email_field'           => $email_field_styles,
569        'submit_button'         => $submit_button_styles,
570        'submit_button_wrapper' => $submit_button_wrapper_styles,
571    );
572}
573
574/**
575 * Retrieve the resolved color for a given attribute.
576 *
577 * @param string $attribute_name The name of the attribute to resolve.
578 * @param array  $attributes     An array of all attributes.
579 * @param string $default_color  A fallback color in case no color can be resolved.
580 *
581 * @return string Returns the resolved color or the default color if no color is found.
582 */
583function get_attribute_color( $attribute_name, $attributes, $default_color ) {
584    if ( has_attribute( $attributes, $attribute_name ) ) {
585        $color_slug     = get_attribute( $attributes, $attribute_name );
586        $resolved_color = get_color_from_slug( $color_slug );
587
588        if ( $resolved_color ) {
589            return $resolved_color;
590        }
591    }
592
593    return get_global_style_color( $attribute_name, $default_color );
594}
595
596/**
597 * Retrieve the global style color based on a provided style key.
598 *
599 * @param string $style_key     The key for the desired style.
600 * @param string $default_color A fallback color in case the global style is not set.
601 *
602 * @return string Returns the color defined in global styles or the default color if not defined.
603 */
604function get_global_style_color( $style_key, $default_color ) {
605    $global_styles = wp_get_global_styles(
606        array( 'color' ),
607        array(
608            'block_name' => 'core/button',
609            'transforms' => array( 'resolve-variables' ),
610        )
611    );
612
613    if ( isset( $global_styles[ $style_key ] ) ) {
614        return $global_styles[ $style_key ];
615    }
616
617    return $default_color;
618}
619
620/**
621 * Convert a color slug into its corresponding color value.
622 *
623 * @param string $slug The slug representation of the color.
624 *
625 * @return string|null Returns the color value if found, or null otherwise.
626 */
627function get_color_from_slug( $slug ) {
628    $color_palettes = wp_get_global_settings( array( 'color', 'palette' ) );
629
630    if ( ! is_array( $color_palettes ) ) {
631        return null;
632    }
633
634    foreach ( $color_palettes as $palette ) {
635        if ( is_array( $palette ) ) {
636            foreach ( $palette as $color ) {
637                if ( isset( $color['slug'] ) && $color['slug'] === $slug && isset( $color['color'] ) ) {
638                    return $color['color'];
639                }
640            }
641        }
642    }
643
644    return null;
645}
646
647/**
648 * Is the Jetpack_Memberships class loaded.
649 */
650function is_jetpack_memberships_loaded(): bool {
651    return class_exists( '\Jetpack_Memberships' );
652}
653
654/**
655 * Subscriptions block render callback.
656 *
657 * @param array $attributes Array containing the block attributes.
658 *
659 * @return string
660 */
661function render_block( $attributes ) {
662    // If the Subscriptions module is not active, don't render the block.
663    if ( ! ( new Modules() )->is_active( 'subscriptions' ) ) {
664        return '';
665    }
666
667    if ( is_jetpack_memberships_loaded() ) {
668        // We only want the sites that have newsletter feature enabled to be graced by this JavaScript.
669        Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
670    } else {
671        Jetpack_Gutenberg::load_styles_as_required( FEATURE_NAME );
672    }
673
674    if ( ! class_exists( 'Jetpack_Subscriptions_Widget' ) ) {
675        return '';
676    }
677
678    // Prefill the email field with the current user's email if they are logged in via Memberships premium content token
679    $subscribe_email = Jetpack_Memberships::get_current_user_email();
680
681    // If no email, then prefill the email field with the current user's email if they are logged in
682    if ( empty( $subscribe_email ) ) {
683        $current_user = wp_get_current_user();
684        if ( ! empty( $current_user->user_email ) ) {
685            $subscribe_email = $current_user->user_email;
686        }
687    }
688
689    // The block is using the Jetpack_Subscriptions_Widget backend, hence the need to increase the instance count.
690    ++Jetpack_Subscriptions_Widget::$instance_count;
691
692    $classes = get_element_class_names_from_attributes( $attributes );
693    $styles  = get_element_styles_from_attributes( $attributes );
694
695    // The default value was previously "true" in block.json. We don't want to rely setting "default" in block.json to falsy,
696    // because it would change the setting for previously saved blocks. Block editor doesn't store default values in attributes at all.
697    // Hence users without this set will still get social counts included in the subscriber counter.
698    // Lowering the subscriber count on their behalf with code change would be controversial.
699    // We want to disencourage including social count as it's misleading.
700    $include_social_followers = isset( $attributes['includeSocialFollowers'] ) ? (bool) get_attribute( $attributes, 'includeSocialFollowers' ) : true;
701
702    $data = array(
703        'widget_id'                         => Jetpack_Subscriptions_Widget::$instance_count,
704        'subscribe_email'                   => $subscribe_email,
705        'is_paid_subscriber'                => get_attribute( $attributes, 'isPaidSubscriber', false ),
706        'wrapper_attributes'                => get_block_wrapper_attributes(
707            array(
708                'class' => $classes['block_wrapper'],
709            )
710        ),
711        'subscribe_placeholder'             => get_attribute( $attributes, 'subscribePlaceholder', __( 'Type your email…', 'jetpack' ) ),
712        'submit_button_text'                => get_attribute( $attributes, 'submitButtonText', __( 'Subscribe', 'jetpack' ) ),
713        'submit_button_text_subscribed'     => get_attribute( $attributes, 'submitButtonTextSubscribed', __( 'Subscribed', 'jetpack' ) ),
714        'submit_button_text_upgrade'        => get_attribute( $attributes, 'submitButtonTextUpgrade', __( 'Upgrade subscription', 'jetpack' ) ),
715        'success_message'                   => get_attribute(
716            $attributes,
717            'successMessage',
718            esc_html__( "Success! An email was just sent to confirm your subscription. Please find the email now and click 'Confirm' to start subscribing.", 'jetpack' )
719        ),
720        'show_subscribers_total'            => (bool) get_attribute( $attributes, 'showSubscribersTotal' ),
721        'subscribers_total'                 => get_attribute( $attributes, 'showSubscribersTotal' ) ? get_subscriber_count( $include_social_followers ) : 0,
722        'referer'                           => esc_url_raw(
723            ( is_ssl() ? 'https' : 'http' ) . '://' . ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) .
724            ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' )
725        ),
726        'source'                            => 'subscribe-block',
727        'app_source'                        => get_attribute( $attributes, 'appSource', null ),
728        'class_name'                        => get_attribute( $attributes, 'className' ),
729        'selected_newsletter_categories'    => get_attribute( $attributes, 'selectedNewsletterCategoryIds', array() ),
730        'preselected_newsletter_categories' => get_attribute( $attributes, 'preselectNewsletterCategories', false ),
731    );
732
733    // Only render the email version in non-frontend contexts.
734    if ( is_feed() || wp_is_xml_request() ||
735        ( defined( 'REST_REQUEST' ) && REST_REQUEST && ! wp_is_json_request() ) ||
736        ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ) ||
737        ( defined( 'WP_CLI' ) && WP_CLI ) ||
738        wp_is_jsonp_request() ) {
739        return render_for_email( $data, $styles );
740    }
741
742    return render_for_website( $data, $classes, $styles );
743}
744
745/**
746 *  Get the post access level for the current post. Defaults to 'everybody' if the query is not for a single post
747 *
748 * @return string the actual post access level (see projects/plugins/jetpack/extensions/blocks/subscriptions/constants.js for the values).
749 */
750function get_post_access_level_for_current_post() {
751    if ( ! is_singular() ) {
752        // There is no "actual" current post.
753        return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
754    }
755
756    return Jetpack_Memberships::get_post_access_level();
757}
758
759/**
760 * Renders the subscriptions block at the site.
761 *
762 * @param array $data    Array containing block view data.
763 * @param array $classes Array containing the classes for different block elements.
764 * @param array $styles  Array containing the styles for different block elements.
765 *
766 * @return string
767 */
768function render_for_website( $data, $classes, $styles ) {
769    $lang                 = get_locale();
770    $blog_id              = \Jetpack_Options::get_option( 'id' );
771    $widget_id_suffix     = Jetpack_Subscriptions_Widget::$instance_count > 1 ? '-' . Jetpack_Subscriptions_Widget::$instance_count : '';
772    $form_id              = 'subscribe-blog' . $widget_id_suffix;
773    $form_url             = 'https://wordpress.com/email-subscriptions';
774    $post_access_level    = get_post_access_level_for_current_post();
775    $is_button_only_style = ! empty( $data['class_name'] ) ? is_button_only_style( $data['class_name'] ) : false;
776
777    // Post ID is used for pulling post-specific paid status, and returning to the right post after confirming subscription
778    $post_id = null;
779    if ( in_the_loop() ) {
780        $post_id = get_the_ID();
781    } elseif ( is_singular( 'post' ) || is_page() ) {
782        $post_id = get_queried_object_id();
783    } else {
784        $post_id = get_option( 'page_on_front' );
785    }
786
787    $subscribe_field_id    = apply_filters( 'subscribe_field_id', 'subscribe-field' . $widget_id_suffix, $data['widget_id'] );
788    $tier_id               = get_post_meta( $post_id, META_NAME_FOR_POST_TIER_ID_SETTINGS, true );
789    $is_subscribed         = Jetpack_Memberships::is_current_user_subscribed();
790    $button_text           = get_submit_button_text( $data );
791    $show_subscriber_count = $data['show_subscribers_total'] && $data['subscribers_total'] && ! $is_subscribed;
792
793    ob_start();
794
795    Jetpack_Subscriptions_Widget::render_widget_status_messages(
796        array(
797            'success_message' => $data['success_message'],
798        )
799    );
800    ?>
801    <div <?php echo wp_kses_data( $data['wrapper_attributes'] ); ?>>
802        <div class="wp-block-jetpack-subscriptions__container<?php echo ! $is_subscribed ? ' is-not-subscriber' : ''; ?>">
803            <?php if ( is_top_subscription() ) : ?>
804                <p id="subscribe-submit" class="is-link"
805                    <?php if ( ! empty( $styles['submit_button_wrapper'] ) ) : ?>
806                        style="<?php echo esc_attr( $styles['submit_button_wrapper'] ); ?>"
807                    <?php endif; ?>
808                >
809                        <a
810                            href="<?php echo esc_url( 'https://wordpress.com/reader/site/subscription/' . $blog_id ); ?>"
811                            <?php if ( ! empty( $classes['submit_button'] ) ) : ?>
812                                class="<?php echo esc_attr( $classes['submit_button'] ); ?>"
813                            <?php endif; ?>
814                            <?php if ( ! empty( $styles['submit_button'] ) ) : ?>
815                                style="<?php echo esc_attr( $styles['submit_button'] ); ?>"
816                            <?php endif; ?>
817                        >
818                            <?php echo sanitize_submit_text( $button_text ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
819                        </a>
820                </p>
821            <?php else : ?>
822                <form
823                    action="<?php echo esc_url( $form_url ); ?>"
824                    method="post"
825                    accept-charset="utf-8"
826                    data-blog="<?php echo esc_attr( $blog_id ); ?>"
827                    data-post_access_level="<?php echo esc_attr( $post_access_level ); ?>"
828                    data-subscriber_email="<?php echo esc_attr( $data['subscribe_email'] ); ?>"
829                    id="<?php echo esc_attr( $form_id ); ?>"
830                >
831                    <div class="wp-block-jetpack-subscriptions__form-elements">
832                        <?php if ( ! $is_subscribed && ! $is_button_only_style ) : ?>
833                        <p id="subscribe-email">
834                            <label
835                                id="<?php echo esc_attr( $subscribe_field_id . '-label' ); ?>"
836                                for="<?php echo esc_attr( $subscribe_field_id ); ?>"
837                                class="screen-reader-text"
838                            >
839                                <?php echo esc_html( $data['subscribe_placeholder'] ); ?>
840                            </label>
841                            <?php
842                            printf(
843                                '<input
844                                    required="required"
845                                    type="email"
846                                    name="email"
847                                    autocomplete="email"
848                                    %1$s
849                                    style="%2$s"
850                                    placeholder="%3$s"
851                                    value="%4$s"
852                                    id="%5$s"
853                                    %6$s
854                                />',
855                                ( ! empty( $classes['email_field'] )
856                                    ? 'class="' . esc_attr( $classes['email_field'] ) . '"'
857                                    : ''
858                                ),
859                                ( ! empty( $styles['email_field'] )
860                                    ? esc_attr( $styles['email_field'] )
861                                    : 'width: 95%; padding: 1px 10px'
862                                ),
863                                esc_attr( $data['subscribe_placeholder'] ),
864                                esc_attr( $data['subscribe_email'] ),
865                                esc_attr( $subscribe_field_id ),
866                                ( ! empty( $data['subscribe_email'] )
867                                    ? 'disabled title="' . esc_attr__( "You're logged in with this email", 'jetpack' ) . '"'
868                                    : 'title="' . esc_attr__( 'Please fill in this field.', 'jetpack' ) . '"'
869                                )
870                            );
871                            ?>
872                        </p>
873                        <?php endif; ?>
874                        <p id="subscribe-submit"
875                            <?php if ( ! empty( $styles['submit_button_wrapper'] ) ) : ?>
876                                style="<?php echo esc_attr( $styles['submit_button_wrapper'] ); ?>"
877                            <?php endif; ?>
878                        >
879                            <input type="hidden" name="action" value="subscribe"/>
880                            <input type="hidden" name="blog_id" value="<?php echo (int) $blog_id; ?>"/>
881                            <input type="hidden" name="source" value="<?php echo esc_url( $data['referer'] ); ?>"/>
882                            <input type="hidden" name="sub-type" value="<?php echo esc_attr( $data['source'] ); ?>"/>
883                            <input type="hidden" name="app_source" value="<?php echo esc_attr( $data['app_source'] ); ?>"/>
884                            <input type="hidden" name="redirect_fragment" value="<?php echo esc_attr( $form_id ); ?>"/>
885                            <input type="hidden" name="lang" value="<?php echo esc_attr( $lang ); ?>"/>
886                            <?php
887                            wp_nonce_field( 'blogsub_subscribe_' . $blog_id );
888
889                            if ( ! empty( $post_id ) ) {
890                                echo '<input type="hidden" name="post_id" value="' . esc_attr( $post_id ) . '"/>';
891                            }
892
893                            if ( ! empty( $tier_id ) ) {
894                                echo '<input type="hidden" name="tier_id" value="' . esc_attr( $tier_id ) . '"/>';
895                            }
896
897                            if ( $data['preselected_newsletter_categories'] && ! empty( $data['selected_newsletter_categories'] ) ) {
898                                echo '<input type="hidden" name="selected_newsletter_categories" value="' . esc_attr( implode( ',', $data['selected_newsletter_categories'] ) ) . '"/>';
899                            }
900                            ?>
901                            <button type="submit"
902                                <?php if ( ! empty( $classes['submit_button'] ) ) : ?>
903                                    class="<?php echo esc_attr( $classes['submit_button'] ); ?>"
904                                <?php endif; ?>
905                                <?php if ( ! empty( $styles['submit_button'] ) ) : ?>
906                                    style="<?php echo esc_attr( $styles['submit_button'] ); ?>"
907                                <?php endif; ?>
908                                name="jetpack_subscriptions_widget"
909                            >
910                                <?php echo sanitize_submit_text( $button_text ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
911                            </button>
912                        </p>
913                    </div>
914                </form>
915            <?php endif; ?>
916            <?php if ( $show_subscriber_count ) : ?>
917                <div class="wp-block-jetpack-subscriptions__subscount">
918                    <?php echo esc_html( Jetpack_Memberships::get_join_others_text( $data['subscribers_total'] ) ); ?>
919                </div>
920            <?php endif; ?>
921        </div>
922    </div>
923    <?php
924    return ob_get_clean();
925}
926
927/**
928 * Renders the email version of the subscriptions block.
929 *
930 * @param array $data    Array containing block view data.
931 * @param array $styles  Array containing the styles for different block elements.
932 *
933 * @return string
934 */
935function render_for_email( $data, $styles ) {
936    $submit_button_wrapper_style = ! empty( $styles['submit_button_wrapper'] ) ? 'style="' . esc_attr( $styles['submit_button_wrapper'] ) . '"' : '';
937    $button_text                 = get_submit_button_text( $data );
938
939    $html = '<div ' . wp_kses_data( $data['wrapper_attributes'] ) . '>
940        <div>
941            <div>
942                <div>
943                    <p ' . $submit_button_wrapper_style . '>
944                        <a href="' . esc_url( get_post_permalink() ) . '" style="' . esc_attr( $styles['submit_button'] ) . ' text-decoration: none; white-space: nowrap; margin-left: 0">' . sanitize_submit_text( $button_text ) . '</a>
945                    </p>
946                </div>
947            </div>
948        </div>
949    </div>';
950
951    return $html;
952}
953
954/**
955 * WooCommerce Email Editor render callback for the subscriptions block.
956 *
957 * @param string $block_content The block content.
958 * @param array  $parsed_block  The parsed block data.
959 * @param object $rendering_context The email rendering context.
960 *
961 * @return string
962 */
963function render_email( $block_content, array $parsed_block, $rendering_context ) {
964    if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) || ! function_exists( '\Automattic\Jetpack\Extensions\Button\render_email' ) || ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Button' ) ) {
965        return '';
966    }
967
968    // Map subscription block attributes to button block attributes
969    $button_attributes = array(
970        'text'                  => ! empty( $parsed_block['attrs']['submitButtonText'] ) ? sanitize_text_field( $parsed_block['attrs']['submitButtonText'] ) : __( 'Subscribe', 'jetpack' ),
971        'url'                   => get_post_permalink(),
972        'element'               => 'a',
973        // Map background colors
974        'backgroundColor'       => $parsed_block['attrs']['buttonBackgroundColor'] ?? null,
975        'customBackgroundColor' => $parsed_block['attrs']['customButtonBackgroundColor'] ?? null,
976        // Map text colors
977        'textColor'             => $parsed_block['attrs']['textColor'] ?? null,
978        'customTextColor'       => $parsed_block['attrs']['customTextColor'] ?? null,
979        // Map borders
980        'borderRadius'          => $parsed_block['attrs']['borderRadius'] ?? 0,
981        'borderWeight'          => $parsed_block['attrs']['borderWeight'] ?? 1,
982        'borderColor'           => $parsed_block['attrs']['borderColor'] ?? null,
983        'customBorderColor'     => $parsed_block['attrs']['customBorderColor'] ?? null,
984        // Map typography
985        'fontSize'              => $parsed_block['attrs']['fontSize'] ?? null,
986        'customFontSize'        => $parsed_block['attrs']['customFontSize'] ?? null,
987        // Map spacing
988        'padding'               => $parsed_block['attrs']['padding'] ?? null,
989    );
990
991    // Create a mock button block structure
992    $button_parsed_block = array(
993        'attrs'       => $button_attributes,
994        'email_attrs' => $parsed_block['email_attrs'] ?? array(),
995    );
996
997    // Call the Jetpack button's email rendering
998    return \Automattic\Jetpack\Extensions\Button\render_email(
999        $block_content,
1000        $button_parsed_block,
1001        $rendering_context
1002    );
1003}
1004
1005/**
1006 * Gate access to posts
1007 *
1008 * @param string $the_content Post content.
1009 *
1010 * @return string
1011 */
1012function add_paywall( $the_content ) {
1013    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1014
1015    $post_access_level = Jetpack_Memberships::get_post_access_level();
1016
1017    if ( Jetpack_Memberships::user_can_view_post() ) {
1018        if ( $post_access_level !== Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY ) {
1019            do_action(
1020                'earn_track_paywalled_post_view',
1021                array(
1022                    'post_id' => get_the_ID(),
1023                )
1024            );
1025        }
1026        return $the_content;
1027    }
1028
1029    $paywalled_content = get_paywall_content();
1030
1031    if ( has_block( \Automattic\Jetpack\Extensions\Paywall\BLOCK_NAME ) ) {
1032        if ( strpos( $the_content, \Automattic\Jetpack\Extensions\Paywall\BLOCK_HTML ) ) {
1033            return strstr( $the_content, \Automattic\Jetpack\Extensions\Paywall\BLOCK_HTML, true ) . $paywalled_content;
1034        }
1035        // WordPress generates excerpts by either rendering or stripping blocks before invoking the `the_content` filter.
1036        // In the context of generating an excerpt, the Paywall block specifically renders THE_EXCERPT_BLOCK.
1037        if ( strpos( $the_content, \Automattic\Jetpack\Extensions\Paywall\THE_EXCERPT_BLOCK ) ) {
1038            return strstr( $the_content, \Automattic\Jetpack\Extensions\Paywall\THE_EXCERPT_BLOCK, true );
1039        }
1040    }
1041
1042    return $paywalled_content;
1043}
1044
1045/**
1046 * Gate access to comments. We want to close comments on private sites.
1047 *
1048 * @param bool $default_comments_open Default state of the comments_open filter.
1049 * @param int  $post_id Current post id.
1050 *
1051 * @return bool
1052 */
1053function maybe_close_comments( $default_comments_open, $post_id ) {
1054    if ( ! $default_comments_open || ! $post_id ) {
1055        return $default_comments_open;
1056    }
1057
1058    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1059    return Jetpack_Memberships::user_can_view_post();
1060}
1061
1062/**
1063 * Gate access to existing comments
1064 *
1065 * @param string $comment The comment.
1066 *
1067 * @return string
1068 */
1069function maybe_gate_existing_comments( $comment ) {
1070    if ( empty( $comment ) ) {
1071        return $comment;
1072    }
1073
1074    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1075    if ( Jetpack_Memberships::user_can_view_post() ) {
1076        return $comment;
1077    }
1078    return '';
1079}
1080
1081/**
1082 * Is the Jetpack_Token_Subscription_Service class loaded
1083 *
1084 * @return bool
1085 */
1086function is_jetpack_token_subscription_service_loaded(): bool {
1087    return class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Jetpack_Token_Subscription_Service' );
1088}
1089
1090/**
1091 * Adds support for WP Super cache and Boost cache
1092 */
1093function maybe_prevent_super_cache_caching() {
1094    // Prevents cached page to be served if the Membership cookie is present
1095    if ( is_jetpack_token_subscription_service_loaded() ) {
1096        do_action( 'wpsc_add_cookie', Jetpack_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME );
1097    }
1098
1099    if ( is_user_auth() ) {
1100        // Do not cache the page if user is auth with Membership token
1101        if ( ! defined( 'DONOTCACHEPAGE' ) ) {
1102            define( 'DONOTCACHEPAGE', true );
1103        }
1104    }
1105}
1106
1107/**
1108 * Returns paywall content blocks
1109 *
1110 * @return string
1111 */
1112function get_paywall_content() {
1113    if ( Jetpack_Memberships::user_is_pending_subscriber() ) {
1114        return get_paywall_blocks_subscribe_pending();
1115    }
1116    if ( doing_filter( 'get_the_excerpt' ) ) {
1117        return '';
1118    }
1119    return get_paywall_blocks();
1120}
1121
1122/**
1123 * Returns the current URL.
1124 *
1125 * TODO: Copied from https://github.com/Automattic/jetpack/blob/bb885061dc3ee7a80a78a5f0116ab3fcebfddb09/projects/packages/boost-core/src/lib/class-url.php#L39
1126 * TODO: Move to a shared package
1127 *
1128 * @return string
1129 */
1130function get_current_url() {
1131    // Fallback to the site URL if we're unable to determine the URL from $_SERVER global.
1132    $current_url = site_url();
1133
1134    if ( isset( $_SERVER ) && is_array( $_SERVER ) ) {
1135        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization happens at the end
1136        $scheme = isset( $_SERVER['HTTPS'] ) && 'on' === $_SERVER['HTTPS'] ? 'https' : 'http';
1137        $host   = ! empty( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : null;
1138        $path   = ! empty( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1139
1140        // Support for local plugin development and testing using ngrok.
1141        if ( ! empty( $_SERVER['HTTP_X_ORIGINAL_HOST'] ) && str_contains( $_SERVER['HTTP_X_ORIGINAL_HOST'], 'ngrok.io' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
1142            $host = wp_unslash( $_SERVER['HTTP_X_ORIGINAL_HOST'] );
1143        }
1144        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1145
1146        if ( $host ) {
1147            $current_url = esc_url_raw( sprintf( '%s://%s%s', $scheme, $host, $path ) );
1148        }
1149    }
1150
1151    return $current_url;
1152}
1153
1154/**
1155 * Get the submit button text based on the subscription status.
1156 *
1157 * @param array $data Array containing block view data.
1158 *
1159 * @return string
1160 */
1161function get_submit_button_text( $data ) {
1162    if ( ! Jetpack_Memberships::is_current_user_subscribed() ) {
1163        return $data['submit_button_text'];
1164    }
1165    if ( ! Jetpack_Memberships::user_can_view_post() ) {
1166        return $data['submit_button_text_upgrade'];
1167    }
1168    return '✓ ' . $data['submit_button_text_subscribed'];
1169}
1170
1171/**
1172 * Returns true if there are no more tiers to upgrade to.
1173 *
1174 * @return boolean
1175 */
1176function is_top_subscription() {
1177    if ( ! Jetpack_Memberships::is_current_user_subscribed() ) {
1178        return false;
1179    }
1180    if ( ! Jetpack_Memberships::user_can_view_post() ) {
1181        return false;
1182    }
1183    return true;
1184}
1185
1186/**
1187 * Sanitize the submit button text.
1188 *
1189 * @param string $text String containing the submit button text.
1190 *
1191 * @return string
1192 */
1193function sanitize_submit_text( $text ) {
1194    return wp_kses(
1195        html_entity_decode( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ),
1196        Jetpack_Subscriptions_Widget::$allowed_html_tags_for_submit_button
1197    );
1198}
1199
1200/**
1201 * Returns paywall content blocks if user is not authenticated
1202 *
1203 * @return string
1204 */
1205function get_paywall_blocks() {
1206    $custom_paywall = apply_filters( 'jetpack_custom_paywall_blocks', false );
1207    if ( ! empty( $custom_paywall ) ) {
1208        return $custom_paywall;
1209    }
1210
1211    if ( ! Request::is_frontend() ) { // emails
1212        return get_paywall_simple();
1213    }
1214
1215    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1216    $is_paid_post       = is_paid_post();
1217    $is_paid_subscriber = Jetpack_Memberships::user_is_paid_subscriber();
1218
1219    $access_heading = $is_paid_subscriber
1220        ? esc_html__( 'Upgrade to continue reading', 'jetpack' )
1221        : esc_html__( 'Subscribe to continue reading', 'jetpack' );
1222
1223    $subscribe_text = $is_paid_post
1224        // translators: %s is the name of the site.
1225        ? (
1226            $is_paid_subscriber
1227                ? esc_html__( 'Upgrade to get access to the rest of this post and other exclusive content.', 'jetpack' )
1228                : esc_html__( 'Become a paid subscriber to get access to the rest of this post and other exclusive content.', 'jetpack' )
1229        )
1230        // translators: %s is the name of the site.
1231        : esc_html__( 'Subscribe to get access to the rest of this post and other subscriber-only content.', 'jetpack' );
1232
1233    $login_block = '';
1234
1235    if ( is_user_auth() ) {
1236        if ( ( new Host() )->is_wpcom_simple() ) {
1237            // We cannot use wpcom_logmein_redirect_url since it returns redirect URL when user is already logged in.
1238            $login_link           = add_query_arg(
1239                array(
1240                    'redirect_to' => rawurlencode( get_current_url() ),
1241                    'blog_id'     => get_current_blog_id(),
1242                ),
1243                'https://wordpress.com/log-in/link'
1244            );
1245            $switch_accounts_link = wp_logout_url( $login_link );
1246            $login_block          = '<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"14px"}}} -->
1247<p class="has-text-align-center" style="font-size:14px">
1248    <a href="' . $switch_accounts_link . '">' . __( 'Switch accounts', 'jetpack' ) . '</a>
1249</p>
1250<!-- /wp:paragraph -->';
1251        }
1252    } else {
1253        $access_question = $is_paid_post ? esc_html__( 'Already a paid subscriber?', 'jetpack' ) : esc_html__( 'Already a subscriber?', 'jetpack' );
1254        $login_block     = '<!-- wp:group {"style":{"typography":{"fontSize":"14px"}},"layout":{"type":"flex","justifyContent":"center"}} -->
1255<div class="wp-block-group" style="font-size:14px">
1256    <!-- wp:jetpack/subscriber-login {"logInLabel":"' . $access_question . '"} /-->
1257</div>
1258<!-- /wp:group -->';
1259    }
1260
1261    $lock_svg = plugins_url( 'images/lock-paywall.svg', JETPACK__PLUGIN_FILE );
1262
1263    return '
1264<!-- wp:group {"style":{"border":{"width":"1px","radius":"4px"},"spacing":{"padding":{"top":"32px","bottom":"32px","left":"32px","right":"32px"}}},"borderColor":"primary","className":"jetpack-subscribe-paywall","layout":{"type":"constrained","contentSize":"400px"}} -->
1265<div class="wp-block-group jetpack-subscribe-paywall has-border-color has-primary-border-color" style="border-width:1px;border-radius:4px;padding-top:32px;padding-right:32px;padding-bottom:32px;padding-left:32px">
1266<!-- wp:image {"align":"center","width":24,"height":24,"sizeSlug":"large","linkDestination":"none"} -->
1267<figure class="wp-block-image aligncenter size-large is-resized"><img src="' . $lock_svg . '" alt="" width="24" height="24"/></figure>
1268<!-- /wp:image -->
1269
1270<!-- wp:heading {"textAlign":"center","style":{"typography":{"fontStyle":"normal","fontWeight":"600","fontSize":"24px"},"layout":{"selfStretch":"fit"}}} -->
1271<h2 class="wp-block-heading has-text-align-center" style="font-size:24px;font-style:normal;font-weight:600">' . $access_heading . '</h2>
1272<!-- /wp:heading -->
1273
1274<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"14px"},"spacing":{"margin":{"top":"10px","bottom":"10px"}}}} -->
1275<p class="has-text-align-center" style="margin-top:10px;margin-bottom:10px;font-size:14px">' . $subscribe_text . '</p>
1276<!-- /wp:paragraph -->
1277
1278<!-- wp:jetpack/subscriptions {"borderRadius":50,"borderColor":"primary","className":"is-style-compact","isPaidSubscriber":' . ( $is_paid_subscriber ? 'true' : 'false' ) . '} /-->
1279' . $login_block . '
1280</div>
1281<!-- /wp:group -->
1282';
1283}
1284
1285/**
1286 * Returns true if user is auth for subscriptions check, otherwise returns false.
1287 *
1288 * @return bool
1289 */
1290function is_user_auth(): bool {
1291    if ( ( new Host() )->is_wpcom_simple() && is_user_logged_in() ) {
1292        return true;
1293    }
1294    if ( current_user_can( 'manage_options' ) ) {
1295        return true;
1296    }
1297
1298    if ( is_jetpack_token_subscription_service_loaded() ) {
1299        if ( Jetpack_Token_Subscription_Service::has_token_from_cookie() ) {
1300            return true;
1301        }
1302    }
1303    return false;
1304}
1305
1306/**
1307 * Returns `true` if the post is a paid post.
1308 */
1309function is_paid_post(): bool {
1310    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1311
1312    // Make sure Stripe is connected and the post is marked for paid subscribers.
1313    if ( Jetpack_Memberships::has_connected_account() && is_jetpack_token_subscription_service_loaded() ) {
1314        return Jetpack_Memberships::get_post_access_level() === Jetpack_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS;
1315    }
1316
1317    return false;
1318}
1319
1320/**
1321 * Returns true if the post is a subscribers post.
1322 */
1323function is_subscribers_post(): bool {
1324    require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php';
1325
1326    // Make sure Stripe is connected and the post is marked for paid subscribers.
1327    if ( Jetpack_Memberships::has_connected_account() && is_jetpack_token_subscription_service_loaded() ) {
1328        return Jetpack_Memberships::get_post_access_level() === Jetpack_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
1329    }
1330
1331    return false;
1332}
1333
1334/**
1335 * Returns paywall content blocks when email confirmation is pending
1336 *
1337 * @return string
1338 */
1339function get_paywall_blocks_subscribe_pending() {
1340    $subscribe_email = Jetpack_Memberships::get_current_user_email();
1341
1342    /** This filter is documented in \Automattic\Jetpack\Forms\ContactForm\Contact_Form */
1343    if ( is_wpcom() || false !== apply_filters( 'jetpack_auto_fill_logged_in_user', false ) ) {
1344        $current_user = wp_get_current_user();
1345        if ( ! empty( $current_user->user_email ) ) {
1346            $subscribe_email = $current_user->user_email;
1347        }
1348    }
1349
1350    $access_heading = esc_html__( 'Confirm your subscription to continue reading', 'jetpack' );
1351
1352    /* translators: %s: email address */
1353    $subscribe_text = sprintf( esc_html__( 'Head to your inbox and confirm your email address %s.', 'jetpack' ), $subscribe_email );
1354
1355    $lock_svg = plugins_url( 'images/lock-paywall.svg', JETPACK__PLUGIN_FILE );
1356
1357    return '
1358<!-- wp:group {"style":{"border":{"width":"1px","radius":"4px"},"spacing":{"padding":{"top":"32px","bottom":"32px","left":"32px","right":"32px"}}},"borderColor":"primary","className":"jetpack-subscribe-paywall","layout":{"type":"constrained","contentSize":"400px"}} -->
1359<div class="wp-block-group jetpack-subscribe-paywall has-border-color has-primary-border-color" style="border-width:1px;border-radius:4px;padding-top:32px;padding-right:32px;padding-bottom:32px;padding-left:32px">
1360<!-- wp:image {"align":"center","width":24,"height":24,"sizeSlug":"large","linkDestination":"none"} -->
1361<figure class="wp-block-image aligncenter size-large is-resized"><img src="' . $lock_svg . '" alt="" width="24" height="24"/></figure>
1362<!-- /wp:image -->
1363
1364<!-- wp:heading {"textAlign":"center","style":{"typography":{"fontStyle":"normal","fontWeight":"600","fontSize":"24px", "maxWidth":"initial"},"layout":{"selfStretch":"fit"}}} -->
1365<h2 class="wp-block-heading has-text-align-center" style="font-size:24px;font-style:normal;font-weight:600;max-width:initial">' . $access_heading . '</h2>
1366<!-- /wp:heading -->
1367
1368<!-- wp:paragraph {"align":"center","style":{"typography":{"fontSize":"14px"},"spacing":{"margin":{"top":"10px","bottom":"10px"}}}} -->
1369<p class="has-text-align-center" style="margin-top:10px;margin-bottom:10px;font-size:14px">' . $subscribe_text . '</p>
1370<!-- /wp:paragraph -->
1371</div>
1372<!-- /wp:group -->
1373';
1374}
1375
1376/**
1377 * Return content for non frontend views like Reader, emails.
1378 */
1379function get_paywall_simple(): string {
1380    $is_paid_post        = is_paid_post();
1381    $is_subscribers_post = is_subscribers_post();
1382    $is_subscriber       = is_jetpack_memberships_loaded() && Jetpack_Memberships::is_current_user_subscribed();
1383    $paywall_heading     = esc_html__( 'Subscribe to keep reading', 'jetpack' );
1384
1385    if ( $is_subscribers_post && ! $is_subscriber ) {
1386        $paywall_description = esc_html__( "It's a subscribers only post. Subscribe to get access to the rest of this post and other subscriber-only content.", 'jetpack' );
1387        $paywall_action_btn  = esc_html__( 'Subscribe', 'jetpack' );
1388    } elseif ( $is_paid_post && $is_subscriber ) {
1389        $paywall_description = esc_html__( "You're currently a free subscriber. Upgrade your subscription to get access to the rest of this post and other paid-subscriber only content.", 'jetpack' );
1390        $paywall_action_btn  = esc_html__( 'Upgrade subscription', 'jetpack' );
1391    } else {
1392        // - For paid post when the user is not a subscriber.
1393        // - Default for all other cases.
1394        $paywall_description = esc_html__( 'Become a paid subscriber to get access to the rest of this post and other exclusive content.', 'jetpack' );
1395        $paywall_action_btn  = esc_html__( 'Subscribe', 'jetpack' );
1396    }
1397
1398    return '
1399<!-- wp:columns -->
1400<div class="wp-block-columns jetpack-paywall-simple" style="display: inline-block; width: 90%">
1401    <!-- wp:column -->
1402    <div class="wp-block-column" style="background-color: #F6F7F7; padding: 32px; 24px;">
1403        <!-- wp:heading -->
1404        <h2 class="has-text-align-center" style="margin: 0 0 12px; font-weight: 600;">' . $paywall_heading . '</h2>
1405        <!-- /wp:heading -->
1406        <!-- wp:paragraph -->
1407        <p class="has-text-align-center"
1408           style="text-align: center;
1409                  color: #50575E;
1410                  font-weight: 400;
1411                  font-size: 16px;
1412                  font-family: \'SF Pro Text\', sans-serif;
1413                  line-height: 28.8px;">
1414        ' . $paywall_description . '
1415        </p>
1416        <!-- /wp:paragraph -->
1417        <!-- wp:buttons -->
1418        <div class="wp-block-buttons" style="text-align: center;">
1419            <!-- wp:button -->
1420            <div class="wp-block-button" style="display: inline-block; margin: 10px 0; border-style: none; padding: 0;">
1421                <a href="' . esc_url( get_post_permalink() ) . '" class="wp-block-button__link wp-element-button"
1422                   data-wpcom-track data-tracks-link-desc="paywall-email-click"
1423                   style="display: inline-block;
1424                          padding: 12px 15px;
1425                          background-color: #3858e9;
1426                          color: #FFFFFF;
1427                          text-decoration: none;
1428                          border-radius: 5px;
1429                          font-family: \'SF Pro Display\', sans-serif;
1430                          font-weight: 500;
1431                          font-size: 16px;
1432                          text-align: center;">' . $paywall_action_btn . '</a>
1433            </div>
1434            <!-- /wp:button -->
1435        </div>
1436        <!-- /wp:buttons -->
1437    </div>
1438    <!-- /wp:column -->
1439</div>
1440<!-- /wp:columns -->
1441';
1442}