PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 15.7.2
Jetpack – WP Security, Backup, Speed, & Growth v15.7.2
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 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / memberships / class-jetpack-memberships.php
jetpack / modules / memberships Last commit date
class-jetpack-memberships.php 5 months ago
class-jetpack-memberships.php
1059 lines
1 <?php
2 /**
3 * Jetpack_Memberships: wrapper for memberships functions.
4 *
5 * @package Jetpack
6 * @since 7.3.0
7 */
8
9 use Automattic\Jetpack\Blocks;
10 use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service;
11 use Automattic\Jetpack\Status;
12 use Automattic\Jetpack\Status\Host;
13 use Automattic\Jetpack\Status\Request;
14 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
15 use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit( 0 );
19 }
20
21 require_once __DIR__ . '/../../extensions/blocks/subscriptions/constants.php';
22
23 /**
24 * Class Jetpack_Memberships
25 * This class represents the Memberships functionality.
26 */
27 class Jetpack_Memberships {
28 /**
29 * CSS class prefix to use in the styling.
30 *
31 * @var string
32 */
33 public static $css_classname_prefix = 'jetpack-memberships';
34 /**
35 * Our CPT type for the product (plan).
36 *
37 * @var string
38 */
39 public static $post_type_plan = 'jp_mem_plan';
40
41 /**
42 * Our CPT type for the product (plan).
43 *
44 * @var string
45 */
46 public static $post_type_coupon = 'memberships_coupon';
47
48 /**
49 * Tier type for plans
50 *
51 * @var string
52 */
53 public static $type_tier = 'tier';
54
55 /**
56 * Option stores status for memberships (Stripe, etc.).
57 *
58 * @var string
59 */
60 public static $has_connected_account_option_name = 'jetpack-memberships-has-connected-account';
61
62 /**
63 * Post meta that will store the level of access for newsletters
64 *
65 * @var string
66 */
67 public static $post_access_level_meta_name = META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS;
68
69 /**
70 * Post meta that will store the tier ID of access for newsletters
71 *
72 * @var string
73 */
74 public static $post_access_tier_meta_name = META_NAME_FOR_POST_TIER_ID_SETTINGS;
75
76 /**
77 * Button block type to use.
78 *
79 * @var string
80 */
81 private static $button_block_name = 'recurring-payments';
82
83 /**
84 * These are defaults for wp_kses ran on the membership button.
85 *
86 * @var array
87 */
88 private static $tags_allowed_in_the_button = array( 'br' => array() );
89
90 /**
91 * The minimum required plan for this Gutenberg block.
92 *
93 * @var string Plan slug
94 */
95 private static $required_plan;
96
97 /**
98 * Track recurring payments block registration.
99 *
100 * @var boolean True if block registration has been executed.
101 */
102 private static $has_registered_block = false;
103
104 /**
105 * Classic singleton pattern
106 *
107 * @var Jetpack_Memberships
108 */
109 private static $instance;
110
111 /**
112 * Cached results of user_can_view_post() method.
113 *
114 * @var array
115 */
116 private static $user_can_view_post_cache = array();
117
118 /**
119 * Cached results of user_is_paid_subscriber() method.
120 *
121 * @var array
122 */
123 private static $user_is_paid_subscriber_cache = array();
124
125 /**
126 * Cached results of get_post_access_level method.
127 *
128 * @var array
129 */
130 private static $post_access_level_cache = array();
131
132 /**
133 * Clear cached results of get_post_access_level method.
134 */
135 public static function clear_post_access_level_cache() {
136 self::$post_access_level_cache = array();
137 }
138
139 /**
140 * Currencies we support and Stripe's minimum amount for a transaction in that currency.
141 *
142 * @link https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
143 *
144 * List has to be in with `SUPPORTED_CURRENCIES` in extensions/shared/currencies.js.
145 */
146 const SUPPORTED_CURRENCIES = array(
147 'USD' => 0.5,
148 'AUD' => 0.5,
149 'BRL' => 0.5,
150 'CAD' => 0.5,
151 'CHF' => 0.5,
152 'DKK' => 2.5,
153 'EUR' => 0.5,
154 'GBP' => 0.3,
155 'HKD' => 4.0,
156 'INR' => 0.5,
157 'JPY' => 50,
158 'MXN' => 10,
159 'NOK' => 3.0,
160 'NZD' => 0.5,
161 'PLN' => 2.0,
162 'SEK' => 3.0,
163 'SGD' => 0.5,
164 'CZK' => 15.0,
165 'HUF' => 175.0,
166 'TWD' => 10.0,
167 'IDR' => 0,
168 'ILS' => 0,
169 'PHP' => 0,
170 'RUB' => 0,
171 'TRY' => 0,
172 );
173
174 /**
175 * Jetpack_Memberships constructor.
176 */
177 private function __construct() {}
178
179 /**
180 * The actual constructor initializing the object.
181 *
182 * @return Jetpack_Memberships
183 */
184 public static function get_instance() {
185 if ( ! self::$instance ) {
186 self::$instance = new self();
187 self::$instance->register_init_hook();
188 // Yes, `pro-plan` with a dash, `jetpack_personal` with an underscore. Check the v1.5 endpoint to verify.
189 $wpcom_plan_slug = defined( 'ENABLE_PRO_PLAN' ) ? 'pro-plan' : 'personal-bundle';
190 self::$required_plan = ( new Host() )->is_wpcom_simple() ? $wpcom_plan_slug : 'jetpack_personal';
191 }
192
193 return self::$instance;
194 }
195 /**
196 * Get the map that defines the shape of CPT post. keys are names of fields and
197 * 'meta' is the name of actual WP post meta field that corresponds.
198 *
199 * @return array
200 */
201 private static function get_plan_property_mapping() {
202 $meta_prefix = 'jetpack_memberships_';
203 $properties = array(
204 'price' => array(
205 'meta' => $meta_prefix . 'price',
206 ),
207 'currency' => array(
208 'meta' => $meta_prefix . 'currency',
209 ),
210 'site_subscriber' => array(
211 'meta' => $meta_prefix . 'site_subscriber',
212 ),
213 'product_id' => array(
214 'meta' => $meta_prefix . 'product_id',
215 ),
216 'tier' => array(
217 'meta' => $meta_prefix . 'tier',
218 ),
219 'is_deleted' => array(
220 'meta' => $meta_prefix . 'is_deleted',
221 ),
222 'is_sandboxed' => array(
223 'meta' => $meta_prefix . 'is_sandboxed',
224 ),
225 );
226 return $properties;
227 }
228
229 /**
230 * Inits further hooks on init hook.
231 */
232 private function register_init_hook() {
233 add_action( 'init', array( $this, 'init_hook_action' ) );
234 add_action( 'jetpack_register_gutenberg_extensions', array( $this, 'register_gutenberg_block' ) );
235 // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
236 add_action( 'switch_blog', array( $this, 'clear_post_access_level_cache' ) );
237 }
238
239 /**
240 * Actual hooks initializing on init.
241 */
242 public function init_hook_action() {
243 add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
244 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
245 $this->setup_cpts();
246
247 if ( Jetpack::is_module_active( 'subscriptions' ) && Request::is_frontend() ) {
248 add_action( 'wp_logout', array( $this, 'subscriber_logout' ) );
249 }
250 }
251
252 /**
253 * Logs the subscriber out by clearing out the premium content cookie.
254 */
255 public function subscriber_logout() {
256 if ( ! class_exists( 'Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Abstract_Token_Subscription_Service' ) ) {
257 return;
258 }
259
260 Abstract_Token_Subscription_Service::clear_token_cookie();
261 }
262
263 /**
264 * Sets up the custom post types for the module.
265 */
266 private function setup_cpts() {
267 /*
268 * PLAN data structure.
269 */
270 $capabilities = array(
271 'edit_post' => 'edit_posts',
272 'read_post' => 'read_private_posts',
273 'delete_post' => 'delete_posts',
274 'edit_posts' => 'edit_posts',
275 'edit_others_posts' => 'edit_others_posts',
276 'publish_posts' => 'publish_posts',
277 'read_private_posts' => 'read_private_posts',
278 );
279 $order_args = array(
280 'label' => esc_html__( 'Plan', 'jetpack' ),
281 'description' => esc_html__( 'Recurring Payments plans', 'jetpack' ),
282 'supports' => array( 'title', 'custom-fields', 'content' ),
283 'hierarchical' => false,
284 'public' => false,
285 'show_ui' => false,
286 'show_in_menu' => false,
287 'show_in_admin_bar' => false,
288 'show_in_nav_menus' => false,
289 'can_export' => true,
290 'has_archive' => false,
291 'exclude_from_search' => true,
292 'publicly_queryable' => false,
293 'rewrite' => false,
294 'capabilities' => $capabilities,
295 'show_in_rest' => false,
296 );
297 register_post_type( self::$post_type_plan, $order_args );
298 $coupon_args = array(
299 'label' => esc_html__( 'Coupon', 'jetpack' ),
300 'description' => esc_html__( 'Memberships coupons', 'jetpack' ),
301 'supports' => array( 'title', 'custom-fields', 'content' ),
302 'hierarchical' => false,
303 'public' => false,
304 'show_ui' => false,
305 'show_in_menu' => false,
306 'show_in_admin_bar' => false,
307 'show_in_nav_menus' => false,
308 'can_export' => true,
309 'has_archive' => false,
310 'exclude_from_search' => true,
311 'publicly_queryable' => false,
312 'rewrite' => false,
313 'capabilities' => $capabilities,
314 'show_in_rest' => false,
315 );
316 register_post_type( self::$post_type_coupon, $coupon_args );
317 }
318
319 /**
320 * Allows custom post types to be used by REST API.
321 *
322 * @param array $post_types - other post types.
323 *
324 * @see hook 'rest_api_allowed_post_types'
325 * @return array
326 */
327 public function allow_rest_api_types( $post_types ) {
328 $post_types[] = self::$post_type_plan;
329 $post_types[] = self::$post_type_coupon;
330
331 return $post_types;
332 }
333
334 /**
335 * Allows custom meta fields to sync.
336 *
337 * @param array $post_meta - previously changet post meta.
338 *
339 * @return array
340 */
341 public function allow_sync_post_meta( $post_meta ) {
342 $meta_keys_plans = array_map(
343 array( $this, 'return_meta' ),
344 self::get_plan_property_mapping()
345 );
346
347 $meta_coupons_prefix = self::$post_type_coupon . '_';
348 $meta_keys_coupons = array(
349 $meta_coupons_prefix . 'coupon_code',
350 $meta_coupons_prefix . 'can_be_combined',
351 $meta_coupons_prefix . 'first_time_purchase_only',
352 $meta_coupons_prefix . 'limit_per_user',
353 $meta_coupons_prefix . 'discount_type',
354 $meta_coupons_prefix . 'discount_value',
355 $meta_coupons_prefix . 'discount_percentage',
356 $meta_coupons_prefix . 'discount_currency',
357 $meta_coupons_prefix . 'start_date',
358 $meta_coupons_prefix . 'end_date',
359 $meta_coupons_prefix . 'plan_ids_allow_list',
360 $meta_coupons_prefix . 'duration',
361 $meta_coupons_prefix . 'email_allow_list',
362 $meta_coupons_prefix . 'is_deleted',
363 $meta_coupons_prefix . 'is_sandboxed',
364 );
365
366 return array_merge(
367 $post_meta,
368 array_values( $meta_keys_plans ),
369 $meta_keys_coupons
370 );
371 }
372
373 /**
374 * This returns meta attribute of passet array.
375 * Used for array functions.
376 *
377 * @param array $map - stuff.
378 *
379 * @return mixed
380 */
381 public function return_meta( $map ) {
382 return $map['meta'];
383 }
384
385 /**
386 * Show an error to the user (or embed a clue in the HTML) when the button does not get rendered properly.
387 *
388 * @param WP_Error $error The error message with error code.
389 * @return string The error message rendered as HTML.
390 */
391 public function render_button_error( $error ) {
392 if ( static::user_can_edit() ) {
393 return '<div><strong>Jetpack Memberships Error: ' . $error->get_error_code() . '</strong><br />' . $error->get_error_message() . '</div>';
394 }
395 return '<div>Sorry! This product is not available for purchase at this time.</div><!-- Jetpack Memberships Error: ' . $error->get_error_code() . ' -->';
396 }
397
398 /**
399 * Renders a preview of the Recurring Payment button, which is not hooked
400 * up to the subscription url. Used to preview the block on the frontend
401 * for site editors when Stripe has not been connected.
402 *
403 * @param array $attrs - attributes in the shortcode.
404 * @param string $content - Recurring Payment block content.
405 *
406 * @return string|void
407 */
408 public function render_button_preview( $attrs, $content = null ) {
409 if ( ! empty( $content ) ) {
410 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
411 $content = str_replace( 'recurring-payments-id', $block_id, $content );
412 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
413 return $content;
414 }
415 return $this->deprecated_render_button_v1( $attrs, null );
416 }
417
418 /**
419 * Determines whether the button preview should be rendered. Returns true
420 * if the user has editing permissions, the button is not configured correctly
421 * (because it requires a plan upgrade or Stripe connection), and the
422 * button is a child of a Premium Content block.
423 *
424 * @param WP_Block $block Recurring Payments block instance.
425 *
426 * @return boolean
427 */
428 public function should_render_button_preview( $block ) {
429 $user_can_edit = static::user_can_edit();
430 $requires_stripe_connection = ! static::has_connected_account();
431
432 $jetpack_ready = ! self::is_enabled_jetpack_recurring_payments();
433
434 $is_premium_content_child = false;
435 if ( isset( $block ) && isset( $block->context['isPremiumContentChild'] ) ) {
436 $is_premium_content_child = (int) $block->context['isPremiumContentChild'];
437 }
438
439 return $is_premium_content_child &&
440 $user_can_edit &&
441 $requires_stripe_connection &&
442 $jetpack_ready;
443 }
444
445 /**
446 * Callback that parses the membership purchase shortcode.
447 *
448 * @param array $attributes - attributes in the shortcode. `id` here is the CPT id of the plan.
449 * @param string $content - Recurring Payment block content.
450 * @param WP_Block $block - Recurring Payment block instance.
451 *
452 * @return string|void - HTML for the button, void removes the button.
453 */
454 public function render_button( $attributes, $content = null, $block = null ) {
455 Jetpack_Gutenberg::load_assets_as_required( self::$button_block_name );
456
457 if ( $this->should_render_button_preview( $block ) ) {
458 return $this->render_button_preview( $attributes, $content );
459 }
460
461 if ( empty( $attributes['planId'] ) && empty( $attributes['planIds'] ) ) {
462 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' ) ) );
463 }
464
465 // This is string of '+` separated plan ids. Loop through them and
466 // filter out the ones that are not valid.
467 $plan_ids = array();
468 if ( ! empty( $attributes['planIds'] ) ) {
469 $plan_ids = $attributes['planIds'];
470 } elseif ( ! empty( $attributes['planId'] ) ) {
471 $plan_ids = explode( '+', $attributes['planId'] );
472 }
473 $valid_plans = array();
474 foreach ( $plan_ids as $plan_id ) {
475 if ( ! is_numeric( $plan_id ) ) {
476 continue;
477 }
478 $product = get_post( $plan_id );
479 if ( ! $product ) {
480 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' ) ) );
481 }
482 if ( is_wp_error( $product ) ) {
483 '@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
484 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' ) ) );
485 }
486 if ( $product->post_type !== self::$post_type_plan ) {
487 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' ) ) );
488 }
489 if ( 'publish' !== $product->post_status ) {
490 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' ) ) );
491 }
492 $valid_plans[] = $plan_id;
493 }
494
495 // If none are valid, return.
496 // (Returning like this makes the button disappear.)
497 if ( empty( $valid_plans ) ) {
498 return;
499 }
500 $plan_id = implode( '+', $valid_plans );
501
502 if ( ! empty( $content ) ) {
503 $block_id = esc_attr( wp_unique_id( 'recurring-payments-block-' ) );
504 $content = str_replace( 'recurring-payments-id', $block_id, $content );
505 $content = str_replace( 'wp-block-jetpack-recurring-payments', 'wp-block-jetpack-recurring-payments wp-block-button', $content );
506 $subscribe_url = $this->get_subscription_url( $plan_id );
507
508 $content = preg_replace( '/(href=".*")/U', 'href="' . $subscribe_url . '"', $content );
509 $content = wp_kses_post( $content );
510
511 return $content;
512 }
513
514 return $this->deprecated_render_button_v1( $attributes, $plan_id );
515 }
516
517 /**
518 * Render email callback.
519 *
520 * @param string $block_content The block content.
521 * @param array $parsed_block The parsed block data.
522 * @param object $rendering_context The email rendering context.
523 *
524 * @return string
525 */
526 public function render_button_email( $block_content, array $parsed_block, $rendering_context ) {
527 // Check for the required renderers.
528 if ( ! function_exists( '\Automattic\Jetpack\Extensions\Button\render_email' ) || ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Button' ) ) {
529 return '';
530 }
531
532 // Get the first inner block, which should be the button block.
533 $button_block = $parsed_block['innerBlocks'][0] ?? array();
534
535 // We should only accept button blocks.
536 if ( empty( $button_block['blockName'] ) || 'jetpack/button' !== $button_block['blockName'] ) {
537 return '';
538 }
539
540 // We need attributes.
541 if ( ! isset( $button_block['attrs'] ) || ! is_array( $button_block['attrs'] ) ) {
542 return '';
543 }
544
545 // If the button block is missing text or url, return empty string.
546 if ( empty( $button_block['attrs']['text'] ) || empty( $button_block['attrs']['url'] ) ) {
547 return '';
548 }
549
550 // Reuse the button block's email rendering method.
551 return \Automattic\Jetpack\Extensions\Button\render_email( $block_content, $button_block, $rendering_context );
552 }
553
554 /**
555 * Builds subscription URL for this membership using the current blog and
556 * supplied plan IDs.
557 *
558 * @param integer $plan_id - Unique ID for the plan being subscribed to.
559 * @return string
560 */
561 public function get_subscription_url( $plan_id ) {
562 global $wp;
563
564 return add_query_arg(
565 array(
566 'blog' => esc_attr( self::get_blog_id() ),
567 'plan' => esc_attr( $plan_id ),
568 'lang' => esc_attr( get_locale() ),
569 'pid' => esc_attr( get_the_ID() ), // Needed for analytics purposes.
570 'redirect' => esc_attr( rawurlencode( home_url( $wp->request ) ) ), // Needed for redirect back in case of redirect-based flow.
571 ),
572 'https://subscribe.wordpress.com/memberships/'
573 );
574 }
575
576 /**
577 * Renders a deprecated legacy version of the button HTML.
578 *
579 * @param array $attrs - Array containing the Recurring Payment block attributes.
580 * @param integer $plan_id - Unique plan ID the membership is for.
581 *
582 * @return string
583 */
584 public function deprecated_render_button_v1( $attrs, $plan_id ) {
585 $button_label = isset( $attrs['submitButtonText'] )
586 ? $attrs['submitButtonText']
587 : __( 'Your contribution', 'jetpack' );
588
589 $button_styles = array();
590 if ( ! empty( $attrs['customBackgroundButtonColor'] ) ) {
591 array_push(
592 $button_styles,
593 sprintf(
594 'background-color: %s',
595 sanitize_hex_color( $attrs['customBackgroundButtonColor'] )
596 )
597 );
598 }
599 if ( ! empty( $attrs['customTextButtonColor'] ) ) {
600 array_push(
601 $button_styles,
602 sprintf(
603 'color: %s',
604 sanitize_hex_color( $attrs['customTextButtonColor'] )
605 )
606 );
607 }
608 $button_styles = implode( ';', $button_styles );
609
610 return sprintf(
611 '<div class="%1$s"><a role="button" href="%2$s" class="%3$s" style="%4$s">%5$s</a></div>',
612 esc_attr(
613 Blocks::classes(
614 self::$button_block_name,
615 $attrs,
616 array( 'wp-block-button' )
617 )
618 ),
619 esc_url( $this->get_subscription_url( $plan_id ) ),
620 isset( $attrs['submitButtonClasses'] ) ? esc_attr( $attrs['submitButtonClasses'] ) : 'wp-block-button__link',
621 esc_attr( $button_styles ),
622 wp_kses( $button_label, self::$tags_allowed_in_the_button )
623 );
624 }
625
626 /**
627 * Get current blog id.
628 *
629 * @return int
630 */
631 public static function get_blog_id() {
632 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
633 return get_current_blog_id();
634 }
635
636 return Jetpack_Options::get_option( 'id' );
637 }
638
639 /**
640 * Get the id of the connected payment acount (Stripe etc).
641 *
642 * @return bool
643 */
644 public static function has_connected_account() {
645
646 // This is the primary solution.
647 $has_option = get_option( self::$has_connected_account_option_name, false ) ? true : false;
648 if ( $has_option ) {
649 return true;
650 }
651
652 return false;
653 }
654
655 /**
656 * Get the post access level
657 *
658 * If no ID is provided, the method tries to get it from the global post object.
659 *
660 * @param int|null $post_id The ID of the post. Default is null.
661 *
662 * @return string the actual post access level (see projects/plugins/jetpack/extensions/blocks/subscriptions/constants.js for the values).
663 */
664 public static function get_post_access_level( $post_id = null ) {
665 if ( ! $post_id ) {
666 $post_id = get_the_ID();
667 }
668 if ( ! $post_id ) {
669 return Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
670 }
671
672 $blog_id = get_current_blog_id();
673 $cache_key = $blog_id . '_' . $post_id;
674
675 if ( isset( self::$post_access_level_cache[ $cache_key ] ) ) {
676 return self::$post_access_level_cache[ $cache_key ];
677 }
678
679 $post_access_level = get_post_meta( $post_id, self::$post_access_level_meta_name, true );
680 if ( empty( $post_access_level ) ) {
681 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY;
682 }
683
684 self::$post_access_level_cache[ $cache_key ] = $post_access_level;
685
686 return $post_access_level;
687 }
688
689 /**
690 * Get the post tier plan
691 *
692 * If no ID is provided, the method tries to get it from the global post object.
693 *
694 * @param int|null $post_id The ID of the post. Default is null.
695 *
696 * @return WP_Post|null the actual post tier.
697 */
698 public static function get_post_tier( $post_id = null ) {
699 if ( ! $post_id ) {
700 $post_id = get_the_ID();
701 }
702
703 if ( ! $post_id ) {
704 return null;
705 }
706
707 $post_tier_id = get_post_meta( $post_id, self::$post_access_tier_meta_name, true );
708 if ( empty( $post_tier_id ) ) {
709 return null;
710 }
711
712 return get_post( $post_tier_id );
713 }
714
715 /**
716 * Determines whether the current user can edit.
717 *
718 * @return bool Whether the user can edit.
719 */
720 public static function user_can_edit() {
721 $user = wp_get_current_user();
722 // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol
723 return 0 !== $user->ID && current_user_can( 'edit_post', get_the_ID() );
724 }
725
726 /**
727 * Clears the static cache for all users or for a given user.
728 *
729 * @param int|null $user_id The user_id to unset in the cache, otherwise the entire static cache is cleared.
730 * @return void
731 */
732 public static function clear_cache( ?int $user_id = null ) {
733 if ( empty( $user_id ) ) {
734 self::$user_is_paid_subscriber_cache = array();
735 self::$user_can_view_post_cache = array();
736 return;
737 }
738 unset( self::$user_is_paid_subscriber_cache[ $user_id ] );
739 unset( self::$user_can_view_post_cache[ $user_id ] );
740 }
741
742 /**
743 * Determines whether the current user is a paid subscriber and caches the result.
744 *
745 * @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.
746 * @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).
747 * @return bool Whether the post can be viewed
748 */
749 public static function user_is_paid_subscriber( $valid_plan_ids = array(), $user_id = null ) {
750 if ( empty( $user_id ) ) {
751 $user_id = get_current_user_id();
752 if ( empty( $user_id ) ) {
753 return false;
754 }
755 }
756 // sort and stringify sorted valid plan ids to use as a cache key
757 sort( $valid_plan_ids );
758 $cache_key = $user_id . '_' . implode( ',', $valid_plan_ids );
759 if ( ! isset( self::$user_is_paid_subscriber_cache[ $cache_key ] ) ) {
760 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
761 if ( empty( $valid_plan_ids ) ) {
762 $valid_plan_ids = self::get_all_newsletter_plan_ids();
763 }
764 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service( $user_id );
765 $is_paid_subscriber = $paywall->visitor_can_view_content( $valid_plan_ids, Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS );
766 self::$user_is_paid_subscriber_cache[ $cache_key ] = $is_paid_subscriber;
767 }
768 return self::$user_is_paid_subscriber_cache[ $cache_key ];
769 }
770
771 /**
772 * Determines whether the current user has a pending subscription.
773 *
774 * @return bool Whether the user has a pending subscription
775 */
776 public static function user_is_pending_subscriber() {
777 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
778 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
779 return $subscription_service->is_current_user_pending_subscriber();
780 }
781
782 /**
783 * Determines whether the current user can view the post based on the newsletter access level
784 * and caches the result.
785 *
786 * @param int|null $post_id Explicit post id to check against.
787 *
788 * @return bool Whether the post can be viewed
789 */
790 public static function user_can_view_post( $post_id = null ) {
791 $user_id = get_current_user_id();
792 if ( null === $post_id ) {
793 $post_id = get_the_ID();
794 }
795
796 if ( false === $post_id ) {
797 $post_id = 0;
798 }
799
800 $cache_key = sprintf( '%d_%d', $user_id, $post_id );
801 if ( isset( self::$user_can_view_post_cache[ $cache_key ] ) ) {
802 return self::$user_can_view_post_cache[ $cache_key ];
803 }
804
805 $post_access_level = self::get_post_access_level( $post_id );
806 if ( Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_EVERYBODY === $post_access_level ) {
807 self::$user_can_view_post_cache[ $cache_key ] = true;
808 return true;
809 }
810
811 // we are sending the post to subscribers so the user is a subscriber
812 if ( defined( 'WPCOM_SENDING_POST_TO_SUBSCRIBERS' ) && WPCOM_SENDING_POST_TO_SUBSCRIBERS && Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS === $post_access_level ) {
813 self::$user_can_view_post_cache[ $cache_key ] = true;
814 return true;
815 }
816
817 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
818 $paywall = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
819
820 $all_newsletters_plan_ids = self::get_all_newsletter_plan_ids();
821
822 if ( 0 === count( $all_newsletters_plan_ids ) &&
823 Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS === $post_access_level ||
824 Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_PAID_SUBSCRIBERS_ALL_TIERS === $post_access_level
825 ) {
826 // The post is paywalled but there is no newsletter plans on the site.
827 // We downgrade the post level to subscribers-only
828 $post_access_level = Abstract_Token_Subscription_Service::POST_ACCESS_LEVEL_SUBSCRIBERS;
829 }
830
831 $can_view_post = $paywall->visitor_can_view_content( $all_newsletters_plan_ids, $post_access_level );
832
833 self::$user_can_view_post_cache[ $cache_key ] = $can_view_post;
834 return $can_view_post;
835 }
836
837 /**
838 * Whether Recurring Payments are enabled. True if the block
839 * is supported by the site's plan, or if it is a Jetpack site
840 * and the feature to enable upgrade nudges is active.
841 *
842 * @return bool
843 */
844 public static function is_enabled_jetpack_recurring_payments() {
845 $api_available = ( new Host() )->is_wpcom_simple() || Jetpack::is_connection_ready();
846 return $api_available;
847 }
848
849 /**
850 * Whether to enable the blocks in the editor.
851 * All Monetize blocks (except Simple Payments) need a user with at least `edit_posts` capability
852 *
853 * @return bool
854 */
855 public static function should_enable_monetize_blocks_in_editor() {
856 if ( ! is_admin() ) {
857 // We enable the block for the front-end in all cases
858 return true;
859
860 }
861
862 $is_offline_mode = ( new Status() )->is_offline_mode();
863 $enable_monetize_blocks_in_editor = ( new Host() )->is_wpcom_simple() || ( ! $is_offline_mode );
864 return $enable_monetize_blocks_in_editor;
865 }
866
867 /**
868 * Whether site has any paid plan.
869 *
870 * @param string $type - Type of a plan for which site is configured. For now supports empty and newsletter.
871 *
872 * @return bool
873 */
874 public static function has_configured_plans_jetpack_recurring_payments( $type = '' ) {
875 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
876 return false;
877 }
878 $query = array(
879 'post_type' => self::$post_type_plan,
880 'posts_per_page' => 1,
881 );
882
883 // We want to see if user has any plan marked as a newsletter set up.
884 if ( 'newsletter' === $type ) {
885 $query['meta_key'] = 'jetpack_memberships_site_subscriber';
886 $query['meta_value'] = true;
887 }
888
889 $plans = get_posts( $query );
890 return ( is_countable( $plans ) && count( $plans ) > 0 );
891 }
892
893 /**
894 * Return the list of plan posts
895 *
896 * @return WP_Post[]|WP_Error
897 */
898 public static function get_all_plans() {
899 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
900 return array();
901 }
902
903 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
904 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
905 if ( ! $is_cached_site ) {
906 return get_posts(
907 array(
908 'posts_per_page' => -1,
909 'post_type' => self::$post_type_plan,
910 )
911 );
912 } else {
913 // On cached site on WPCOM
914 require_lib( 'memberships' );
915 return Memberships_Product::get_plans_posts_list( get_current_blog_id() );
916 }
917 }
918
919 /**
920 * Return all membership plans ids (deleted or not)
921 * This function is used both on WPCOM or on Jetpack self-hosted.
922 * Depending on the environment we need to mitigate where the data is retrieved from.
923 *
924 * @param bool $allow_deleted Whether to allow deleted plans to be returned. Defaults to true.
925 *
926 * @return array
927 */
928 public static function get_all_newsletter_plan_ids( $allow_deleted = true ) {
929
930 if ( ! self::is_enabled_jetpack_recurring_payments() ) {
931 return array();
932 }
933
934 // We can retrieve the data directly except on a Jetpack/Atomic cached site or
935 $is_cached_site = ( new Host() )->is_wpcom_simple() && is_jetpack_site();
936 if ( ! $is_cached_site ) {
937 $meta_query = array(
938 array(
939 'key' => 'jetpack_memberships_type',
940 'value' => self::$type_tier,
941 ),
942 );
943
944 if ( $allow_deleted === false ) {
945 $meta_query[] = array(
946 'key' => 'jetpack_memberships_is_deleted',
947 'compare' => 'NOT EXISTS',
948 );
949 }
950
951 return get_posts(
952 array(
953 'posts_per_page' => -1,
954 'fields' => 'ids',
955 'post_type' => self::$post_type_plan,
956 'meta_query' => $meta_query,
957 )
958 );
959
960 } else {
961 // On cached site on WPCOM
962 require_lib( 'memberships' );
963 $list = Memberships_Product::get_product_list( get_current_blog_id(), self::$type_tier, null, $allow_deleted );
964
965 if ( is_wp_error( $list ) ) {
966 return array();
967 }
968
969 return array_map(
970 function ( $product ) {
971 return $product['id'];
972 }, // Returning only post ids
973 $list
974 );
975 }
976 }
977
978 /**
979 * Register the Recurring Payments Gutenberg block
980 */
981 public function register_gutenberg_block() {
982 // This gate was introduced to prevent duplicate registration. A race condition exists where
983 // the registration that happens via extensions/blocks/recurring-payments/recurring-payments.php
984 // was adding the registration action after the action had been run in some contexts.
985 if ( self::$has_registered_block ) {
986 return;
987 }
988
989 if ( self::is_enabled_jetpack_recurring_payments() ) {
990 Blocks::jetpack_register_block(
991 'jetpack/recurring-payments',
992 array(
993 'render_callback' => array( $this, 'render_button' ),
994 'render_email_callback' => array( $this, 'render_button_email' ),
995 'uses_context' => array( 'isPremiumContentChild' ),
996 'provides_context' => array(
997 'jetpack/parentBlockWidth' => 'width',
998 ),
999 )
1000 );
1001 } else {
1002 Jetpack_Gutenberg::set_extension_unavailable(
1003 'recurring-payments',
1004 'missing_plan',
1005 array(
1006 'required_feature' => 'memberships',
1007 'required_plan' => self::$required_plan,
1008 )
1009 );
1010 }
1011
1012 self::$has_registered_block = true;
1013 }
1014
1015 /**
1016 * Transforms a number into it's short human-readable version.
1017 *
1018 * @param int $subscribers_total The extrapolated excerpt string.
1019 *
1020 * @return string Human-readable version of the number. ie. 1.9 M.
1021 */
1022 public static function get_join_others_text( $subscribers_total ) {
1023 if ( $subscribers_total >= 1000000 ) {
1024 /* translators: %s: number of folks following the blog, millions(M) with one decimal. i.e. 1.1 */
1025 return sprintf( __( 'Join %sM other subscribers', 'jetpack' ), floatval( number_format_i18n( $subscribers_total / 1000000, 1 ) ) );
1026 }
1027 if ( $subscribers_total >= 10000 ) {
1028 /* translators: %s: number of folks following the blog, thousands(K) with one decimal. i.e. 1.1 */
1029 return sprintf( __( 'Join %sK other subscribers', 'jetpack' ), floatval( number_format_i18n( $subscribers_total / 1000, 1 ) ) );
1030 }
1031
1032 /* translators: %s: number of folks following the blog */
1033 return sprintf( _n( 'Join %s other subscriber', 'Join %s other subscribers', $subscribers_total, 'jetpack' ), number_format_i18n( $subscribers_total ) );
1034 }
1035
1036 /**
1037 * Returns the email of the current user.
1038 *
1039 * @return string
1040 */
1041 public static function get_current_user_email() {
1042 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
1043 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
1044 return $subscription_service->get_subscriber_email();
1045 }
1046
1047 /**
1048 * Returns if the current user is subscribed or not.
1049 *
1050 * @return boolean
1051 */
1052 public static function is_current_user_subscribed() {
1053 require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php';
1054 $subscription_service = \Automattic\Jetpack\Extensions\Premium_Content\subscription_service();
1055 return $subscription_service->is_current_user_subscribed();
1056 }
1057 }
1058 Jetpack_Memberships::get_instance();
1059