Completed
Push — branch-6.7 ( 1b58e6...1c7176 )
by Jeremy
22:30 queued 14:34
created

Jetpack_Simple_Payments::sanitize_price()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
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
class Jetpack_Simple_Payments {
8
	// These have to be under 20 chars because that is CPT limit.
9
	static $post_type_order = 'jp_pay_order';
10
	static $post_type_product = 'jp_pay_product';
11
12
	static $shortcode = 'simple-payment';
13
14
	static $css_classname_prefix = 'jetpack-simple-payments';
15
16
	// Increase this number each time there's a change in CSS or JS to bust cache.
17
	static $version = '0.25';
18
19
	// Classic singleton pattern:
20
	private static $instance;
21
	private function __construct() {}
22
	static function getInstance() {
23
		if ( ! self::$instance ) {
24
			self::$instance = new self();
25
			self::$instance->register_init_hooks();
26
		}
27
		return self::$instance;
28
	}
29
30
	private function register_scripts_and_styles() {
31
		/**
32
		 * Paypal heavily discourages putting that script in your own server:
33
		 * @see https://developer.paypal.com/docs/integration/direct/express-checkout/integration-jsv4/add-paypal-button/
34
		 */
35
		wp_register_script( 'paypal-checkout-js', 'https://www.paypalobjects.com/api/checkout.js', array(), null, true );
36
		wp_register_script( 'paypal-express-checkout', plugins_url( '/paypal-express-checkout.js', __FILE__ ),
37
			array( 'jquery', 'paypal-checkout-js' ), self::$version );
38
		wp_register_style( 'jetpack-simple-payments', plugins_url( '/simple-payments.css', __FILE__ ), array( 'dashicons' ) );
39
	}
40
41
	private function register_init_hooks() {
42
		add_action( 'init', array( $this, 'init_hook_action' ) );
43
		add_action( 'rest_api_init', array( $this, 'register_meta_fields_in_rest_api' ) );
44
	}
45
46
	private function register_shortcode() {
47
		add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
48
	}
49
50
	public function init_hook_action() {
51
		add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
52
		add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
53
		if ( ! is_admin() ) {
54
			$this->register_scripts_and_styles();
55
		}
56
		$this->register_shortcode();
57
		$this->setup_cpts();
58
59
		add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
60
	}
61
62
	function remove_auto_paragraph_from_product_description( $content ) {
63
		if ( get_post_type() === self::$post_type_product ) {
64
			remove_filter( 'the_content', 'wpautop' );
65
		}
66
67
		return $content;
68
	}
69
70
	function get_blog_id() {
71
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
72
			return get_current_blog_id();
73
		}
74
75
		return Jetpack_Options::get_option( 'id' );
76
	}
77
78
	/**
79
	 * Used to check whether Simple Payments are enabled for given site.
80
	 *
81
	 * @return bool True if Simple Payments are enabled, false otherwise.
82
	 */
83
	function is_enabled_jetpack_simple_payments() {
84
		/**
85
		 * Can be used by plugin authors to disable the conflicting output of Simple Payments.
86
		 *
87
		 * @since 6.3.0
88
		 *
89
		 * @param bool True if Simple Payments should be disabled, false otherwise.
90
		 */
91
		if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
92
			return false;
93
		}
94
95
		// For WPCOM sites
96
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'has_blog_sticker' ) ) {
97
			$site_id = $this->get_blog_id();
98
			return has_blog_sticker( 'premium-plan', $site_id ) || has_blog_sticker( 'business-plan', $site_id );
99
		}
100
101
		// For all Jetpack sites
102
		return Jetpack::is_active() && Jetpack::active_plan_supports( 'simple-payments');
103
	}
