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