PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 15.2.1
Jetpack – WP Security, Backup, Speed, & Growth v15.2.1
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / modules / memberships / class-jetpack-memberships.php
class-jetpack-memberships.php
1,055 lines 33.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Jetpack_Memberships: wrapper for memberships functions.
4 *
5 * @package Jetpack
6 * @since 7.3.0
7 */
8
9 use Automattic\Jetpack\Blocks;
10 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11 use Automattic\Jetpack\Status;
12 use Automattic\Jetpack\Status\Host;
13 use Automattic\Jetpack\Status\Request;
14 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
15 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit( 0 );
19 }
20
21 require_once __DIR__ . '/../../extensions/blocks/subscriptions/constants.php';
22
23 /**
24 * Class Jetpack_Memberships
25 * This class represents the Memberships functionality.
26 */
27 class 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 }
1054 Jetpack_Memberships::get_instance();
1055