PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 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 All 506 releases
← All changes | modules/memberships/class-jetpack-memberships.php +279 -33 13.3.3 → 16.3-a.5 View file →
@@ -7,12 +7,18 @@
7 7 */
8 8
9 9 use Automattic\Jetpack\Blocks;
10 10 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11 +use Automattic\Jetpack\Status;
11 12 use Automattic\Jetpack\Status\Host;
13 +use Automattic\Jetpack\Status\Request;
12 14 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
13 15 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS;
14 16
17 +if ( ! defined( 'ABSPATH' ) ) {
18 + exit( 0 );
19 +}
20 +
15 21 require_once __DIR__ . '/../../extensions/blocks/subscriptions/constants.php';
16 22
17 23 /**
18 24 * Class Jetpack_Memberships
@@ -32,8 +38,15 @@
32 38 */
33 39 public static $post_type_plan = 'jp_mem_plan';
34 40
35 41 /**
42 + * Our CPT type for the product (plan).
43 + *
44 + * @var string
45 + */
46 + public static $post_type_coupon = 'memberships_coupon';
47 +
48 + /**
36 49 * Tier type for plans
37 50 *
38 51 * @var string
39 52 */
@@ -74,8 +87,33 @@
74 87 */
75 88 private static $tags_allowed_in_the_button = array( 'br' => array() );
76 89
77 90 /**
91 + * Allowed HTML tags for a rendered tier description. Mirrors the wp.com
92 + * subscribe modal's allowlist so the rendered markdown stays consistent
93 + * across surfaces.
94 + *
95 + * @var array
96 + */
97 + const TIER_DESCRIPTION_ALLOWED_HTML = array(
98 + 'p' => array(),
99 + 'br' => array(),
100 + 'ul' => array(),
101 + 'ol' => array(),
102 + 'li' => array(),
103 + 'strong' => array(),
104 + 'em' => array(),
105 + 'del' => array(),
106 + 'code' => array(),
107 + 'blockquote' => array(),
108 + 'a' => array(
109 + 'href' => true,
110 + 'rel' => true,
111 + 'target' => true,
112 + ),
113 + );
114 +
115 + /**
78 116 * The minimum required plan for this Gutenberg block.
79 117 *
80 118 * @var string Plan slug
81 119 */
@@ -109,8 +147,22 @@
109 147 */
110 148 private static $user_is_paid_subscriber_cache = array();
111 149
112 150 /**
151 + * Cached results of get_post_access_level method.
152 + *
153 + * @var array
154 + */
155 + private static $post_access_level_cache = array();
156 +
157 + /**
158 + * Clear cached results of get_post_access_level method.
159 + */
160 + public static function clear_post_access_level_cache() {
161 + self::$post_access_level_cache = array();
162 + }
163 +
164 + /**
113 165 * Currencies we support and Stripe's minimum amount for a transaction in that currency.
114 166 *
115 167 * @link https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
116 168 *
@@ -133,8 +185,17 @@
133 185 'NZD' => 0.5,
134 186 'PLN' => 2.0,
135 187 'SEK' => 3.0,
136 188 'SGD' => 0.5,
189 + 'CZK' => 15.0,
190 + 'HUF' => 175.0,
191 + 'TWD' => 10.0,
192 + 'IDR' => 0,
193 + 'ILS' => 0,
194 + 'PHP' => 0,
195 + 'RUB' => 0,
196 + 'TRY' => 0,
197 + 'MYR' => 2.00,
137 198 );
138 199
139 200 /**
140 201 * Jetpack_Memberships constructor.
@@ -151,9 +212,9 @@
151 212 self::$instance = new self();
152 213 self::$instance->register_init_hook();
153 214 // Yes, `pro-plan` with a dash, `jetpack_personal` with an underscore. Check the v1.5 endpoint to verify.
154 215 $wpcom_plan_slug = defined( 'ENABLE_PRO_PLAN' ) ? 'pro-plan' : 'personal-bundle';
155 - self::$required_plan = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? $wpcom_plan_slug : 'jetpack_personal';
216 + self::$required_plan = ( new Host() )->is_wpcom_simple() ? $wpcom_plan_slug : 'jetpack_personal';
156 217 }
157 218
158 219 return self::$instance;
159 220 }
@@ -183,8 +244,11 @@
183 244 ),
184 245 'is_deleted' => array(
185 246 'meta' => $meta_prefix . 'is_deleted',
186 247 ),
248 + 'is_sandboxed' => array(
249 + 'meta' => $meta_prefix . 'is_sandboxed',
250 + ),
187 251 );
188 252 return $properties;
189 253 }
190 254
@@ -193,8 +257,10 @@
193 257 */
194 258 private function register_init_hook() {
195 259 add_action( 'init', array( $this, 'init_hook_action' ) );
196 260 add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_block' ) );
261 + // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
262 + add_action( 'switch_blog', array( $this, 'clear_post_access_level_cache' ) );
197 263 }
198 264
199 265 /**
200 266 * Actual hooks initializing on init.
@@ -203,9 +269,9 @@
203 269 add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
204 270 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
205 271 $this->setup_cpts();
206 272
207 - if ( Jetpack::is_module_active( 'subscriptions' ) && jetpack_is_frontend() ) {
273 + if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
208 274 add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
209 275 }
210 276 }
211 277
@@ -212,8 +278,12 @@
212 278 /**
213 279 * Logs the subscriber out by clearing out the premium content cookie.
214 280 */
215 281 public function subscriber_logout() {
282 + if ( ! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' ) ) {
283 + return;
284 + }
285 +
216 286 Abstract_Token_Subscription_Service::clear_token_cookie();
217 287 }
218 288
219 289 /**
@@ -250,8 +320,27 @@
250 320 'capabilities' => $capabilities,
251 321 'show_in_rest' => false,
252 322 );
253 323 register_post_type( self::$post_type_plan, $order_args );
324 + $coupon_args = array(
325 + 'label' => esc_html__( 'Coupon', 'jetpack' ),
326 + 'description' => esc_html__( 'Memberships coupons', '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_coupon, $coupon_args );
254 343 }
255 344
256 345 /**
257 346 * Allows custom post types to be used by REST API.
@@ -262,8 +351,9 @@
262 351 * @return array
263 352 */
264 353 public function allow_rest_api_types( $post_types ) {
265 354 $post_types[] = self::$post_type_plan;
355 + $post_types[] = self::$post_type_coupon;
266 356
267 357 return $post_types;
268 358 }
269 359
@@ -274,13 +364,37 @@
274 364 *
275 365 * @return array
276 366 */
277 367 public function allow_sync_post_meta( $post_meta ) {
278 - $meta_keys = array_map(
368 + $meta_keys_plans = array_map(
279 369 array( $this, 'return_meta' ),
280 370 self::get_plan_property_mapping()
281 371 );
282 - return array_merge( $post_meta, array_values( $meta_keys ) );
372 +
373 + $meta_coupons_prefix = self::$post_type_coupon . '_';
374 + $meta_keys_coupons = array(
375 + $meta_coupons_prefix . 'coupon_code',
376 + $meta_coupons_prefix . 'can_be_combined',
377 + $meta_coupons_prefix . 'first_time_purchase_only',
378 + $meta_coupons_prefix . 'limit_per_user',
379 + $meta_coupons_prefix . 'discount_type',
380 + $meta_coupons_prefix . 'discount_value',
381 + $meta_coupons_prefix . 'discount_percentage',
382 + $meta_coupons_prefix . 'discount_currency',
383 + $meta_coupons_prefix . 'start_date',
384 + $meta_coupons_prefix . 'end_date',
385 + $meta_coupons_prefix . 'plan_ids_allow_list',
386 + $meta_coupons_prefix . 'duration',
387 + $meta_coupons_prefix . 'email_allow_list',
388 + $meta_coupons_prefix . 'is_deleted',
389 + $meta_coupons_prefix . 'is_sandboxed',
390 + );
391 +
392 + return array_merge(
393 + $post_meta,
394 + array_values( $meta_keys_plans ),
395 + $meta_keys_coupons
396 + );
283 397 }
284 398
285 399 /**
286 400 * This returns meta attribute of passet array.
@@ -391,8 +505,9 @@
391 505 if ( ! $product ) {
392 506 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' ) ) );
393 507 }
394 508 if ( is_wp_error( $product ) ) {
509 + '@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
395 510 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' ) ) );
396 511 }
397 512 if ( $product->post_type !== self::$post_type_plan ) {
398 513 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' ) ) );
@@ -414,9 +529,13 @@
414 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
415 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
416 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
417 532 $subscribe_url = $this->get_subscription_url( $plan_id );
418 - return preg_replace( '/(href=".*")/U', 'href="' . $subscribe_url . '"', $content );
533 +
534 + $content = preg_replace( '/(href=".*")/U', 'href="' . $subscribe_url . '"', $content );
535 + $content = wp_kses_post( $content );
536 +
537 + return $content;
419 538 }
420 539
421 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
422 541 }
@@ -421,8 +540,45 @@
421 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
422 541 }
423 542
424 543 /**
544 + * Render email callback.
545 + *
546 + * @param string $block_content The block content.
547 + * @param array $parsed_block The parsed block data.
548 + * @param object $rendering_context The email rendering context.
549 + *
550 + * @return string
551 + */
552 + public function render_button_email( $block_content, array $parsed_block, $rendering_context ) {
553 + // Check for the required renderers.
554 + if ( ! function_exists( '\Automattic\Jetpack\Extensions\Button\render_email' ) || ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Button' ) ) {
555 + return '';
556 + }
557 +
558 + // Get the first inner block, which should be the button block.
559 + $button_block = $parsed_block['innerBlocks'][0] ?? array();
560 +
561 + // We should only accept button blocks.
562 + if ( empty( $button_block['blockName'] ) || 'jetpack/button' !== $button_block['blockName'] ) {
563 + return '';
564 + }
565 +
566 + // We need attributes.
567 + if ( ! isset( $button_block['attrs'] ) || ! is_array( $button_block['attrs'] ) ) {
568 + return '';
569 + }
570 +
571 + // If the button block is missing text or url, return empty string.
572 + if ( empty( $button_block['attrs']['text'] ) || empty( $button_block['attrs']['url'] ) ) {
573 + return '';
574 + }
575 +
576 + // Reuse the button block's email rendering method.
577 + return \Automattic\Jetpack\Extensions\Button\render_email( $block_content, $button_block, $rendering_context );
578 + }
579 +
580 + /**
425 581 * Builds subscription URL for this membership using the current blog and
426 582 * supplied plan IDs.
427 583 *
428 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -451,11 +607,9 @@
451 607 *
452 608 * @return string
453 609 */
454 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
455 - $button_label = isset( $attrs['submitButtonText'] )
456 - ? $attrs['submitButtonText']
457 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
458 612
459 613 $button_styles = array();
460 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
461 615 array_push(
@@ -538,12 +692,28 @@
538 692 if ( ! $post_id ) {
539 693 return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
540 694 }
541 695
696 + $blog_id = get_current_blog_id();
697 + $cache_key = $blog_id . '_' . $post_id;
698 +
699 + if ( isset( self::$post_access_level_cache[ $cache_key ] ) ) {
700 + return self::$post_access_level_cache[ $cache_key ];
701 + }
702 +
542 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
543 - if ( empty( $post_access_level ) ) {
704 + // Defaults to "everybody" when unset, and also when the stored value is not a
705 + // string. Corrupt rows (e.g. a serialized array like a:1:{i:0;s:0:"";}) can be
706 + // persisted by non-REST write paths, and an array flows unchanged into the
707 + // strict string-typed `earn_user_has_access` callback on WPCOM, fataling the
708 + // render. Coercing here keeps this canonical accessor's documented string
709 + // contract regardless of how the meta was written.
710 + if ( empty( $post_access_level ) || ! is_string( $post_access_level ) ) {
544 711 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
545 712 }
713 +
714 + self::$post_access_level_cache[ $cache_key ] = $post_access_level;
715 +
546 716 return $post_access_level;
547 717 }
548 718
549 719 /**
@@ -578,9 +748,8 @@
578 748 * @return bool Whether the user can edit.
579 749 */
580 750 public static function user_can_edit() {
581 751 $user = wp_get_current_user();
582 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
583 752 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
584 753 }
585 754
586 755 /**
@@ -588,9 +757,9 @@
588 757 *
589 758 * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
590 759 * @return void
591 760 */
592 - public static function clear_cache( int $user_id = null ) {
761 + public static function clear_cache( ?int $user_id = null ) {
593 762 if ( empty( $user_id ) ) {
594 763 self::$user_is_paid_subscriber_cache = array();
595 764 self::$user_can_view_post_cache = array();
596 765 return;
@@ -608,10 +777,16 @@
608 777 */
609 778 public static function user_is_paid_subscriber( $valid_plan_ids = array(), $user_id = null ) {
610 779 if ( empty( $user_id ) ) {
611 780 $user_id = get_current_user_id();
781 + if ( empty( $user_id ) ) {
782 + return false;
783 + }
612 784 }
613 - if ( ! isset( self::$user_is_paid_subscriber_cache[ $user_id ] ) ) {
785 + // sort and stringify sorted valid plan ids to use as a cache key
786 + sort( $valid_plan_ids );
787 + $cache_key = $user_id . '_' . implode( ',', $valid_plan_ids );
788 + if ( ! isset( self::$user_is_paid_subscriber_cache[ $cache_key ] ) ) {
614 789 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
615 790 if ( empty( $valid_plan_ids ) ) {
616 791 $valid_plan_ids = self::get_all_newsletter_plan_ids();
617 792 }
@@ -616,11 +791,11 @@
616 791 $valid_plan_ids = self::get_all_newsletter_plan_ids();
617 792 }
618 793 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service( $user_id );
619 794 $is_paid_subscriber = $paywall->visitor_can_view_content( $valid_plan_ids, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS );
620 - self::$user_is_paid_subscriber_cache[ $user_id ] = $is_paid_subscriber;
795 + self::$user_is_paid_subscriber_cache[ $cache_key ] = $is_paid_subscriber;
621 796 }
622 - return self::$user_is_paid_subscriber_cache[ $user_id ];
797 + return self::$user_is_paid_subscriber_cache[ $cache_key ];
623 798 }
624 799
625 800 /**
626 801 * Determines whether the current user has a pending subscription.
@@ -651,9 +826,9 @@
651 826 $post_id = 0;
652 827 }
653 828
654 829 $cache_key = sprintf( '%d_%d', $user_id, $post_id );
655 - if ( $user_id !== 0 && isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
830 + if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
656 831 return self::$user_can_view_post_cache[ $cache_key ];
657 832 }
658 833
659 834 $post_access_level = self::get_post_access_level( $post_id );
@@ -661,14 +836,12 @@
661 836 self::$user_can_view_post_cache[ $cache_key ] = true;
662 837 return true;
663 838 }
664 839
665 - if ( $user_id === 0 ) {
666 - if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS ) {
667 - if ( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
668 - return true;
669 - }
670 - }
840 + // we are sending the post to subscribers so the user is a subscriber
841 + if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS && Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
842 + self::$user_can_view_post_cache[ $cache_key ] = true;
843 + return true;
671 844 }
672 845
673 846 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
674 847 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
@@ -675,10 +848,12 @@
675 848
676 849 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
677 850
678 851 if ( 0 === count( $all_newsletters_plan_ids ) &&
679 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
680 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
852 + (
853 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
854 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
855 + )
681 856 ) {
682 857 // The post is paywalled but there is no newsletter plans on the site.
683 858 // We downgrade the post level to subscribers-only
684 859 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
@@ -697,13 +872,31 @@
697 872 *
698 873 * @return bool
699 874 */
700 875 public static function is_enabled_jetpack_recurring_payments() {
701 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
876 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
702 877 return $api_available;
703 878 }
704 879
705 880 /**
881 + * Whether to enable the blocks in the editor.
882 + * All Monetize blocks (except Simple Payments) need a user with at least `edit_posts` capability
883 + *
884 + * @return bool
885 + */
886 + public static function should_enable_monetize_blocks_in_editor() {
887 + if ( ! is_admin() ) {
888 + // We enable the block for the front-end in all cases
889 + return true;
890 +
891 + }
892 +
893 + $is_offline_mode = ( new Status() )->is_offline_mode();
894 + $enable_monetize_blocks_in_editor = ( new Host() )->is_wpcom_simple() || ( ! $is_offline_mode );
895 + return $enable_monetize_blocks_in_editor;
896 + }
897 +
898 + /**
706 899 * Whether site has any paid plan.
707 900 *
708 901 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
709 902 *
@@ -758,11 +951,13 @@
758 951 * Return all membership plans ids (deleted or not)
759 952 * This function is used both on WPCOM or on Jetpack self-hosted.
760 953 * Depending on the environment we need to mitigate where the data is retrieved from.
761 954 *
955 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
956 + *
762 957 * @return array
763 958 */
764 - public static function get_all_newsletter_plan_ids() {
959 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
765 960
766 961 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
767 962 return array();
768 963 }
@@ -769,15 +964,28 @@
769 964
770 965 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
771 966 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
772 967 if ( ! $is_cached_site ) {
968 + $meta_query = array(
969 + array(
970 + 'key' => 'jetpack_memberships_type',
971 + 'value' => self::$type_tier,
972 + ),
973 + );
974 +
975 + if ( $allow_deleted === false ) {
976 + $meta_query[] = array(
977 + 'key' => 'jetpack_memberships_is_deleted',
978 + 'compare' => 'NOT EXISTS',
979 + );
980 + }
981 +
773 982 return get_posts(
774 983 array(
775 984 'posts_per_page' => -1,
776 985 'fields' => 'ids',
777 986 'post_type' => self::$post_type_plan,
778 - 'meta_key' => 'jetpack_memberships_type',
779 - 'meta_value' => self::$type_tier,
987 + 'meta_query' => $meta_query,
780 988 )
781 989 );
782 990
783 991 } else {
@@ -782,10 +990,9 @@
782 990
783 991 } else {
784 992 // On cached site on WPCOM
785 993 require_lib( 'memberships' );
786 - $allow_deleted = true;
787 - $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
994 + $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
788 995
789 996 if ( is_wp_error( $list ) ) {
790 997 return array();
791 998 }
@@ -813,11 +1020,12 @@
813 1020 if ( self::is_enabled_jetpack_recurring_payments() ) {
814 1021 Blocks::jetpack_register_block(
815 1022 'jetpack/recurring-payments',
816 1023 array(
817 - 'render_callback' => array( $this, 'render_button' ),
818 - 'uses_context' => array( 'isPremiumContentChild' ),
819 - 'provides_context' => array(
1024 + 'render_callback' => array( $this, 'render_button' ),
1025 + 'render_email_callback' => array( $this, 'render_button_email' ),
1026 + 'uses_context' => array( 'isPremiumContentChild' ),
1027 + 'provides_context' => array(
820 1028 'jetpack/parentBlockWidth' => 'width',
821 1029 ),
822 1030 )
823 1031 );
@@ -822,9 +1030,9 @@
822 1030 )
823 1031 );
824 1032 } else {
825 1033 Jetpack_Gutenberg::set_extension_unavailable(
826 - 'jetpack/recurring-payments',
1034 + 'recurring-payments',
827 1035 'missing_plan',
828 1036 array(
829 1037 'required_feature' => 'memberships',
830 1038 'required_plan' => self::$required_plan,
@@ -875,7 +1083,45 @@
875 1083 public static function is_current_user_subscribed() {
876 1084 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
877 1085 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
878 1086 return $subscription_service->is_current_user_subscribed();
1087 + }
1088 +
1089 + /**
1090 + * Render a tier description (stored as markdown text) to safe HTML.
1091 + *
1092 + * Uses Jetpack's markdown parser, restores paragraph structure (the parser
1093 + * strips <p> tags expecting wpautop to run later), forces links to open in a
1094 + * new tab (descriptions are shown inside the subscribe modal's iframe), and
1095 + * finally sanitizes the output to a small tag allowlist.
1096 + *
1097 + * @param mixed $description Raw tier description (markdown text). Non-scalar
1098 + * values are treated as empty.
1099 + * @return string Sanitized HTML, or an empty string for an empty description.
1100 + */
1101 + public static function render_tier_description_html( $description ) {
1102 + if ( ! is_scalar( $description ) ) {
1103 + return '';
1104 + }
1105 + $description = (string) $description;
1106 + if ( '' === trim( $description ) ) {
1107 + return '';
1108 + }
1109 +
1110 + if ( ! class_exists( 'WPCom_Markdown' ) ) {
1111 + require_once JETPACK__PLUGIN_DIR . 'modules/markdown/easy-markdown.php';
1112 + }
1113 +
1114 + $html = WPCom_Markdown::get_instance()->transform(
1115 + $description,
1116 + array(
1117 + 'unslash' => false,
1118 + 'id' => false,
1119 + )
1120 + );
1121 + $html = wpautop( $html );
1122 + $html = links_add_target( $html, '_blank' );
1123 +
1124 + return wp_kses( $html, self::TIER_DESCRIPTION_ALLOWED_HTML );
879 1125 }
880 1126 }
881 1127 Jetpack_Memberships::get_instance();