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 +224 -23 13.8.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 */
@@ -155,8 +193,9 @@
155 193 'ILS' => 0,
156 194 'PHP' => 0,
157 195 'RUB' => 0,
158 196 'TRY' => 0,
197 + 'MYR' => 2.00,
159 198 );
160 199
161 200 /**
162 201 * Jetpack_Memberships constructor.
@@ -173,9 +212,9 @@
173 212 self::$instance = new self();
174 213 self::$instance->register_init_hook();
175 214 // Yes, `pro-plan` with a dash, `jetpack_personal` with an underscore. Check the v1.5 endpoint to verify.
176 215 $wpcom_plan_slug = defined( 'ENABLE_PRO_PLAN' ) ? 'pro-plan' : 'personal-bundle';
177 - 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';
178 217 }
179 218
180 219 return self::$instance;
181 220 }
@@ -218,8 +257,9 @@
218 257 */
219 258 private function register_init_hook() {
220 259 add_action( 'init', array( $this, 'init_hook_action' ) );
221 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.
222 262 add_action( 'switch_blog', array( $this, 'clear_post_access_level_cache' ) );
223 263 }
224 264
225 265 /**
@@ -229,9 +269,9 @@
229 269 add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
230 270 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
231 271 $this->setup_cpts();
232 272
233 - if ( Jetpack::is_module_active( 'subscriptions' ) && jetpack_is_frontend() ) {
273 + if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
234 274 add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
235 275 }
236 276 }
237 277
@@ -280,8 +320,27 @@
280 320 'capabilities' => $capabilities,
281 321 'show_in_rest' => false,
282 322 );
283 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 );
284 343 }
285 344
286 345 /**
287 346 * Allows custom post types to be used by REST API.
@@ -292,8 +351,9 @@
292 351 * @return array
293 352 */
294 353 public function allow_rest_api_types( $post_types ) {
295 354 $post_types[] = self::$post_type_plan;
355 + $post_types[] = self::$post_type_coupon;
296 356
297 357 return $post_types;
298 358 }
299 359
@@ -304,13 +364,37 @@
304 364 *
305 365 * @return array
306 366 */
307 367 public function allow_sync_post_meta( $post_meta ) {
308 - $meta_keys = array_map(
368 + $meta_keys_plans = array_map(
309 369 array( $this, 'return_meta' ),
310 370 self::get_plan_property_mapping()
311 371 );
312 - 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 + );
313 397 }
314 398
315 399 /**
316 400 * This returns meta attribute of passet array.
@@ -445,9 +529,13 @@
445 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
446 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
447 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
448 532 $subscribe_url = $this->get_subscription_url( $plan_id );
449 - 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;
450 538 }
451 539
452 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
453 541 }
@@ -452,8 +540,45 @@
452 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
453 541 }
454 542
455 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 + /**
456 581 * Builds subscription URL for this membership using the current blog and
457 582 * supplied plan IDs.
458 583 *
459 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -482,11 +607,9 @@
482 607 *
483 608 * @return string
484 609 */
485 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
486 - $button_label = isset( $attrs['submitButtonText'] )
487 - ? $attrs['submitButtonText']
488 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
489 612
490 613 $button_styles = array();
491 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
492 615 array_push(
@@ -577,9 +700,15 @@
577 700 return self::$post_access_level_cache[ $cache_key ];
578 701 }
579 702
580 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
581 - 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 ) ) {
582 711 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
583 712 }
584 713
585 714 self::$post_access_level_cache[ $cache_key ] = $post_access_level;
@@ -619,9 +748,8 @@
619 748 * @return bool Whether the user can edit.
620 749 */
621 750 public static function user_can_edit() {
622 751 $user = wp_get_current_user();
623 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
624 752 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
625 753 }
626 754
627 755 /**
@@ -629,9 +757,9 @@
629 757 *
630 758 * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
631 759 * @return void
632 760 */
633 - public static function clear_cache( int $user_id = null ) {
761 + public static function clear_cache( ?int $user_id = null ) {
634 762 if ( empty( $user_id ) ) {
635 763 self::$user_is_paid_subscriber_cache = array();
636 764 self::$user_can_view_post_cache = array();
637 765 return;
@@ -720,10 +848,12 @@
720 848
721 849 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
722 850
723 851 if ( 0 === count( $all_newsletters_plan_ids ) &&
724 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
725 - 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 + )
726 856 ) {
727 857 // The post is paywalled but there is no newsletter plans on the site.
728 858 // We downgrade the post level to subscribers-only
729 859 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
@@ -742,13 +872,31 @@
742 872 *
743 873 * @return bool
744 874 */
745 875 public static function is_enabled_jetpack_recurring_payments() {
746 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
876 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
747 877 return $api_available;
748 878 }
749 879
750 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 + /**
751 899 * Whether site has any paid plan.
752 900 *
753 901 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
754 902 *
@@ -803,11 +951,13 @@
803 951 * Return all membership plans ids (deleted or not)
804 952 * This function is used both on WPCOM or on Jetpack self-hosted.
805 953 * Depending on the environment we need to mitigate where the data is retrieved from.
806 954 *
955 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
956 + *
807 957 * @return array
808 958 */
809 - public static function get_all_newsletter_plan_ids() {
959 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
810 960
811 961 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
812 962 return array();
813 963 }
@@ -814,15 +964,28 @@
814 964
815 965 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
816 966 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
817 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 +
818 982 return get_posts(
819 983 array(
820 984 'posts_per_page' => -1,
821 985 'fields' => 'ids',
822 986 'post_type' => self::$post_type_plan,
823 - 'meta_key' => 'jetpack_memberships_type',
824 - 'meta_value' => self::$type_tier,
987 + 'meta_query' => $meta_query,
825 988 )
826 989 );
827 990
828 991 } else {
@@ -827,10 +990,9 @@
827 990
828 991 } else {
829 992 // On cached site on WPCOM
830 993 require_lib( 'memberships' );
831 - $allow_deleted = true;
832 - $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 );
833 995
834 996 if ( is_wp_error( $list ) ) {
835 997 return array();
836 998 }
@@ -858,11 +1020,12 @@
858 1020 if ( self::is_enabled_jetpack_recurring_payments() ) {
859 1021 Blocks::jetpack_register_block(
860 1022 'jetpack/recurring-payments',
861 1023 array(
862 - 'render_callback' => array( $this, 'render_button' ),
863 - 'uses_context' => array( 'isPremiumContentChild' ),
864 - '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(
865 1028 'jetpack/parentBlockWidth' => 'width',
866 1029 ),
867 1030 )
868 1031 );
@@ -867,9 +1030,9 @@
867 1030 )
868 1031 );
869 1032 } else {
870 1033 Jetpack_Gutenberg::set_extension_unavailable(
871 - 'jetpack/recurring-payments',
1034 + 'recurring-payments',
872 1035 'missing_plan',
873 1036 array(
874 1037 'required_feature' => 'memberships',
875 1038 'required_plan' => self::$required_plan,
@@ -920,7 +1083,45 @@
920 1083 public static function is_current_user_subscribed() {
921 1084 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
922 1085 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
923 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 );
924 1125 }
925 1126 }
926 1127 Jetpack_Memberships::get_instance();