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 +318 -38 13.2.4 → 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.
@@ -202,11 +268,26 @@
202 268 public function init_hook_action() {
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();
272 +
273 + if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
274 + add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
275 + }
206 276 }
207 277
208 278 /**
279 + * Logs the subscriber out by clearing out the premium content cookie.
280 + */
281 + public function subscriber_logout() {
282 + if ( ! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' ) ) {
283 + return;
284 + }
285 +
286 + Abstract_Token_Subscription_Service::clear_token_cookie();
287 + }
288 +
289 + /**
209 290 * Sets up the custom post types for the module.
210 291 */
211 292 private function setup_cpts() {
212 293 /*
@@ -239,8 +320,27 @@
239 320 'capabilities' => $capabilities,
240 321 'show_in_rest' => false,
241 322 );
242 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 );
243 343 }
244 344
245 345 /**
246 346 * Allows custom post types to be used by REST API.
@@ -251,8 +351,9 @@
251 351 * @return array
252 352 */
253 353 public function allow_rest_api_types( $post_types ) {
254 354 $post_types[] = self::$post_type_plan;
355 + $post_types[] = self::$post_type_coupon;
255 356
256 357 return $post_types;
257 358 }
258 359
@@ -263,13 +364,37 @@
263 364 *
264 365 * @return array
265 366 */
266 367 public function allow_sync_post_meta( $post_meta ) {
267 - $meta_keys = array_map(
368 + $meta_keys_plans = array_map(
268 369 array( $this, 'return_meta' ),
269 370 self::get_plan_property_mapping()
270 371 );
271 - 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 + );
272 397 }
273 398
274 399 /**
275 400 * This returns meta attribute of passet array.
@@ -289,9 +414,9 @@
289 414 * @param WP_Error $error The error message with error code.
290 415 * @return string The error message rendered as HTML.
291 416 */
292 417 public function render_button_error( $error ) {
293 - if ( $this->user_can_edit() ) {
418 + if ( static::user_can_edit() ) {
294 419 return '<div><strong>Jetpack Memberships Error: ' . $error->get_error_code() . '</strong><br />' . $error->get_error_message() . '</div>';
295 420 }
296 421 return '<div>Sorry! This product is not available for purchase at this time.</div><!-- Jetpack Memberships Error: ' . $error->get_error_code() . ' -->';
297 422 }
@@ -380,8 +505,9 @@
380 505 if ( ! $product ) {
381 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' ) ) );
382 507 }
383 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
384 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' ) ) );
385 511 }
386 512 if ( $product->post_type !== self::$post_type_plan ) {
387 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' ) ) );
@@ -403,9 +529,13 @@
403 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
404 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
405 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
406 532 $subscribe_url = $this->get_subscription_url( $plan_id );
407 - 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;
408 538 }
409 539
410 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
411 541 }
@@ -410,8 +540,45 @@
410 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
411 541 }
412 542
413 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 + /**
414 581 * Builds subscription URL for this membership using the current blog and
415 582 * supplied plan IDs.
416 583 *
417 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -440,11 +607,9 @@
440 607 *
441 608 * @return string
442 609 */
443 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
444 - $button_label = isset( $attrs['submitButtonText'] )
445 - ? $attrs['submitButtonText']
446 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
447 612
448 613 $button_styles = array();
449 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
450 615 array_push(
@@ -527,12 +692,28 @@
527 692 if ( ! $post_id ) {
528 693 return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
529 694 }
530 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 +
531 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
532 - 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 ) ) {
533 711 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
534 712 }
713 +
714 + self::$post_access_level_cache[ $cache_key ] = $post_access_level;
715 +
535 716 return $post_access_level;
536 717 }
537 718
538 719 /**
@@ -567,26 +748,54 @@
567 748 * @return bool Whether the user can edit.
568 749 */
569 750 public static function user_can_edit() {
570 751 $user = wp_get_current_user();
571 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
572 752 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
573 753 }
574 754
575 755 /**
756 + * Clears the static cache for all users or for a given user.
757 + *
758 + * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
759 + * @return void
760 + */
761 + public static function clear_cache( ?int $user_id = null ) {
762 + if ( empty( $user_id ) ) {
763 + self::$user_is_paid_subscriber_cache = array();
764 + self::$user_can_view_post_cache = array();
765 + return;
766 + }
767 + unset( self::$user_is_paid_subscriber_cache[ $user_id ] );
768 + unset( self::$user_can_view_post_cache[ $user_id ] );
769 + }
770 +
771 + /**
576 772 * Determines whether the current user is a paid subscriber and caches the result.
577 773 *
774 + * @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.
775 + * @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).
578 776 * @return bool Whether the post can be viewed
579 777 */
580 - public static function user_is_paid_subscriber() {
581 - $user_id = get_current_user_id();
582 -
583 - require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
584 - $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
585 - $is_paid_subscriber = $paywall->visitor_can_view_content( self::get_all_newsletter_plan_ids(), Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS );
586 -
587 - self::$user_is_paid_subscriber_cache[ $user_id ] = $is_paid_subscriber;
588 - return $is_paid_subscriber;
778 + public static function user_is_paid_subscriber( $valid_plan_ids = array(), $user_id = null ) {
779 + if ( empty( $user_id ) ) {
780 + $user_id = get_current_user_id();
781 + if ( empty( $user_id ) ) {
782 + return false;
783 + }
784 + }
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 ] ) ) {
789 + require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
790 + if ( empty( $valid_plan_ids ) ) {
791 + $valid_plan_ids = self::get_all_newsletter_plan_ids();
792 + }
793 + $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service( $user_id );
794 + $is_paid_subscriber = $paywall->visitor_can_view_content( $valid_plan_ids, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS );
795 + self::$user_is_paid_subscriber_cache[ $cache_key ] = $is_paid_subscriber;
796 + }
797 + return self::$user_is_paid_subscriber_cache[ $cache_key ];
589 798 }
590 799
591 800 /**
592 801 * Determines whether the current user has a pending subscription.
@@ -617,9 +826,9 @@
617 826 $post_id = 0;
618 827 }
619 828
620 829 $cache_key = sprintf( '%d_%d', $user_id, $post_id );
621 - if ( $user_id !== 0 && isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
830 + if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
622 831 return self::$user_can_view_post_cache[ $cache_key ];
623 832 }
624 833
625 834 $post_access_level = self::get_post_access_level( $post_id );
@@ -627,14 +836,12 @@
627 836 self::$user_can_view_post_cache[ $cache_key ] = true;
628 837 return true;
629 838 }
630 839
631 - if ( $user_id === 0 ) {
632 - if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS ) {
633 - if ( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
634 - return true;
635 - }
636 - }
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;
637 844 }
638 845
639 846 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
640 847 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
@@ -641,10 +848,12 @@
641 848
642 849 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
643 850
644 851 if ( 0 === count( $all_newsletters_plan_ids ) &&
645 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
646 - 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 + )
647 856 ) {
648 857 // The post is paywalled but there is no newsletter plans on the site.
649 858 // We downgrade the post level to subscribers-only
650 859 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
@@ -663,13 +872,31 @@
663 872 *
664 873 * @return bool
665 874 */
666 875 public static function is_enabled_jetpack_recurring_payments() {
667 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
876 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
668 877 return $api_available;
669 878 }
670 879
671 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 + /**
672 899 * Whether site has any paid plan.
673 900 *
674 901 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
675 902 *
@@ -724,11 +951,13 @@
724 951 * Return all membership plans ids (deleted or not)
725 952 * This function is used both on WPCOM or on Jetpack self-hosted.
726 953 * Depending on the environment we need to mitigate where the data is retrieved from.
727 954 *
955 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
956 + *
728 957 * @return array
729 958 */
730 - public static function get_all_newsletter_plan_ids() {
959 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
731 960
732 961 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
733 962 return array();
734 963 }
@@ -735,15 +964,28 @@
735 964
736 965 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
737 966 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
738 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 +
739 982 return get_posts(
740 983 array(
741 984 'posts_per_page' => -1,
742 985 'fields' => 'ids',
743 986 'post_type' => self::$post_type_plan,
744 - 'meta_key' => 'jetpack_memberships_type',
745 - 'meta_value' => self::$type_tier,
987 + 'meta_query' => $meta_query,
746 988 )
747 989 );
748 990
749 991 } else {
@@ -748,10 +990,9 @@
748 990
749 991 } else {
750 992 // On cached site on WPCOM
751 993 require_lib( 'memberships' );
752 - $allow_deleted = true;
753 - $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 );
754 995
755 996 if ( is_wp_error( $list ) ) {
756 997 return array();
757 998 }
@@ -779,11 +1020,12 @@
779 1020 if ( self::is_enabled_jetpack_recurring_payments() ) {
780 1021 Blocks::jetpack_register_block(
781 1022 'jetpack/recurring-payments',
782 1023 array(
783 - 'render_callback' => array( $this, 'render_button' ),
784 - 'uses_context' => array( 'isPremiumContentChild' ),
785 - '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(
786 1028 'jetpack/parentBlockWidth' => 'width',
787 1029 ),
788 1030 )
789 1031 );
@@ -788,9 +1030,9 @@
788 1030 )
789 1031 );
790 1032 } else {
791 1033 Jetpack_Gutenberg::set_extension_unavailable(
792 - 'jetpack/recurring-payments',
1034 + 'recurring-payments',
793 1035 'missing_plan',
794 1036 array(
795 1037 'required_feature' => 'memberships',
796 1038 'required_plan' => self::$required_plan,
@@ -841,7 +1083,45 @@
841 1083 public static function is_current_user_subscribed() {
842 1084 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
843 1085 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
844 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 );
845 1125 }
846 1126 }
847 1127 Jetpack_Memberships::get_instance();