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();