Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.60% covered (danger)
5.60%
20 / 357
14.29% covered (danger)
14.29%
4 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Simple_Payments
5.08% covered (danger)
5.08%
18 / 354
14.29% covered (danger)
14.29%
4 / 28
3677.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_instance
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 register_scripts_and_styles
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 register_init_hooks
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 register_shortcode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 init_hook_action
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_frontend_assets
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 setup_paypal_checkout_button
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 remove_auto_paragraph_from_product_description
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_blog_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled_jetpack_simple_payments
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 get_product
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 parse_shortcode
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 output_purchase_box
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 output_shortcode
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 format_price
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allow_rest_api_types
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 allow_sync_post_meta
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 register_meta_fields_in_rest_api
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
2
 redact_spay_email_for_unauthorized
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 sanitize_currency
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 sanitize_price
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 setup_cpts
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
2
 is_valid
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 validate_paypal_email
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 validate_price
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 validate_product
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 register_widget_simple_payments
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
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 * @package automattic/jetpack-paypal-payments
8 */
9
10namespace Automattic\Jetpack\Paypal_Payments;
11
12use Automattic\Jetpack\Assets;
13use Automattic\Jetpack\Connection\Manager;
14use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
15use Automattic\Jetpack\PayPal_Payments;
16use PayPal_Payments_Currencies;
17use WP_Post;
18
19if ( ! defined( 'ABSPATH' ) ) {
20    exit( 0 );
21}
22
23/**
24 * Simple_Payments
25 */
26class Simple_Payments {
27
28    const PACKAGE_VERSION = PayPal_Payments::PACKAGE_VERSION;
29
30    /**
31     * Post type order.
32     *
33     * @var string
34     */
35    public static $post_type_order = 'jp_pay_order';
36
37    /**
38     * Post type product.
39     *
40     * @var string
41     */
42    public static $post_type_product = 'jp_pay_product';
43
44    /**
45     * Define simple payment shortcode.
46     *
47     * @var string
48     */
49    public static $shortcode = 'simple-payment';
50
51    /**
52     * Define simple payment CSS prefix.
53     *
54     * @var string
55     */
56    public static $css_classname_prefix = 'jetpack-simple-payments';
57
58    /**
59     * Which plan the user is on.
60     *
61     * @var string value_bundle or jetpack_premium
62     */
63    public static $required_plan;
64
65    /**
66     * Instance of the class.
67     *
68     * @var Simple_Payments
69     */
70    private static $instance;
71
72    /**
73     * Construction function.
74     */
75    private function __construct() {}
76
77    /**
78     * Create instance of class.
79     */
80    public static function get_instance() {
81
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(
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        Assets::register_script(
107            'jetpack-paypal-express-checkout',
108            './paypal-express-checkout.js',
109            __FILE__,
110            array(
111                'dependencies' => array(
112                    'jquery',
113                    'paypal-checkout-js',
114                ),
115            )
116        );
117        wp_register_style(
118            'jetpack-simple-payments',
119            plugin_dir_url( __FILE__ ) . '/../../../dist/legacy-simple-payments.css',
120            array( 'dashicons' ),
121            self::PACKAGE_VERSION,
122            false /* @phan-suppress-current-line PhanTypeMismatchArgument */
123        );
124    }
125
126    /**
127     * Register init hooks.
128     */
129    private function register_init_hooks() {
130        add_action( 'init', array( $this, 'init_hook_action' ) );
131        add_action( 'rest_api_init', array( $this, 'register_meta_fields_in_rest_api' ) );
132        add_filter( 'rest_prepare_' . self::$post_type_product, array( $this, 'redact_spay_email_for_unauthorized' ), 10, 2 );
133    }
134
135    /**
136     * Register the shortcode.
137     */
138    private function register_shortcode() {
139        add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
140    }
141
142    /**
143     * Actions that are run on init.
144     */
145    public function init_hook_action() {
146        add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
147        add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
148        if ( ! is_admin() ) {
149            $this->register_scripts_and_styles();
150        }
151        $this->register_shortcode();
152        $this->setup_cpts();
153
154        add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
155    }
156
157    /**
158     * Enqueue the static assets needed in the frontend.
159     */
160    public function enqueue_frontend_assets() {
161        if ( ! wp_style_is( 'jetpack-simple-payments', 'enqueued' ) ) {
162            wp_enqueue_style( 'jetpack-simple-payments' );
163        }
164
165        if ( ! wp_script_is( 'jetpack-paypal-express-checkout', 'enqueued' ) ) {
166            wp_enqueue_script( 'jetpack-paypal-express-checkout' );
167        }
168    }
169
170    /**
171     * Add an inline script for setting up the PayPal checkout button.
172     *
173     * @param string  $id Product ID.
174     * @param string  $dom_id ID of the DOM element with the purchase message.
175     * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
176     */
177    public function setup_paypal_checkout_button( $id, $dom_id, $is_multiple ) {
178        wp_add_inline_script(
179            'jetpack-paypal-express-checkout',
180            sprintf(
181                "try{PaypalExpressCheckout.renderButton( '%d', '%d', %s, '%d' );}catch(e){}",
182                intval( $this->get_blog_id() ),
183                intval( $id ),
184                wp_json_encode( $dom_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ),
185                intval( $is_multiple )
186            )
187        );
188    }
189
190    /**
191     * Remove auto paragraph from product description.
192     *
193     * @param string $content - the content of the post.
194     */
195    public function remove_auto_paragraph_from_product_description( $content ) {
196        if ( get_post_type() === self::$post_type_product ) {
197            remove_filter( 'the_content', 'wpautop' );
198        }
199
200        return $content;
201    }
202
203    /** Return the blog ID */
204    public function get_blog_id() {
205        return ( new Manager() )->get_site_id();
206    }
207
208    /**
209     * Used to check whether Simple Payments are enabled for given site.
210     *
211     * @return bool True if Simple Payments are enabled, false otherwise.
212     */
213    public static function is_enabled_jetpack_simple_payments() {
214        /**
215         * Can be used by plugin authors to disable the conflicting output of Simple Payments.
216         *
217         * @since 6.3.0
218         *
219         * @param bool True if Simple Payments should be disabled, false otherwise.
220         */
221        if ( apply_filters( 'jetpack_disable_simple_payments', false ) ) {
222            return false;
223        }
224
225        return ( ( defined( 'IS_WPCOM' ) && IS_WPCOM )
226            || ( new Manager() )->is_connected()
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 ( ! self::is_enabled_jetpack_simple_payments() ) {
291            return;
292        }
293
294        $this->enqueue_frontend_assets();
295        $this->setup_paypal_checkout_button( $attrs['id'], $data['dom_id'], $data['multiple'] );
296
297        return $this->output_shortcode( $data );
298    }
299
300    /**
301     * Get the HTML output to use as PayPal purchase box.
302     *
303     * @param string  $dom_id ID of the DOM element with the purchase message.
304     * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
305     *
306     * @return string
307     */
308    public function output_purchase_box( $dom_id, $is_multiple ) {
309        $items      = '';
310        $css_prefix = self::$css_classname_prefix;
311
312        if ( $is_multiple ) {
313            $items = sprintf(
314                '
315                <div class="%1$s">
316                    <input class="%2$s" type="number" value="1" min="1" id="%3$s" />
317                </div>
318                ',
319                esc_attr( "{$css_prefix}-items" ),
320                esc_attr( "{$css_prefix}-items-number" ),
321                esc_attr( "{$dom_id}_number" )
322            );
323        }
324
325        return sprintf(
326            '<div class="%1$s" id="%2$s"></div><div class="%3$s">%4$s<div class="%5$s" id="%6$s"></div></div>',
327            esc_attr( "{$css_prefix}-purchase-message" ),
328            esc_attr( "{$dom_id}-message-container" ),
329            esc_attr( "{$css_prefix}-purchase-box" ),
330            $items,
331            esc_attr( "{$css_prefix}-button" ),
332            esc_attr( "{$dom_id}_button" )
333        );
334    }
335
336    /**
337     * Get the HTML output to replace the `simple-payments` shortcode.
338     *
339     * @param array $data Product data.
340     * @return string
341     */
342    public function output_shortcode( $data ) {
343        $css_prefix = self::$css_classname_prefix;
344
345        $image = '';
346        if ( has_post_thumbnail( $data['id'] ) ) {
347            $image = sprintf(
348                '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
349                esc_attr( "{$css_prefix}-product-image" ),
350                esc_attr( "{$css_prefix}-image" ),
351                get_the_post_thumbnail( $data['id'], 'full' )
352            );
353        }
354
355        return sprintf(
356            '
357<div class="%1$s">
358    <div class="%2$s">
359        %3$s
360        <div class="%4$s">
361            <div class="%5$s"><p>%6$s</p></div>
362            <div class="%7$s"><p>%8$s</p></div>
363            <div class="%9$s"><p>%10$s</p></div>
364            %11$s
365        </div>
366    </div>
367</div>
368',
369            esc_attr( "{$data['class']} {$css_prefix}-wrapper" ),
370            esc_attr( "{$css_prefix}-product" ),
371            $image,
372            esc_attr( "{$css_prefix}-details" ),
373            esc_attr( "{$css_prefix}-title" ),
374            esc_html( $data['title'] ),
375            esc_attr( "{$css_prefix}-description" ),
376            wp_kses( $data['description'], wp_kses_allowed_html( 'post' ) ),
377            esc_attr( "{$css_prefix}-price" ),
378            esc_html( $data['price'] ),
379            $this->output_purchase_box( $data['dom_id'], $data['multiple'] )
380        );
381    }
382
383    /**
384     * Format a price with currency
385     *
386     * Uses currency-aware formatting to output a formatted price with a simple fallback.
387     *
388     * Largely inspired by WordPress.com's Store_Price::display_currency
389     *
390     * @param  string $price    Price.
391     * @param  string $currency Currency.
392     * @return string           Formatted price.
393     */
394    private function format_price( $price, $currency ) {
395        return PayPal_Payments_Currencies::format_price( $price, $currency );
396    }
397
398    /**
399     * Allows custom post types to be used by REST API.
400     *
401     * @param array $post_types - the allows post types.
402     * @see hook 'rest_api_allowed_post_types'
403     * @return array
404     */
405    public function allow_rest_api_types( $post_types ) {
406        $post_types[] = self::$post_type_order;
407        $post_types[] = self::$post_type_product;
408        return $post_types;
409    }
410
411    /**
412     * Merge $post_meta with additional meta information.
413     *
414     * @param array $post_meta - the post's meta information.
415     */
416    public function allow_sync_post_meta( $post_meta ) {
417        return array_merge(
418            $post_meta,
419            array(
420                'spay_paypal_id',
421                'spay_status',
422                'spay_product_id',
423                'spay_quantity',
424                'spay_price',
425                'spay_customer_email',
426                'spay_currency',
427                'spay_cta',
428                'spay_email',
429                'spay_multiple',
430                'spay_formatted_price',
431            )
432        );
433    }
434
435    /**
436     * Enable Simple payments custom meta values for access through the REST API.
437     * Field's value will be exposed on a .meta key in the endpoint response,
438     * and WordPress will handle setting up the callbacks for reading and writing
439     * to that meta key.
440     *
441     * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
442     */
443    public function register_meta_fields_in_rest_api() {
444        register_meta(
445            'post',
446            'spay_price',
447            array(
448                'description'       => esc_html__( 'Simple payments; price.', 'jetpack-paypal-payments' ),
449                'object_subtype'    => self::$post_type_product,
450                'sanitize_callback' => array( $this, 'sanitize_price' ),
451                'show_in_rest'      => true,
452                'single'            => true,
453                'type'              => 'number',
454            )
455        );
456
457        register_meta(
458            'post',
459            'spay_currency',
460            array(
461                'description'       => esc_html__( 'Simple payments; currency code.', 'jetpack-paypal-payments' ),
462                'object_subtype'    => self::$post_type_product,
463                'sanitize_callback' => array( $this, 'sanitize_currency' ),
464                'show_in_rest'      => true,
465                'single'            => true,
466                'type'              => 'string',
467            )
468        );
469
470        register_meta(
471            'post',
472            'spay_cta',
473            array(
474                'description'       => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack-paypal-payments' ),
475                'object_subtype'    => self::$post_type_product,
476                'sanitize_callback' => 'sanitize_text_field',
477                'show_in_rest'      => true,
478                'single'            => true,
479                'type'              => 'string',
480            )
481        );
482
483        register_meta(
484            'post',
485            'spay_multiple',
486            array(
487                'description'       => esc_html__( 'Simple payments; allow multiple items', 'jetpack-paypal-payments' ),
488                'object_subtype'    => self::$post_type_product,
489                'sanitize_callback' => 'rest_sanitize_boolean',
490                'show_in_rest'      => true,
491                'single'            => true,
492                'type'              => 'boolean',
493            )
494        );
495
496        register_meta(
497            'post',
498            'spay_email',
499            array(
500                'description'       => esc_html__( 'Simple payments button; paypal email.', 'jetpack-paypal-payments' ),
501                'object_subtype'    => self::$post_type_product,
502                'sanitize_callback' => 'sanitize_email',
503                'show_in_rest'      => true,
504                'single'            => true,
505                'type'              => 'string',
506            )
507        );
508
509        register_meta(
510            'post',
511            'spay_status',
512            array(
513                'description'       => esc_html__( 'Simple payments; status.', 'jetpack-paypal-payments' ),
514                'object_subtype'    => self::$post_type_product,
515                'sanitize_callback' => 'sanitize_text_field',
516                'show_in_rest'      => true,
517                'single'            => true,
518                'type'              => 'string',
519            )
520        );
521    }
522
523    /**
524     * Strip the seller's PayPal email (`spay_email`) from REST responses when the
525     * requester cannot edit the product. The meta stays `show_in_rest => true` so
526     * the block editor's read/write round-trip keeps working â€” but unauthenticated
527     * or read-only callers no longer see the address in collection or single-item
528     * responses for the `jp_pay_product` post type.
529     *
530     * @param mixed $response The response object (expected: \WP_REST_Response).
531     * @param mixed $post     The product post (expected: \WP_Post).
532     * @return mixed
533     */
534    public function redact_spay_email_for_unauthorized( $response, $post ) {
535        if ( ! $response instanceof \WP_REST_Response || ! $post instanceof WP_Post ) {
536            return $response;
537        }
538
539        if ( current_user_can( 'edit_post', $post->ID ) ) {
540            return $response;
541        }
542
543        $data = $response->get_data();
544        if ( isset( $data['meta']['spay_email'] ) ) {
545            unset( $data['meta']['spay_email'] );
546            $response->set_data( $data );
547        }
548
549        return $response;
550    }
551
552    /**
553     * Sanitize three-character ISO-4217 Simple payments currency
554     *
555     * List has to be in sync with list at the block's client side and widget's backend side:
556     *
557     * @param array $currency - list of currencies.
558     * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
559     * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/widgets/simple-payments.php#L19-L44
560     *
561     * Currencies should be supported by PayPal:
562     * @link https://developer.paypal.com/docs/api/reference/currency-codes/
563     *
564     * Indian Rupee (INR) not supported because at the time of the creation of this file
565     * because it's limited to in-country PayPal India accounts only.
566     * Discussion: https://github.com/Automattic/wp-calypso/pull/28236
567     */
568    public static function sanitize_currency( $currency ) {
569        $valid_currencies = array(
570            'USD',
571            'EUR',
572            'AUD',
573            'BRL',
574            'CAD',
575            'CZK',
576            'DKK',
577            'HKD',
578            'HUF',
579            'ILS',
580            'JPY',
581            'MYR',
582            'MXN',
583            'TWD',
584            'NZD',
585            'NOK',
586            'PHP',
587            'PLN',
588            'GBP',
589            'RUB',
590            'SGD',
591            'SEK',
592            'CHF',
593            'THB',
594        );
595
596        return in_array( $currency, $valid_currencies, true ) ? $currency : false;
597    }
598
599    /**
600     * Sanitize price:
601     *
602     * Positive integers and floats
603     * Supports two decimal places.
604     * Maximum length: 10.
605     *
606     * See `price` from PayPal docs:
607     *
608     * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
609     *
610     * @param string $price - the price we want to sanitize.
611     * @return null|string
612     */
613    public static function sanitize_price( $price ) {
614        return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
615    }
616
617    /**
618     * Sets up the custom post types for the module.
619     */
620    public function setup_cpts() {
621        /*
622         * ORDER data structure. holds:
623         * title = customer_name | 4xproduct_name
624         * excerpt = customer_name + customer contact info + customer notes from paypal form
625         * metadata:
626         * spay_paypal_id - paypal id of transaction
627         * spay_status
628         * spay_product_id - post_id of bought product
629         * spay_quantity - quantity of product
630         * spay_price - item price at the time of purchase
631         * spay_customer_email - customer email
632         * ... (WIP)
633         */
634        $order_capabilities = array(
635            'edit_post'          => 'edit_posts',
636            'read_post'          => 'read_private_posts',
637            'delete_post'        => 'delete_posts',
638            'edit_posts'         => 'edit_posts',
639            'edit_others_posts'  => 'edit_others_posts',
640            'publish_posts'      => 'publish_posts',
641            'read_private_posts' => 'read_private_posts',
642        );
643        $order_args         = array(
644            'label'                 => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack-paypal-payments' ),
645            'description'           => esc_html__( 'Simple Payments orders', 'jetpack-paypal-payments' ),
646            'supports'              => array( 'custom-fields', 'excerpt' ),
647            'hierarchical'          => false,
648            'public'                => false,
649            'show_ui'               => false,
650            'show_in_menu'          => false,
651            'show_in_admin_bar'     => false,
652            'show_in_nav_menus'     => false,
653            'can_export'            => true,
654            'has_archive'           => false,
655            'exclude_from_search'   => true,
656            'publicly_queryable'    => false,
657            'rewrite'               => false,
658            'capabilities'          => $order_capabilities,
659            'show_in_rest'          => true,
660            'rest_controller_class' => Order_REST_Controller::class,
661        );
662        register_post_type( self::$post_type_order, $order_args );
663
664        /*
665         * PRODUCT data structure. Holds:
666         * title - title
667         * content - description
668         * thumbnail - image
669         * metadata:
670         * spay_price - price
671         * spay_formatted_price
672         * spay_currency - currency code
673         * spay_cta - text with "Buy" or other CTA
674         * spay_email - paypal email
675         * spay_multiple - allow for multiple items
676         * spay_status - status. { enabled | disabled }
677         */
678        $product_capabilities = array(
679            'edit_post'          => 'edit_posts',
680            'read_post'          => 'read_private_posts',
681            'delete_post'        => 'delete_posts',
682            'edit_posts'         => 'publish_posts',
683            'edit_others_posts'  => 'edit_others_posts',
684            'publish_posts'      => 'publish_posts',
685            'read_private_posts' => 'read_private_posts',
686        );
687        $product_args         = array(
688            'label'               => esc_html__( 'Product', 'jetpack-paypal-payments' ),
689            'description'         => esc_html__( 'Simple Payments products', 'jetpack-paypal-payments' ),
690            'supports'            => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ),
691            'hierarchical'        => false,
692            'public'              => false,
693            'show_ui'             => false,
694            'show_in_menu'        => false,
695            'show_in_admin_bar'   => false,
696            'show_in_nav_menus'   => false,
697            'can_export'          => true,
698            'has_archive'         => false,
699            'exclude_from_search' => true,
700            'publicly_queryable'  => false,
701            'rewrite'             => false,
702            'capabilities'        => $product_capabilities,
703            'show_in_rest'        => true,
704        );
705        register_post_type( self::$post_type_product, $product_args );
706    }
707
708    /**
709     * Validate the block attributes
710     *
711     * @param array $attrs The block attributes, expected to contain:
712     *                      * email - an email address.
713     *                      * price - a float between 0.01 and 9999999999.99.
714     *                      * productId - the ID of the product being paid for.
715     *
716     * @return bool
717     */
718    public function is_valid( $attrs ) {
719        if ( ! $this->validate_paypal_email( $attrs ) ) {
720            return false;
721        }
722
723        if ( ! $this->validate_price( $attrs ) ) {
724            return false;
725        }
726
727        if ( ! $this->validate_product( $attrs ) ) {
728            return false;
729        }
730
731        return true;
732    }
733
734    /**
735     * Check that the email address to make a payment to is valid
736     *
737     * @param array $attrs Key-value array of attributes.
738     *
739     * @return boolean
740     */
741    private function validate_paypal_email( $attrs ) {
742        if ( empty( $attrs['email'] ) ) {
743            return false;
744        }
745        return (bool) filter_var( $attrs['email'], FILTER_VALIDATE_EMAIL );
746    }
747
748    /**
749     * Check that the price is valid
750     *
751     * @param array $attrs Key-value array of attributes.
752     *
753     * @return bool
754     */
755    private function validate_price( $attrs ) {
756        if ( empty( $attrs['price'] ) ) {
757            return false;
758        }
759        return (bool) self::sanitize_price( $attrs['price'] );
760    }
761
762    /**
763     * Check that the stored product is valid
764     *
765     * Valid means it has a title, and the currency is accepted.
766     *
767     * @param array $attrs Key-value array of attributes.
768     *
769     * @return bool
770     */
771    private function validate_product( $attrs ) {
772        if ( empty( $attrs['productId'] ) ) {
773            return false;
774        }
775        $product = $this->get_product( $attrs['productId'] );
776        if ( ! $product ) {
777            return false;
778        }
779        // This title is the one used by paypal, it's set from the title set in the block content, unless the block
780        // content title is blank.
781        if ( ! get_the_title( $product ) ) {
782            return false;
783        }
784
785        $currency = get_post_meta( $product->ID, 'spay_currency', true );
786        return (bool) self::sanitize_currency( $currency );
787    }
788
789    /**
790     * Register Simple_Payments_Widget widget.
791     */
792    public static function register_widget_simple_payments() {
793        if ( ! self::is_enabled_jetpack_simple_payments() ) {
794            return;
795        }
796
797        register_widget( 'Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget' );
798    }
799}
800Simple_Payments::get_instance();