Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.50% covered (danger)
18.50%
79 / 427
5.71% covered (danger)
5.71%
2 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Memberships
18.68% covered (danger)
18.68%
79 / 423
5.71% covered (danger)
5.71%
2 / 35
6617.88
0.00% covered (danger)
0.00%
0 / 1
 clear_post_access_level_cache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_instance
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 get_plan_property_mapping
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 register_init_hook
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 init_hook_action
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 subscriber_logout
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setup_cpts
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
2
 allow_rest_api_types
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 allow_sync_post_meta
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 return_meta
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 render_button_error
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 render_button_preview
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 should_render_button_preview
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 render_button
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
210
 get_subscription_url
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 deprecated_render_button_v1
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 get_blog_id
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 has_connected_account
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_post_access_level
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 get_post_tier
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 user_can_edit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 clear_cache
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 user_is_paid_subscriber
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 user_is_pending_subscriber
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 user_can_view_post
80.00% covered (warning)
80.00%
20 / 25
0.00% covered (danger)
0.00%
0 / 1
11.97
 is_enabled_jetpack_recurring_payments
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 should_enable_monetize_blocks_in_editor
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 has_configured_plans_jetpack_recurring_payments
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 get_all_plans
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
 get_all_newsletter_plan_ids
57.58% covered (warning)
57.58%
19 / 33
0.00% covered (danger)
0.00%
0 / 1
8.75
 register_gutenberg_block
