PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 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 All 508 releases
← All changes | modules/memberships/class-jetpack-memberships.php +279 -26 13.4.5 → 16.3 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 */
@@ -147,8 +185,17 @@
147 185 'NZD' => 0.5,
148 186 'PLN' => 2.0,
149 187 'SEK' => 3.0,
150 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,
151 198 );
152 199
153 200 /**
154 201 * Jetpack_Memberships constructor.
@@ -165,9 +212,9 @@
165 212 self::$instance = new self();
166 213 self::$instance->register_init_hook();
167 214 // Yes, `pro-plan` with a dash, `jetpack_personal` with an underscore. Check the v1.5 endpoint to verify.
168 215 $wpcom_plan_slug = defined( 'ENABLE_PRO_PLAN' ) ? 'pro-plan' : 'personal-bundle';
169 - 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';
170 217 }
171 218
172 219 return self::$instance;
173 220 }
@@ -210,8 +257,9 @@
210 257 */
211 258 private function register_init_hook() {
212 259 add_action( 'init', array( $this, 'init_hook_action' ) );
213 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.
214 262 add_action( 'switch_blog', array( $this, 'clear_post_access_level_cache' ) );
215 263 }
216 264
217 265 /**
@@ -221,9 +269,9 @@
221 269 add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
222 270 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
223 271 $this->setup_cpts();
224 272
225 - if ( Jetpack::is_module_active( 'subscriptions' ) && jetpack_is_frontend() ) {
273 + if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
226 274 add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
227 275 }
228 276 }
229 277
@@ -230,8 +278,12 @@
230 278 /**
231 279 * Logs the subscriber out by clearing out the premium content cookie.
232 280 */
233 281 public function subscriber_logout() {
282 + if ( ! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' ) ) {
283 + return;
284 + }
285 +
234 286 Abstract_Token_Subscription_Service::clear_token_cookie();
235 287 }
236 288
237 289 /**
@@ -268,8 +320,27 @@
268 320 'capabilities' => $capabilities,
269 321 'show_in_rest' => false,
270 322 );
271 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 );
272 343 }
273 344
274 345 /**
275 346 * Allows custom post types to be used by REST API.
@@ -280,8 +351,9 @@
280 351 * @return array
281 352 */
282 353 public function allow_rest_api_types( $post_types ) {
283 354 $post_types[] = self::$post_type_plan;
355 + $post_types[] = self::$post_type_coupon;
284 356
285 357 return $post_types;
286 358 }
287 359
@@ -292,13 +364,37 @@
292 364 *
293 365 * @return array
294 366 */
295 367 public function allow_sync_post_meta( $post_meta ) {
296 - $meta_keys = array_map(
368 + $meta_keys_plans = array_map(
297 369 array( $this, 'return_meta' ),
298 370 self::get_plan_property_mapping()
299 371 );
300 - 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 + );
301 397 }
302 398
303 399 /**
304 400 * This returns meta attribute of passet array.
@@ -409,8 +505,9 @@
409 505 if ( ! $product ) {
410 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' ) ) );
411 507 }
412 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
413 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' ) ) );
414 511 }
415 512 if ( $product->post_type !== self::$post_type_plan ) {
416 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' ) ) );
@@ -432,9 +529,13 @@
432 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
433 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
434 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
435 532 $subscribe_url = $this->get_subscription_url( $plan_id );
436 - 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;
437 538 }
438 539
439 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
440 541 }
@@ -439,8 +540,45 @@
439 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
440 541 }
441 542
442 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 + /**
443 581 * Builds subscription URL for this membership using the current blog and
444 582 * supplied plan IDs.
445 583 *
446 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -469,11 +607,9 @@
469 607 *
470 608 * @return string
471 609 */
472 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
473 - $button_label = isset( $attrs['submitButtonText'] )
474 - ? $attrs['submitButtonText']
475 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
476 612
477 613 $button_styles = array();
478 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
479 615 array_push(
@@ -564,12 +700,27 @@
564 700 return self::$post_access_level_cache[ $cache_key ];
565 701 }
566 702
567 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
568 - 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 ) ) {
569 711 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
570 712 }
571 713
714 + // Only the editor switches a Paywall post to subscribers; REST, WP-CLI and importer saves don't.
715 + // The block's name constant isn't loaded everywhere this runs, hence the literal.
716 + if (
717 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY === $post_access_level
718 + && has_block( 'jetpack/paywall', $post_id )
719 + ) {
720 + $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
721 + }
722 +
572 723 self::$post_access_level_cache[ $cache_key ] = $post_access_level;
573 724
574 725 return $post_access_level;
575 726 }
@@ -606,9 +757,8 @@
606 757 * @return bool Whether the user can edit.
607 758 */
608 759 public static function user_can_edit() {
609 760 $user = wp_get_current_user();
610 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
611 761 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
612 762 }
613 763
614 764 /**
@@ -616,9 +766,9 @@
616 766 *
617 767 * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
618 768 * @return void
619 769 */
620 - public static function clear_cache( int $user_id = null ) {
770 + public static function clear_cache( ?int $user_id = null ) {
621 771 if ( empty( $user_id ) ) {
622 772 self::$user_is_paid_subscriber_cache = array();
623 773 self::$user_can_view_post_cache = array();
624 774 return;
@@ -675,8 +825,31 @@
675 825 *
676 826 * @return bool Whether the post can be viewed
677 827 */
678 828 public static function user_can_view_post( $post_id = null ) {
829 + return self::check_post_access( $post_id, true );
830 + }
831 +
832 + /**
833 + * Check the post's subscription requirement without granting access for editing it.
834 + *
835 + * @since $$next-version$$
836 + *
837 + * @param int|null $post_id Explicit post ID, or the loop post when omitted.
838 + * @return bool Whether the visitor meets the post's subscription requirement.
839 + */
840 + public static function user_has_subscription_access( $post_id = null ) {
841 + return self::check_post_access( $post_id, false );
842 + }
843 +
844 + /**
845 + * Evaluate and cache post access with or without the editorial exception.
846 + *
847 + * @param int|null $post_id Post to check.
848 + * @param bool $allow_editor_access Whether editing the post can grant access.
849 + * @return bool Whether access is granted.
850 + */
851 + private static function check_post_access( $post_id, $allow_editor_access ) {
679 852 $user_id = get_current_user_id();
680 853 if ( null === $post_id ) {
681 854 $post_id = get_the_ID();
682 855 }
@@ -684,9 +857,9 @@
684 857 if ( false === $post_id ) {
685 858 $post_id = 0;
686 859 }
687 860
688 - $cache_key = sprintf( '%d_%d', $user_id, $post_id );
861 + $cache_key = sprintf( '%d_%d_%d', $user_id, $post_id, (int) $allow_editor_access );
689 862 if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
690 863 return self::$user_can_view_post_cache[ $cache_key ];
691 864 }
692 865
@@ -696,9 +869,9 @@
696 869 return true;
697 870 }
698 871
699 872 // we are sending the post to subscribers so the user is a subscriber
700 - if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS && Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
873 + if ( $allow_editor_access && defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS && Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
701 874 self::$user_can_view_post_cache[ $cache_key ] = true;
702 875 return true;
703 876 }
704 877
@@ -707,10 +880,12 @@
707 880
708 881 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
709 882
710 883 if ( 0 === count( $all_newsletters_plan_ids ) &&
711 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
712 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
884 + (
885 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
886 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
887 + )
713 888 ) {
714 889 // The post is paywalled but there is no newsletter plans on the site.
715 890 // We downgrade the post level to subscribers-only
716 891 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
@@ -715,9 +890,16 @@
715 890 // We downgrade the post level to subscribers-only
716 891 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
717 892 }
718 893
719 - $can_view_post = $paywall->visitor_can_view_content( $all_newsletters_plan_ids, $post_access_level );
894 + // Pass the post explicitly: callers outside the loop have no get_the_ID() to fall back on.
895 + if ( $allow_editor_access ) {
896 + // @phan-suppress-next-line PhanParamTooMany -- Concrete services accept the optional $post_id; interface omits it on purpose.
897 + $can_view_post = $paywall->visitor_can_view_content( $all_newsletters_plan_ids, $post_access_level, $post_id );
898 + } else {
899 + $can_view_post = is_callable( array( $paywall, 'visitor_has_subscription_access' ) )
900 + && $paywall->visitor_has_subscription_access( $all_newsletters_plan_ids, $post_access_level, $post_id );
901 + }
720 902
721 903 self::$user_can_view_post_cache[ $cache_key ] = $can_view_post;
722 904 return $can_view_post;
723 905 }
@@ -729,13 +911,31 @@
729 911 *
730 912 * @return bool
731 913 */
732 914 public static function is_enabled_jetpack_recurring_payments() {
733 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
915 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
734 916 return $api_available;
735 917 }
736 918
737 919 /**
920 + * Whether to enable the blocks in the editor.
921 + * All Monetize blocks (except Simple Payments) need a user with at least `edit_posts` capability
922 + *
923 + * @return bool
924 + */
925 + public static function should_enable_monetize_blocks_in_editor() {
926 + if ( ! is_admin() ) {
927 + // We enable the block for the front-end in all cases
928 + return true;
929 +
930 + }
931 +
932 + $is_offline_mode = ( new Status() )->is_offline_mode();
933 + $enable_monetize_blocks_in_editor = ( new Host() )->is_wpcom_simple() || ( ! $is_offline_mode );
934 + return $enable_monetize_blocks_in_editor;
935 + }
936 +
937 + /**
738 938 * Whether site has any paid plan.
739 939 *
740 940 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
741 941 *
@@ -790,11 +990,13 @@
790 990 * Return all membership plans ids (deleted or not)
791 991 * This function is used both on WPCOM or on Jetpack self-hosted.
792 992 * Depending on the environment we need to mitigate where the data is retrieved from.
793 993 *
994 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
995 + *
794 996 * @return array
795 997 */
796 - public static function get_all_newsletter_plan_ids() {
998 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
797 999
798 1000 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
799 1001 return array();
800 1002 }
@@ -801,15 +1003,28 @@
801 1003
802 1004 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
803 1005 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
804 1006 if ( ! $is_cached_site ) {
1007 + $meta_query = array(
1008 + array(
1009 + 'key' => 'jetpack_memberships_type',
1010 + 'value' => self::$type_tier,
1011 + ),
1012 + );
1013 +
1014 + if ( $allow_deleted === false ) {
1015 + $meta_query[] = array(
1016 + 'key' => 'jetpack_memberships_is_deleted',
1017 + 'compare' => 'NOT EXISTS',
1018 + );
1019 + }
1020 +
805 1021 return get_posts(
806 1022 array(
807 1023 'posts_per_page' => -1,
808 1024 'fields' => 'ids',
809 1025 'post_type' => self::$post_type_plan,
810 - 'meta_key' => 'jetpack_memberships_type',
811 - 'meta_value' => self::$type_tier,
1026 + 'meta_query' => $meta_query,
812 1027 )
813 1028 );
814 1029
815 1030 } else {
@@ -814,10 +1029,9 @@
814 1029
815 1030 } else {
816 1031 // On cached site on WPCOM
817 1032 require_lib( 'memberships' );
818 - $allow_deleted = true;
819 - $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
1033 + $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
820 1034
821 1035 if ( is_wp_error( $list ) ) {
822 1036 return array();
823 1037 }
@@ -845,11 +1059,12 @@
845 1059 if ( self::is_enabled_jetpack_recurring_payments() ) {
846 1060 Blocks::jetpack_register_block(
847 1061 'jetpack/recurring-payments',
848 1062 array(
849 - 'render_callback' => array( $this, 'render_button' ),
850 - 'uses_context' => array( 'isPremiumContentChild' ),
851 - 'provides_context' => array(
1063 + 'render_callback' => array( $this, 'render_button' ),
1064 + 'render_email_callback' => array( $this, 'render_button_email' ),
1065 + 'uses_context' => array( 'isPremiumContentChild' ),
1066 + 'provides_context' => array(
852 1067 'jetpack/parentBlockWidth' => 'width',
853 1068 ),
854 1069 )
855 1070 );
@@ -854,9 +1069,9 @@
854 1069 )
855 1070 );
856 1071 } else {
857 1072 Jetpack_Gutenberg::set_extension_unavailable(
858 - 'jetpack/recurring-payments',
1073 + 'recurring-payments',
859 1074 'missing_plan',
860 1075 array(
861 1076 'required_feature' => 'memberships',
862 1077 'required_plan' => self::$required_plan,
@@ -907,7 +1122,45 @@
907 1122 public static function is_current_user_subscribed() {
908 1123 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
909 1124 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
910 1125 return $subscription_service->is_current_user_subscribed();
1126 + }
1127 +
1128 + /**
1129 + * Render a tier description (stored as markdown text) to safe HTML.
1130 + *
1131 + * Uses Jetpack's markdown parser, restores paragraph structure (the parser
1132 + * strips <p> tags expecting wpautop to run later), forces links to open in a
1133 + * new tab (descriptions are shown inside the subscribe modal's iframe), and
1134 + * finally sanitizes the output to a small tag allowlist.
1135 + *
1136 + * @param mixed $description Raw tier description (markdown text). Non-scalar
1137 + * values are treated as empty.
1138 + * @return string Sanitized HTML, or an empty string for an empty description.
1139 + */
1140 + public static function render_tier_description_html( $description ) {
1141 + if ( ! is_scalar( $description ) ) {
1142 + return '';
1143 + }
1144 + $description = (string) $description;
1145 + if ( '' === trim( $description ) ) {
1146 + return '';
1147 + }
1148 +
1149 + if ( ! class_exists( 'WPCom_Markdown' ) ) {
1150 + require_once JETPACK__PLUGIN_DIR . 'modules/markdown/easy-markdown.php';
1151 + }
1152 +
1153 + $html = WPCom_Markdown::get_instance()->transform(
1154 + $description,
1155 + array(
1156 + 'unslash' => false,
1157 + 'id' => false,
1158 + )
1159 + );
1160 + $html = wpautop( $html );
1161 + $html = links_add_target( $html, '_blank' );
1162 +
1163 + return wp_kses( $html, self::TIER_DESCRIPTION_ALLOWED_HTML );
911 1164 }
912 1165 }
913 1166 Jetpack_Memberships::get_instance();