104
105
	function parse_shortcode( $attrs, $content = false ) {
106
		if ( empty( $attrs['id'] ) ) {
107
			return;
108
		}
109
		$product = get_post( $attrs['id'] );
110
		if ( ! $product || is_wp_error( $product ) ) {
111
			return;
112
		}
113
		if ( $product->post_type !== self::$post_type_product || 'trash' === $product->post_status ) {
114
			return;
115
		}
116
117
		// We allow for overriding the presentation labels
118
		$data = shortcode_atts( array(
119
			'blog_id'     => $this->get_blog_id(),
120
			'dom_id'      => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
121
			'class'       => self::$css_classname_prefix . '-' . $product->ID,
122
			'title'       => get_the_title( $product ),
123
			'description' => $product->post_content,
124
			'cta'         => get_post_meta( $product->ID, 'spay_cta', true ),
125
			'multiple'    => get_post_meta( $product->ID, 'spay_multiple', true ) || '0'
126
		), $attrs );
127
128
		$data['price'] = $this->format_price(
129
			get_post_meta( $product->ID, 'spay_formatted_price', true ),
130
			get_post_meta( $product->ID, 'spay_price', true ),
131
			get_post_meta( $product->ID, 'spay_currency', true ),
132
			$data
133
		);
134
135
		$data['id'] = $attrs['id'];
136
137
		if( ! wp_style_is( 'jetpack-simple-payments', 'enqueue' ) ) {
138
			wp_enqueue_style( 'jetpack-simple-payments' );
139
		}
140
141
		if ( ! $this->is_enabled_jetpack_simple_payments() ) {
142
			return $this->output_admin_warning( $data );
143
		}
144
145
		if ( ! wp_script_is( 'paypal-express-checkout', 'enqueued' ) ) {
146
			wp_enqueue_script( 'paypal-express-checkout' );
147
		}
148
149
		wp_add_inline_script( 'paypal-express-checkout', sprintf(
150
			"try{PaypalExpressCheckout.renderButton( '%d', '%d', '%s', '%d' );}catch(e){}",
151
			esc_js( $data['blog_id'] ),
152
			esc_js( $attrs['id'] ),
153
			esc_js( $data['dom_id'] ),
154
			esc_js( $data['multiple'] )
155
		) );
156
157
		return $this->output_shortcode( $data );
158
	}
159
160
	function output_admin_warning( $data ) {
161
		if ( ! current_user_can( 'manage_options' ) ) {
162
			return;
163
		}
164
		$css_prefix = self::$css_classname_prefix;
165
166
		$support_url = ( defined( 'IS_WPCOM' ) && IS_WPCOM )
167
			? 'https://support.wordpress.com/simple-payments/'
168
			: 'https://jetpack.com/support/simple-payment-button/';
169
170
		return sprintf( '
171
<div class="%1$s">
172
	<div class="%2$s">
173
		<div class="%3$s">
174
			<div class="%4$s" id="%5$s">
175
				<p>%6$s</p>
176
				<p>%7$s</p>
177
			</div>
178
		</div>
179
	</div>
180
</div>
181
',
182
			esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
183
			esc_attr( "${css_prefix}-product" ),
184
			esc_attr( "${css_prefix}-details" ),
185
			esc_attr( "${css_prefix}-purchase-message show error" ),
186
			esc_attr( "{$data['dom_id']}-message-container" ),
187
			sprintf(
188
				wp_kses(
189
					__( 'Your plan doesn\'t include Simple Payments. <a href="%s" rel="noopener noreferrer" target="_blank">Learn more and upgrade</a>.', 'jetpack' ),
190
					array( 'a' => array( 'href' => array(), 'rel' => array(), 'target' => array() ) )
191
				),
192
				esc_url( $support_url )
193
			),
194
			esc_html__( '(Only administrators will see this message.)', 'jetpack' )
195
		);
196
	}
197
198
	function output_shortcode( $data ) {
199
		$items = '';
200
		$css_prefix = self::$css_classname_prefix;
201
202
		if ( $data['multiple'] ) {
203
			$items = sprintf( '
204
				<div class="%1$s">
205
					<input class="%2$s" type="number" value="1" min="1" id="%3$s" />
206
				</div>
207
				',
208
				esc_attr( "${css_prefix}-items" ),
209
				esc_attr( "${css_prefix}-items-number" ),
210
				esc_attr( "{$data['dom_id']}_number" )
211
			);
212
		}
213
		$image = "";
214
		if( has_post_thumbnail( $data['id'] ) ) {
215
			$image = sprintf( '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
216
				esc_attr( "${css_prefix}-product-image" ),
217
				esc_attr( "${css_prefix}-image" ),
218
				get_the_post_thumbnail( $data['id'], 'full' )
219
			);
220
		}
221
		return sprintf( '
222
<div class="%1$s">
223
	<div class="%2$s">
224
		%3$s
225
		<div class="%4$s">
226
			<div class="%5$s"><p>%6$s</p></div>
227
			<div class="%7$s"><p>%8$s</p></div>
228
			<div class="%9$s"><p>%10$s</p></div>
229
			<div class="%11$s" id="%12$s"></div>
230
			<div class="%13$s">
231
				%14$s
232
				<div class="%15$s" id="%16$s"></div>
233
			</div>
234
		</div>
235
	</div>
236
</div>
237
',
238
			esc_attr( "{$data['class']} ${css_prefix}-wrapper" ),
239
			esc_attr( "${css_prefix}-product" ),
240
			$image,
241
			esc_attr( "${css_prefix}-details" ),
242
			esc_attr( "${css_prefix}-title" ),
243
			$data['title'],
244
			esc_attr( "${css_prefix}-description" ),
245
			$data['description'],
246
			esc_attr( "${css_prefix}-price" ),
247
			esc_html( $data['price'] ),
248
			esc_attr( "${css_prefix}-purchase-message" ),
249
			esc_attr( "{$data['dom_id']}-message-container" ),
250
			esc_attr( "${css_prefix}-purchase-box" ),
251
			$items,
252
			esc_attr( "${css_prefix}-button" ),
253
			esc_attr( "{$data['dom_id']}_button" )
254
		);
255
	}
