Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 314
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Instagram_Widget
0.00% covered (danger)
0.00%
0 / 305
0.00% covered (danger)
0.00%
0 / 13
7656
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 hide_widget_in_block_editor
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_css
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_widget_token_id
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 ajax_update_widget_token_id
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 update_widget_token_legacy_status
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_token_status
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 get_data
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 widget
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
240
 get_connect_url
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 removing_widgets_stored_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
30
 form
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 1
702
 update
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Instagram Widget. Display some Instagram photos via a widget.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\Client;
9use Automattic\Jetpack\Connection\Manager;
10
11if ( ! defined( 'ABSPATH' ) ) {
12    exit( 0 );
13}
14
15/**
16 * This is the actual Instagram widget along with other code that only applies to the widget.
17 */
18class Jetpack_Instagram_Widget extends WP_Widget {
19
20    const ID_BASE = 'wpcom_instagram_widget'; // Don't change this as Atomic widgets will break.
21
22    /**
23     * Options for the widget.
24     *
25     * @access public
26     *
27     * @var array
28     */
29    public $valid_options;
30
31    /**
32     * Default settings for the widgets.
33     *
34     * @access public
35     *
36     * @var array
37     */
38    public $defaults;
39
40    /**
41     * Sets the widget properties in WordPress, hooks a few functions, and sets some widget options.
42     */
43    public function __construct() {
44        parent::__construct(
45            self::ID_BASE,
46            /** This filter is documented in modules/widgets/facebook-likebox.php */
47            apply_filters( 'jetpack_widget_name', esc_html__( 'Instagram', 'jetpack' ) ),
48            array(
49                'description'           => __( 'Display your latest Instagram photos.', 'jetpack' ),
50                'show_instance_in_rest' => true,
51            )
52        );
53
54        add_action( 'wp_ajax_wpcom_instagram_widget_update_widget_token_id', array( $this, 'ajax_update_widget_token_id' ) );
55
56        $this->valid_options = array(
57            /**
58             * Allow changing the maximum number of columns available for the Instagram widget.
59             *
60             * @module widgets
61             *
62             * @since 8.8.0
63             *
64             * @param int $max_columns maximum number of columns.
65             */
66            'max_columns' => apply_filters( 'wpcom_instagram_widget_max_columns', 3 ),
67            'max_count'   => 20,
68        );
69
70        $this->defaults = array(
71            'token_id' => null,
72            'title'    => __( 'Instagram', 'jetpack' ),
73            'columns'  => 2,
74            'count'    => 6,
75        );
76
77        add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_widget_in_block_editor' ) );
78    }
79
80    /**
81     * Remove the "Instagram" widget from the Legacy Widget block
82     *
83     * @param array $widget_types List of widgets that are currently removed from the Legacy Widget block.
84     * @return array $widget_types New list of widgets that will be removed.
85     */
86    public function hide_widget_in_block_editor( $widget_types ) {
87        $widget_types[] = self::ID_BASE;
88        return $widget_types;
89    }
90
91    /**
92     * Enqueues the widget's frontend CSS but only if the widget is currently in use.
93     */
94    public function enqueue_css() {
95        wp_enqueue_style( self::ID_BASE, plugins_url( 'instagram/instagram.css', __FILE__ ), array(), JETPACK__VERSION );
96    }
97
98    /**
99     * Updates the widget's option in the database to have the passed Keyring token ID.
100     * This is so the user doesn't have to click the "Save" button when we want to set it.
101     *
102     * @param int $token_id A Keyring token ID.
103     * @param int $number The widget ID.
104     */
105    public function update_widget_token_id( $token_id, $number = null ) {
106        $widget_options = $this->get_settings();
107
108        if ( empty( $number ) ) {
109            $number = $this->number;
110        }
111
112        if ( ! isset( $widget_options[ $number ] ) || ! is_array( $widget_options[ $number ] ) ) {
113            $widget_options[ $number ] = $this->defaults;
114        }
115
116        $widget_options[ $number ]['token_id'] = (int) $token_id;
117
118        $this->save_settings( $widget_options );
119    }
120
121    /**
122     * Updates the widget's option in the database to have the passed Keyring token ID.
123     *
124     * Sends a json success or error response.
125     */
126    public function ajax_update_widget_token_id() {
127        if ( ! check_ajax_referer( 'instagram-widget-save-token', 'savetoken', false ) ) {
128            wp_send_json_error( array( 'message' => 'bad_nonce' ), 403 );
129        }
130
131        if ( ! current_user_can( 'customize' ) ) {
132            wp_send_json_error( array( 'message' => 'not_authorized' ), 403 );
133        }
134
135        $token_id  = ! empty( $_POST['keyring_id'] ) ? (int) $_POST['keyring_id'] : null;
136        $widget_id = ! empty( $_POST['instagram_widget_id'] ) ? (int) $_POST['instagram_widget_id'] : null;
137
138        // For Simple sites check if the token is valid.
139        // (For Atomic sites, this check is done via the api: wpcom/v2/instagram/<token_id>).
140        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
141            $token = Keyring::init()->get_token_store()->get_token(
142                array(
143                    'type' => 'access',
144                    'id'   => $token_id,
145                )
146            );
147            if ( get_current_user_id() !== (int) $token->meta['user_id'] ) {
148                return wp_send_json_error( array( 'message' => 'not_authorized' ), 403 );
149            }
150        }
151
152        $this->update_widget_token_id( $token_id, $widget_id );
153        $this->update_widget_token_legacy_status( false );
154
155        return wp_send_json_success( null, 200 );
156    }
157
158    /**
159     * Updates the widget's option in the database to show if it is for legacy API or not.
160     *
161     * @param bool $is_legacy_token A flag to indicate if a token is for the legacy Instagram API.
162     */
163    public function update_widget_token_legacy_status( $is_legacy_token ) {
164        $widget_options = $this->get_settings();
165
166        if ( ! is_array( $widget_options[ $this->number ] ) ) {
167            $widget_options[ $this->number ] = $this->defaults;
168        }
169
170        $widget_options[ $this->number ]['is_legacy_token'] = $is_legacy_token;
171        $this->save_settings( $widget_options );
172
173        return $is_legacy_token;
174    }
175
176    /**
177     * Get's the status of the token from the API
178     *
179     * @param int $token_id A Keyring token ID.
180     * @return array The status of the token's connection.
181     */
182    private function get_token_status( $token_id ) {
183        if ( empty( $token_id ) ) {
184            return array( 'valid' => false );
185        }
186        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
187            $token = Keyring::init()->get_token_store()->get_token(
188                array(
189                    'type' => 'access',
190                    'id'   => $token_id,
191                )
192            );
193
194            return array(
195                'valid'  => ! empty( $token ),
196                'legacy' => $token && 'instagram' === $token->name,
197            );
198        }
199
200        $site          = Jetpack_Options::get_option( 'id' );
201        $path          = sprintf( '/sites/%s/instagram/%s/check-token', $site, $token_id );
202        $result        = Client::wpcom_json_api_request_as_blog( $path, 2, array( 'headers' => array( 'content-type' => 'application/json' ) ), null, 'wpcom' );
203        $response_code = wp_remote_retrieve_response_code( $result );
204        if ( 200 !== $response_code ) {
205            return array(
206                // We assume the token is valid if the response_code is anything but the invalid
207                // token codes we send back. This is to make sure it's not reset, if the API is down
208                // or something.
209                'valid'  => ! ( 403 === $response_code || 401 === $response_code ),
210                'legacy' => 'ERROR',
211            );
212        }
213        $status = json_decode( $result['body'], true );
214        return $status;
215    }
216
217    /**
218     * Validates the widget instance's token ID and then uses it to fetch images from Instagram.
219     * It then caches the result which it will use on subsequent pageviews.
220     * Keyring is not loaded nor is a remote request is not made in the event of a cache hit.
221     *
222     * @param array $instance A widget $instance, as passed to a widget's widget() method.
223     * @return WP_Error|array A WP_Error on error, an array of images on success.
224     */
225    public function get_data( $instance ) {
226        if ( empty( $instance['token_id'] ) ) {
227            return new WP_Error( 'empty_token', esc_html__( 'The token id was empty', 'jetpack' ), 403 );
228        }
229
230        $transient_key = implode( '|', array( 'jetpack_instagram_widget', $instance['token_id'], $instance['count'] ) );
231        $cached_images = get_transient( $transient_key );
232        if ( $cached_images ) {
233            return $cached_images;
234        }
235
236        $site   = Jetpack_Options::get_option( 'id' );
237        $path   = sprintf( '/sites/%s/instagram/%s?count=%s', $site, $instance['token_id'], $instance['count'] );
238        $result = Client::wpcom_json_api_request_as_blog( $path, 2, array( 'headers' => array( 'content-type' => 'application/json' ) ), null, 'wpcom' );
239
240        $response_code = wp_remote_retrieve_response_code( $result );
241        if ( 200 !== $response_code ) {
242            return new WP_Error( 'invalid_response', esc_html__( 'The response was invalid', 'jetpack' ), $response_code );
243        }
244
245        $data = json_decode( wp_remote_retrieve_body( $result ), true );
246        if ( ! isset( $data['images'] ) || ! is_array( $data['images'] ) ) {
247            return new WP_Error( 'missing_images', esc_html__( 'The images were missing', 'jetpack' ), $response_code );
248        }
249
250        set_transient( $transient_key, $data, HOUR_IN_SECONDS );
251        return $data;
252    }
253
254    /**
255     * Outputs the contents of the widget on the front end.
256     *
257     * If the widget is unconfigured, a configuration message is displayed to users with admin access
258     * and the entire widget is hidden from everyone else to avoid displaying an empty widget.
259     *
260     * @param array $args The sidebar arguments that control the wrapping HTML.
261     * @param array $instance The widget instance (configuration options).
262     */
263    public function widget( $args, $instance ) {
264        $instance = wp_parse_args( $instance, $this->defaults );
265        $data     = $this->get_data( $instance );
266        if ( is_wp_error( $data ) ) {
267            return;
268        }
269
270        $images = $data['images'];
271
272        $status = $this->get_token_status( $instance['token_id'] );
273        // Don't display anything to non-blog admins if the widgets is unconfigured or API call fails.
274        if ( ( ! $status['valid'] || ! is_array( $images ) ) && ! current_user_can( 'edit_theme_options' ) ) {
275            return;
276        }
277
278        // Enqueue front end assets.
279        $this->enqueue_css();
280
281        echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
282
283        // Always show a title on an unconfigured widget.
284        if ( ! $status['valid'] && empty( $instance['title'] ) ) {
285            $instance['title'] = $this->defaults['title'];
286        }
287
288        if ( ! empty( $instance['title'] ) ) {
289            echo $args['before_title']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
290            echo $instance['title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
291            echo $args['after_title']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
292        }
293
294        if ( $status['valid'] && current_user_can( 'edit_theme_options' ) && $status['legacy'] ) {
295            echo '<p><em>' . sprintf(
296                wp_kses(
297                    /* translators: %s is a link to reconnect the Instagram widget */
298                    __( 'In order to continue using this widget you must <a href="%s">reconnect to Instagram</a>.', 'jetpack' ),
299                    array(
300                        'a' => array(
301                            'href' => array(),
302                        ),
303                    )
304                ),
305                esc_url( add_query_arg( 'instagram_widget_id', $this->number, admin_url( 'widgets.php' ) ) )
306            ) . '</em></p>';
307        }
308
309        if ( ! $status['valid'] ) {
310            echo '<p><em>' . sprintf(
311                wp_kses(
312                    /* translators: %s is a link to configure the Instagram widget */
313                    __( 'In order to use this Instagram widget, you must <a href="%s">configure it</a> first.', 'jetpack' ),
314                    array(
315                        'a' => array(
316                            'href' => array(),
317                        ),
318                    )
319                ),
320                esc_url( add_query_arg( 'instagram_widget_id', $this->number, admin_url( 'widgets.php' ) ) )
321            ) . '</em></p>';
322        } elseif ( ! is_array( $images ) ) {
323            echo '<p>' . esc_html__( 'There was an error retrieving images from Instagram. An attempt will be remade in a few minutes.', 'jetpack' ) . '</p>';
324        } elseif ( ! $images ) {
325            echo '<p>' . esc_html__( 'No Instagram images were found.', 'jetpack' ) . '</p>';
326        } else {
327            echo '<div class="' . esc_attr( 'wpcom-instagram-images wpcom-instagram-columns-' . (int) $instance['columns'] ) . '">' . "\n";
328            foreach ( $images as $image ) {
329                /**
330                 * Filter how Instagram image links open in the Instagram widget.
331                 *
332                 * @module widgets
333                 *
334                 * @since 8.8.0
335                 *
336                 * @param string $target Target attribute.
337                 */
338                $image_target = apply_filters( 'wpcom_instagram_widget_target', '_self' );
339                echo '<a href="' . esc_url( $image['link'] ) . '" target="' . esc_attr( $image_target ) . '"><div class="sq-bg-image" style="background-image: url(' . esc_url( set_url_scheme( $image['url'] ) ) . ')"><span class="screen-reader-text">' . esc_attr( $image['title'] ) . '</span></div></a>' . "\n";
340            }
341            echo "</div>\n";
342        }
343
344        echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
345
346        /** This action is already documented in modules/widgets/gravatar-profile.php */
347        do_action( 'jetpack_stats_extra', 'widget_view', 'instagram' );
348    }
349
350    /**
351     * Get the URL to connect the widget to Instagram
352     *
353     * @return string the conneciton URL.
354     */
355    private function get_connect_url() {
356        $connect_url = '';
357
358        if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'wpcom_keyring_get_connect_URL' ) ) {
359            $connect_url = wpcom_keyring_get_connect_URL( 'instagram-basic-display', 'instagram-widget' );
360        } else {
361            $jetpack_blog_id = Jetpack_Options::get_option( 'id' );
362            $response        = Client::wpcom_json_api_request_as_user(
363                sprintf( '/sites/%d/external-services', $jetpack_blog_id )
364            );
365
366            if ( is_wp_error( $response ) ) {
367                return $response;
368            }
369
370            $body        = json_decode( $response['body'] );
371            $connect_url = new WP_Error( 'connect_url_not_found', 'Connect URL not found' );
372            if ( ! empty( $body->services->{'instagram-basic-display'}->connect_URL ) ) {
373                $connect_url = $body->services->{'instagram-basic-display'}->connect_URL;
374            }
375        }
376
377        return $connect_url;
378    }
379
380    /**
381     * Is this request trying to remove the widgets stored id?
382     *
383     * @param array $status The status of the token's connection.
384     * @return bool if this request trying to remove the widgets stored id.
385     */
386    public function removing_widgets_stored_id( $status ) {
387        return $status['valid'] && isset( $_GET['instagram_widget_id'] ) && (int) $_GET['instagram_widget_id'] === (int) $this->number && ! empty( $_GET['instagram_widget'] ) && 'remove_token' === $_GET['instagram_widget']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
388    }
389
390    /**
391     * Outputs the widget configuration form for the widget administration page.
392     * Allows the user to add new Instagram Keyring tokens and more.
393     *
394     * @param array $instance The widget instance (configuration options).
395     * @return string|void
396     */
397    public function form( $instance ) {
398        $instance = wp_parse_args( $instance, $this->defaults );
399
400        if ( ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) && ! ( new Manager() )->is_user_connected() ) {
401            echo '<p>';
402            printf(
403                // translators: %1$1 and %2$s are the opening and closing a tags creating a link to the Jetpack dashboard.
404                esc_html__( 'In order to use this widget you need to %1$scomplete your Jetpack connection%2$s by authorizing your user.', 'jetpack' ),
405                '<a href="' . esc_url( Jetpack::admin_url( array( 'page' => 'jetpack#/connect-user' ) ) ) . '">',
406                '</a>'
407            );
408            echo '</p>';
409            return;
410        }
411
412        // If coming back to the widgets page from an action, expand this widget.
413        if ( isset( $_GET['instagram_widget_id'] ) && (int) $_GET['instagram_widget_id'] === (int) $this->number ) {
414            echo '<script type="text/javascript">jQuery(document).ready(function($){ $(\'.widget[id$="wpcom_instagram_widget-' . esc_js( $this->number ) . '"] .widget-inside\').slideDown(\'fast\'); });</script>';
415        }
416
417        $status = $this->get_token_status( $instance['token_id'] );
418
419        // If removing the widget's stored token ID.
420        if ( $this->removing_widgets_stored_id( $status ) ) {
421            if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'instagram-widget-remove-token-' . $this->number . '-' . $instance['token_id'] ) ) {
422                wp_die( esc_html__( 'Missing or invalid security nonce.', 'jetpack' ) );
423            }
424
425            $instance['token_id'] = $this->defaults['token_id'];
426
427            $this->update_widget_token_id( $instance['token_id'] );
428            $this->update_widget_token_legacy_status( false );
429        } elseif ( $status['valid'] && ( ! isset( $instance['is_legacy_token'] ) || 'ERROR' === $instance['is_legacy_token'] ) ) { // If a token ID is stored, check if we know if it is a legacy API token or not.
430            $instance['is_legacy_token'] = $this->update_widget_token_legacy_status( $status['legacy'] );
431        } elseif ( ! $status['valid'] ) { // If the token isn't valid reset it.
432            $instance['token_id'] = $this->defaults['token_id'];
433            $this->update_widget_token_id( $instance['token_id'] );
434        }
435
436        // No connection, or a legacy API token? Display a connection link.
437        $is_legacy_token = ( isset( $instance['is_legacy_token'] ) && true === $instance['is_legacy_token'] );
438
439        if ( $is_legacy_token ) {
440            echo '<p><strong>' . esc_html__( 'In order to continue using this widget you must reconnect to Instagram.', 'jetpack' ) . '</strong></p>';
441        }
442
443        if ( is_customize_preview() && ! $instance['token_id'] ) {
444            echo '<p>';
445            echo wp_kses(
446                __( '<strong>Important: You must first click Publish to activate this widget <em>before</em> connecting your account.</strong> After saving the widget, click the button below to connect your Instagram account.', 'jetpack' ),
447                array(
448                    'strong' => array(),
449                    'em'     => array(),
450                )
451            );
452            echo '</p>';
453        }
454
455        if ( ! $instance['token_id'] || $is_legacy_token ) {
456            ?>
457            <script type="text/javascript">
458                function getScreenCenterSpecs( width, height ) {
459                    const screenTop = typeof window.screenTop !== 'undefined' ? window.screenTop : window.screenY,
460                        screenLeft = typeof window.screenLeft !== 'undefined' ? window.screenLeft : window.screenX;
461
462                    return [
463                        'width=' + width,
464                        'height=' + height,
465                        'top=' + ( screenTop + window.innerHeight / 2 - height / 2 ),
466                        'left=' + ( screenLeft + window.innerWidth / 2 - width / 2 ),
467                    ].join();
468                };
469                function openWindow( button ) {
470                    // let's just double check that we aren't getting an unknown random domain injected in here somehow.
471                    if (! /^https:\/\/public-api.wordpress.com\/connect\//.test(button.dataset.connecturl) ) {
472                        return;
473                    }
474                    window.open(
475                        button.dataset.connecturl, //TODO: Check if this needs validation it could be a XSS problem. Check the domain maybe?
476                        '_blank',
477                        'toolbar=0,location=0,menubar=0,' + getScreenCenterSpecs( 700, 700 )
478                    );
479                    button.innerText = '<?php echo esc_js( __( 'Connecting…', 'jetpack' ) ); ?>';
480                    button.disabled = true;
481                    window.onmessage = function( { data } ) {
482                        if ( !! data.keyring_id ) {
483                            var payload = {
484                                action: 'wpcom_instagram_widget_update_widget_token_id',
485                                savetoken: '<?php echo esc_js( wp_create_nonce( 'instagram-widget-save-token' ) ); ?>',
486                                keyring_id: data.keyring_id,
487                                instagram_widget_id: button.dataset.widgetid,
488                            };
489                            jQuery.post( ajaxurl, payload, function( response ) {
490                                var widget = jQuery(button).closest('div.widget');
491                                if ( ! window.wpWidgets ) {
492                                    window.location = '<?php echo esc_js( add_query_arg( array( 'autofocus[panel]' => 'widgets' ), admin_url( 'customize.php' ) ) ); ?>';
493                                } else {
494                                    wpWidgets.save( widget, 0, 1, 1 );
495                                }
496                            } );
497                        }
498                    };
499                }
500            </script>
501            <?php
502            $connect_url = $this->get_connect_url();
503            if ( is_wp_error( $connect_url ) ) {
504                echo '<p>' . esc_html__( 'Instagram is currently experiencing connectivity issues, please try again later to connect.', 'jetpack' ) . '</p>';
505                return;
506            }
507            ?>
508            <p style="text-align:center"><button class="button-primary" onclick="openWindow(this); return false;" data-widgetid="<?php echo esc_attr( $this->number ); ?>" data-connecturl="<?php echo esc_attr( $connect_url ); ?>"><?php echo esc_html( __( 'Connect Instagram Account', 'jetpack' ) ); ?></button></p>
509
510            <?php // Include hidden fields for the widget settings before a connection is made, otherwise the default settings are lost after connecting. ?>
511            <input type="hidden" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" value="<?php echo esc_attr( $instance['title'] ); ?>" />
512            <input type="hidden" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>" value="<?php echo esc_attr( $instance['count'] ); ?>" />
513            <input type="hidden" id="<?php echo esc_attr( $this->get_field_id( 'columns' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'columns' ) ); ?>" value="<?php echo esc_attr( $instance['columns'] ); ?>" />
514
515            <?php
516            echo '<p><small>' . sprintf(
517                wp_kses(
518                    /* translators: %s is a link to log in to Instagram */
519                    __( 'Having trouble? Try <a href="%s" target="_blank" rel="noopener noreferrer">logging into the correct account</a> on Instagram.com first.', 'jetpack' ),
520                    array(
521                        'a' => array(
522                            'href'   => array(),
523                            'target' => array(),
524                            'rel'    => array(),
525                        ),
526                    )
527                ),
528                'https://instagram.com/accounts/login/'
529            ) . '</small></p>';
530            return;
531        }
532
533        // Connected account.
534        $page = ( is_customize_preview() ) ? 'customize.php' : 'widgets.php';
535
536        $query_args = array(
537            'instagram_widget_id' => $this->number,
538            'instagram_widget'    => 'remove_token',
539            'nonce'               => wp_create_nonce( 'instagram-widget-remove-token-' . $this->number . '-' . $instance['token_id'] ),
540        );
541
542        if ( is_customize_preview() ) {
543            $query_args['autofocus[panel]'] = 'widgets';
544        }
545
546        $remove_token_id_url = add_query_arg( $query_args, admin_url( $page ) );
547
548        $data = $this->get_data( $instance );
549        // TODO: Revisit the error handling. I think we should be using WP_Error here and
550        // Jetpack::Client is the legacy check.
551        if ( is_wp_error( $data ) || 'ERROR' === $instance['is_legacy_token'] ) {
552            echo '<p>' . esc_html__( 'Instagram is currently experiencing connectivity issues, please try again later to connect.', 'jetpack' ) . '</p>';
553            return;
554        }
555        echo '<p>';
556        printf(
557            wp_kses(
558                /* translators: %1$s is the URL of the connected Instagram account, %2$s is the username of the connected Instagram account, %3$s is the URL to disconnect the account. */
559                __( '<strong>Connected Instagram Account</strong><br /> <a target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a> | <a href="%3$s">remove</a>', 'jetpack' ),
560                array(
561                    'a'      => array(
562                        'href'   => array(),
563                        'rel'    => array(),
564                        'target' => array(),
565                    ),
566                    'strong' => array(),
567                    'br'     => array(),
568                )
569            ),
570            esc_url( 'https://instagram.com/' . $data['external_name'] ),
571            esc_html( $data['external_name'] ),
572            esc_url( $remove_token_id_url )
573        );
574        echo '</p>';
575
576        // Title.
577        echo '<p><label><strong>' . esc_html__( 'Widget Title', 'jetpack' ) . '</strong> <input type="text" id="' . esc_attr( $this->get_field_id( 'title' ) ) . '" name="' . esc_attr( $this->get_field_name( 'title' ) ) . '" value="' . esc_attr( $instance['title'] ) . '" class="widefat" /></label></p>';
578
579        // Number of images to show.
580        echo '<p><label>';
581        echo '<strong>' . esc_html__( 'Images', 'jetpack' ) . '</strong><br />';
582        echo esc_html__( 'Number to display:', 'jetpack' ) . ' ';
583        echo '<select name="' . esc_attr( $this->get_field_name( 'count' ) ) . '">';
584        for ( $i = 1; $i <= $this->valid_options['max_count']; $i++ ) {
585            echo '<option value="' . esc_attr( $i ) . '"' . selected( $i, $instance['count'], false ) . '>' . esc_attr( $i ) . '</option>';
586        }
587        echo '</select>';
588        echo '</label></p>';
589
590        // Columns.
591        echo '<p><label>';
592        echo '<strong>' . esc_html__( 'Layout', 'jetpack' ) . '</strong><br />';
593        echo esc_html__( 'Number of columns:', 'jetpack' ) . ' ';
594        echo '<select name="' . esc_attr( $this->get_field_name( 'columns' ) ) . '">';
595        for ( $i = 1; $i <= $this->valid_options['max_columns']; $i++ ) {
596            echo '<option value="' . esc_attr( $i ) . '"' . selected( $i, $instance['columns'], false ) . '>' . esc_attr( $i ) . '</option>';
597        }
598        echo '</select>';
599        echo '</label></p>';
600
601        echo '<p><small>' . esc_html__( 'New images may take up to 15 minutes to show up on your site.', 'jetpack' ) . '</small></p>';
602    }
603
604    /**
605     * Validates and sanitizes the user-supplied widget options.
606     *
607     * @param array $new_instance The user-supplied values.
608     * @param array $old_instance The existing widget options.
609     * @return array A validated and sanitized version of $new_instance.
610     */
611    public function update( $new_instance, $old_instance ) {
612        $instance = $this->defaults;
613
614        if ( ! empty( $old_instance['token_id'] ) ) {
615            $instance['token_id'] = $old_instance['token_id'];
616        }
617
618        if ( isset( $new_instance['title'] ) ) {
619            $instance['title'] = wp_strip_all_tags( $new_instance['title'] );
620        }
621
622        if ( isset( $new_instance['columns'] ) ) {
623            $instance['columns'] = max( 1, min( $this->valid_options['max_columns'], (int) $new_instance['columns'] ) );
624        }
625
626        if ( isset( $new_instance['count'] ) ) {
627            $instance['count'] = max( 1, min( $this->valid_options['max_count'], (int) $new_instance['count'] ) );
628        }
629
630        return $instance;
631    }
632}
633
634add_action(
635    'widgets_init',
636    function () {
637        if ( Jetpack::is_connection_ready() ) {
638            register_widget( 'Jetpack_Instagram_Widget' );
639        }
640    }
641);
642