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 +437 -86 12.8.3 → 16.3 View file →
@@ -6,13 +6,19 @@
6 6 * @since 7.3.0
7 7 */
8 8
9 9 use Automattic\Jetpack\Blocks;
10 -use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Token_Subscription_Service;
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.
@@ -283,8 +408,21 @@
283 408 return $map['meta'];
284 409 }
285 410
286 411 /**
412 + * Show an error to the user (or embed a clue in the HTML) when the button does not get rendered properly.
413 + *
414 + * @param WP_Error $error The error message with error code.
415 + * @return string The error message rendered as HTML.
416 + */
417 + public function render_button_error( $error ) {
418 + if ( static::user_can_edit() ) {
419 + return '<div><strong>Jetpack Memberships Error: ' . $error->get_error_code() . '</strong><br />' . $error->get_error_message() . '</div>';
420 + }
421 + return '<div>Sorry! This product is not available for purchase at this time.</div><!-- Jetpack Memberships Error: ' . $error->get_error_code() . ' -->';
422 + }
423 +
424 + /**
287 425 * Renders a preview of the Recurring Payment button, which is not hooked
288 426 * up to the subscription url. Used to preview the block on the frontend
289 427 * for site editors when Stripe has not been connected.
290 428 *
@@ -336,38 +474,68 @@
336 474 * @param array $attributes - attributes in the shortcode. `id` here is the CPT id of the plan.
337 475 * @param string $content - Recurring Payment block content.
338 476 * @param WP_Block $block - Recurring Payment block instance.
339 477 *
340 - * @return string|void
478 + * @return string|void - HTML for the button, void removes the button.
341 479 */
342 480 public function render_button( $attributes, $content = null, $block = null ) {
343 - Jetpack_Gutenberg::load_assets_as_required( self::$button_block_name, array( 'thickbox', 'wp-polyfill' ) );
481 + Jetpack_Gutenberg::load_assets_as_required( self::$button_block_name );
344 482
345 483 if ( $this->should_render_button_preview( $block ) ) {
346 484 return $this->render_button_preview( $attributes, $content );
347 485 }
348 486
349 - if ( empty( $attributes['planId'] ) ) {
350 - return;
487 + if ( empty( $attributes['planId'] ) && empty( $attributes['planIds'] ) ) {
488 + return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-npi', __( 'No plan was configured for this button.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that an existing payment plan is selected for this block.', 'jetpack' ) ) );
351 489 }
352 490
353 - $plan_id = (int) $attributes['planId'];
354 - $product = get_post( $plan_id );
355 - if ( ! $product || is_wp_error( $product ) ) {
356 - return;
491 + // This is string of '+` separated plan ids. Loop through them and
492 + // filter out the ones that are not valid.
493 + $plan_ids = array();
494 + if ( ! empty( $attributes['planIds'] ) ) {
495 + $plan_ids = $attributes['planIds'];
496 + } elseif ( ! empty( $attributes['planId'] ) ) {
497 + $plan_ids = explode( '+', $attributes['planId'] );
357 498 }
358 - if ( $product->post_type !== self::$post_type_plan || 'publish' !== $product->post_status ) {
499 + $valid_plans = array();
500 + foreach ( $plan_ids as $plan_id ) {
501 + if ( ! is_numeric( $plan_id ) ) {
502 + continue;
503 + }
504 + $product = get_post( $plan_id );
505 + if ( ! $product ) {
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' ) ) );
507 + }
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
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' ) ) );
511 + }
512 + if ( $product->post_type !== self::$post_type_plan ) {
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' ) ) );
514 + }
515 + if ( 'publish' !== $product->post_status ) {
516 + return $this->render_button_error( new WP_Error( 'jetpack-memberships-rb-psnpub', __( 'The selected payment plan is not active.', 'jetpack' ) . ' ' . __( 'Edit this post and confirm that the selected payment plan still exists and is available for purchase.', 'jetpack' ) ) );
517 + }
518 + $valid_plans[] = $plan_id;
519 + }
520 +
521 + // If none are valid, return.
522 + // (Returning like this makes the button disappear.)
523 + if ( empty( $valid_plans ) ) {
359 524 return;
360 525 }
526 + $plan_id = implode( '+', $valid_plans );
361 527
362 - add_thickbox();
363 -
364 528 if ( ! empty( $content ) ) {
365 529 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
366 530 $content = str_replace( 'recurring-payments-id', $block_id, $content );
367 531 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
368 532 $subscribe_url = $this->get_subscription_url( $plan_id );
369 - 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;
370 538 }
371 539
372 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
373 541 }
@@ -372,8 +540,45 @@
372 540 return $this->deprecated_render_button_v1( $attributes, $plan_id );
373 541 }
374 542
375 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 + /**
376 581 * Builds subscription URL for this membership using the current blog and
377 582 * supplied plan IDs.
378 583 *
379 584 * @param integer $plan_id - Unique ID for the plan being subscribed to.
@@ -402,11 +607,9 @@
402 607 *
403 608 * @return string
404 609 */
405 610 public function deprecated_render_button_v1( $attrs, $plan_id ) {
406 - $button_label = isset( $attrs['submitButtonText'] )
407 - ? $attrs['submitButtonText']
408 - : __( 'Your contribution', 'jetpack' );
611 + $button_label = $attrs['submitButtonText'] ?? __( 'Your contribution', 'jetpack' );
409 612
410 613 $button_styles = array();
411 614 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
412 615 array_push(
@@ -486,15 +689,40 @@
486 689 if ( ! $post_id ) {
487 690 $post_id = get_the_ID();
488 691 }
489 692 if ( ! $post_id ) {
490 - return Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
693 + return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
491 694 }
492 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 +
493 703 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
494 - if ( empty( $post_access_level ) ) {
495 - $post_access_level = Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
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 ) ) {
711 + $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
496 712 }
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 +
723 + self::$post_access_level_cache[ $cache_key ] = $post_access_level;
724 +
497 725 return $post_access_level;
498 726 }
499 727
500 728 /**
@@ -529,27 +757,65 @@
529 757 * @return bool Whether the user can edit.
530 758 */
531 759 public static function user_can_edit() {
532 760 $user = wp_get_current_user();
533 - // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
534 761 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
535 762 }
536 763
537 764 /**
538 - * Determines whether the current user can view the post based on the newsletter access level
539 - * and caches the result.
765 + * Clears the static cache for all users or for a given user.
540 766 *
767 + * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
768 + * @return void
769 + */
770 + public static function clear_cache( ?int $user_id = null ) {
771 + if ( empty( $user_id ) ) {
772 + self::$user_is_paid_subscriber_cache = array();
773 + self::$user_can_view_post_cache = array();
774 + return;
775 + }
776 + unset( self::$user_is_paid_subscriber_cache[ $user_id ] );
777 + unset( self::$user_can_view_post_cache[ $user_id ] );
778 + }
779 +
780 + /**
781 + * Determines whether the current user is a paid subscriber and caches the result.
782 + *
783 + * @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.
784 + * @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).
541 785 * @return bool Whether the post can be viewed
542 786 */
543 - public static function user_is_paid_subscriber() {
544 - $user_id = get_current_user_id();
787 + public static function user_is_paid_subscriber( $valid_plan_ids = array(), $user_id = null ) {
788 + if ( empty( $user_id ) ) {
789 + $user_id = get_current_user_id();
790 + if ( empty( $user_id ) ) {
791 + return false;
792 + }
793 + }
794 + // sort and stringify sorted valid plan ids to use as a cache key
795 + sort( $valid_plan_ids );
796 + $cache_key = $user_id . '_' . implode( ',', $valid_plan_ids );
797 + if ( ! isset( self::$user_is_paid_subscriber_cache[ $cache_key ] ) ) {
798 + require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
799 + if ( empty( $valid_plan_ids ) ) {
800 + $valid_plan_ids = self::get_all_newsletter_plan_ids();
801 + }
802 + $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service( $user_id );
803 + $is_paid_subscriber = $paywall->visitor_can_view_content( $valid_plan_ids, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS );
804 + self::$user_is_paid_subscriber_cache[ $cache_key ] = $is_paid_subscriber;
805 + }
806 + return self::$user_is_paid_subscriber_cache[ $cache_key ];
807 + }
545 808
809 + /**
810 + * Determines whether the current user has a pending subscription.
811 + *
812 + * @return bool Whether the user has a pending subscription
813 + */
814 + public static function user_is_pending_subscriber() {
546 815 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
547 - $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
548 - $is_paid_subscriber = $paywall->visitor_can_view_content( self::get_all_newsletter_plan_ids(), Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS );
549 -
550 - self::$user_is_paid_subscriber_cache[ $user_id ] = $is_paid_subscriber;
551 - return $is_paid_subscriber;
816 + $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
817 + return $subscription_service->is_current_user_pending_subscriber();
552 818 }
553 819
554 820 /**
555 821 * Determines whether the current user can view the post based on the newsletter access level
@@ -554,35 +820,60 @@
554 820 /**
555 821 * Determines whether the current user can view the post based on the newsletter access level
556 822 * and caches the result.
557 823 *
824 + * @param int|null $post_id Explicit post id to check against.
825 + *
558 826 * @return bool Whether the post can be viewed
559 827 */
560 - public static function user_can_view_post() {
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 ) {
561 852 $user_id = get_current_user_id();
562 - $post_id = get_the_ID();
853 + if ( null === $post_id ) {
854 + $post_id = get_the_ID();
855 + }
563 856
564 857 if ( false === $post_id ) {
565 858 $post_id = 0;
566 859 }
567 860
568 - $cache_key = sprintf( '%d_%d', $user_id, $post_id );
569 - if ( $user_id !== 0 && isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
861 + $cache_key = sprintf( '%d_%d_%d', $user_id, $post_id, (int) $allow_editor_access );
862 + if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
570 863 return self::$user_can_view_post_cache[ $cache_key ];
571 864 }
572 865
573 - $post_access_level = self::get_post_access_level();
574 - if ( Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY === $post_access_level ) {
866 + $post_access_level = self::get_post_access_level( $post_id );
867 + if ( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY === $post_access_level ) {
575 868 self::$user_can_view_post_cache[ $cache_key ] = true;
576 869 return true;
577 870 }
578 871
579 - if ( $user_id === 0 ) {
580 - if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS ) {
581 - if ( Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
582 - return true;
583 - }
584 - }
872 + // we are sending the post to subscribers so the user is a subscriber
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 ) {
874 + self::$user_can_view_post_cache[ $cache_key ] = true;
875 + return true;
585 876 }
586 877
587 878 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
588 879 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
@@ -589,17 +880,26 @@
589 880
590 881 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
591 882
592 883 if ( 0 === count( $all_newsletters_plan_ids ) &&
593 - Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
594 - 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 + )
595 888 ) {
596 889 // The post is paywalled but there is no newsletter plans on the site.
597 890 // We downgrade the post level to subscribers-only
598 - $post_access_level = Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
891 + $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
599 892 }
600 893
601 - $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 + }
602 902
603 903 self::$user_can_view_post_cache[ $cache_key ] = $can_view_post;
604 904 return $can_view_post;
605 905 }
@@ -611,13 +911,31 @@
611 911 *
612 912 * @return bool
613 913 */
614 914 public static function is_enabled_jetpack_recurring_payments() {
615 - $api_available = ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_connection_ready() );
915 + $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
616 916 return $api_available;
617 917 }
618 918
619 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 + /**
620 938 * Whether site has any paid plan.
621 939 *
622 940 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
623 941 *
@@ -672,11 +990,13 @@
672 990 * Return all membership plans ids (deleted or not)
673 991 * This function is used both on WPCOM or on Jetpack self-hosted.
674 992 * Depending on the environment we need to mitigate where the data is retrieved from.
675 993 *
994 + * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
995 + *
676 996 * @return array
677 997 */
678 - public static function get_all_newsletter_plan_ids() {
998 + public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
679 999
680 1000 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
681 1001 return array();
682 1002 }
@@ -683,36 +1003,28 @@
683 1003
684 1004 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
685 1005 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
686 1006 if ( ! $is_cached_site ) {
687 - return array_merge(
688 - get_posts(
689 - array(
690 - 'posts_per_page' => -1,
691 - 'fields' => 'ids',
692 - 'post_type' => self::$post_type_plan,
693 - 'meta_query' => array(
694 - 'relation' => 'AND',
695 - array(
696 - 'key' => 'jetpack_memberships_site_subscriber',
697 - 'value' => true,
698 - ),
699 - array(
700 - 'key' => 'jetpack_memberships_interval',
701 - 'value' => 'one-time',
702 - 'compare' => '!=',
703 - ),
704 - ),
705 - )
1007 + $meta_query = array(
1008 + array(
1009 + 'key' => 'jetpack_memberships_type',
1010 + 'value' => self::$type_tier,
706 1011 ),
707 - get_posts(
708 - array(
709 - 'posts_per_page' => -1,
710 - 'fields' => 'ids',
711 - 'post_type' => self::$post_type_plan,
712 - 'meta_key' => 'jetpack_memberships_type',
713 - 'meta_value' => self::$type_tier,
714 - )
1012 + );
1013 +
1014 + if ( $allow_deleted === false ) {
1015 + $meta_query[] = array(
1016 + 'key' => 'jetpack_memberships_is_deleted',
1017 + 'compare' => 'NOT EXISTS',
1018 + );
1019 + }
1020 +
1021 + return get_posts(
1022 + array(
1023 + 'posts_per_page' => -1,
1024 + 'fields' => 'ids',
1025 + 'post_type' => self::$post_type_plan,
1026 + 'meta_query' => $meta_query,
715 1027 )
716 1028 );
717 1029
718 1030 } else {
@@ -717,14 +1029,14 @@
717 1029
718 1030 } else {
719 1031 // On cached site on WPCOM
720 1032 require_lib( 'memberships' );
721 - $only_tiers = true;
722 - $allow_deleted = true;
723 - // In https://github.com/Automattic/gold/issues/190, it needs to be changed to
724 - // Memberships_Product::get_product_list( $this->blog_id, Membership_Product::TIER_TYPE)
725 - $list = Memberships_Product::get_product_list( get_current_blog_id(), null, null, $only_tiers, $allow_deleted );
1033 + $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
726 1034
1035 + if ( is_wp_error( $list ) ) {
1036 + return array();
1037 + }
1038 +
727 1039 return array_map(
728 1040 function ( $product ) {
729 1041 return $product['id'];
730 1042 }, // Returning only post ids
@@ -747,11 +1059,12 @@
747 1059 if ( self::is_enabled_jetpack_recurring_payments() ) {
748 1060 Blocks::jetpack_register_block(
749 1061 'jetpack/recurring-payments',
750 1062 array(
751 - 'render_callback' => array( $this, 'render_button' ),
752 - 'uses_context' => array( 'isPremiumContentChild' ),
753 - '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(
754 1067 'jetpack/parentBlockWidth' => 'width',
755 1068 ),
756 1069 )
757 1070 );
@@ -756,9 +1069,9 @@
756 1069 )
757 1070 );
758 1071 } else {
759 1072 Jetpack_Gutenberg::set_extension_unavailable(
760 - 'jetpack/recurring-payments',
1073 + 'recurring-payments',
761 1074 'missing_plan',
762 1075 array(
763 1076 'required_feature' => 'memberships',
764 1077 'required_plan' => self::$required_plan,
@@ -809,7 +1122,45 @@
809 1122 public static function is_current_user_subscribed() {
810 1123 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
811 1124 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
812 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 );
813 1164 }
814 1165 }
815 1166 Jetpack_Memberships::get_instance();