Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
47.62% |
50 / 105 |
|
14.29% |
1 / 7 |
CRAP | |
0.00% |
0 / 1 |
| PayPal_Payment_Buttons | |
47.62% |
50 / 105 |
|
14.29% |
1 / 7 |
114.83 | |
0.00% |
0 / 1 |
| sanitize_paypal_script_url | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
7 | |||
| register_block | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| render_block | |
55.77% |
29 / 52 |
|
0.00% |
0 / 1 |
27.62 | |||
| load_editor_styles | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
| load_editor_scripts | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
| add_style_display | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| register_hooks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * PayPal Payment Buttons block lets users embed a PayPal button to sell products on their site. |
| 4 | * |
| 5 | * @package automattic/jetpack-paypal-payments |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\PaypalPayments; |
| 9 | |
| 10 | use Automattic\Jetpack\Assets; |
| 11 | use Automattic\Jetpack\Blocks; |
| 12 | |
| 13 | /** |
| 14 | * Class PayPal_Payment_Buttons |
| 15 | * |
| 16 | * @package Automattic\Jetpack\PaypalPayments |
| 17 | */ |
| 18 | class PayPal_Payment_Buttons { |
| 19 | /** |
| 20 | * The block full slugname. |
| 21 | * |
| 22 | * @var string |
| 23 | */ |
| 24 | public const BLOCK_NAME = 'jetpack/paypal-payment-buttons'; |
| 25 | |
| 26 | /** |
| 27 | * PayPal partner attribution ID used for tracking. |
| 28 | * |
| 29 | * @var string |
| 30 | */ |
| 31 | public const PAYPAL_PARTNER_ATTRIBUTION_ID = 'WooNCPS_Ecom_Wordpress'; |
| 32 | |
| 33 | /** |
| 34 | * Validates and sanitizes a script URL to ensure it's from an allowed PayPal domain. |
| 35 | * |
| 36 | * @param string $url The URL to validate and sanitize. |
| 37 | * @return string|false The sanitized URL, or false if URL is not from an allowed PayPal domain. |
| 38 | */ |
| 39 | public static function sanitize_paypal_script_url( $url ) { |
| 40 | if ( empty( $url ) ) { |
| 41 | return false; |
| 42 | } |
| 43 | |
| 44 | $parsed_url = wp_parse_url( $url ); |
| 45 | if ( ! $parsed_url || empty( $parsed_url['host'] ) ) { |
| 46 | return false; |
| 47 | } |
| 48 | |
| 49 | // Normalize the host |
| 50 | $host = strtolower( $parsed_url['host'] ); |
| 51 | $host = rtrim( $host, '.' ); |
| 52 | |
| 53 | // Only allow specific PayPal domains |
| 54 | $allowed_hosts = array( |
| 55 | 'www.paypal.com', |
| 56 | 'paypal.com', |
| 57 | 'www.sandbox.paypal.com', |
| 58 | 'sandbox.paypal.com', |
| 59 | ); |
| 60 | |
| 61 | if ( ! in_array( $host, $allowed_hosts, true ) ) { |
| 62 | return false; |
| 63 | } |
| 64 | |
| 65 | // Rebuild the URL with HTTPS |
| 66 | $sanitized_url = 'https://' . $host; |
| 67 | |
| 68 | if ( isset( $parsed_url['path'] ) ) { |
| 69 | $sanitized_url .= $parsed_url['path']; |
| 70 | } |
| 71 | if ( isset( $parsed_url['query'] ) ) { |
| 72 | $sanitized_url .= '?' . $parsed_url['query']; |
| 73 | } |
| 74 | |
| 75 | return $sanitized_url; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Registers the block for use in Gutenberg |
| 80 | * This is done via an action so that we can disable |
| 81 | * registration if we need to. |
| 82 | */ |
| 83 | public static function register_block() { |
| 84 | Blocks::jetpack_register_block( |
| 85 | __DIR__, |
| 86 | array( |
| 87 | 'render_callback' => array( __CLASS__, 'render_block' ), |
| 88 | 'plan_check' => true, |
| 89 | ) |
| 90 | ); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Render the block. |
| 95 | * |
| 96 | * @param array $attributes The block attributes. |
| 97 | * @param string $content The block content. |
| 98 | * @return string|void |
| 99 | */ |
| 100 | public static function render_block( $attributes, $content ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 101 | $button_type = $attributes['buttonType'] ?? ''; |
| 102 | $script_src = $attributes['scriptSrc'] ?? ''; |
| 103 | $hosted_button_id = $attributes['hostedButtonId'] ?? ''; |
| 104 | $button_text = $attributes['buttonText'] ?? ''; |
| 105 | |
| 106 | if ( empty( $button_type ) || empty( $hosted_button_id ) ) { |
| 107 | return; |
| 108 | } |
| 109 | |
| 110 | // For stacked buttons, we need both scriptSrc and hostedButtonId |
| 111 | if ( 'stacked' === $button_type && empty( $script_src ) ) { |
| 112 | return; |
| 113 | } |
| 114 | |
| 115 | // For single buttons, we need buttonText |
| 116 | if ( 'single' === $button_type && empty( $button_text ) ) { |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | if ( 'stacked' === $button_type ) { |
| 121 | // Sanitize the script URL to ensure it's from an allowed PayPal domain |
| 122 | $sanitized_url = self::sanitize_paypal_script_url( $script_src ); |
| 123 | if ( false === $sanitized_url ) { |
| 124 | return; |
| 125 | } |
| 126 | |
| 127 | $script_url = esc_url( $sanitized_url ); |
| 128 | // We can't include the version number here. If we do, it is appended to the URL and causes a 400 response. |
| 129 | wp_enqueue_script( 'paypal-payment-buttons-block-head', $script_url, array(), null, false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion |
| 130 | add_filter( |
| 131 | 'script_loader_tag', |
| 132 | function ( $tag, $handle, $src ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 133 | if ( 'paypal-payment-buttons-block-head' === $handle ) { |
| 134 | // Add namespace to avoid conflicts with other PayPal SDK versions |
| 135 | if ( ! str_contains( $tag, 'data-namespace' ) ) { |
| 136 | $tag = preg_replace( '/(\s+)src=([\'"])/', '$1 data-namespace="paypal_payment_buttons" src=$2', $tag ); |
| 137 | } |
| 138 | // Add partner attribution ID |
| 139 | if ( ! str_contains( $tag, 'data-paypal-partner-attribution-id' ) ) { |
| 140 | $tag = preg_replace( '/(\s+)src=([\'"])/', '$1 data-paypal-partner-attribution-id="' . self::PAYPAL_PARTNER_ATTRIBUTION_ID . '" src=$2', $tag ); |
| 141 | } |
| 142 | } |
| 143 | return $tag; |
| 144 | }, |
| 145 | 10, |
| 146 | 3 |
| 147 | ); |
| 148 | |
| 149 | // Generate the button HTML and inline script |
| 150 | $container_id = 'paypal-container-' . $hosted_button_id; |
| 151 | $button_html = '<div id="' . esc_attr( $container_id ) . '"></div>'; |
| 152 | |
| 153 | $inline_script = sprintf( |
| 154 | '(window.paypal_payment_buttons || window.paypal).HostedButtons({ |
| 155 | hostedButtonId: %s, |
| 156 | }).render(%s);', |
| 157 | wp_json_encode( $hosted_button_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ), |
| 158 | wp_json_encode( '#' . $container_id, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) |
| 159 | ); |
| 160 | |
| 161 | wp_add_inline_script( 'paypal-payment-buttons-block-head', $inline_script ); |
| 162 | |
| 163 | return $button_html; |
| 164 | } |
| 165 | |
| 166 | // Single button type - generate the complete form HTML |
| 167 | if ( 'single' === $button_type ) { |
| 168 | self::register_hooks(); |
| 169 | |
| 170 | $payment_id = esc_attr( $hosted_button_id ); |
| 171 | $button_text_escaped = esc_attr( $button_text ); |
| 172 | $action_url = esc_url( 'https://www.paypal.com/ncp/payment/' . $payment_id . '?at_code=' . self::PAYPAL_PARTNER_ATTRIBUTION_ID ); |
| 173 | |
| 174 | $button_html = sprintf( |
| 175 | '<style>.pp-%1$s{text-align:center;border:none;border-radius:0.25rem;min-width:11.625rem;padding:0 2rem;height:2.625rem;font-weight:bold;background-color:#FFD140;color:#000000;font-family:"Helvetica Neue",Arial,sans-serif;font-size:1rem;line-height:1.25rem;cursor:pointer;}</style> |
| 176 | <div> |
| 177 | <form action="%2$s" method="post" target="_blank" style="display:inline-grid;justify-items:center;align-content:start;gap:0.5rem;"> |
| 178 | <input class="pp-%1$s" type="submit" value="%3$s" /> |
| 179 | <img src="https://www.paypalobjects.com/images/Debit_Credit_APM.svg" alt="cards" /> |
| 180 | <section style="font-size: 0.75rem;"> Powered by <img src="https://www.paypalobjects.com/paypal-ui/logos/svg/paypal-wordmark-color.svg" alt="paypal" style="height:0.875rem;vertical-align:middle;"/></section> |
| 181 | </form> |
| 182 | </div>', |
| 183 | $payment_id, |
| 184 | $action_url, |
| 185 | $button_text_escaped |
| 186 | ); |
| 187 | |
| 188 | return $button_html; |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Load editor styles for the block. |
| 194 | * These are loaded via enqueue_block_assets to ensure proper loading in the editor iframe context. |
| 195 | */ |
| 196 | public static function load_editor_styles() { |
| 197 | $handle = 'jp-paypal-payments-ncps-blocks'; |
| 198 | |
| 199 | Assets::register_script( |
| 200 | $handle, |
| 201 | '../../dist/paypal-payment-buttons/editor.js', |
| 202 | __FILE__, |
| 203 | array( |
| 204 | 'css_path' => '../../dist/paypal-payment-buttons/editor.css', |
| 205 | 'textdomain' => 'jetpack-paypal-payments', |
| 206 | ) |
| 207 | ); |
| 208 | wp_enqueue_style( $handle ); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Loads scripts |
| 213 | */ |
| 214 | public static function load_editor_scripts() { |
| 215 | Assets::register_script( |
| 216 | 'jp-paypal-payments-ncps-blocks', |
| 217 | '../../dist/paypal-payment-buttons/editor.js', |
| 218 | __FILE__, |
| 219 | array( |
| 220 | 'in_footer' => true, |
| 221 | 'textdomain' => 'jetpack-paypal-payments', |
| 222 | 'enqueue' => true, |
| 223 | // Editor styles are loaded separately, see load_editor_styles(). |
| 224 | 'css_path' => null, |
| 225 | ) |
| 226 | ); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Add display to the allowed styles. |
| 231 | * |
| 232 | * @see https://developer.wordpress.org/reference/hooks/safe_style_css/ |
| 233 | * |
| 234 | * @param array $safe_styles The allowed styles. |
| 235 | * @return array The allowed styles. |
| 236 | */ |
| 237 | public static function add_style_display( array $safe_styles ): array { |
| 238 | $safe_styles[] = 'display'; |
| 239 | return $safe_styles; |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Register hooks. |
| 244 | */ |
| 245 | public static function register_hooks() { |
| 246 | add_filter( 'safe_style_css', array( __CLASS__, 'add_style_display' ) ); |
| 247 | } |
| 248 | } |