63.64% covered (warning)
63.64%
14 / 22
0.00% covered (danger)
0.00%
0 / 1
3.43
 get_join_others_text
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_current_user_email
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 is_current_user_subscribed
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jetpack_Memberships: wrapper for memberships functions.
4 *
5 * @package    Jetpack
6 * @since      7.3.0
7 */
8
9use Automattic\Jetpack\Blocks;
10use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11use Automattic\Jetpack\Status;
12use Automattic\Jetpack\Status\Host;
13use Automattic\Jetpack\Status\Request;
14use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
15use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS;
16
17if ( ! defined( 'ABSPATH' ) ) {
18    exit( 0 );
19}
20
21require_once __DIR__ . '/../../extensions/blocks/subscriptions/constants.php';
22
23/**
24 * Class Jetpack_Memberships
25 * This class represents the Memberships functionality.
26 */
27class Jetpack_Memberships {
28    /**
29     * CSS class prefix to use in the styling.
30     *
31     * @var string
32     */
33    public static $css_classname_prefix = 'jetpack-memberships';
34    /**
35     * Our CPT type for the product (plan).
36     *
37     * @var string
38     */
39    public static $post_type_plan = 'jp_mem_plan';
40
41    /**
42     * Our CPT type for the product (plan).
43     *
44     * @var string
45     */
46    public static $post_type_coupon = 'memberships_coupon';
47
48    /**
49     * Our CPT type for the product (plan).
50     *
51     * @var string
52     */
53    public static $post_type_gift = 'memberships_gift';
54
55    /**
56     * Tier type for plans
57     *
58     * @var string
59     */
60    public static $type_tier = 'tier';
61
62    /**
63     * Option stores status for memberships (Stripe, etc.).
64     *
65     * @var string
66     */
67    public static $has_connected_account_option_name = 'jetpack-memberships-has-connected-account';
68
69    /**
70     * Post meta that will store the level of access for newsletters
71     *
72     * @var string
73     */
74    public static $post_access_level_meta_name = META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
75
76    /**
77     * Post meta that will store the tier ID of access for newsletters
78     *
79     * @var string
80     */
81    public static $post_access_tier_meta_name = META_NAME_FOR_POST_TIER_ID_SETTINGS;
82
83    /**
84     * Button block type to use.
85     *
86     * @var string
87     */
88    private static $button_block_name = 'recurring-payments';
89
90    /**
91     * These are defaults for wp_kses ran on the membership button.
92     *
93     * @var array
94     */
95    private static $tags_allowed_in_the_button = array( 'br' => array() );
96
97    /**
98     * The minimum required plan for this Gutenberg block.
99     *
100     * @var string Plan slug
101     */
102    private static $required_plan;
103
104    /**
105     * Track recurring payments block registration.
106     *
107     * @var boolean True if block registration has been executed.
108     */
109    private static $has_registered_block = false;
110
111    /**
112     * Classic singleton pattern
113     *
114     * @var Jetpack_Memberships
115     */
116    private static $instance;
117
118    /**
119     * Cached results of user_can_view_post() method.
120     *
121     * @var array
122     */
123    private static $user_can_view_post_cache = array();
124
125    /**
126     * Cached results of user_is_paid_subscriber() method.
127     *
128     * @var array
129     */
130    private static $user_is_paid_subscriber_cache = array();
131
132    /**
133     * Cached results of get_post_access_level method.
134     *
135     * @var array
136     */
137    private static $post_access_level_cache = array();
138
139    /**
140     * Clear cached results of get_post_access_level method.
141     */
142    public static function clear_post_access_level_cache() {
143        self::$post_access_level_cache = array();
144    }
145
146    /**
147     * Currencies we support and Stripe's minimum amount for a transaction in that currency.
148     *
149     * @link https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
150     *
151     * List has to be in with `SUPPORTED_CURRENCIES` in extensions/shared/currencies.js.
152     */
153    const SUPPORTED_CURRENCIES = array(
154        'USD' => 0.5,
155        'AUD' => 0.5,
156        'BRL' => 0.5,
157        'CAD' => 0.5,
158        'CHF' => 0.5,
159        'DKK' => 2.5,
160        'EUR' => 0.5,
161        'GBP' => 0.3,
162        'HKD' => 4.0,
163        'INR' => 0.5,
164        'JPY' => 50,
165        'MXN' => 10,
166        'NOK' => 3.0,
167        'NZD' => 0.5,
168        'PLN' => 2.0,
169        'SEK' => 3.0,
170        'SGD' => 0.5,
171        'CZK' => 15.0,
172        'HUF' => 175.0,
173        'TWD' => 10.0,
174        'IDR' => 0,
175        'ILS' => 0,
176        'PHP' => 0,
177        'RUB' => 0,
178        'TRY' => 0,
179    );
180
181    /**
182     * Jetpack_Memberships constructor.
183     */
184    private function __construct() {}
185
186    /**
187     * The actual constructor initializing the object.
188     *
189     * @return Jetpack_Memberships
190     */
191    public static function get_instance() {
192        if ( ! self::$instance ) {
193            self::$instance = new self();
194            self::$instance->register_init_hook();
195            // Yes, `pro-plan` with a dash, `jetpack_personal` with an underscore. Check the v1.5 endpoint to verify.
196            $wpcom_plan_slug     = defined( 'ENABLE_PRO_PLAN' ) ? 'pro-plan' : 'personal-bundle';
197            self::$required_plan = ( new Host() )->is_wpcom_simple() ? $wpcom_plan_slug : 'jetpack_personal';
198        }
199
200        return self::$instance;
201    }
202    /**
203     * Get the map that defines the shape of CPT post. keys are names of fields and
204     * 'meta' is the name of actual WP post meta field that corresponds.
205     *
206     * @return array
207     */
208    private static function get_plan_property_mapping() {
209        $meta_prefix = 'jetpack_memberships_';
210        $properties  = array(
211            'price'           => array(
212                'meta' => $meta_prefix . 'price',
213            ),
214            'currency'        => array(
215                'meta' => $meta_prefix . 'currency',
216            ),
217            'site_subscriber' => array(
218                'meta' => $meta_prefix . 'site_subscriber',
219            ),
220            'product_id'      => array(
221                'meta' => $meta_prefix . 'product_id',
222            ),
223            'tier'            => array(
224                'meta' => $meta_prefix . 'tier',
225            ),
226            'is_deleted'      => array(
227                'meta' => $meta_prefix . 'is_deleted',
228            ),
229            'is_sandboxed'    => array(
230                'meta' => $meta_prefix . 'is_sandboxed',
231            ),
232        );
233        return $properties;
234    }
235
236    /**
237     * Inits further hooks on init hook.
238     */
239    private function register_init_hook() {
240        add_action( 'init', array( $this, 'init_hook_action' ) );
241        add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_block' ) );
242        // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
243        add_action( 'switch_blog', array( $this, 'clear_post_access_level_cache' ) );
244    }
245
246    /**
247     * Actual hooks initializing on init.
248     */
249    public function init_hook_action() {
250        add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
251        add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
252        $this->setup_cpts();
253
254        if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
255            add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
256        }
257    }
258
259    /**
260     * Logs the subscriber out by clearing out the premium content cookie.
261     */
262    public function subscriber_logout() {
263        if ( ! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' ) ) {
264            return;
265        }
266
267        Abstract_Token_Subscription_Service::clear_token_cookie();
268    }
269
270    /**
271     * Sets up the custom post types for the module.
272     */
273    private function setup_cpts() {
274        /*
275         * PLAN data structure.
276         */
277        $capabilities = array(
278            'edit_post'          => 'edit_posts',
279            'read_post'          => 'read_private_posts',
280            'delete_post'        => 'delete_posts',
281            'edit_posts'         => 'edit_posts',
282            'edit_others_posts'  => 'edit_others_posts',
283            'publish_posts'      => 'publish_posts',
284            'read_private_posts' => 'read_private_posts',
285        );
286        $order_args   = array(
287            'label'               => esc_html__( 'Plan', 'jetpack' ),
288            'description'         => esc_html__( 'Recurring Payments plans', 'jetpack' ),
289            'supports'            => array( 'title', 'custom-fields', 'content' ),
290            'hierarchical'        => false,
291            'public'              => false,
292            'show_ui'             => false,
293            'show_in_menu'        => false,
294            'show_in_admin_bar'   => false,
295            'show_in_nav_menus'   => false,
296            'can_export'          => true,
297            'has_archive'         => false,
298            'exclude_from_search' => true,
299            'publicly_queryable'  => false,
300            'rewrite'             => false,
301            'capabilities'        => $capabilities,
302            'show_in_rest'        => false,
303        );
304        register_post_type( self::$post_type_plan, $order_args );
305        $coupon_args = array(
306            'label'               => esc_html__( 'Coupon', 'jetpack' ),
307            'description'         => esc_html__( 'Memberships coupons', 'jetpack' ),
308            'supports'            => array( 'title', 'custom-fields', 'content' ),
309            'hierarchical'        => false,
310            'public'              => false,
311            'show_ui'             => false,
312            'show_in_menu'        => false,
313            'show_in_admin_bar'   => false,
314            'show_in_nav_menus'   => false,
315            'can_export'          => true,
316            'has_archive'         => false,
317            'exclude_from_search' => true,
318            'publicly_queryable'  => false,
319            'rewrite'             => false,
320            'capabilities'        => $capabilities,
321            'show_in_rest'        => false,
322        );
323        register_post_type( self::$post_type_coupon, $coupon_args );
324        $gift_args = array(
325            'label'               => esc_html__( 'Gift', 'jetpack' ),
326            'description'         => esc_html__( 'Memberships gifts', 'jetpack' ),
327            'supports'            => array( 'title', 'custom-fields', 'content' ),
328            'hierarchical'        => false,
329            'public'              => false,
330            'show_ui'             => false,
331            'show_in_menu'        => false,
332            'show_in_admin_bar'   => false,
333            'show_in_nav_menus'   => false,
334            'can_export'          => true,
335            'has_archive'         => false,
336            'exclude_from_search' => true,
337            'publicly_queryable'  => false,
338            'rewrite'             => false,
339            'capabilities'        => $capabilities,
340            'show_in_rest'        => false,
341        );
342        register_post_type( self::$post_type_gift, $gift_args );
343    }
344
345    /**
346     * Allows custom post types to be used by REST API.
347     *
348     * @param array $post_types - other post types.
349     *
350     * @see hook 'rest_api_allowed_post_types'
351     * @return array
352     */
353    public function allow_rest_api_types( $post_types ) {
354        $post_types[] = self::$post_type_plan;
355        $post_types[] = self::$post_type_coupon;
356        $post_types[] = self::$post_type_gift;
357
358        return $post_types;
359    }
360
361    /**
362     * Allows custom meta fields to sync.
363     *
364     * @param array $post_meta - previously changet post meta.
365     *
366     * @return array
367     */
368    public function allow_sync_post_meta( $post_meta ) {
369        $meta_keys_plans = array_map(
370            array( $this, 'return_meta' ),
371            self::get_plan_property_mapping()
372        );
373
374        $meta_coupons_prefix = self::$post_type_coupon . '_';
375        $meta_keys_coupons   = array(
376            $meta_coupons_prefix . 'coupon_code',
377            $meta_coupons_prefix . 'can_be_combined',
378            $meta_coupons_prefix . 'first_time_purchase_only',
379            $meta_coupons_prefix . 'limit_per_user',
380            $meta_coupons_prefix . 'discount_type',
381            $meta_coupons_prefix . 'discount_value',
382            $meta_coupons_prefix . 'discount_percentage',
383            $meta_coupons_prefix . 'discount_currency',
384            $meta_coupons_prefix . 'start_date',
385            $meta_coupons_prefix . 'end_date',
386            $meta_coupons_prefix . 'plan_ids_allow_list',
387            $meta_coupons_prefix . 'duration',
388            $meta_coupons_prefix . 'email_allow_list',
389            $meta_coupons_prefix . 'is_deleted',
390            $meta_coupons_prefix . 'is_sandboxed',
391        );
392
393        $meta_gifts_prefix = self::$post_type_gift . '_';
394        $meta_keys_gifts   = array(
395            $meta_gifts_prefix . 'user_id',
396            $meta_gifts_prefix . 'plan_id',
397            $meta_gifts_prefix . 'is_deleted',
398        );
399        return array_merge(
400            $post_meta,
401            array_values( $meta_keys_plans ),
402            $meta_keys_coupons,
403            $meta_keys_gifts
404        );
405    }
406
407    /**
408     * This returns meta attribute of passet array.
409     * Used for array functions.
410     *
411     * @param array $map - stuff.
412     *
413     * @return mixed
414     */
415    public function return_meta( $map ) {
416        return $map['meta'];
417    }
418
419    /**
420     * Show an error to the user (or embed a clue in the HTML) when the button does not get rendered properly.
421     *
422     * @param WP_Error $error The error message with error code.
423     * @return string The error message rendered as HTML.
424     */
425    public function render_button_error( $error ) {
426        if ( static::user_can_edit() ) {
427            return '<div><strong>Jetpack Memberships Error: ' . $error->get_error_code() . '</strong><br />' . $error->get_error_message() . '</div>';
428        }
429        return '<div>Sorry! This product is not available for purchase at this time.</div><!-- Jetpack Memberships Error: ' . $error->get_error_code() . ' -->';
430    }
431
432    /**
433     * Renders a preview of the Recurring Payment button, which is not hooked
434     * up to the subscription url. Used to preview the block on the frontend
435     * for site editors when Stripe has not been connected.
436     *
437     * @param array  $attrs - attributes in the shortcode.
438     * @param string $content - Recurring Payment block content.
439     *
440     * @return string|void
441     */
442    public function render_button_preview( $attrs, $content = null ) {
443        if ( ! empty( $content ) ) {
444            $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
445            $content  = str_replace( 'recurring-payments-id', $block_id, $content );
446            $content  = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
447            return $content;
448        }
449        return $this->deprecated_render_button_v1( $attrs, null );
450    }
451
452    /**
453     * Determines whether the button preview should be rendered. Returns true
454     * if the user has editing permissions, the button is not configured correctly
455     * (because it requires a plan upgrade or Stripe connection), and the
456     * button is a child of a Premium Content block.
457     *
458     * @param WP_Block $block Recurring Payments block instance.
459     *
460     * @return boolean
461     */
462    public function should_render_button_preview( $block ) {
463        $user_can_edit              = static::user_can_edit();
464        $requires_stripe_connection = ! static::has_connected_account();
465
466        $jetpack_ready = ! self::is_enabled_jetpack_recurring_payments();
467
468        $is_premium_content_child = false;
469        if ( isset( $block ) && isset( $block->context['isPremiumContentChild'] ) ) {
470            $is_premium_content_child = (int) $block->context['isPremiumContentChild'];
471        }
472
473        return $is_premium_content_child &&
474            $user_can_edit &&
475            $requires_stripe_connection &&
476            $jetpack_ready;
477    }
478
479    /**
480     * Callback that parses the membership purchase shortcode.
481     *
482     * @param array    $attributes - attributes in the shortcode. `id` here is the CPT id of the plan.
483     * @param string   $content - Recurring Payment block content.
484     * @param WP_Block $block - Recurring Payment block instance.
485     *
486     * @return string|void - HTML for the button, void removes the button.
487     */
488    public function render_button( $attributes, $content = null, $block = null ) {
489        Jetpack_Gutenberg::load_assets_as_required( self::$button_block_name );
490
491        if ( $this->should_render_button_preview( $block ) ) {
492            return $this->render_button_preview( $attributes, $content );
493        }
494
495        if ( empty( $attributes['planId'] ) && empty( $attributes['planIds'] ) ) {
496            return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-npi', __( 'No plan was configured for this button.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that an existing payment plan is selected for this block.', 'jetpack' ) ) );
497        }
498
499        // This is string of '+` separated plan ids. Loop through them and
500        // filter out the ones that are not valid.
501        $plan_ids = array();
502        if ( ! empty( $attributes['planIds'] ) ) {
503            $plan_ids = $attributes['planIds'];
504        } elseif ( ! empty( $attributes['planId'] ) ) {
505            $plan_ids = explode( '+', $attributes['planId'] );
506        }
507        $valid_plans = array();
508        foreach ( $plan_ids as $plan_id ) {
509            if ( ! is_numeric( $plan_id ) ) {
510                continue;
511            }
512            $product = get_post( $plan_id );
513            if ( ! $product ) {
514                return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-npf', __( 'Could not find a plan for this button.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that the selected payment plan still exists and is available for purchase.', 'jetpack' ) ) );
515            }
516            if ( is_wp_error( $product ) ) {
517                '@phan-var WP_Error $product'; // `get_post` isn't supposed to return a WP_Error, so Phan is confused here. See also https://github.com/phan/phan/issues/3127
518                return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-npf-we', __( 'Encountered an error when getting the plan associated with this button:', 'jetpack' ) . ' ' . $product->get_error_message() . '. ' . __( ' Edit this post and confirm that the selected payment plan still exists and is available for purchase.', 'jetpack' ) ) );
519            }
520            if ( $product->post_type !== self::$post_type_plan ) {
521                return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-pnplan', __( 'The payment plan selected is not actually a payment plan.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that the selected payment plan still exists and is available for purchase.', 'jetpack' ) ) );
522            }
523            if ( 'publish' !== $product->post_status ) {
524                return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-psnpub', __( 'The selected payment plan is not active.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that the selected payment plan still exists and is available for purchase.', 'jetpack' ) ) );
525            }
526            $valid_plans[] = $plan_id;
527        }
528
529        // If none are valid, return.
530        // (Returning like this makes the button disappear.)
531        if ( empty( $valid_plans ) ) {
532            return;
533        }
534        $plan_id = implode( '+', $valid_plans );
535
536        if ( ! empty( $content ) ) {
537            $block_id      = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
538            $content       = str_replace( 'recurring-payments-id', $block_id, $content );
539            $content       = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
540            $subscribe_url = $this->get_subscription_url( $plan_id );
541
542            $content = preg_replace( '/(href=".*")/U', 'href="' . $subscribe_url . '"', $content );
543            $content = wp_kses_post( $content );
544
545            return $content;
546        }
547
548        return $this->deprecated_render_button_v1( $attributes, $plan_id );
549    }
550
551    /**
552     * Builds subscription URL for this membership using the current blog and
553     * supplied plan IDs.
554     *
555     * @param integer $plan_id - Unique ID for the plan being subscribed to.
556     * @return string
557     */
558    public function get_subscription_url( $plan_id ) {
559        global $wp;
560
561        return add_query_arg(
562            array(
563                'blog'     => esc_attr( self::get_blog_id() ),
564                'plan'     => esc_attr( $plan_id ),
565                'lang'     => esc_attr( get_locale() ),
566                'pid'      => esc_attr( get_the_ID() ), // Needed for analytics purposes.
567                'redirect' => esc_attr( rawurlencode( home_url( $wp->request ) ) ), // Needed for redirect back in case of redirect-based flow.
568            ),
569            'https://subscribe.wordpress.com/memberships/'
570        );
571    }
572
573    /**
574     * Renders a deprecated legacy version of the button HTML.
575     *
576     * @param array   $attrs - Array containing the Recurring Payment block attributes.
577     * @param integer $plan_id - Unique plan ID the membership is for.
578     *
579     * @return string
580     */
581    public function deprecated_render_button_v1( $attrs, $plan_id ) {
582        $button_label = isset( $attrs['submitButtonText'] )
583            ? $attrs['submitButtonText']
584            : __( 'Your contribution', 'jetpack' );
585
586        $button_styles = array();
587        if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
588            array_push(
589                $button_styles,
590                sprintf(
591                    'background-color: %s',
592                    sanitize_hex_color( $attrs['customBackgroundButtonColor'] )
593                )
594            );
595        }
596        if ( ! empty( $attrs['customTextButtonColor'] ) ) {
597            array_push(
598                $button_styles,
599                sprintf(
600                    'color: %s',
601                    sanitize_hex_color( $attrs['customTextButtonColor'] )
602                )
603            );
604        }
605        $button_styles = implode( ';', $button_styles );
606
607        return sprintf(
608            '<div class="%1$s"><a role="button" href="%2$s" class="%3$s" style="%4$s">%5$s</a></div>',
609            esc_attr(
610                Blocks::classes(
611                    self::$button_block_name,
612                    $attrs,
613                    array( 'wp-block-button' )
614                )
615            ),
616            esc_url( $this->get_subscription_url( $plan_id ) ),
617            isset( $attrs['submitButtonClasses'] ) ? esc_attr( $attrs['submitButtonClasses'] ) : 'wp-block-button__link',
618            esc_attr( $button_styles ),
619            wp_kses( $button_label, self::$tags_allowed_in_the_button )
620        );
621    }
622
623    /**
624     * Get current blog id.
625     *
626     * @return int
627     */
628    public static function get_blog_id() {
629        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
630            return get_current_blog_id();
631        }
632
633        return Jetpack_Options::get_option( 'id' );
634    }
635
636    /**
637     * Get the id of the connected payment acount (Stripe etc).
638     *
639     * @return bool
640     */
641    public static function has_connected_account() {
642
643        // This is the primary solution.
644        $has_option = get_option( self::$has_connected_account_option_name, false ) ? true : false;
645        if ( $has_option ) {
646            return true;
647        }
648
649        return false;
650    }
651
652    /**
653     * Get the post access level
654     *
655     * If no ID is provided, the method tries to get it from the global post object.
656     *
657     * @param int|null $post_id The ID of the post. Default is null.
658     *
659     * @return string the actual post access level (see projects/plugins/jetpack/extensions/blocks/subscriptions/constants.js for the values).
660     */
661    public static function get_post_access_level( $post_id = null ) {
662        if ( ! $post_id ) {
663            $post_id = get_the_ID();
664        }
665        if ( ! $post_id ) {
666            return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
667        }
668
669        $blog_id   = get_current_blog_id();
670        $cache_key = $blog_id . '_' . $post_id;
671
672        if ( isset( self::$post_access_level_cache[ $cache_key ] ) ) {
673            return self::$post_access_level_cache[ $cache_key ];
674        }
675
676        $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
677        if ( empty( $post_access_level ) ) {
678            $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
679        }
680
681        self::$post_access_level_cache[ $cache_key ] = $post_access_level;
682
683        return $post_access_level;
684    }
685
686    /**
687     * Get the post tier plan
688     *
689     * If no ID is provided, the method tries to get it from the global post object.
690     *
691     * @param int|null $post_id The ID of the post. Default is null.
692     *
693     * @return WP_Post|null the actual post tier.
694     */
695    public static function get_post_tier( $post_id = null ) {
696        if ( ! $post_id ) {
697            $post_id = get_the_ID();
698        }
699
700        if ( ! $post_id ) {
701            return null;
702        }
703
704        $post_tier_id = get_post_meta( $post_id, self::$post_access_tier_meta_name, true );
705        if ( empty( $post_tier_id ) ) {
706            return null;
707        }
708
709        return get_post( $post_tier_id );
710    }
711
712    /**
713     * Determines whether the current user can edit.
714     *
715     * @return bool Whether the user can edit.
716     */
717    public static function user_can_edit() {
718        $user = wp_get_current_user();
719        // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
720        return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
721    }
722
723    /**
724     * Clears the static cache for all users or for a given user.
725     *
726     * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
727     * @return void
728     */
729    public static function clear_cache( ?int $user_id = null ) {
730        if ( empty( $user_id ) ) {
731            self::$user_is_paid_subscriber_cache = array();
732            self::$user_can_view_post_cache      = array();
733            return;
734        }
735        unset( self::$user_is_paid_subscriber_cache[ $user_id ] );
736        unset( self::$user_can_view_post_cache[ $user_id ] );
737    }
738
739    /**
740     * Determines whether the current user is a paid subscriber and caches the result.
741     *
742     * @param array    $valid_plan_ids An array of valid plan ids that the user could be subscribed to which would make the user able to view this content. Defaults to an empty array which will be filled with all newsletter plan IDs.
743     * @param int|null $user_id An optional user_id that can be used to determine service availability (defaults to checking if user is logged in if omitted).
744     * @return bool Whether the post can be viewed
745     */
746    public static function user_is_paid_subscriber( $valid_plan_ids = array(), $user_id = null ) {
747        if ( empty( $user_id ) ) {
748            $user_id = get_current_user_id();
749            if ( empty( $user_id ) ) {
750                return false;
751            }
752        }
753        // sort and stringify sorted valid plan ids to use as a cache key
754        sort( $valid_plan_ids );
755        $cache_key = $user_id . '_' . implode( ',', $valid_plan_ids );
756        if ( ! isset( self::$user_is_paid_subscriber_cache[ $cache_key ] ) ) {
757            require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
758            if ( empty( $valid_plan_ids ) ) {
759                $valid_plan_ids = self::get_all_newsletter_plan_ids();
760            }
761            $paywall            = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service( $user_id );
762            $is_paid_subscriber = $paywall->visitor_can_view_content( $valid_plan_ids, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS );
763            self::$user_is_paid_subscriber_cache[ $cache_key ] = $is_paid_subscriber;
764        }
765        return self::$user_is_paid_subscriber_cache[ $cache_key ];
766    }
767
768    /**
769     * Determines whether the current user has a pending subscription.
770     *
771     * @return bool Whether the user has a pending subscription
772     */
773    public static function user_is_pending_subscriber() {
774        require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
775        $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
776        return $subscription_service->is_current_user_pending_subscriber();
777    }
778
779    /**
780     * Determines whether the current user can view the post based on the newsletter access level
781     * and caches the result.
782     *
783     * @param int|null $post_id Explicit post id to check against.
784     *
785     * @return bool Whether the post can be viewed
786     */
787    public static function user_can_view_post( $post_id = null ) {
788        $user_id = get_current_user_id();
789        if ( null === $post_id ) {
790            $post_id = get_the_ID();
791        }
792
793        if ( false === $post_id ) {
794            $post_id = 0;
795        }
796
797        $cache_key = sprintf( '%d_%d', $user_id, $post_id );
798        if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
799            return self::$user_can_view_post_cache[ $cache_key ];
800        }
801
802        $post_access_level = self::get_post_access_level( $post_id );
803        if ( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY === $post_access_level ) {
804            self::$user_can_view_post_cache[ $cache_key ] = true;
805            return true;
806        }
807
808        // we are sending the post to subscribers so the user is a subscriber
809        if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS && Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
810            self::$user_can_view_post_cache[ $cache_key ] = true;
811            return true;
812        }
813
814        require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
815        $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
816
817        $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
818
819        if ( 0 === count( $all_newsletters_plan_ids ) &&
820            Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
821            Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
822        ) {
823            // The post is paywalled but there is no newsletter plans on the site.
824            // We downgrade the post level to subscribers-only
825            $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
826        }
827
828        $can_view_post = $paywall->visitor_can_view_content( $all_newsletters_plan_ids, $post_access_level );
829
830        self::$user_can_view_post_cache[ $cache_key ] = $can_view_post;
831        return $can_view_post;
832    }
833
834    /**
835     * Whether Recurring Payments are enabled. True if the block
836     * is supported by the site's plan, or if it is a Jetpack site
837     * and the feature to enable upgrade nudges is active.
838     *
839     * @return bool
840     */
841    public static function is_enabled_jetpack_recurring_payments() {
842        $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
843        return $api_available;
844    }
845
846    /**
847     * Whether to enable the blocks in the editor.
848     * All Monetize blocks (except Simple Payments) need a user with at least `edit_posts` capability
849     *
850     * @return bool
851     */
852    public static function should_enable_monetize_blocks_in_editor() {
853        if ( ! is_admin() ) {
854            // We enable the block for the front-end in all cases
855            return true;
856
857        }
858
859        $is_offline_mode                  = ( new Status() )->is_offline_mode();
860        $enable_monetize_blocks_in_editor = ( new Host() )->is_wpcom_simple() || ( ! $is_offline_mode );
861        return $enable_monetize_blocks_in_editor;
862    }
863
864    /**
865     * Whether site has any paid plan.
866     *
867     * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
868     *
869     * @return bool
870     */
871    public static function has_configured_plans_jetpack_recurring_payments( $type = '' ) {
872        if ( ! self::is_enabled_jetpack_recurring_payments() ) {
873            return false;
874        }
875        $query = array(
876            'post_type'      => self::$post_type_plan,
877            'posts_per_page' => 1,
878        );
879
880        // We want to see if user has any plan marked as a newsletter set up.
881        if ( 'newsletter' === $type ) {
882            $query['meta_key']   = 'jetpack_memberships_site_subscriber';
883            $query['meta_value'] = true;
884        }
885
886        $plans = get_posts( $query );
887        return ( is_countable( $plans ) && count( $plans ) > 0 );
888    }
889
890    /**
891     * Return the list of plan posts
892     *
893     * @return WP_Post[]|WP_Error
894     */
895    public static function get_all_plans() {
896        if ( ! self::is_enabled_jetpack_recurring_payments() ) {
897            return array();
898        }
899
900        // We can retrieve the data directly except on a Jetpack/Atomic cached site or
901        $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
902        if ( ! $is_cached_site ) {
903            return get_posts(
904                array(
905                    'posts_per_page' => -1,
906                    'post_type'      => self::$post_type_plan,
907                )
908            );
909        } else {
910            // On cached site on WPCOM
911            require_lib( 'memberships' );
912            return Memberships_Product::get_plans_posts_list( get_current_blog_id() );
913        }
914    }
915
916    /**
917     * Return all membership plans ids (deleted or not)
918     * This function is used both on WPCOM or on Jetpack self-hosted.
919     * Depending on the environment we need to mitigate where the data is retrieved from.
920     *
921     * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
922     *
923     * @return array
924     */
925    public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
926
927        if ( ! self::is_enabled_jetpack_recurring_payments() ) {
928            return array();
929        }
930
931        // We can retrieve the data directly except on a Jetpack/Atomic cached site or
932        $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
933        if ( ! $is_cached_site ) {
934            $meta_query = array(
935                array(
936                    'key'   => 'jetpack_memberships_type',
937                    'value' => self::$type_tier,
938                ),
939            );
940
941            if ( $allow_deleted === false ) {
942                $meta_query[] = array(
943                    'key'     => 'jetpack_memberships_is_deleted',
944                    'compare' => 'NOT EXISTS',
945                );
946            }
947
948            return get_posts(
949                array(
950                    'posts_per_page' => -1,
951                    'fields'         => 'ids',
952                    'post_type'      => self::$post_type_plan,
953                    'meta_query'     => $meta_query,
954                )
955            );
956
957        } else {
958            // On cached site on WPCOM
959            require_lib( 'memberships' );
960            $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
961
962            if ( is_wp_error( $list ) ) {
963                return array();
964            }
965
966            return array_map(
967                function ( $product ) {
968                    return $product['id'];
969                }, // Returning only post ids
970                $list
971            );
972        }
973    }
974
975    /**
976     * Register the Recurring Payments Gutenberg block
977     */
978    public function register_gutenberg_block() {
979        // This gate was introduced to prevent duplicate registration. A race condition exists where
980        // the registration that happens via extensions/blocks/recurring-payments/recurring-payments.php
981        // was adding the registration action after the action had been run in some contexts.
982        if ( self::$has_registered_block ) {
983            return;
984        }
985
986        if ( self::is_enabled_jetpack_recurring_payments() ) {
987            Blocks::jetpack_register_block(
988                'jetpack/recurring-payments',
989                array(
990                    'render_callback'  => array( $this, 'render_button' ),
991                    'uses_context'     => array( 'isPremiumContentChild' ),
992                    'provides_context' => array(
993                        'jetpack/parentBlockWidth' => 'width',
994                    ),
995                )
996            );
997        } else {
998            Jetpack_Gutenberg::set_extension_unavailable(
999                'recurring-payments',
1000                'missing_plan',
1001                array(
1002                    'required_feature' => 'memberships',
1003                    'required_plan'    => self::$required_plan,
1004                )
1005            );
1006        }
1007
1008        self::$has_registered_block = true;
1009    }
1010
1011    /**
1012     * Transforms a number into it's short human-readable version.
1013     *
1014     * @param int $subscribers_total The extrapolated excerpt string.
1015     *
1016     * @return string Human-readable version of the number. ie. 1.9 M.
1017     */
1018    public static function get_join_others_text( $subscribers_total ) {
1019        if ( $subscribers_total >= 1000000 ) {
1020            /* translators: %s: number of folks following the blog, millions(M) with one decimal. i.e. 1.1 */
1021            return sprintf( __( 'Join %sM other subscribers', 'jetpack' ), floatval( number_format_i18n( $subscribers_total / 1000000, 1 ) ) );
1022        }
1023        if ( $subscribers_total >= 10000 ) {
1024            /* translators: %s: number of folks following the blog, thousands(K) with one decimal. i.e. 1.1 */
1025            return sprintf( __( 'Join %sK other subscribers', 'jetpack' ), floatval( number_format_i18n( $subscribers_total / 1000, 1 ) ) );
1026        }
1027
1028        /* translators: %s: number of folks following the blog */
1029        return sprintf( _n( 'Join %s other subscriber', 'Join %s other subscribers', $subscribers_total, 'jetpack' ), number_format_i18n( $subscribers_total ) );
1030    }
1031
1032    /**
1033     * Returns the email of the current user.
1034     *
1035     * @return string
1036     */
1037    public static function get_current_user_email() {
1038        require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
1039        $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
1040        return $subscription_service->get_subscriber_email();
1041    }
1042
1043    /**
1044     * Returns if the current user is subscribed or not.
1045     *
1046     * @return boolean
1047     */
1048    public static function is_current_user_subscribed() {
1049        require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
1050        $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
1051        return $subscription_service->is_current_user_subscribed();
1052    }
1053}
1054Jetpack_Memberships::get_instance();