256
257
	function format_price( $formatted_price, $price, $currency, $all_data ) {
258
		if ( $formatted_price ) {
259
			return $formatted_price;
260
		}
261
		return "$price $currency";
262
	}
263
264
	/**
265
	 * Allows custom post types to be used by REST API.
266
	 * @param $post_types
267
	 * @see hook 'rest_api_allowed_post_types'
268
	 * @return array
269
	 */
270
	function allow_rest_api_types( $post_types ) {
271
		$post_types[] = self::$post_type_order;
272
		$post_types[] = self::$post_type_product;
273
		return $post_types;
274
	}
275
276
	function allow_sync_post_meta( $post_meta ) {
277
		return array_merge( $post_meta, array(
278
			'spay_paypal_id',
279
			'spay_status',
280
			'spay_product_id',
281
			'spay_quantity',
282
			'spay_price',
283
			'spay_customer_email',
284
			'spay_currency',
285
			'spay_cta',
286
			'spay_email',
287
			'spay_multiple',
288
			'spay_formatted_price',
289
		) );
290
	}
291
292
	/**
293
	 * Enable Simple payments custom meta values for access through the REST API.
294
	 * Field’s value will be exposed on a .meta key in the endpoint response,
295
	 * and WordPress will handle setting up the callbacks for reading and writing
296
	 * to that meta key.
297
	 *
298
	 * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
299
	 */
300
	public function register_meta_fields_in_rest_api() {
301
		register_meta( 'post', 'spay_price', array(
302
			'description'       => esc_html__( 'Simple payments; price.', 'jetpack' ),
303
			'object_subtype'    => self::$post_type_product,
304
			'sanitize_callback' => array( $this, 'sanitize_price' ),
305
			'show_in_rest'      => true,
306
			'single'            => true,
307
			'type'              => 'number',
308
		) );
309
310
		register_meta( 'post', 'spay_currency', array(
311
			'description'       => esc_html__( 'Simple payments; currency code.', 'jetpack' ),
312
			'object_subtype'    => self::$post_type_product,
313
			'sanitize_callback' => array( $this, 'sanitize_currency' ),
314
			'show_in_rest'      => true,
315
			'single'            => true,
316
			'type'              => 'string',
317
		) );
318
319
		register_meta( 'post', 'spay_cta', array(
320
			'description'       => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack' ),
321
			'object_subtype'    => self::$post_type_product,
322
			'sanitize_callback' => 'sanitize_text_field',
323
			'show_in_rest'      => true,
324
			'single'            => true,
325
			'type'              => 'string',
326
		) );
327
328
		register_meta( 'post', 'spay_multiple', array(
329
			'description'       => esc_html__( 'Simple payments; allow multiple items', 'jetpack' ),
330
			'object_subtype'    => self::$post_type_product,
331
			'sanitize_callback' => 'rest_sanitize_boolean',
332
			'show_in_rest'      => true,
333
			'single'            => true,
334
			'type'              => 'boolean',
335
		) );
336
337
		register_meta( 'post', 'spay_email', array(
338
			'description'       => esc_html__( 'Simple payments button; paypal email.', 'jetpack' ),
339
			'sanitize_callback' => 'sanitize_email',
340
			'show_in_rest'      => true,
341
			'single'            => true,
342
			'type'              => 'string',
343
		) );
344
345
		register_meta( 'post', 'spay_status', array(
346
			'description'       => esc_html__( 'Simple payments; status.', 'jetpack' ),
347
			'object_subtype'    => self::$post_type_product,
348
			'sanitize_callback' => 'sanitize_text_field',
349
			'show_in_rest'      => true,
350
			'single'            => true,
351
			'type'              => 'string',
352
		) );
353
	}
354
355
	/**
356
	 * Sanitize three-character ISO-4217 Simple payments currency
357
	 *
358
	 * List has to be in sync with list at the client side:
359
	 * @link https://github.com/Automattic/wp-calypso/blob/6d02ffe73cc073dea7270a22dc30881bff17d8fb/client/lib/simple-payments/constants.js
360
	 *
361
	 * Currencies should be supported by PayPal:
362
	 * @link https://developer.paypal.com/docs/integration/direct/rest/currency-codes/
363
	 */
