Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 276
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Simple_Payments_Widget
0.00% covered (danger)
0.00%
0 / 273
0.00% covered (danger)
0.00%
0 / 20
6162
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 hide_simple_payment_widget
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 defaults
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 filter_nonces
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_style
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 admin_enqueue_styles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 admin_enqueue_scripts
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 ajax_get_payment_buttons
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 format_product_post_for_ajax_reponse
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ajax_save_payment_button
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
240
 ajax_delete_payment_button
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 get_decimal_places
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 validate_ajax_params
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
132
 get_first_product_id
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 widget
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 get_latest_field_value
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_product_from_post
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 record_event
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 update
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
30
 form
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Pay with PayPal (aka Simple Payments)
4 *
5 * Display a Pay with PayPal button as a Widget.
6 *
7 * @package automattic/jetpack-paypal-payments
8 */
9
10namespace Automattic\Jetpack\Paypal_Payments\Widgets;
11
12use Automattic\Jetpack\PayPal_Payments;
13use Automattic\Jetpack\Paypal_Payments\Simple_Payments;
14use Automattic\Jetpack\Tracking;
15use Jetpack;
16use WP_Error;
17use WP_Widget;
18
19// Disable direct access/execution to/of the widget code.
20if ( ! defined( 'ABSPATH' ) ) {
21    exit( 0 );
22}
23
24if ( ! class_exists( 'Simple_Payments_Widget' ) ) {
25    /**
26     * Pay with PayPal (aka Simple Payments)
27     *
28     * Display a Pay with PayPal button as a Widget.
29     */
30    class Simple_Payments_Widget extends WP_Widget {
31        /**
32         * The package version.
33         *
34         * @var string
35         */
36        private $package_version = PayPal_Payments::PACKAGE_VERSION;
37
38        /**
39         * Currencies should be supported by PayPal:
40         *
41         * @var array $supported_currency_list
42         * @link https://developer.paypal.com/docs/api/reference/currency-codes/
43         *
44         * List has to be in sync with list at the block's client side and API's backend side:
45         * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/extensions/blocks/simple-payments/constants.js#L9-L39
46         * @link https://github.com/Automattic/jetpack/blob/31efa189ad223c0eb7ad085ac0650a23facf9ef5/modules/simple-payments/simple-payments.php#L386-L415
47         *
48         * Indian Rupee (INR) is listed here for backwards compatibility with previously added widgets.
49         * It's not supported by Pay with PayPal because at the time of the creation of this file
50         * because it's limited to in-country PayPal India accounts only.
51         * Discussion: https://github.com/Automattic/wp-calypso/pull/28236
52         */
53        public static $supported_currency_list = array(
54            'USD' => '$',
55            'GBP' => '&#163;',
56            'JPY' => '&#165;',
57            'BRL' => 'R$',
58            'EUR' => '&#8364;',
59            'NZD' => 'NZ$',
60            'AUD' => 'A$',
61            'CAD' => 'C$',
62            'INR' => '₹',
63            'ILS' => '₪',
64            'RUB' => '₽',
65            'MXN' => 'MX$',
66            'SEK' => 'Skr',
67            'HUF' => 'Ft',
68            'CHF' => 'CHF',
69            'CZK' => 'Kč',
70            'DKK' => 'Dkr',
71            'HKD' => 'HK$',
72            'NOK' => 'Kr',
73            'PHP' => '₱',
74            'PLN' => 'PLN',
75            'SGD' => 'S$',
76            'TWD' => 'NT$',
77            'THB' => '฿',
78        );
79
80        /**
81         * Constructor.
82         */
83        public function __construct() {
84            parent::__construct(
85                'jetpack_simple_payments_widget',
86                /** This filter is documented in modules/widgets/facebook-likebox.php */
87                apply_filters( 'jetpack_widget_name', __( 'Pay with PayPal', 'jetpack-paypal-payments' ) ),
88                array(
89                    'classname'                   => 'simple-payments',
90                    'description'                 => __( 'Add a Pay with PayPal button as a Widget.', 'jetpack-paypal-payments' ),
91                    'customize_selective_refresh' => true,
92                )
93            );
94
95            global $pagenow;
96            if ( is_customize_preview() || 'widgets.php' === $pagenow ) {
97                add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_styles' ) );
98            }
99
100            if ( is_customize_preview() && Simple_Payments::is_enabled_jetpack_simple_payments() ) {
101                add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
102
103                add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
104                add_action( 'wp_ajax_customize-jetpack-simple-payments-buttons-get', array( $this, 'ajax_get_payment_buttons' ) );
105                add_action( 'wp_ajax_customize-jetpack-simple-payments-button-save', array( $this, 'ajax_save_payment_button' ) );
106                add_action( 'wp_ajax_customize-jetpack-simple-payments-button-delete', array( $this, 'ajax_delete_payment_button' ) );
107            }
108
109            add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_simple_payment_widget' ) );
110        }
111
112        /**
113         * Return an array of the widgets hidden from the Legacy Widget block.
114         *
115         * This is used to hide the Pay with PayPal from the Legacy Widget block.
116         *
117         * @param array $widget_types the widget types that are currently hidden.
118         * @return array Widget types hidden from the Legacy Widget block
119         */
120        public function hide_simple_payment_widget( $widget_types ) {
121            $widget_types[] = 'simple_payments_widget';
122            return $widget_types;
123        }
124
125        /**
126         * Return an associative array of default values.
127         *
128         * These values are used in new widgets.
129         *
130         * @return array Default values for the widget options.
131         */
132        private function defaults() {
133            $current_user       = wp_get_current_user();
134            $default_product_id = $this->get_first_product_id();
135
136            return array(
137                'title'                    => '',
138                'product_post_id'          => $default_product_id,
139                'form_action'              => '',
140                'form_product_id'          => 0,
141                'form_product_title'       => '',
142                'form_product_description' => '',
143                'form_product_image_id'    => 0,
144                'form_product_image_src'   => '',
145                'form_product_currency'    => '',
146                'form_product_price'       => '',
147                'form_product_multiple'    => '',
148                'form_product_email'       => $current_user->user_email,
149            );
150        }
151
152        /**
153         * Adds a nonce for customizing menus.
154         *
155         * @param array $nonces Array of nonces.
156         * @return array $nonces Modified array of nonces.
157         */
158        public function filter_nonces( $nonces ) {
159            $nonces['customize-jetpack-simple-payments'] = wp_create_nonce( 'customize-jetpack-simple-payments' );
160            return $nonces;
161        }
162
163        /**
164         * Enqueue styles.
165         */
166        public function enqueue_style() {
167            wp_enqueue_style( 'simple-payments-widget-style', plugins_url( 'simple-payments/style.css', __FILE__ ), array(), '20180518' );
168        }
169
170        /**
171         * Enqueue admin styles.
172         */
173        public function admin_enqueue_styles() {
174            wp_enqueue_style(
175                'simple-payments-widget-customizer',
176                plugins_url( 'simple-payments/customizer.css', __FILE__ ),
177                array(),
178                $this->package_version
179            );
180        }
181
182        /**
183         * Enqueue admin scripts.
184         */
185        public function admin_enqueue_scripts() {
186                wp_enqueue_media();
187                wp_enqueue_script(
188                    'simple-payments-widget-customizer',
189                    plugins_url( '/simple-payments/customizer.js', __FILE__ ),
190                    array( 'jquery' ),
191                    $this->package_version,
192                    true
193                );
194                wp_localize_script(
195                    'simple-payments-widget-customizer',
196                    'jpSimplePaymentsStrings',
197                    array(
198                        'deleteConfirmation' => __( 'Are you sure you want to delete this item? It will be disabled and removed from all locations where it currently appears.', 'jetpack-paypal-payments' ),
199                    )
200                );
201        }
202
203        /**
204         * Get payment buttons.
205         */
206        public function ajax_get_payment_buttons() {
207            if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
208                wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
209            }
210
211            if ( ! current_user_can( 'customize' ) ) {
212                wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
213            }
214
215            $post_type_object = get_post_type_object( Simple_Payments::$post_type_product );
216            if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
217                wp_send_json_error( 'insufficient_post_permissions', 403, JSON_UNESCAPED_SLASHES );
218            }
219
220            $product_posts = get_posts(
221                array(
222                    'numberposts' => 100,
223                    'orderby'     => 'date',
224                    'post_type'   => Simple_Payments::$post_type_product,
225                    'post_status' => 'publish',
226                )
227            );
228
229            $formatted_products = array_map( array( $this, 'format_product_post_for_ajax_reponse' ), $product_posts );
230
231            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
232            wp_send_json_success( $formatted_products, null, JSON_UNESCAPED_SLASHES );
233        }
234
235        /**
236         * Format product_post object.
237         *
238         * @param object $product_post - info about the post the product is on.
239         */
240        public function format_product_post_for_ajax_reponse( $product_post ) {
241            return array(
242                'ID'         => $product_post->ID,
243                'post_title' => $product_post->post_title,
244            );
245        }
246
247        /**
248         * Handle saving the simple payments widget.
249         */
250        public function ajax_save_payment_button() {
251            if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
252                wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
253            }
254
255            if ( ! current_user_can( 'customize' ) ) {
256                wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
257            }
258
259            $post_type_object = get_post_type_object( Simple_Payments::$post_type_product );
260            if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
261                wp_send_json_error( 'insufficient_post_permissions', 403, JSON_UNESCAPED_SLASHES );
262            }
263
264            if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
265                wp_send_json_error( 'missing_params', 400, JSON_UNESCAPED_SLASHES );
266            }
267
268            $params = wp_unslash( $_POST['params'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Manually validated by validate_ajax_params().
269            $errors = $this->validate_ajax_params( $params );
270            if ( ! empty( $errors->errors ) ) {
271                // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
272                wp_send_json_error( $errors, null, JSON_UNESCAPED_SLASHES );
273            }
274
275            $product_post_id = isset( $params['product_post_id'] ) ? (int) $params['product_post_id'] : 0;
276
277            $product_post = array(
278                'ID'            => $product_post_id,
279                'post_type'     => Simple_Payments::$post_type_product,
280                'post_status'   => 'publish',
281                'post_title'    => $params['post_title'],
282                'post_content'  => $params['post_content'],
283                '_thumbnail_id' => ! empty( $params['image_id'] ) ? $params['image_id'] : -1,
284                'meta_input'    => array(
285                    'spay_currency' => $params['currency'],
286                    'spay_price'    => $params['price'],
287                    'spay_multiple' => isset( $params['multiple'] ) ? (int) $params['multiple'] : 0,
288                    'spay_email'    => is_email( $params['email'] ),
289                ),
290            );
291
292            if ( empty( $product_post_id ) ) {
293                $product_post_id = wp_insert_post( $product_post );
294            } else {
295                $product_post_id = wp_update_post( $product_post );
296            }
297
298            if ( ! $product_post_id || is_wp_error( $product_post_id ) ) {
299                // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
300                wp_send_json_error( $product_post_id, null, JSON_UNESCAPED_SLASHES );
301            }
302
303            $tracks_properties = array(
304                'id'       => $product_post_id,
305                'currency' => $params['currency'],
306                'price'    => $params['price'],
307            );
308            if ( 0 === $product_post['ID'] ) {
309                $this->record_event( 'created', 'create', $tracks_properties );
310            } else {
311                $this->record_event( 'updated', 'update', $tracks_properties );
312            }
313
314            wp_send_json_success(
315                array(
316                    'product_post_id'    => $product_post_id,
317                    'product_post_title' => $params['post_title'],
318                ),
319                null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
320                JSON_UNESCAPED_SLASHES
321            );
322        }
323
324        /**
325         * Handle deleting the simple payment widget.
326         */
327        public function ajax_delete_payment_button() {
328            if ( ! check_ajax_referer( 'customize-jetpack-simple-payments', 'customize-jetpack-simple-payments-nonce', false ) ) {
329                wp_send_json_error( 'bad_nonce', 400, JSON_UNESCAPED_SLASHES );
330            }
331
332            if ( ! current_user_can( 'customize' ) ) {
333                wp_send_json_error( 'customize_not_allowed', 403, JSON_UNESCAPED_SLASHES );
334            }
335
336            if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
337                wp_send_json_error( 'missing_params', 400, JSON_UNESCAPED_SLASHES );
338            }
339
340            $params         = wp_unslash( $_POST['params'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Manually validated just below.
341            $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id' ) );
342            if ( ! empty( $illegal_params ) ) {
343                wp_send_json_error( 'illegal_params', 400, JSON_UNESCAPED_SLASHES );
344            }
345
346            $product_id   = (int) $params['product_post_id'];
347            $product_post = get_post( $product_id );
348
349            $return = array( 'status' => $product_post->post_status );
350
351            wp_delete_post( $product_id, true );
352            $status = get_post_status( $product_id );
353            if ( false === $status ) {
354                $return['status'] = 'deleted';
355            }
356
357            $this->record_event( 'deleted', 'delete', array( 'id' => $product_id ) );
358
359            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
360            wp_send_json_success( $return, null, JSON_UNESCAPED_SLASHES );
361        }
362
363        /**
364         * Returns the number of decimal places on string representing a price.
365         *
366         * @param string $number Price to check.
367         * @return int|null number of decimal places.
368         */
369        private function get_decimal_places( $number ) {
370            $parts = explode( '.', $number );
371            if ( count( $parts ) > 2 ) {
372                return null;
373            }
374
375            return isset( $parts[1] ) ? strlen( $parts[1] ) : 0;
376        }
377
378        /**
379         * Validate ajax parameters.
380         *
381         * @param array $params - the parameters.
382         */
383        public function validate_ajax_params( $params ) {
384            $errors = new WP_Error();
385
386            $illegal_params = array_diff( array_keys( $params ), array( 'product_post_id', 'post_title', 'post_content', 'image_id', 'currency', 'price', 'multiple', 'email' ) );
387            if ( ! empty( $illegal_params ) ) {
388                $errors->add( 'illegal_params', __( 'Invalid parameters.', 'jetpack-paypal-payments' ) );
389            }
390
391            if ( empty( $params['post_title'] ) ) {
392                $errors->add( 'post_title', __( "People need to know what they're paying for! Please add a brief title.", 'jetpack-paypal-payments' ) );
393            }
394
395            if ( empty( $params['price'] ) || ! is_numeric( $params['price'] ) || (float) $params['price'] <= 0 ) {
396                $errors->add( 'price', __( 'Everything comes with a price tag these days. Please add a your product price.', 'jetpack-paypal-payments' ) );
397            }
398
399            // Japan's Yen is the only supported currency with a zero decimal precision.
400            $precision            = strtoupper( $params['currency'] ) === 'JPY' ? 0 : 2;
401            $price_decimal_places = $this->get_decimal_places( $params['price'] );
402            if ( $price_decimal_places === null || $price_decimal_places > $precision ) {
403                $errors->add( 'price', __( 'Invalid price', 'jetpack-paypal-payments' ) );
404            }
405
406            if ( empty( $params['email'] ) || ! is_email( $params['email'] ) ) {
407                $errors->add( 'email', __( 'We want to make sure payments reach you, so please add an email address.', 'jetpack-paypal-payments' ) );
408            }
409
410            return $errors;
411        }
412
413        /**
414         * Get the id of the first product.
415         */
416        public function get_first_product_id() {
417            $product_posts = get_posts(
418                array(
419                    'numberposts' => 1,
420                    'orderby'     => 'date',
421                    'post_type'   => Simple_Payments::$post_type_product,
422                    'post_status' => 'publish',
423                )
424            );
425
426            return ! empty( $product_posts ) ? $product_posts[0]->ID : null;
427        }
428
429        /**
430         * Front-end display of widget.
431         *
432         * @see WP_Widget::widget()
433         *
434         * @html-template-var array $instance
435         *
436         * @param array $args     Widget arguments.
437         * @param array $instance Saved values from database.
438         */
439        public function widget( $args, $instance ) {
440            $instance = wp_parse_args( $instance, $this->defaults() );
441
442            // Enqueue front end assets.
443            $this->enqueue_style();
444
445            echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
446
447            /** This filter is documented in core/src/wp-includes/default-widgets.php */
448            $title = apply_filters( 'widget_title', $instance['title'] );
449            if ( ! empty( $title ) ) {
450                echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
451            }
452
453            echo '<div class="jetpack-simple-payments-content">';
454
455            if ( ! empty( $instance['form_action'] ) && in_array( $instance['form_action'], array( 'add', 'edit' ), true ) && is_customize_preview() ) {
456                require __DIR__ . '/simple-payments/widget.php';
457            } else {
458                $jsp                    = Simple_Payments::get_instance();
459                $simple_payments_button = $jsp->parse_shortcode(
460                    array(
461                        'id' => $instance['product_post_id'],
462                    )
463                );
464
465                if ( $simple_payments_button !== null || is_customize_preview() ) {
466                    echo $simple_payments_button; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
467                }
468            }
469
470            echo '</div><!--simple-payments-->';
471
472            echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
473
474            /** This action is already documented in modules/widgets/gravatar-profile.php */
475            do_action( 'jetpack_stats_extra', 'widget_view', 'simple_payments' );
476        }
477
478        /**
479         * Gets the latests field value from either the old instance or the new instance.
480         *
481         * @param array $new_instance mixed Array of values for the new form instance.
482         * @param array $old_instance mixed Array of values for the old form instance.
483         * @param mixed $field mixed Field value.
484         */
485        private function get_latest_field_value( $new_instance, $old_instance, $field ) {
486            return ! empty( $new_instance[ $field ] )
487                ? sanitize_text_field( $new_instance[ $field ] )
488                : $old_instance[ $field ];
489        }
490
491        /**
492         * Gets the product fields from the product post. If no post found
493         * it returns the default values.
494         *
495         * @param int $product_post_id Product Post ID.
496         * @return array $fields Product Fields from the Product Post.
497         */
498        private function get_product_from_post( $product_post_id ) {
499            $product_post    = get_post( $product_post_id );
500            $form_product_id = $product_post_id;
501            if ( ! empty( $product_post ) ) {
502                $form_product_image_id = get_post_thumbnail_id( $product_post_id );
503
504                return array(
505                    'form_product_id'          => $form_product_id,
506                    'form_product_title'       => get_the_title( $product_post ),
507                    'form_product_description' => $product_post->post_content,
508                    'form_product_image_id'    => $form_product_image_id,
509                    'form_product_image_src'   => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
510                    'form_product_currency'    => get_post_meta( $product_post_id, 'spay_currency', true ),
511                    'form_product_price'       => get_post_meta( $product_post_id, 'spay_price', true ),
512                    'form_product_multiple'    => get_post_meta( $product_post_id, 'spay_multiple', true ) || '0',
513                    'form_product_email'       => get_post_meta( $product_post_id, 'spay_email', true ),
514                );
515            }
516
517            return $this->defaults();
518        }
519
520        /**
521         * Record a Track event and bump a MC stat.
522         *
523         * @param string $stat_name - the name of the stat.
524         * @param string $event_action - the action we're recording.
525         * @param array  $event_properties - proprties of the event.
526         */
527        private function record_event( $stat_name, $event_action, $event_properties = array() ) {
528            $current_user = wp_get_current_user();
529
530            // `bumps_stats_extra` only exists on .com
531            if ( function_exists( 'bump_stats_extras' ) && function_exists( 'require_lib' ) ) {
532                require_lib( 'tracks/client' );
533                tracks_record_event( $current_user, 'simple_payments_button_' . $event_action, $event_properties );
534                /** This action is documented in modules/widgets/social-media-icons.php */
535                do_action( 'jetpack_bump_stats_extra', 'simple_payments', $stat_name );
536                return;
537            }
538
539            $tracking = new Tracking();
540            $tracking->tracks_record_event( $current_user, 'jetpack_wpa_simple_payments_button_' . $event_action, $event_properties );
541
542            if ( class_exists( 'Jetpack' ) ) {
543                $jetpack = Jetpack::init();
544                // $jetpack->stat automatically prepends the stat group with 'jetpack-'
545                $jetpack->stat( 'simple_payments', $stat_name );
546                $jetpack->do_stats( 'server_side' );
547            }
548        }
549
550        /**
551         * Sanitize widget form values as they are saved.
552         *
553         * @see WP_Widget::update()
554         *
555         * @param array $new_instance Values just sent to be saved.
556         * @param array $old_instance Previously saved values from database.
557         *
558         * @return array Updated safe values to be saved.
559         */
560        public function update( $new_instance, $old_instance ) {
561            $defaults = $this->defaults();
562            // do not overrite `product_post_id` for `$new_instance` with the defaults.
563            $new_instance = wp_parse_args( $new_instance, array_diff_key( $defaults, array( 'product_post_id' => 0 ) ) );
564            $old_instance = wp_parse_args( $old_instance, $defaults );
565
566            $required_widget_props = array(
567                'title'           => $this->get_latest_field_value( $new_instance, $old_instance, 'title' ),
568                'product_post_id' => $this->get_latest_field_value( $new_instance, $old_instance, 'product_post_id' ),
569                'form_action'     => $this->get_latest_field_value( $new_instance, $old_instance, 'form_action' ),
570            );
571
572            if ( strcmp( $new_instance['form_action'], $old_instance['form_action'] ) !== 0 ) {
573                if ( 'edit' === $new_instance['form_action'] ) {
574                    return array_merge( $this->get_product_from_post( (int) $old_instance['product_post_id'] ), $required_widget_props );
575                }
576
577                if ( 'clear' === $new_instance['form_action'] ) {
578                    return array_merge( $this->defaults(), $required_widget_props );
579                }
580            }
581
582            $form_product_image_id = (int) $new_instance['form_product_image_id'];
583
584            $form_product_email = ! empty( $new_instance['form_product_email'] )
585                ? sanitize_text_field( $new_instance['form_product_email'] )
586                : $defaults['form_product_email'];
587
588            return array_merge(
589                $required_widget_props,
590                array(
591                    'form_product_id'          => (int) $new_instance['form_product_id'],
592                    'form_product_title'       => sanitize_text_field( $new_instance['form_product_title'] ),
593                    'form_product_description' => sanitize_text_field( $new_instance['form_product_description'] ),
594                    'form_product_image_id'    => $form_product_image_id,
595                    'form_product_image_src'   => wp_get_attachment_image_url( $form_product_image_id, 'thumbnail' ),
596                    'form_product_currency'    => sanitize_text_field( $new_instance['form_product_currency'] ),
597                    'form_product_price'       => sanitize_text_field( $new_instance['form_product_price'] ),
598                    'form_product_multiple'    => sanitize_text_field( $new_instance['form_product_multiple'] ),
599                    'form_product_email'       => $form_product_email,
600                )
601            );
602        }
603
604        /**
605         * Back-end widget form.
606         *
607         * @see WP_Widget::form()
608         *
609         * @html-template-var array $instance
610         * @html-template-var WP_Post[] $product_posts
611         *
612         * @param array $instance Previously saved values from database.
613         * @return string|void
614         */
615        public function form( $instance ) {
616            if ( ! Simple_Payments::is_enabled_jetpack_simple_payments() ) {
617                require __DIR__ . '/simple-payments/admin-warning.php';
618                return;
619            }
620
621            $instance = wp_parse_args( $instance, $this->defaults() );
622
623            $product_posts = get_posts( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
624                array(
625                    'numberposts' => 100,
626                    'orderby'     => 'date',
627                    'post_type'   => Simple_Payments::$post_type_product,
628                    'post_status' => 'publish',
629                )
630            );
631
632            require __DIR__ . '/simple-payments/form.php';
633        }
634    }
635}