Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 346
0.00% covered (danger)
0.00%
0 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Simple_Payments
0.00% covered (danger)
0.00%
0 / 343
0.00% covered (danger)
0.00%
0 / 27
3660
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_instance
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 register_scripts_and_styles
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 register_init_hooks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 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
 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 / 56
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( // 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        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    }
133
134    /**
135     * Register the shortcode.
136     */
137    private function register_shortcode() {
138        add_shortcode( self::$shortcode, array( $this, 'parse_shortcode' ) );
139    }
140
141    /**
142     * Actions that are run on init.
143     */
144    public function init_hook_action() {
145        add_filter( 'rest_api_allowed_post_types', array( $this, 'allow_rest_api_types' ) );
146        add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'allow_sync_post_meta' ) );
147        if ( ! is_admin() ) {
148            $this->register_scripts_and_styles();
149        }
150        $this->register_shortcode();
151        $this->setup_cpts();
152
153        add_filter( 'the_content', array( $this, 'remove_auto_paragraph_from_product_description' ), 0 );
154    }
155
156    /**
157     * Enqueue the static assets needed in the frontend.
158     */
159    public function enqueue_frontend_assets() {
160        if ( ! wp_style_is( 'jetpack-simple-payments', 'enqueued' ) ) {
161            wp_enqueue_style( 'jetpack-simple-payments' );
162        }
163
164        if ( ! wp_script_is( 'jetpack-paypal-express-checkout', 'enqueued' ) ) {
165            wp_enqueue_script( 'jetpack-paypal-express-checkout' );
166        }
167    }
168
169    /**
170     * Add an inline script for setting up the PayPal checkout button.
171     *
172     * @param string  $id Product ID.
173     * @param string  $dom_id ID of the DOM element with the purchase message.
174     * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
175     */
176    public function setup_paypal_checkout_button( $id, $dom_id, $is_multiple ) {
177        wp_add_inline_script(
178            'jetpack-paypal-express-checkout',
179            sprintf(
180                "try{PaypalExpressCheckout.renderButton( '%d', '%d', %s, '%d' );}catch(e){}",
181                intval( $this->get_blog_id() ),
182                intval( $id ),
183                wp_json_encode( $dom_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ),
184                intval( $is_multiple )
185            )
186        );
187    }
188
189    /**
190     * Remove auto paragraph from product description.
191     *
192     * @param string $content - the content of the post.
193     */
194    public function remove_auto_paragraph_from_product_description( $content ) {
195        if ( get_post_type() === self::$post_type_product ) {
196            remove_filter( 'the_content', 'wpautop' );
197        }
198
199        return $content;
200    }
201
202    /** Return the blog ID */
203    public function get_blog_id() {
204        return ( new Manager() )->get_site_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 static 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            || ( new Manager() )->is_connected()
226            && Jetpack_Plan::supports( 'simple-payments' ) );
227    }
228
229    /**
230     * Get a WP_Post representation of a product
231     *
232     * @param int $id The ID of the product.
233     *
234     * @return array|false|WP_Post
235     */
236    private function get_product( $id ) {
237        if ( ! $id ) {
238            return false;
239        }
240
241        $product = get_post( $id );
242        if ( ! $product || is_wp_error( $product ) ) {
243            return false;
244        }
245        if ( $product->post_type !== self::$post_type_product || 'publish' !== $product->post_status ) {
246            return false;
247        }
248        return $product;
249    }
250
251    /**
252     * Creates the content from a shortcode
253     *
254     * @param array $attrs Shortcode attributes.
255     * @param mixed $content unused.
256     *
257     * @return string|void
258     */
259    public function parse_shortcode( $attrs, $content = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
260        if ( empty( $attrs['id'] ) ) {
261            return;
262        }
263        $product = $this->get_product( $attrs['id'] );
264        if ( ! $product ) {
265            return;
266        }
267
268        // We allow for overriding the presentation labels.
269        $data = shortcode_atts(
270            array(
271                'blog_id'     => $this->get_blog_id(),
272                'dom_id'      => uniqid( self::$css_classname_prefix . '-' . $product->ID . '_', true ),
273                'class'       => self::$css_classname_prefix . '-' . $product->ID,
274                'title'       => get_the_title( $product ),
275                'description' => $product->post_content,
276                'cta'         => get_post_meta( $product->ID, 'spay_cta', true ),
277                'multiple'    => get_post_meta( $product->ID, 'spay_multiple', true ) || '0',
278            ),
279            $attrs
280        );
281
282        $data['price'] = $this->format_price(
283            get_post_meta( $product->ID, 'spay_price', true ),
284            get_post_meta( $product->ID, 'spay_currency', true )
285        );
286
287        $data['id'] = $attrs['id'];
288
289        if ( ! self::is_enabled_jetpack_simple_payments() ) {
290            return;
291        }
292
293        $this->enqueue_frontend_assets();
294        $this->setup_paypal_checkout_button( $attrs['id'], $data['dom_id'], $data['multiple'] );
295
296        return $this->output_shortcode( $data );
297    }
298
299    /**
300     * Get the HTML output to use as PayPal purchase box.
301     *
302     * @param string  $dom_id ID of the DOM element with the purchase message.
303     * @param boolean $is_multiple Whether multiple items of the same product can be purchased.
304     *
305     * @return string
306     */
307    public function output_purchase_box( $dom_id, $is_multiple ) {
308        $items      = '';
309        $css_prefix = self::$css_classname_prefix;
310
311        if ( $is_multiple ) {
312            $items = sprintf(
313                '
314                <div class="%1$s">
315                    <input class="%2$s" type="number" value="1" min="1" id="%3$s" />
316                </div>
317                ',
318                esc_attr( "{$css_prefix}-items" ),
319                esc_attr( "{$css_prefix}-items-number" ),
320                esc_attr( "{$dom_id}_number" )
321            );
322        }
323
324        return sprintf(
325            '<div class="%1$s" id="%2$s"></div><div class="%3$s">%4$s<div class="%5$s" id="%6$s"></div></div>',
326            esc_attr( "{$css_prefix}-purchase-message" ),
327            esc_attr( "{$dom_id}-message-container" ),
328            esc_attr( "{$css_prefix}-purchase-box" ),
329            $items,
330            esc_attr( "{$css_prefix}-button" ),
331            esc_attr( "{$dom_id}_button" )
332        );
333    }
334
335    /**
336     * Get the HTML output to replace the `simple-payments` shortcode.
337     *
338     * @param array $data Product data.
339     * @return string
340     */
341    public function output_shortcode( $data ) {
342        $css_prefix = self::$css_classname_prefix;
343
344        $image = '';
345        if ( has_post_thumbnail( $data['id'] ) ) {
346            $image = sprintf(
347                '<div class="%1$s"><div class="%2$s">%3$s</div></div>',
348                esc_attr( "{$css_prefix}-product-image" ),
349                esc_attr( "{$css_prefix}-image" ),
350                get_the_post_thumbnail( $data['id'], 'full' )
351            );
352        }
353
354        return sprintf(
355            '
356<div class="%1$s">
357    <div class="%2$s">
358        %3$s
359        <div class="%4$s">
360            <div class="%5$s"><p>%6$s</p></div>
361            <div class="%7$s"><p>%8$s</p></div>
362            <div class="%9$s"><p>%10$s</p></div>
363            %11$s
364        </div>
365    </div>
366</div>
367',
368            esc_attr( "{$data['class']} {$css_prefix}-wrapper" ),
369            esc_attr( "{$css_prefix}-product" ),
370            $image,
371            esc_attr( "{$css_prefix}-details" ),
372            esc_attr( "{$css_prefix}-title" ),
373            esc_html( $data['title'] ),
374            esc_attr( "{$css_prefix}-description" ),
375            wp_kses( $data['description'], wp_kses_allowed_html( 'post' ) ),
376            esc_attr( "{$css_prefix}-price" ),
377            esc_html( $data['price'] ),
378            $this->output_purchase_box( $data['dom_id'], $data['multiple'] )
379        );
380    }
381
382    /**
383     * Format a price with currency
384     *
385     * Uses currency-aware formatting to output a formatted price with a simple fallback.
386     *
387     * Largely inspired by WordPress.com's Store_Price::display_currency
388     *
389     * @param  string $price    Price.
390     * @param  string $currency Currency.
391     * @return string           Formatted price.
392     */
393    private function format_price( $price, $currency ) {
394        return PayPal_Payments_Currencies::format_price( $price, $currency );
395    }
396
397    /**
398     * Allows custom post types to be used by REST API.
399     *
400     * @param array $post_types - the allows post types.
401     * @see hook 'rest_api_allowed_post_types'
402     * @return array
403     */
404    public function allow_rest_api_types( $post_types ) {
405        $post_types[] = self::$post_type_order;
406        $post_types[] = self::$post_type_product;
407        return $post_types;
408    }
409
410    /**
411     * Merge $post_meta with additional meta information.
412     *
413     * @param array $post_meta - the post's meta information.
414     */
415    public function allow_sync_post_meta( $post_meta ) {
416        return array_merge(
417            $post_meta,
418            array(
419                'spay_paypal_id',
420                'spay_status',
421                'spay_product_id',
422                'spay_quantity',
423                'spay_price',
424                'spay_customer_email',
425                'spay_currency',
426                'spay_cta',
427                'spay_email',
428                'spay_multiple',
429                'spay_formatted_price',
430            )
431        );
432    }
433
434    /**
435     * Enable Simple payments custom meta values for access through the REST API.
436     * Field's value will be exposed on a .meta key in the endpoint response,
437     * and WordPress will handle setting up the callbacks for reading and writing
438     * to that meta key.
439     *
440     * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/modifying-responses/
441     */
442    public function register_meta_fields_in_rest_api() {
443        register_meta(
444            'post',
445            'spay_price',
446            array(
447                'description'       => esc_html__( 'Simple payments; price.', 'jetpack-paypal-payments' ),
448                'object_subtype'    => self::$post_type_product,
449                'sanitize_callback' => array( $this, 'sanitize_price' ),
450                'show_in_rest'      => true,
451                'single'            => true,
452                'type'              => 'number',
453            )
454        );
455
456        register_meta(
457            'post',
458            'spay_currency',
459            array(
460                'description'       => esc_html__( 'Simple payments; currency code.', 'jetpack-paypal-payments' ),
461                'object_subtype'    => self::$post_type_product,
462                'sanitize_callback' => array( $this, 'sanitize_currency' ),
463                'show_in_rest'      => true,
464                'single'            => true,
465                'type'              => 'string',
466            )
467        );
468
469        register_meta(
470            'post',
471            'spay_cta',
472            array(
473                'description'       => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack-paypal-payments' ),
474                'object_subtype'    => self::$post_type_product,
475                'sanitize_callback' => 'sanitize_text_field',
476                'show_in_rest'      => true,
477                'single'            => true,
478                'type'              => 'string',
479            )
480        );
481
482        register_meta(
483            'post',
484            'spay_multiple',
485            array(
486                'description'       => esc_html__( 'Simple payments; allow multiple items', 'jetpack-paypal-payments' ),
487                'object_subtype'    => self::$post_type_product,
488                'sanitize_callback' => 'rest_sanitize_boolean',
489                'show_in_rest'      => true,
490                'single'            => true,
491                'type'              => 'boolean',
492            )
493        );
494
495        register_meta(
496            'post',
497            'spay_email',
498            array(
499                'description'       => esc_html__( 'Simple payments button; paypal email.', 'jetpack-paypal-payments' ),
500                'object_subtype'    => self::$post_type_product,
501                'sanitize_callback' => 'sanitize_email',
502                'show_in_rest'      => true,
503                'single'            => true,
504                'type'              => 'string',
505            )
506        );
507
508        register_meta(
509            'post',
510            'spay_status',
511            array(
512                'description'       => esc_html__( 'Simple payments; status.', 'jetpack-paypal-payments' ),
513                'object_subtype'    => self::$post_type_product,
514                'sanitize_callback' => 'sanitize_text_field',
515                'show_in_rest'      => true,
516                'single'            => true,
517                'type'              => 'string',
518            )
519        );
520    }
521
522    /**
523     * Sanitize three-character ISO-4217 Simple payments currency
524     *
525     * List has to be in sync with list at the block's client side and widget's backend side:
526     *
527     * @param array $currency - list of currencies.
528     * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
529     * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/widgets/simple-payments.php#L19-L44
530     *
531     * Currencies should be supported by PayPal:
532     * @link https://developer.paypal.com/docs/api/reference/currency-codes/
533     *
534     * Indian Rupee (INR) not supported because at the time of the creation of this file
535     * because it's limited to in-country PayPal India accounts only.
536     * Discussion: https://github.com/Automattic/wp-calypso/pull/28236
537     */
538    public static function sanitize_currency( $currency ) {
539        $valid_currencies = array(
540            'USD',
541            'EUR',
542            'AUD',
543            'BRL',
544            'CAD',
545            'CZK',
546            'DKK',
547            'HKD',
548            'HUF',
549            'ILS',
550            'JPY',
551            'MYR',
552            'MXN',
553            'TWD',
554            'NZD',
555            'NOK',
556            'PHP',
557            'PLN',
558            'GBP',
559            'RUB',
560            'SGD',
561            'SEK',
562            'CHF',
563            'THB',
564        );
565
566        return in_array( $currency, $valid_currencies, true ) ? $currency : false;
567    }
568
569    /**
570     * Sanitize price:
571     *
572     * Positive integers and floats
573     * Supports two decimal places.
574     * Maximum length: 10.
575     *
576     * See `price` from PayPal docs:
577     *
578     * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item
579     *
580     * @param string $price - the price we want to sanitize.
581     * @return null|string
582     */
583    public static function sanitize_price( $price ) {
584        return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false;
585    }
586
587    /**
588     * Sets up the custom post types for the module.
589     */
590    public function setup_cpts() {
591        /*
592         * ORDER data structure. holds:
593         * title = customer_name | 4xproduct_name
594         * excerpt = customer_name + customer contact info + customer notes from paypal form
595         * metadata:
596         * spay_paypal_id - paypal id of transaction
597         * spay_status
598         * spay_product_id - post_id of bought product
599         * spay_quantity - quantity of product
600         * spay_price - item price at the time of purchase
601         * spay_customer_email - customer email
602         * ... (WIP)
603         */
604        $order_capabilities = array(
605            'edit_post'          => 'edit_posts',
606            'read_post'          => 'read_private_posts',
607            'delete_post'        => 'delete_posts',
608            'edit_posts'         => 'edit_posts',
609            'edit_others_posts'  => 'edit_others_posts',
610            'publish_posts'      => 'publish_posts',
611            'read_private_posts' => 'read_private_posts',
612        );
613        $order_args         = array(
614            'label'               => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack-paypal-payments' ),
615            'description'         => esc_html__( 'Simple Payments orders', 'jetpack-paypal-payments' ),
616            'supports'            => array( 'custom-fields', 'excerpt' ),
617            'hierarchical'        => false,
618            'public'              => false,
619            'show_ui'             => false,
620            'show_in_menu'        => false,
621            'show_in_admin_bar'   => false,
622            'show_in_nav_menus'   => false,
623            'can_export'          => true,
624            'has_archive'         => false,
625            'exclude_from_search' => true,
626            'publicly_queryable'  => false,
627            'rewrite'             => false,
628            'capabilities'        => $order_capabilities,
629            'show_in_rest'        => true,
630        );
631        register_post_type( self::$post_type_order, $order_args );
632
633        /*
634         * PRODUCT data structure. Holds:
635         * title - title
636         * content - description
637         * thumbnail - image
638         * metadata:
639         * spay_price - price
640         * spay_formatted_price
641         * spay_currency - currency code
642         * spay_cta - text with "Buy" or other CTA
643         * spay_email - paypal email
644         * spay_multiple - allow for multiple items
645         * spay_status - status. { enabled | disabled }
646         */
647        $product_capabilities = array(
648            'edit_post'          => 'edit_posts',
649            'read_post'          => 'read_private_posts',
650            'delete_post'        => 'delete_posts',
651            'edit_posts'         => 'publish_posts',
652            'edit_others_posts'  => 'edit_others_posts',
653            'publish_posts'      => 'publish_posts',
654            'read_private_posts' => 'read_private_posts',
655        );
656        $product_args         = array(
657            'label'               => esc_html__( 'Product', 'jetpack-paypal-payments' ),
658            'description'         => esc_html__( 'Simple Payments products', 'jetpack-paypal-payments' ),
659            'supports'            => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ),
660            'hierarchical'        => false,
661            'public'              => false,
662            'show_ui'             => false,
663            'show_in_menu'        => false,
664            'show_in_admin_bar'   => false,
665            'show_in_nav_menus'   => false,
666            'can_export'          => true,
667            'has_archive'         => false,
668            'exclude_from_search' => true,
669            'publicly_queryable'  => false,
670            'rewrite'             => false,
671            'capabilities'        => $product_capabilities,
672            'show_in_rest'        => true,
673        );
674        register_post_type( self::$post_type_product, $product_args );
675    }
676
677    /**
678     * Validate the block attributes
679     *
680     * @param array $attrs The block attributes, expected to contain:
681     *                      * email - an email address.
682     *                      * price - a float between 0.01 and 9999999999.99.
683     *                      * productId - the ID of the product being paid for.
684     *
685     * @return bool
686     */
687    public function is_valid( $attrs ) {
688        if ( ! $this->validate_paypal_email( $attrs ) ) {
689            return false;
690        }
691
692        if ( ! $this->validate_price( $attrs ) ) {
693            return false;
694        }
695
696        if ( ! $this->validate_product( $attrs ) ) {
697            return false;
698        }
699
700        return true;
701    }
702
703    /**
704     * Check that the email address to make a payment to is valid
705     *
706     * @param array $attrs Key-value array of attributes.
707     *
708     * @return boolean
709     */
710    private function validate_paypal_email( $attrs ) {
711        if ( empty( $attrs['email'] ) ) {
712            return false;
713        }
714        return (bool) filter_var( $attrs['email'], FILTER_VALIDATE_EMAIL );
715    }
716
717    /**
718     * Check that the price is valid
719     *
720     * @param array $attrs Key-value array of attributes.
721     *
722     * @return bool
723     */
724    private function validate_price( $attrs ) {
725        if ( empty( $attrs['price'] ) ) {
726            return false;
727        }
728        return (bool) self::sanitize_price( $attrs['price'] );
729    }
730
731    /**
732     * Check that the stored product is valid
733     *
734     * Valid means it has a title, and the currency is accepted.
735     *
736     * @param array $attrs Key-value array of attributes.
737     *
738     * @return bool
739     */
740    private function validate_product( $attrs ) {
741        if ( empty( $attrs['productId'] ) ) {
742            return false;
743        }
744        $product = $this->get_product( $attrs['productId'] );
745        if ( ! $product ) {
746            return false;
747        }
748        // This title is the one used by paypal, it's set from the title set in the block content, unless the block
749        // content title is blank.
750        if ( ! get_the_title( $product ) ) {
751            return false;
752        }
753
754        $currency = get_post_meta( $product->ID, 'spay_currency', true );
755        return (bool) self::sanitize_currency( $currency );
756    }
757
758    /**
759     * Register Simple_Payments_Widget widget.
760     */
761    public static function register_widget_simple_payments() {
762        if ( ! self::is_enabled_jetpack_simple_payments() ) {
763            return;
764        }
765
766        register_widget( 'Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget' );
767    }
768}
769Simple_Payments::get_instance();