364
	public static function sanitize_currency( $currency ) {
365
		$valid_currencies = array(
366
			'USD',
367
			'EUR',
368
			'AUD',
369
			'BRL',
370
			'CAD',
371
			'CZK',
372
			'DKK',
373
			'HKD',
374
			'HUF',
375
			'ILS',
376
			'JPY',
377
			'MYR',
378
			'MXN',
379
			'TWD',
380
			'NZD',
381
			'NOK',
382
			'PHP',
383
			'PLN',
384
			'GBP',
385
			'RUB',
386
			'SGD',
387
			'SEK',
388
			'CHF',
389
			'THB',
390
		);
391
392
		return in_array( $currency, $valid_currencies ) ? $currency : false;
393
	}
394
395
	/**
396
	 * Sanitize price:
397
	 *
398
	 * Positive integers and floats
399
	 * Supports two decimal places.
400
	 * Maximum length: 10.
401
	 *
402
	 * See `price` from PayPal docs:
403
	 * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
404
	 *
405
	 * @param      $value
406
	 * @return null|string
407
	 */
408
	public static function sanitize_price( $price ) {
409
		return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
410
	}
411
412
	/**
413
	 * Sets up the custom post types for the module.
414
	 */
415
	function setup_cpts() {
416
417
		/*
418
		 * ORDER data structure. holds:
419
		 * title = customer_name | 4xproduct_name
420
		 * excerpt = customer_name + customer contact info + customer notes from paypal form
421
		 * metadata:
422
		 * spay_paypal_id - paypal id of transaction
423
		 * spay_status
424
		 * spay_product_id - post_id of bought product
425
		 * spay_quantity - quantity of product
426
		 * spay_price - item price at the time of purchase
427
		 * spay_customer_email - customer email
428
		 * ... (WIP)
429
		 */
430
		$order_capabilities = array(
431
			'edit_post'             => 'edit_posts',
432
			'read_post'             => 'read_private_posts',
433
			'delete_post'           => 'delete_posts',
434
			'edit_posts'            => 'edit_posts',
435
			'edit_others_posts'     => 'edit_others_posts',
436
			'publish_posts'         => 'publish_posts',
437
			'read_private_posts'    => 'read_private_posts',
438
		);
439
		$order_args = array(
440
			'label'                 => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack' ),
441
			'description'           => esc_html__( 'Simple Payments orders', 'jetpack' ),
442
			'supports'              => array( 'custom-fields', 'excerpt' ),
443
			'hierarchical'          => false,
444
			'public'                => false,
445
			'show_ui'               => false,
446
			'show_in_menu'          => false,
447
			'show_in_admin_bar'     => false,
448
			'show_in_nav_menus'     => false,
449
			'can_export'            => true,
450
			'has_archive'           => false,
451
			'exclude_from_search'   => true,
452
			'publicly_queryable'    => false,
453
			'rewrite'               => false,
454
			'capabilities'          => $order_capabilities,
455
			'show_in_rest'          => true,
456
		);
457
		register_post_type( self::$post_type_order, $order_args );
458
459
		/*
460
		 * PRODUCT data structure. Holds:
461
		 * title - title
462
		 * content - description
463
		 * thumbnail - image
464
		 * metadata:
465
		 * spay_price - price
466
		 * spay_formatted_price
467
		 * spay_currency - currency code
468
		 * spay_cta - text with "Buy" or other CTA
469
		 * spay_email - paypal email
470
		 * spay_multiple - allow for multiple items
471
		 * spay_status - status. { enabled | disabled }
472
		 */
473
		$product_capabilities = array(
474
			'edit_post'             => 'edit_posts',
475
			'read_post'             => 'read_private_posts',
476
			'delete_post'           => 'delete_posts',
477
			'edit_posts'            => 'publish_posts',
478
			'edit_others_posts'     => 'edit_others_posts',
479
			'publish_posts'         => 'publish_posts',
480
			'read_private_posts'    => 'read_private_posts',
481
		);
482
		$product_args = array(
483
			'label'                 => esc_html__( 'Product', 'jetpack' ),
484
			'description'           => esc_html__( 'Simple Payments products', 'jetpack' ),
485
			'supports'              => array( 'title', 'editor','thumbnail', 'custom-fields', 'author' ),
486
			'hierarchical'          => false,
487
			'public'                => false,
488
			'show_ui'               => false,
489
			'show_in_menu'          => false,
490
			'show_in_admin_bar'     => false,
491
			'show_in_nav_menus'     => false,
492
			'can_export'            => true,
493
			'has_archive'           => false,
494
			'exclude_from_search'   => true,
495
			'publicly_queryable'    => false,
496
			'rewrite'               => false,
497
			'capabilities'          => $product_capabilities,
498
			'show_in_rest'          => true,
499
		);
500
		register_post_type( self::$post_type_product, $product_args );
501
	}
502
503
}
504
Jetpack_Simple_Payments::getInstance();
505