PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.8.1
Jetpack – WP Security, Backup, Speed, & Growth v14.8.1
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 / simple-payments / simple-payments.php
jetpack / modules / simple-payments Last commit date
paypal-express-checkout.js 1 year ago simple-payments.css 1 year ago simple-payments.php 1 year ago
simple-payments.php
806 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Simple Payments lets users embed a PayPal button fully integrated with wpcom to sell products on the site.
4 * This is not a proper module yet, because not all the pieces are in place. Until everything is shipped, it can be turned
5 * into module that can be enabled/disabled.
6 *
7 * @package automattic/jetpack
8 */
9
10 use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
11 use Automattic\Jetpack\Status\Request;
12
13 /**
14 * Jetpack_Simple_Payments
15 */
16 class Jetpack_Simple_Payments {
17 // These have to be under 20 chars because that is CPT limit.
18
19 /**
20 * Post type order.
21 *
22 * @var string
23 */
24 public static $post_type_order = 'jp_pay_order';
25
26 /**
27 * Post type product.
28 *
29 * @var string
30 */
31 public static $post_type_product = 'jp_pay_product';
32
33 /**
34 * Define simple payment shortcode.
35 *
36 * @var string
37 */
38 public static $shortcode = 'simple-payment';
39
40 /**
41 * Define simple payment CSS prefix.
42 *
43 * @var string
44 */
45 public static $css_classname_prefix = 'jetpack-simple-payments';
46
47 /**
48 * Which plan the user is on.
49 *
50 * @var string value_bundle or jetpack_premium
51 */
52 public static $required_plan;
53
54 /**
55 * Instance of the class.
56 *
57 * @var Jetpack_Simple_Payments
58 */
59 private static $instance;
60
61 /**
62 * Construction function.
63 */
64 private function __construct() {}
65
66 /**
67 * Original singleton.
68 *
69 * @todo Remove this when nothing calles getInstance anymore.
70 *
71 * @deprecated 10.8
72 */
73 public static function getInstance() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
74 _deprecated_function( __METHOD__, 'Jetpack 10.7.0', 'Jetpack_Simple_Payments::get_instance' );
75 return self::get_instance();
76 }
77
78 /**
79 * Create instance of class.
80 */
81 public static function get_instance() {
82 if ( ! self::$instance ) {
83 self::$instance = new self();
84 self::$instance->register_init_hooks();
85 self::$required_plan = ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ? 'value_bundle' : 'jetpack_premium';
86 }
87 return self::$instance;
88 }
89
90 /**
91 * Register scripts and styles.
92 */
93 private function register_scripts_and_styles() {
94 /**
95 * Paypal heavily discourages putting that script in your own server:
96 *
97 * @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/
98 */
99 wp_register_script( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Ignored here instead of on the $ver param line since wpcom isn't in sync with ruleset changes in: https://github.com/Automattic/jetpack/pull/28199
100 'paypal-checkout-js',
101 'https://www.paypalobjects.com/api/checkout.js',
102 array(),
103 null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
104 true
105 );
106 wp_register_script(
107 'jetpack-paypal-express-checkout',
108 plugins_url( '/paypal-express-checkout.js', __FILE__ ),
109 array( 'jquery', 'paypal-checkout-js' ),
110 JETPACK__VERSION,
111 false
112 );
113 wp_register_style(
114 'jetpack-simple-payments',
115 plugins_url( '/simple-payments.css', __FILE__ ),
116 array( 'dashicons' ),
117 JETPACK__VERSION,
118 false
119 );
120 }
121
122 /**
123 * Register init hooks.
124 */
125 private function register_init_hooks() {
126 add_action( 'init', array( $this, 'init_hook_action' ) );
127 add_action( 'rest_api_init', array( $this, 'register_meta_fields_in_rest_api' ) );
128 }
129
130 /**
131 * Register the shortcode.
132 */
133 private function register_shortcode() {
134 add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
135 }
136
137 /**
138 * Actions that are run on init.
139 */
140 public function init_hook_action() {
141 add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
142 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
143 if ( ! is_admin() ) {
144 $this->register_scripts_and_styles();
145 }
146 $this->register_shortcode();
147 $this->setup_cpts();
148
149 add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
150 }
151
152 /**
153 * Enqueue the static assets needed in the frontend.
154 */
155 public function enqueue_frontend_assets() {
156 if ( ! wp_style_is( 'jetpack-simple-payments', 'enqueued' ) ) {
157 wp_enqueue_style( 'jetpack-simple-payments' );
158 }
159
160 if ( ! wp_script_is( 'jetpack-paypal-express-checkout', 'enqueued' ) ) {
161 wp_enqueue_script( 'jetpack-paypal-express-checkout' );
162 }
163 }
164
165 /**
166 * Add an inline script for setting up the PayPal checkout button.
167 *
168 * @param int $id Product ID.
169 * @param int $dom_id ID of the DOM element with the purchase message.
170 * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
171 */
172 public function setup_paypal_checkout_button( $id, $dom_id, $is_multiple ) {
173 wp_add_inline_script(
174 'jetpack-paypal-express-checkout',
175 sprintf(
176 "try{PaypalExpressCheckout.renderButton( '%d', '%d', '%s', '%d' );}catch(e){}",
177 esc_js( $this->get_blog_id() ),
178 esc_js( $id ),
179 esc_js( $dom_id ),
180 esc_js( $is_multiple )
181 )
182 );
183 }
184
185 /**
186 * Remove auto paragraph from product description.
187 *
188 * @param string $content - the content of the post.
189 */
190 public function remove_auto_paragraph_from_product_description( $content ) {
191 if ( get_post_type() === self::$post_type_product ) {
192 remove_filter( 'the_content', 'wpautop' );
193 }
194
195 return $content;
196 }
197
198 /** Return the blog ID */
199 public function get_blog_id() {
200 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
201 return get_current_blog_id();
202 }
203
204 return Jetpack_Options::get_option( 'id' );
205 }
206
207 /**
208 * Used to check whether Simple Payments are enabled for given site.
209 *
210 * @return bool True if Simple Payments are enabled, false otherwise.
211 */
212 public function is_enabled_jetpack_simple_payments() {
213 /**
214 * Can be used by plugin authors to disable the conflicting output of Simple Payments.
215 *
216 * @since 6.3.0
217 *
218 * @param bool True if Simple Payments should be disabled, false otherwise.
219 */
220 if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
221 return false;
222 }
223
224 return ( ( defined( 'IS_WPCOM' ) && IS_WPCOM )
225 || Jetpack::is_connection_ready() )
226 &&
227 Jetpack_Plan::supports( 'simple-payments' );
228 }
229
230 /**
231 * Get a WP_Post representation of a product
232 *
233 * @param int $id The ID of the product.
234 *
235 * @return array|false|WP_Post
236 */
237 private function get_product( $id ) {
238 if ( ! $id ) {
239 return false;
240 }
241
242 $product = get_post( $id );
243 if ( ! $product || is_wp_error( $product ) ) {
244 return false;
245 }
246 if ( $product->post_type !== self::$post_type_product || 'publish' !== $product->post_status ) {
247 return false;
248 }
249 return $product;
250 }
251
252 /**
253 * Creates the content from a shortcode
254 *
255 * @param array $attrs Shortcode attributes.
256 * @param mixed $content unused.
257 *
258 * @return string|void
259 */
260 public function parse_shortcode( $attrs, $content = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
261 if ( empty( $attrs['id'] ) ) {
262 return;
263 }
264 $product = $this->get_product( $attrs['id'] );
265 if ( ! $product ) {
266 return;
267 }
268
269 // We allow for overriding the presentation labels.
270 $data = shortcode_atts(
271 array(
272 'blog_id' => $this->get_blog_id(),
273 'dom_id' => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
274 'class' => self::$css_classname_prefix . '-' . $product->ID,
275 'title' => get_the_title( $product ),
276 'description' => $product->post_content,
277 'cta' => get_post_meta( $product->ID, 'spay_cta', true ),
278 'multiple' => get_post_meta( $product->ID, 'spay_multiple', true ) || '0',
279 ),
280 $attrs
281 );
282
283 $data['price'] = $this->format_price(
284 get_post_meta( $product->ID, 'spay_price', true ),
285 get_post_meta( $product->ID, 'spay_currency', true )
286 );
287
288 $data['id'] = $attrs['id'];
289
290 if ( ! $this->is_enabled_jetpack_simple_payments() ) {
291 if ( Request::is_frontend() ) {
292 return $this->output_admin_warning( $data );
293 }
294 return;
295 }
296
297 $this->enqueue_frontend_assets();
298 $this->setup_paypal_checkout_button( $attrs['id'], $data['dom_id'], $data['multiple'] );
299
300 return $this->output_shortcode( $data );
301 }
302
303 /**
304 * Output an admin warning if user can't use Pay with PayPal.
305 *
306 * @param array $data unused.
307 */
308 public function output_admin_warning( $data ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
309 if ( ! current_user_can( 'manage_options' ) ) {
310 return;
311 }
312
313 require_once JETPACK__PLUGIN_DIR . '_inc/lib/components.php';
314 return Jetpack_Components::render_upgrade_nudge(
315 array(
316 'plan' => self::$required_plan,
317 )
318 );
319 }
320
321 /**
322 * Get the HTML output to use as PayPal purchase box.
323 *
324 * @param string $dom_id ID of the DOM element with the purchase message.
325 * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
326 *
327 * @return string
328 */
329 public function output_purchase_box( $dom_id, $is_multiple ) {
330 $items = '';
331 $css_prefix = self::$css_classname_prefix;
332
333 if ( $is_multiple ) {
334 $items = sprintf(
335 '
336 <div class="%1$s">
337 <input class="%2$s" type="number" value="1" min="1" id="%3$s" />
338 </div>
339 ',
340 esc_attr( "{$css_prefix}-items" ),
341 esc_attr( "{$css_prefix}-items-number" ),
342 esc_attr( "{$dom_id}_number" )
343 );
344 }
345
346 return sprintf(
347 '<div class="%1$s" id="%2$s"></div><div class="%3$s">%4$s<div class="%5$s" id="%6$s"></div></div>',
348 esc_attr( "{$css_prefix}-purchase-message" ),
349 esc_attr( "{$dom_id}-message-container" ),
350 esc_attr( "{$css_prefix}-purchase-box" ),
351 $items,
352 esc_attr( "{$css_prefix}-button" ),
353 esc_attr( "{$dom_id}_button" )
354 );
355 }
356
357 /**
358 * Get the HTML output to replace the `simple-payments` shortcode.
359 *
360 * @param array $data Product data.
361 * @return string
362 */
363 public function output_shortcode( $data ) {
364 $css_prefix = self::$css_classname_prefix;
365
366 $image = '';
367 if ( has_post_thumbnail( $data['id'] ) ) {
368 $image = sprintf(
369 '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
370 esc_attr( "{$css_prefix}-product-image" ),
371 esc_attr( "{$css_prefix}-image" ),
372 get_the_post_thumbnail( $data['id'], 'full' )
373 );
374 }
375
376 return sprintf(
377 '
378 <div class="%1$s">
379 <div class="%2$s">
380 %3$s
381 <div class="%4$s">
382 <div class="%5$s"><p>%6$s</p></div>
383 <div class="%7$s"><p>%8$s</p></div>
384 <div class="%9$s"><p>%10$s</p></div>
385 %11$s
386 </div>
387 </div>
388 </div>
389 ',
390 esc_attr( "{$data['class']} {$css_prefix}-wrapper" ),
391 esc_attr( "{$css_prefix}-product" ),
392 $image,
393 esc_attr( "{$css_prefix}-details" ),
394 esc_attr( "{$css_prefix}-title" ),
395 esc_html( $data['title'] ),
396 esc_attr( "{$css_prefix}-description" ),
397 wp_kses( $data['description'], wp_kses_allowed_html( 'post' ) ),
398 esc_attr( "{$css_prefix}-price" ),
399 esc_html( $data['price'] ),
400 $this->output_purchase_box( $data['dom_id'], $data['multiple'] )
401 );
402 }
403
404 /**
405 * Format a price with currency
406 *
407 * Uses currency-aware formatting to output a formatted price with a simple fallback.
408 *
409 * Largely inspired by WordPress.com's Store_Price::display_currency
410 *
411 * @param string $price Price.
412 * @param string $currency Currency.
413 * @return string Formatted price.
414 */
415 private function format_price( $price, $currency ) {
416 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-currencies.php';
417 return Jetpack_Currencies::format_price( $price, $currency );
418 }
419
420 /**
421 * Allows custom post types to be used by REST API.
422 *
423 * @param array $post_types - the allows post types.
424 * @see hook 'rest_api_allowed_post_types'
425 * @return array
426 */
427 public function allow_rest_api_types( $post_types ) {
428 $post_types[] = self::$post_type_order;
429 $post_types[] = self::$post_type_product;
430 return $post_types;
431 }
432
433 /**
434 * Merge $post_meta with additional meta information.
435 *
436 * @param array $post_meta - the post's meta information.
437 */
438 public function allow_sync_post_meta( $post_meta ) {
439 return array_merge(
440 $post_meta,
441 array(
442 'spay_paypal_id',
443 'spay_status',
444 'spay_product_id',
445 'spay_quantity',
446 'spay_price',
447 'spay_customer_email',
448 'spay_currency',
449 'spay_cta',
450 'spay_email',
451 'spay_multiple',
452 'spay_formatted_price',
453 )
454 );
455 }
456
457 /**
458 * Enable Simple payments custom meta values for access through the REST API.
459 * Field’s value will be exposed on a .meta key in the endpoint response,
460 * and WordPress will handle setting up the callbacks for reading and writing
461 * to that meta key.
462 *
463 * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
464 */
465 public function register_meta_fields_in_rest_api() {
466 register_meta(
467 'post',
468 'spay_price',
469 array(
470 'description' => esc_html__( 'Simple payments; price.', 'jetpack' ),
471 'object_subtype' => self::$post_type_product,
472 'sanitize_callback' => array( $this, 'sanitize_price' ),
473 'show_in_rest' => true,
474 'single' => true,
475 'type' => 'number',
476 )
477 );
478
479 register_meta(
480 'post',
481 'spay_currency',
482 array(
483 'description' => esc_html__( 'Simple payments; currency code.', 'jetpack' ),
484 'object_subtype' => self::$post_type_product,
485 'sanitize_callback' => array( $this, 'sanitize_currency' ),
486 'show_in_rest' => true,
487 'single' => true,
488 'type' => 'string',
489 )
490 );
491
492 register_meta(
493 'post',
494 'spay_cta',
495 array(
496 'description' => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack' ),
497 'object_subtype' => self::$post_type_product,
498 'sanitize_callback' => 'sanitize_text_field',
499 'show_in_rest' => true,
500 'single' => true,
501 'type' => 'string',
502 )
503 );
504
505 register_meta(
506 'post',
507 'spay_multiple',
508 array(
509 'description' => esc_html__( 'Simple payments; allow multiple items', 'jetpack' ),
510 'object_subtype' => self::$post_type_product,
511 'sanitize_callback' => 'rest_sanitize_boolean',
512 'show_in_rest' => true,
513 'single' => true,
514 'type' => 'boolean',
515 )
516 );
517
518 register_meta(
519 'post',
520 'spay_email',
521 array(
522 'description' => esc_html__( 'Simple payments button; paypal email.', 'jetpack' ),
523 'object_subtype' => self::$post_type_product,
524 'sanitize_callback' => 'sanitize_email',
525 'show_in_rest' => true,
526 'single' => true,
527 'type' => 'string',
528 )
529 );
530
531 register_meta(
532 'post',
533 'spay_status',
534 array(
535 'description' => esc_html__( 'Simple payments; status.', 'jetpack' ),
536 'object_subtype' => self::$post_type_product,
537 'sanitize_callback' => 'sanitize_text_field',
538 'show_in_rest' => true,
539 'single' => true,
540 'type' => 'string',
541 )
542 );
543 }
544
545 /**
546 * Sanitize three-character ISO-4217 Simple payments currency
547 *
548 * List has to be in sync with list at the block's client side and widget's backend side:
549 *
550 * @param array $currency - list of currencies.
551 * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
552 * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/widgets/simple-payments.php#L19-L44
553 *
554 * Currencies should be supported by PayPal:
555 * @link https://developer.paypal.com/docs/api/reference/currency-codes/
556 *
557 * Indian Rupee (INR) not supported because at the time of the creation of this file
558 * because it's limited to in-country PayPal India accounts only.
559 * Discussion: https://github.com/Automattic/wp-calypso/pull/28236
560 */
561 public static function sanitize_currency( $currency ) {
562 $valid_currencies = array(
563 'USD',
564 'EUR',
565 'AUD',
566 'BRL',
567 'CAD',
568 'CZK',
569 'DKK',
570 'HKD',
571 'HUF',
572 'ILS',
573 'JPY',
574 'MYR',
575 'MXN',
576 'TWD',
577 'NZD',
578 'NOK',
579 'PHP',
580 'PLN',
581 'GBP',
582 'RUB',
583 'SGD',
584 'SEK',
585 'CHF',
586 'THB',
587 );
588
589 return in_array( $currency, $valid_currencies, true ) ? $currency : false;
590 }
591
592 /**
593 * Sanitize price:
594 *
595 * Positive integers and floats
596 * Supports two decimal places.
597 * Maximum length: 10.
598 *
599 * See `price` from PayPal docs:
600 *
601 * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
602 *
603 * @param string $price - the price we want to sanitize.
604 * @return null|string
605 */
606 public static function sanitize_price( $price ) {
607 return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
608 }
609
610 /**
611 * Sets up the custom post types for the module.
612 */
613 public function setup_cpts() {
614 /*
615 * ORDER data structure. holds:
616 * title = customer_name | 4xproduct_name
617 * excerpt = customer_name + customer contact info + customer notes from paypal form
618 * metadata:
619 * spay_paypal_id - paypal id of transaction
620 * spay_status
621 * spay_product_id - post_id of bought product
622 * spay_quantity - quantity of product
623 * spay_price - item price at the time of purchase
624 * spay_customer_email - customer email
625 * ... (WIP)
626 */
627 $order_capabilities = array(
628 'edit_post' => 'edit_posts',
629 'read_post' => 'read_private_posts',
630 'delete_post' => 'delete_posts',
631 'edit_posts' => 'edit_posts',
632 'edit_others_posts' => 'edit_others_posts',
633 'publish_posts' => 'publish_posts',
634 'read_private_posts' => 'read_private_posts',
635 );
636 $order_args = array(
637 'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack' ),
638 'description' => esc_html__( 'Simple Payments orders', 'jetpack' ),
639 'supports' => array( 'custom-fields', 'excerpt' ),
640 'hierarchical' => false,
641 'public' => false,
642 'show_ui' => false,
643 'show_in_menu' => false,
644 'show_in_admin_bar' => false,
645 'show_in_nav_menus' => false,
646 'can_export' => true,
647 'has_archive' => false,
648 'exclude_from_search' => true,
649 'publicly_queryable' => false,
650 'rewrite' => false,
651 'capabilities' => $order_capabilities,
652 'show_in_rest' => true,
653 );
654 register_post_type( self::$post_type_order, $order_args );
655
656 /*
657 * PRODUCT data structure. Holds:
658 * title - title
659 * content - description
660 * thumbnail - image
661 * metadata:
662 * spay_price - price
663 * spay_formatted_price
664 * spay_currency - currency code
665 * spay_cta - text with "Buy" or other CTA
666 * spay_email - paypal email
667 * spay_multiple - allow for multiple items
668 * spay_status - status. { enabled | disabled }
669 */
670 $product_capabilities = array(
671 'edit_post' => 'edit_posts',
672 'read_post' => 'read_private_posts',
673 'delete_post' => 'delete_posts',
674 'edit_posts' => 'publish_posts',
675 'edit_others_posts' => 'edit_others_posts',
676 'publish_posts' => 'publish_posts',
677 'read_private_posts' => 'read_private_posts',
678 );
679 $product_args = array(
680 'label' => esc_html__( 'Product', 'jetpack' ),
681 'description' => esc_html__( 'Simple Payments products', 'jetpack' ),
682 'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ),
683 'hierarchical' => false,
684 'public' => false,
685 'show_ui' => false,
686 'show_in_menu' => false,
687 'show_in_admin_bar' => false,
688 'show_in_nav_menus' => false,
689 'can_export' => true,
690 'has_archive' => false,
691 'exclude_from_search' => true,
692 'publicly_queryable' => false,
693 'rewrite' => false,
694 'capabilities' => $product_capabilities,
695 'show_in_rest' => true,
696 );
697 register_post_type( self::$post_type_product, $product_args );
698 }
699
700 /**
701 * Validate the block attributes
702 *
703 * @param array $attrs The block attributes, expected to contain:
704 * * email - an email address.
705 * * price - a float between 0.01 and 9999999999.99.
706 * * productId - the ID of the product being paid for.
707 *
708 * @return bool
709 */
710 public function is_valid( $attrs ) {
711 if ( ! $this->validate_paypal_email( $attrs ) ) {
712 return false;
713 }
714
715 if ( ! $this->validate_price( $attrs ) ) {
716 return false;
717 }
718
719 if ( ! $this->validate_product( $attrs ) ) {
720 return false;
721 }
722
723 return true;
724 }
725
726 /**
727 * Check that the email address to make a payment to is valid
728 *
729 * @param array $attrs Key-value array of attributes.
730 *
731 * @return boolean
732 */
733 private function validate_paypal_email( $attrs ) {
734 if ( empty( $attrs['email'] ) ) {
735 return false;
736 }
737 return (bool) filter_var( $attrs['email'], FILTER_VALIDATE_EMAIL );
738 }
739
740 /**
741 * Check that the price is valid
742 *
743 * @param array $attrs Key-value array of attributes.
744 *
745 * @return bool
746 */
747 private function validate_price( $attrs ) {
748 if ( empty( $attrs['price'] ) ) {
749 return false;
750 }
751 return (bool) self::sanitize_price( $attrs['price'] );
752 }
753
754 /**
755 * Check that the stored product is valid
756 *
757 * Valid means it has a title, and the currency is accepted.
758 *
759 * @param array $attrs Key-value array of attributes.
760 *
761 * @return bool
762 */
763 private function validate_product( $attrs ) {
764 if ( empty( $attrs['productId'] ) ) {
765 return false;
766 }
767 $product = $this->get_product( $attrs['productId'] );
768 if ( ! $product ) {
769 return false;
770 }
771 // This title is the one used by paypal, it's set from the title set in the block content, unless the block
772 // content title is blank.
773 if ( ! get_the_title( $product ) ) {
774 return false;
775 }
776
777 $currency = get_post_meta( $product->ID, 'spay_currency', true );
778 return (bool) self::sanitize_currency( $currency );
779 }
780
781 /**
782 * Format a price for display
783 *
784 * Largely taken from WordPress.com Store_Price class
785 *
786 * The currency array will have the shape:
787 * format => string sprintf format with placeholders `%1$s`: Symbol `%2$s`: Price.
788 * symbol => string Symbol string
789 * desc => string Text description of currency
790 * decimal => int Number of decimal places
791 *
792 * @param string $the_currency The desired currency, e.g. 'USD'.
793 * @return ?array Currency object or null if not found.
794 */
795 private static function get_currency( $the_currency ) {
796 require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-currencies.php';
797 $currencies = Jetpack_Currencies::CURRENCIES;
798
799 if ( isset( $currencies[ $the_currency ] ) ) {
800 return $currencies[ $the_currency ];
801 }
802 return null;
803 }
804 }
805 Jetpack_Simple_Payments::get_instance();
806