PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 +241 -23 13.5.2 → 16.3-a.7 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
@@ -272,8 +320,27 @@
272 320 'capabilities' => $capabilities,
273 321 'show_in_rest' => false,
274 322 );
275 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 );
276 343 }
277 344
278 345 /**
279 346 * Allows custom post types to be used by REST API.
@@ -284,8 +351,9 @@
284 351 * @return array
285 352 */
286 353 public function allow_rest_api_types( $post_types ) {
287 354 $post_types[] = self::$post_type_plan;
355 + $post_types[] = self::$post_type_coupon;
288 356
289 357 return $post_types;
290 358 }
291 359
@@ -296,13 +364,37 @@
296 364 *
297 365 * @return array
298 366 */
299 367 public function allow_sync_post_meta( $post_meta ) {
300 - $meta_keys = array_map(
368 + $meta_keys_plans = array_map(
301 369 array( $this, 'return_meta' ),
302 370 self::get_plan_property_mapping()
303 371 );
304 - 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 + );
305 397 }
306 398
307 399 /**
308 400 * This returns meta attribute of passet array.
@@ -437,9 +529,13 @@
437 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
438 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
439 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
440 532 $subscribe_url = $this->get_subscription_url( $plan_id );
441 - 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;
442 538 }
443 539
444 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
445 541 }
@@ -444,8 +540,45 @@
444 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
445 541 }
446 542
447 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 + /**
448 581 * Builds subscription URL for this membership using the current blog and
449 582 * supplied plan IDs.
450 583 *
451 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -474,11 +607,9 @@
474 607 *
475 608 * @return string
476 609 */
477 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
478 - $button_label = isset( $attrs['submitButtonText'] )
479 - ? $attrs['submitButtonText']
480 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
481 612
482 613 $button_styles = array();
483 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
484 615 array_push(
@@ -569,12 +700,27 @@
569 700 return self::$post_access_level_cache[ $cache_key ];
570 701 }
571 702
572 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
573 - 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 ) ) {
574 711 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
575 712 }
576 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 +
577 723 self::$post_access_level_cache[ $cache_key ] = $post_access_level;
578 724
579 725 return $post_access_level;
580 726 }
@@ -611,9 +757,8 @@
611 757 * @return bool Whether the user can edit.
612 758 */
613 759 public static function user_can_edit() {
614 760 $user = wp_get_current_user();
615 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
616 761 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
617 762 }
618 763
619 764 /**
@@ -621,9 +766,9 @@
621 766 *
622 767 * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
623 768 * @return void
624 769 */
625 - public static function clear_cache( int $user_id = null ) {
770 + public static function clear_cache( ?int $user_id = null ) {
626 771 if ( empty( $user_id ) ) {
627 772 self::$user_is_paid_subscriber_cache = array();
628 773 self::$user_can_view_post_cache = array();
629 774 return;
@@ -712,10 +857,12 @@
712 857
713 858 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
714 859
715 860 if ( 0 === count( $all_newsletters_plan_ids ) &&
716 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
717 - Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
861 + (
862 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
863 + Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
864 + )
718 865 ) {
719 866 // The post is paywalled but there is no newsletter plans on the site.
720 867 // We downgrade the post level to subscribers-only
721 868 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
@@ -734,13 +881,31 @@
734 881 *
735 882 * @return bool
736 883 */
737 884 public static function is_enabled_jetpack_recurring_payments() {
738 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
885 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
739 886 return $api_available;
740 887 }
741 888
742 889 /**
890 + * Whether to enable the blocks in the editor.
891 + * All Monetize blocks (except Simple Payments) need a user with at least `edit_posts` capability
892 + *
893 + * @return bool
894 + */
895 + public static function should_enable_monetize_blocks_in_editor() {
896 + if ( ! is_admin() ) {
897 + // We enable the block for the front-end in all cases
898 + return true;
899 +
900 + }
901 +
902 + $is_offline_mode = ( new Status() )->is_offline_mode();
903 + $enable_monetize_blocks_in_editor = ( new Host() )->is_wpcom_simple() || ( ! $is_offline_mode );
904 + return $enable_monetize_blocks_in_editor;
905 + }
906 +
907 + /**
743 908 * Whether site has any paid plan.
744 909 *
745 910 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
746 911 *
@@ -795,11 +960,13 @@
795 960 * Return all membership plans ids (deleted or not)
796 961 * This function is used both on WPCOM or on Jetpack self-hosted.
797 962 * Depending on the environment we need to mitigate where the data is retrieved from.
798 963 *
964 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
965 + *
799 966 * @return array
800 967 */
801 - public static function get_all_newsletter_plan_ids() {
968 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
802 969
803 970 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
804 971 return array();
805 972 }
@@ -806,15 +973,28 @@
806 973
807 974 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
808 975 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
809 976 if ( ! $is_cached_site ) {
977 + $meta_query = array(
978 + array(
979 + 'key' => 'jetpack_memberships_type',
980 + 'value' => self::$type_tier,
981 + ),
982 + );
983 +
984 + if ( $allow_deleted === false ) {
985 + $meta_query[] = array(
986 + 'key' => 'jetpack_memberships_is_deleted',
987 + 'compare' => 'NOT EXISTS',
988 + );
989 + }
990 +
810 991 return get_posts(
811 992 array(
812 993 'posts_per_page' => -1,
813 994 'fields' => 'ids',
814 995 'post_type' => self::$post_type_plan,
815 - 'meta_key' => 'jetpack_memberships_type',
816 - 'meta_value' => self::$type_tier,
996 + 'meta_query' => $meta_query,
817 997 )
818 998 );
819 999
820 1000 } else {
@@ -819,10 +999,9 @@
819 999
820 1000 } else {
821 1001 // On cached site on WPCOM
822 1002 require_lib( 'memberships' );
823 - $allow_deleted = true;
824 - $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
1003 + $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
825 1004
826 1005 if ( is_wp_error( $list ) ) {
827 1006 return array();
828 1007 }
@@ -850,11 +1029,12 @@
850 1029 if ( self::is_enabled_jetpack_recurring_payments() ) {
851 1030 Blocks::jetpack_register_block(
852 1031 'jetpack/recurring-payments',
853 1032 array(
854 - 'render_callback' => array( $this, 'render_button' ),
855 - 'uses_context' => array( 'isPremiumContentChild' ),
856 - 'provides_context' => array(
1033 + 'render_callback' => array( $this, 'render_button' ),
1034 + 'render_email_callback' => array( $this, 'render_button_email' ),
1035 + 'uses_context' => array( 'isPremiumContentChild' ),
1036 + 'provides_context' => array(
857 1037 'jetpack/parentBlockWidth' => 'width',
858 1038 ),
859 1039 )
860 1040 );
@@ -859,9 +1039,9 @@
859 1039 )
860 1040 );
861 1041 } else {
862 1042 Jetpack_Gutenberg::set_extension_unavailable(
863 - 'jetpack/recurring-payments',
1043 + 'recurring-payments',
864 1044 'missing_plan',
865 1045 array(
866 1046 'required_feature' => 'memberships',
867 1047 'required_plan' => self::$required_plan,
@@ -912,7 +1092,45 @@
912 1092 public static function is_current_user_subscribed() {
913 1093 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
914 1094 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
915 1095 return $subscription_service->is_current_user_subscribed();
1096 + }
1097 +
1098 + /**
1099 + * Render a tier description (stored as markdown text) to safe HTML.
1100 + *
1101 + * Uses Jetpack's markdown parser, restores paragraph structure (the parser
1102 + * strips <p> tags expecting wpautop to run later), forces links to open in a
1103 + * new tab (descriptions are shown inside the subscribe modal's iframe), and
1104 + * finally sanitizes the output to a small tag allowlist.
1105 + *
1106 + * @param mixed $description Raw tier description (markdown text). Non-scalar
1107 + * values are treated as empty.
1108 + * @return string Sanitized HTML, or an empty string for an empty description.
1109 + */
1110 + public static function render_tier_description_html( $description ) {
1111 + if ( ! is_scalar( $description ) ) {
1112 + return '';
1113 + }
1114 + $description = (string) $description;
1115 + if ( '' === trim( $description ) ) {
1116 + return '';
1117 + }
1118 +
1119 + if ( ! class_exists( 'WPCom_Markdown' ) ) {
1120 + require_once JETPACK__PLUGIN_DIR . 'modules/markdown/easy-markdown.php';
1121 + }
1122 +
1123 + $html = WPCom_Markdown::get_instance()->transform(
1124 + $description,
1125 + array(
1126 + 'unslash' => false,
1127 + 'id' => false,
1128 + )
1129 + );
1130 + $html = wpautop( $html );
1131 + $html = links_add_target( $html, '_blank' );
1132 +
1133 + return wp_kses( $html, self::TIER_DESCRIPTION_ALLOWED_HTML );
916 1134 }
917 1135 }
918 1136 Jetpack_Memberships::get_instance();