__( 'Display your latest Instagram photos.', 'jetpack' ),
'show_instance_in_rest' => true,
)
);
if ( is_active_widget( false, false, self::ID_BASE ) || is_active_widget( false, false, 'monster' ) || is_customize_preview() ) {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_css' ) );
}
add_action( 'wp_ajax_wpcom_instagram_widget_update_widget_token_id', array( $this, 'ajax_update_widget_token_id' ) );
$this->valid_options = array(
/**
* Allow changing the maximum number of columns available for the Instagram widget.
*
* @module widgets
*
* @since 8.8.0
*
* @param int $max_columns maximum number of columns.
*/
'max_columns' => apply_filters( 'wpcom_instagram_widget_max_columns', 3 ),
'max_count' => 20,
);
$this->defaults = array(
'token_id' => null,
'title' => __( 'Instagram', 'jetpack' ),
'columns' => 2,
'count' => 6,
);
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_widget_in_block_editor' ) );
}
/**
* Remove Instagram widget from Legacy Widget block.
*
* @param array $widget_types Widget type data.
* This only applies to new blocks being added.
*/
public function hide_widget_in_block_editor( $widget_types ) {
$widget_types[] = self::ID_BASE;
return $widget_types;
}
/**
* Enqueues the widget's frontend CSS but only if the widget is currently in use.
*/
public function enqueue_css() {
wp_enqueue_style( self::ID_BASE, plugins_url( 'instagram/instagram.css', __FILE__ ), array(), JETPACK__VERSION );
}
/**
* Updates the widget's option in the database to have the passed Keyring token ID.
* This is so the user doesn't have to click the "Save" button when we want to set it.
*
* @param int $token_id A Keyring token ID.
* @param int $number The widget ID.
*/
public function update_widget_token_id( $token_id, $number = null ) {
$widget_options = $this->get_settings();
if ( empty( $number ) ) {
$number = $this->number;
}
if ( ! isset( $widget_options[ $number ] ) || ! is_array( $widget_options[ $number ] ) ) {
$widget_options[ $number ] = $this->defaults;
}
$widget_options[ $number ]['token_id'] = (int) $token_id;
$this->save_settings( $widget_options );
}
/**
* Updates the widget's option in the database to have the passed Keyring token ID.
*
* Sends a json success or error response.
*/
public function ajax_update_widget_token_id() {
if ( ! check_ajax_referer( 'instagram-widget-save-token', 'savetoken', false ) ) {
wp_send_json_error( array( 'message' => 'bad_nonce' ), 403 );
}
if ( ! current_user_can( 'customize' ) ) {
wp_send_json_error( array( 'message' => 'not_authorized' ), 403 );
}
$token_id = ! empty( $_POST['keyring_id'] ) ? (int) $_POST['keyring_id'] : null;
$widget_id = ! empty( $_POST['instagram_widget_id'] ) ? (int) $_POST['instagram_widget_id'] : null;
// For Simple sites check if the token is valid.
// (For Atomic sites, this check is done via the api: wpcom/v2/instagram/).
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$token = Keyring::init()->get_token_store()->get_token(
array(
'type' => 'access',
'id' => $token_id,
)
);
if ( get_current_user_id() !== (int) $token->meta['user_id'] ) {
return wp_send_json_error( array( 'message' => 'not_authorized' ), 403 );
}
}
$this->update_widget_token_id( $token_id, $widget_id );
$this->update_widget_token_legacy_status( false );
return wp_send_json_success( null, 200 );
}
/**
* Updates the widget's option in the database to show if it is for legacy API or not.
*
* @param bool $is_legacy_token A flag to indicate if a token is for the legacy Instagram API.
*/
public function update_widget_token_legacy_status( $is_legacy_token ) {
$widget_options = $this->get_settings();
if ( ! is_array( $widget_options[ $this->number ] ) ) {
$widget_options[ $this->number ] = $this->defaults;
}
$widget_options[ $this->number ]['is_legacy_token'] = $is_legacy_token;
$this->save_settings( $widget_options );
return $is_legacy_token;
}
/**
* Get's the status of the token from the API
*
* @param int $token_id A Keyring token ID.
* @return array The status of the token's connection.
*/
private function get_token_status( $token_id ) {
if ( empty( $token_id ) ) {
return array( 'valid' => false );
}
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$token = Keyring::init()->get_token_store()->get_token(
array(
'type' => 'access',
'id' => $token_id,
)
);
return array(
'valid' => ! empty( $token ),
'legacy' => $token && 'instagram' === $token->name,
);
}
$site = Jetpack_Options::get_option( 'id' );
$path = sprintf( '/sites/%s/instagram/%s/check-token', $site, $token_id );
$result = Client::wpcom_json_api_request_as_blog( $path, 2, array( 'headers' => array( 'content-type' => 'application/json' ) ), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $result );
if ( 200 !== $response_code ) {
return array(
// We assume the token is valid if the response_code is anything but the invalid
// token codes we send back. This is to make sure it's not reset, if the API is down
// or something.
'valid' => ! ( 403 === $response_code || 401 === $response_code ),
'legacy' => 'ERROR',
);
}
$status = json_decode( $result['body'], true );
return $status;
}
/**
* Validates the widget instance's token ID and then uses it to fetch images from Instagram.
* It then caches the result which it will use on subsequent pageviews.
* Keyring is not loaded nor is a remote request is not made in the event of a cache hit.
*
* @param array $instance A widget $instance, as passed to a widget's widget() method.
* @return WP_Error|array A WP_Error on error, an array of images on success.
*/
public function get_data( $instance ) {
if ( empty( $instance['token_id'] ) ) {
return new WP_Error( 'empty_token', esc_html__( 'The token id was empty', 'jetpack' ), 403 );
}
$transient_key = implode( '|', array( 'jetpack_instagram_widget', $instance['token_id'], $instance['count'] ) );
$cached_images = get_transient( $transient_key );
if ( $cached_images ) {
return $cached_images;
}
$site = Jetpack_Options::get_option( 'id' );
$path = sprintf( '/sites/%s/instagram/%s?count=%s', $site, $instance['token_id'], $instance['count'] );
$result = Client::wpcom_json_api_request_as_blog( $path, 2, array( 'headers' => array( 'content-type' => 'application/json' ) ), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $result );
if ( 200 !== $response_code ) {
return new WP_Error( 'invalid_response', esc_html__( 'The response was invalid', 'jetpack' ), $response_code );
}
$data = json_decode( wp_remote_retrieve_body( $result ), true );
if ( ! isset( $data['images'] ) || ! is_array( $data['images'] ) ) {
return new WP_Error( 'missing_images', esc_html__( 'The images were missing', 'jetpack' ), $response_code );
}
set_transient( $transient_key, $data, HOUR_IN_SECONDS );
return $data;
}
/**
* Outputs the contents of the widget on the front end.
*
* If the widget is unconfigured, a configuration message is displayed to users with admin access
* and the entire widget is hidden from everyone else to avoid displaying an empty widget.
*
* @param array $args The sidebar arguments that control the wrapping HTML.
* @param array $instance The widget instance (configuration options).
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args( $instance, $this->defaults );
$data = $this->get_data( $instance );
if ( is_wp_error( $data ) ) {
return;
}
$images = $data['images'];
$status = $this->get_token_status( $instance['token_id'] );
// Don't display anything to non-blog admins if the widgets is unconfigured or API call fails.
if ( ( ! $status['valid'] || ! is_array( $images ) ) && ! current_user_can( 'edit_theme_options' ) ) {
return;
}
echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// Always show a title on an unconfigured widget.
if ( ! $status['valid'] && empty( $instance['title'] ) ) {
$instance['title'] = $this->defaults['title'];
}
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo esc_html( $instance['title'] );
echo $args['after_title']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( $status['valid'] && current_user_can( 'edit_theme_options' ) && $status['legacy'] ) {
echo '
' . sprintf(
wp_kses(
/* translators: %s is a link to reconnect the Instagram widget */
__( 'In order to continue using this widget you must reconnect to Instagram.', 'jetpack' ),
array(
'a' => array(
'href' => array(),
),
)
),
esc_url( add_query_arg( 'instagram_widget_id', $this->number, admin_url( 'widgets.php' ) ) )
) . '
';
}
if ( ! $status['valid'] ) {
echo '
' . sprintf(
wp_kses(
/* translators: %s is a link to configure the Instagram widget */
__( 'In order to use this Instagram widget, you must configure it first.', 'jetpack' ),
array(
'a' => array(
'href' => array(),
),
)
),
esc_url( add_query_arg( 'instagram_widget_id', $this->number, admin_url( 'widgets.php' ) ) )
) . '
';
} else {
if ( ! is_array( $images ) ) {
echo '
' . esc_html__( 'There was an error retrieving images from Instagram. An attempt will be remade in a few minutes.', 'jetpack' ) . '
\n";
}
}
echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
/** This action is already documented in modules/widgets/gravatar-profile.php */
do_action( 'jetpack_stats_extra', 'widget_view', 'instagram' );
}
/**
* Get the URL to connect the widget to Instagram
*
* @return string the conneciton URL.
*/
private function get_connect_url() {
$connect_url = '';
if ( defined( 'IS_WPCOM' ) && IS_WPCOM && function_exists( 'wpcom_keyring_get_connect_URL' ) ) {
$connect_url = wpcom_keyring_get_connect_URL( 'instagram-basic-display', 'instagram-widget' );
} else {
$jetpack_blog_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
sprintf( '/sites/%d/external-services', $jetpack_blog_id )
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( $response['body'] );
$connect_url = new WP_Error( 'connect_url_not_found', 'Connect URL not found' );
if ( ! empty( $body->services->{'instagram-basic-display'}->connect_URL ) ) {
$connect_url = $body->services->{'instagram-basic-display'}->connect_URL;
}
}
return $connect_url;
}
/**
* Is this request trying to remove the widgets stored id?
*
* @param array $status The status of the token's connection.
* @return bool if this request trying to remove the widgets stored id.
*/
public function removing_widgets_stored_id( $status ) {
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
}
/**
* Outputs the widget configuration form for the widget administration page.
* Allows the user to add new Instagram Keyring tokens and more.
*
* @param array $instance The widget instance (configuration options).
*/
public function form( $instance ) {
$instance = wp_parse_args( $instance, $this->defaults );
if ( ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) && ! ( new Manager() )->is_user_connected() ) {
echo '
';
printf(
// translators: %1$1 and %2$s are the opening and closing a tags creating a link to the Jetpack dashboard.
esc_html__( 'In order to use this widget you need to %1$scomplete your Jetpack connection%2$s by authorizing your user.', 'jetpack' ),
'',
''
);
echo '
';
return;
}
// If coming back to the widgets page from an action, expand this widget.
if ( isset( $_GET['instagram_widget_id'] ) && (int) $_GET['instagram_widget_id'] === (int) $this->number ) {
echo '';
}
$status = $this->get_token_status( $instance['token_id'] );
// If removing the widget's stored token ID.
if ( $this->removing_widgets_stored_id( $status ) ) {
if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'instagram-widget-remove-token-' . $this->number . '-' . $instance['token_id'] ) ) {
wp_die( esc_html__( 'Missing or invalid security nonce.', 'jetpack' ) );
}
$instance['token_id'] = $this->defaults['token_id'];
$this->update_widget_token_id( $instance['token_id'] );
$this->update_widget_token_legacy_status( false );
} 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.
$instance['is_legacy_token'] = $this->update_widget_token_legacy_status( $status['legacy'] );
} elseif ( ! $status['valid'] ) { // If the token isn't valid reset it.
$instance['token_id'] = $this->defaults['token_id'];
$this->update_widget_token_id( $instance['token_id'] );
}
// No connection, or a legacy API token? Display a connection link.
$is_legacy_token = ( isset( $instance['is_legacy_token'] ) && true === $instance['is_legacy_token'] );
if ( $is_legacy_token ) {
echo '
' . esc_html__( 'In order to continue using this widget you must reconnect to Instagram.', 'jetpack' ) . '
';
echo wp_kses(
__( 'Important: You must first click Publish to activate this widget before connecting your account. After saving the widget, click the button below to connect your Instagram account.', 'jetpack' ),
array(
'strong' => array(),
'em' => array(),
)
);
echo '