cription' => esc_html__( 'Simple payments; currency code.', 'jetpack-paypal-payments' ), 'object_subtype' => self::$post_type_product, 'sanitize_callback' => array( $this, 'sanitize_currency' ), 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); register_meta( 'post', 'spay_cta', array( 'description' => esc_html__( 'Simple payments; text with "Buy" or other CTA', 'jetpack-paypal-payments' ), 'object_subtype' => self::$post_type_product, 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); register_meta( 'post', 'spay_multiple', array( 'description' => esc_html__( 'Simple payments; allow multiple items', 'jetpack-paypal-payments' ), 'object_subtype' => self::$post_type_product, 'sanitize_callback' => 'rest_sanitize_boolean', 'show_in_rest' => true, 'single' => true, 'type' => 'boolean', ) ); register_meta( 'post', 'spay_email', array( 'description' => esc_html__( 'Simple payments button; paypal email.', 'jetpack-paypal-payments' ), 'object_subtype' => self::$post_type_product, 'sanitize_callback' => 'sanitize_email', 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); register_meta( 'post', 'spay_status', array( 'description' => esc_html__( 'Simple payments; status.', 'jetpack-paypal-payments' ), 'object_subtype' => self::$post_type_product, 'sanitize_callback' => 'sanitize_text_field', 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); } /** * Strip the seller's PayPal email (`spay_email`) from REST responses when the * requester cannot edit the product. The meta stays `show_in_rest => true` so * the block editor's read/write round-trip keeps working — but unauthenticated * or read-only callers no longer see the address in collection or single-item * responses for the `jp_pay_product` post type. * * @param mixed $response The response object (expected: \WP_REST_Response). * @param mixed $post The product post (expected: \WP_Post). * @return mixed */ public function redact_spay_email_for_unauthorized( $response, $post ) { if ( ! $response instanceof \WP_REST_Response || ! $post instanceof WP_Post ) { return $response; } if ( current_user_can( 'edit_post', $post->ID ) ) { return $response; } $data = $response->get_data(); if ( isset( $data['meta']['spay_email'] ) ) { unset( $data['meta']['spay_email'] ); $response->set_data( $data ); } return $response; } /** * Sanitize three-character ISO-4217 Simple payments currency * * List has to be in sync with list at the block's client side and widget's backend side: * * @param array $currency - list of currencies. * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39 * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/widgets/simple-payments.php#L19-L44 * * Currencies should be supported by PayPal: * @link https://developer.paypal.com/docs/api/reference/currency-codes/ * * Indian Rupee (INR) not supported because at the time of the creation of this file * because it's limited to in-country PayPal India accounts only. * Discussion: https://github.com/Automattic/wp-calypso/pull/28236 */ public static function sanitize_currency( $currency ) { $valid_currencies = array( 'USD', 'EUR', 'AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'HKD', 'HUF', 'ILS', 'JPY', 'MYR', 'MXN', 'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', ); return in_array( $currency, $valid_currencies, true ) ? $currency : false; } /** * Sanitize price: * * Positive integers and floats * Supports two decimal places. * Maximum length: 10. * * See `price` from PayPal docs: * * @link https://developer.paypal.com/docs/api/orders/v1/#definition-item * * @param string $price - the price we want to sanitize. * @return null|string */ public static function sanitize_price( $price ) { return preg_match( '/^[0-9]{0,10}(\.[0-9]{0,2})?$/', $price ) ? $price : false; } /** * Sets up the custom post types for the module. */ public function setup_cpts() { /* * ORDER data structure. holds: * title = customer_name | 4xproduct_name * excerpt = customer_name + customer contact info + customer notes from paypal form * metadata: * spay_paypal_id - paypal id of transaction * spay_status * spay_product_id - post_id of bought product * spay_quantity - quantity of product * spay_price - item price at the time of purchase * spay_customer_email - customer email * ... (WIP) */ $order_capabilities = array( 'edit_post' => 'edit_posts', 'read_post' => 'read_private_posts', 'delete_post' => 'delete_posts', 'edit_posts' => 'edit_posts', 'edit_others_posts' => 'edit_others_posts', 'publish_posts' => 'publish_posts', 'read_private_posts' => 'read_private_posts', ); $order_args = array( 'label' => esc_html_x( 'Order', 'noun: a quantity of goods or items purchased or sold', 'jetpack-paypal-payments' ), 'description' => esc_html__( 'Simple Payments orders', 'jetpack-paypal-payments' ), 'supports' => array( 'custom-fields', 'excerpt' ), 'hierarchical' => false, 'public' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_admin_bar' => false, 'show_in_nav_menus' => false, 'can_export' => true, 'has_archive' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'rewrite' => false, 'capabilities' => $order_capabilities, 'show_in_rest' => true, 'rest_controller_class' => Order_REST_Controller::class, ); register_post_type( self::$post_type_order, $order_args ); /* * PRODUCT data structure. Holds: * title - title * content - description * thumbnail - image * metadata: * spay_price - price * spay_formatted_price * spay_currency - currency code * spay_cta - text with "Buy" or other CTA * spay_email - paypal email * spay_multiple - allow for multiple items * spay_status - status. { enabled | disabled } */ $product_capabilities = array( 'edit_post' => 'edit_posts', 'read_post' => 'read_private_posts', 'delete_post' => 'delete_posts', 'edit_posts' => 'publish_posts', 'edit_others_posts' => 'edit_others_posts', 'publish_posts' => 'publish_posts', 'read_private_posts' => 'read_private_posts', ); $product_args = array( 'label' => esc_html__( 'Product', 'jetpack-paypal-payments' ), 'description' => esc_html__( 'Simple Payments products', 'jetpack-paypal-payments' ), 'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ), 'hierarchical' => false, 'public' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_admin_bar' => false, 'show_in_nav_menus' => false, 'can_export' => true, 'has_archive' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'rewrite' => false, 'capabilities' => $product_capabilities, 'show_in_rest' => true, ); register_post_type( self::$post_type_product, $product_args ); } /** * Validate the block attributes * * @param array $attrs The block attributes, expected to contain: * * email - an email address. * * price - a float between 0.01 and 9999999999.99. * * productId - the ID of the product being paid for. * * @return bool */ public function is_valid( $attrs ) { if ( ! $this->validate_paypal_email( $attrs ) ) { return false; } if ( ! $this->validate_price( $attrs ) ) { return false; } if ( ! $this->validate_product( $attrs ) ) { return false; } return true; } /** * Check that the email address to make a payment to is valid * * @param array $attrs Key-value array of attributes. * * @return boolean */ private function validate_paypal_email( $attrs ) { if ( empty( $attrs['email'] ) ) { return false; } return (bool) filter_var( $attrs['email'], FILTER_VALIDATE_EMAIL ); } /** * Check that the price is valid * * @param array $attrs Key-value array of attributes. * * @return bool */ private function validate_price( $attrs ) { if ( empty( $attrs['price'] ) ) { return false; } return (bool) self::sanitize_price( $attrs['price'] ); } /** * Check that the stored product is valid * * Valid means it has a title, and the currency is accepted. * * @param array $attrs Key-value array of attributes. * * @return bool */ private function validate_product( $attrs ) { if ( empty( $attrs['productId'] ) ) { return false; } $product = $this->get_product( $attrs['productId'] ); if ( ! $product ) { return false; } // This title is the one used by paypal, it's set from the title set in the block content, unless the block // content title is blank. if ( ! get_the_title( $product ) ) { return false; } $currency = get_post_meta( $product->ID, 'spay_currency', true ); return (bool) self::sanitize_currency( $currency ); } /** * Register Simple_Payments_Widget widget. */ public static function register_widget_simple_payments() { if ( ! self::is_enabled_jetpack_simple_payments() ) { return; } register_widget( 'Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget' ); } } Simple_Payments::get_instance();