Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- namespace aviaFramework\widgets;
- use \WP_Error;
- use \AviaHelper;
- /**
- * AVIA INSTAGRAM WIDGET
- *
- * Widget that displays your latest Instagram Photos
- *
- * Modified version - SECURITY HARDENED
- * =====================================
- *
- * Adds a background caching of images on own server to avoid to access instagram to display the images
- *
- * Security fixes applied (vs. original):
- * 1. sslverify no longer disabled when contacting instagram.com (MITM protection)
- * 2. Host allowlist enforced before downloading any URL returned by instagram (SSRF protection)
- * 3. Downloaded files are validated as real images (wp_check_filetype + getimagesize) before being kept
- * 4. File permissions reduced from 0777 to 0644
- * 5. Filenames are run through sanitize_file_name() instead of using raw basename() from remote data
- * 6. download_url() calls are given an explicit timeout and downloaded files are size-checked
- *
- * @package AviaFramework
- * @since ???
- * @since 4.3.1 extended, improved and modified by günter
- * @since 4.9 Code was moved from class-framework-widgets.php
- * @since 4.3.1-secure hardened by Guenni007 - see notes above
- */
- if( ! defined( 'AVIA_FW' ) ) { exit( 'No direct script access allowed' ); }
- if( ! class_exists( __NAMESPACE__ . '\avia_instagram_widget', false ) )
- {
- class avia_instagram_widget extends \aviaFramework\widgets\base\Avia_Widget
- {
- /**
- * @since 4.3.1
- * @var array
- */
- protected $upload_folders;
- /**
- * Stores the expire time for cached images in seconds.
- *
- * @since 4.3.1
- * @var int
- */
- protected $expire_time;
- /**
- * @since 4.3.1
- * @var boolean
- */
- protected $activate_cron;
- /**
- * Holds all caching info for each widget instance.
- *
- * @since 4.3.1
- * @var array
- */
- protected $cache;
- /**
- * @since 4.3.1
- * @var array
- */
- protected $cached_file_sizes;
- /**
- * Allowed remote hosts for image downloads (SSRF protection).
- * Instagram/Meta serves media from these CDN domains. Anything else is rejected.
- *
- * @since 4.3.1-secure
- * @var array
- */
- protected $allowed_download_hosts;
- /**
- * Allowed mime types for cached image files.
- *
- * @since 4.3.1-secure
- * @var array
- */
- protected $allowed_mime_types;
- /**
- * Maximum accepted file size (bytes) for a single downloaded image.
- *
- * @since 4.3.1-secure
- * @var int
- */
- protected $max_file_size;
- public function __construct()
- {
- $id_base = 'avia-instagram-feed';
- $name = THEMENAME . ' ' . __( 'Instagram', 'avia_framework' );
- $widget_options = array(
- 'classname' => 'avia-instagram-feed avia_no_block_preview',
- 'description' => __( 'Displays your latest Instagram photos', 'avia_framework' ),
- 'show_instance_in_rest' => true,
- 'customize_selective_refresh' => false
- );
- parent::__construct( $id_base, $name, $widget_options );
- $this->defaults = array(
- 'title' => __( 'Instagram', 'avia_framework' ),
- 'username' => '',
- 'cache' => apply_filters( 'avf_instagram_default_cache_location', '' ), // '' | 'server'
- 'number' => 9,
- 'columns' => 3,
- 'size' => 'thumbnail',
- 'target' => 'lightbox' ,
- 'link' => __( 'Follow Me!', 'avia_framework' ),
- 'avia_key' => ''
- );
- $this->upload_folders = wp_upload_dir();
- if( is_ssl() )
- {
- $this->upload_folders['baseurl'] = str_replace( 'http://', 'https://', $this->upload_folders['baseurl'] );
- }
- $folder = apply_filters( 'avf_instagram_cache_folder_name', 'avia_instagram_cache' );
- $this->upload_folders['instagram_dir'] = trailingslashit( trailingslashit( $this->upload_folders['basedir'] ) . $folder );
- $this->upload_folders['instagram_url'] = trailingslashit( trailingslashit( $this->upload_folders['baseurl'] ) . $folder );
- $this->expire_time = HOUR_IN_SECONDS * 2;
- $this->expire_time = apply_filters_deprecated( 'null_instagram_cache_time', array( $this->expire_time ), '4.3.1', 'avf_instagram_file_cache_time', __( 'Adding possible file caching on server might need a longer period of time to invalidate cache.', 'avia_framework' ) );
- $this->expire_time = apply_filters( 'avf_instagram_file_cache_time', $this->expire_time );
- $this->activate_cron = ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON );
- $this->activate_cron = apply_filters( 'avf_instagram_activate_cron', $this->activate_cron );
- $this->cache = $this->get_cache();
- $this->cached_file_sizes = array( 'thumbnail', 'small', 'large', 'original' );
- /**
- * SECURITY: only these hosts may ever be contacted for a media download.
- * Adjust via filter if Meta/Instagram changes their CDN domains.
- *
- * @since 4.3.1-secure
- */
- $this->allowed_download_hosts = apply_filters( 'avf_instagram_allowed_download_hosts', array(
- 'cdninstagram.com',
- 'fbcdn.net',
- ) );
- /**
- * SECURITY: only these mime types are accepted for cached files.
- *
- * @since 4.3.1-secure
- */
- $this->allowed_mime_types = apply_filters( 'avf_instagram_allowed_mime_types', array(
- 'jpg|jpeg|jpe' => 'image/jpeg',
- 'png' => 'image/png',
- 'webp' => 'image/webp',
- ) );
- /**
- * SECURITY: reject anything larger than this (default 15 MB) - avoids disk-fill / DoS via
- * an attacker-controlled or compromised response serving an oversized payload.
- *
- * @since 4.3.1-secure
- */
- $this->max_file_size = apply_filters( 'avf_instagram_max_file_size', 15 * MB_IN_BYTES );
- /**
- * WP Cron job events
- */
- if( $this->activate_cron )
- {
- add_action( 'av_instagram_scheduled_filecheck', array( $this, 'handler_scheduled_filecheck' ), 10 );
- }
- /**
- * Makes sure to keep cron job alive as fallback
- */
- if( is_admin() )
- {
- add_action( 'admin_init', array( $this, 'handler_init_filecheck' ), 99999 );
- add_action( 'delete_widget', array( $this, 'handler_delete_widget' ), 10, 3 );
- }
- else
- {
- add_action( 'init', array( $this, 'handler_init_filecheck' ), 99999 );
- }
- }
- /**
- * @since 4.3.1
- */
- public function __destruct()
- {
- parent::__destruct();
- unset( $this->upload_folders );
- unset( $this->cache );
- unset( $this->cached_file_sizes );
- }
- /**
- * Returns the cache info array
- *
- * @since 4.3.1
- * @return array
- */
- public function get_cache()
- {
- if( is_null( $this->cache ) )
- {
- $cache = get_option( 'avia_instagram_widgets_cache', '' );
- if( is_array( $cache ) )
- {
- $this->cache = $cache;
- }
- else if( ! is_string( $cache ) || empty( $cache ) )
- {
- $this->cache = null;
- }
- else
- {
- $cache = json_decode( $cache, true );
- $this->cache = is_array( $cache ) ? $cache : null;
- }
- if( empty( $this->cache ) )
- {
- $this->cache = array(
- 'last_updated' => 0,
- 'instances' => array()
- );
- }
- }
- return $this->cache;
- }
- /**
- * Update the cache array in DB
- *
- * @since 4.3.1
- * @param array|null $cache
- */
- public function update_cache( ?array $cache = null )
- {
- if( ! is_null( $cache) )
- {
- $this->cache = $cache;
- }
- $save = json_encode( $this->cache );
- update_option( 'avia_instagram_widgets_cache', $save );
- }
- /**
- * Ensure a valid instance array filled with defaults
- *
- * @since 4.3.1
- * @param array $instance_cache
- * @return array
- */
- protected function parse_args_instance_cache( array $instance_cache )
- {
- $instance_cache = wp_parse_args( (array) $instance_cache, array(
- 'upload_folder' => '',
- 'path_error' => '',
- 'instagram_error' => '',
- 'upload_errors' => false,
- 'last_update' => 0,
- 'cached_list' => array(),
- 'instagram_list' => array()
- ));
- return $instance_cache;
- }
- /**
- * Creates a unique key for the given instance for our cache array
- *
- * @since 4.3.1
- * @param array $instance
- * @param string $id_widget
- * @return string
- */
- protected function create_avia_key( array $instance, $id_widget )
- {
- $k = 0;
- $key = str_replace( $this->id_base . '-', '', $id_widget ) . '-' . AviaHelper::save_string( $instance['title'], '-' );
- $orig_key = $key;
- while( array_key_exists( $key, $this->cache['instances'] ) )
- {
- $key = $orig_key . "-{$k}";
- $k++;
- }
- return $key;
- }
- /**
- * Output the widget in frontend
- *
- * NOTE: rendering logic (widget/form/update/block preview) is unchanged from the
- * original - the security issues in the original were all in the data-fetching /
- * file-caching layer below, not in output escaping, which already looked correct.
- *
- * @param array $args
- * @param array $instance
- */
- public function widget( $args, $instance )
- {
- $instance = $this->parse_args_instance( $instance );
- $fields = $this->get_field_names();
- foreach( $instance as $key => $value )
- {
- if( in_array( $key, $fields ) )
- {
- $instance[ $key ] = esc_attr( $value );
- }
- }
- $instance = apply_filters( 'avf_widget_front_instance', $instance, $args, get_called_class() );
- if( $this->in_block_editor_preview( $args, $instance ) )
- {
- return;
- }
- extract( $args, EXTR_SKIP );
- extract( $instance, EXTR_SKIP );
- $original_cache = $cache;
- $cache = apply_filters( 'avf_conditional_setting_external_links', $cache, __CLASS__, $this, $args, $instance );
- if( ! in_array( $cache, array( '', 'server' ) ) )
- {
- $cache = $original_cache;
- }
- $title = apply_filters( 'widget_title', $title, $args );
- if( ( trim( $username ) == '' ) && ! is_user_logged_in() && ! current_user_can( 'edit_posts' ) )
- {
- return;
- }
- echo $before_widget;
- if ( ! empty( $title ) )
- {
- echo $before_title . $title . $after_title;
- }
- do_action( 'aviw_before_widget', $instance );
- if( $username != '' )
- {
- $errors = array();
- $media_array = array();
- $instance_cache = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : null;
- if( ! is_null( $instance_cache ) )
- {
- if( ! empty( $instance_cache['instagram_error'] ) )
- {
- $errors = array( $instance_cache['instagram_error'] );
- }
- if( ! empty( $instance_cache['upload_errors'] ) && ( 'server' == $instance['cache'] ) )
- {
- foreach( $instance_cache['cached_list'] as $img )
- {
- if( ! empty( $img['errors'] ) )
- {
- $errors = array_merge( $errors, $img['errors'] );
- }
- }
- }
- if( 'server' == $instance['cache'] )
- {
- $media_array = $instance_cache['cached_list'];
- $url = trailingslashit( trailingslashit( $this->upload_folders['instagram_url'] ) . $instance_cache['upload_folder'] );
- foreach( $media_array as $key => $media )
- {
- if( ! empty( $media['errors'] ) )
- {
- $errors = array_merge( $errors, $media['errors'] );
- }
- if( ! empty( $media[ $size ] ) )
- {
- $media_array[ $key ][ $size ] = $url . $media[ $size ];
- }
- if( ! empty( $media[ 'original' ] ) )
- {
- $media_array[ $key ]['original'] = $url . $media['original'];
- }
- }
- }
- else
- {
- $media_array = $instance_cache['instagram_list'];
- }
- }
- if( ! empty( $errors ) && is_user_logged_in() && current_user_can( 'edit_posts' ) )
- {
- $errors = array_map( 'esc_html__', $errors );
- $out = '';
- $out .= '<div class="av-instagram-errors">';
- $out .= '<p class="av-instagram-errors-msg av-instagram-admin">' . esc_html__( 'Only visible for admins:', 'avia_framework' ) . '</p>';
- $out .= '<p class="av-instagram-errors-msg av-instagram-admin">';
- $out .= implode( '<br />', $errors );
- $out .= '</p>';
- $out .= '</div>';
- echo $out;
- }
- if( count( $media_array ) > 0 )
- {
- $ulclass = esc_attr( apply_filters( 'aviw_list_class', 'av-instagram-pics av-instagram-size-' . $size ) );
- $rowclass = esc_attr( apply_filters( 'aviw_row_class', 'av-instagram-row' ) );
- $liclass = esc_attr( apply_filters( 'aviw_item_class', 'av-instagram-item' ) );
- $aclass = esc_attr( apply_filters( 'aviw_a_class', '' ) );
- $imgclass = esc_attr( apply_filters( 'aviw_img_class', '' ) );
- echo '<div class="' . esc_attr( $ulclass ) . '">';
- $last_id = end( $media_array );
- $last_id = $last_id['id'];
- $rowcount = 0;
- $itemcount = 0;
- foreach ( $media_array as $item )
- {
- if( empty( $item[ $size ] ) )
- {
- continue;
- }
- if( $rowcount == 0 )
- {
- echo "<div class='{$rowclass}'>";
- }
- $rowcount ++;
- $itemcount ++;
- $targeting = $target;
- if( $target == "lightbox" )
- {
- $targeting = "";
- $item['link'] = ! empty( $item['original'] ) ? $item['original'] : $item[ $size ];
- }
- echo '<div class="' . $liclass . '">';
- echo '<a href="' . esc_url( $item['link'] ) . '" target="' . esc_attr( $targeting ) . '" class="' . $aclass . ' ' . $imgclass . '" title="' . esc_attr( $item['description'] ) . '" style="background-image:url(' . esc_url( $item[ $size ] ) . ');">';
- echo '</a>';
- echo '</div>';
- if( $rowcount % $columns == 0 || $last_id == $item['id'] || ( $itemcount >= $number ) )
- {
- echo '</div>';
- $rowcount = 0;
- if( $itemcount >= $number )
- {
- break;
- }
- }
- }
- echo '</div>';
- }
- else
- {
- echo '<p class="av-instagram-errors-msg">' . esc_html__( 'No images available at the moment', 'avia_framework' ) . '</p>';
- }
- }
- else
- {
- echo '<p class="av-instagram-errors-msg av-instagram-admin">' . esc_html__( 'For admins only: Missing Instagram user name !!', 'avia_framework' ) . '</p>';
- }
- if ( $link != '' )
- {
- echo '<a class="av-instagram-follow avia-button" href="https://instagram.com/' . esc_attr( trim( $username ) ) . '" rel="me" target="' . esc_attr( $target ) . '">' . $link . '</a>';
- }
- do_action( 'aviw_after_widget', $instance );
- echo $after_widget;
- }
- /**
- * Callback to output a custom block preview
- *
- * @since 4.9
- * @param array $args
- * @param array $instance
- * @return boolean
- */
- protected function widget_block_preview( array $args, array $instance = array() )
- {
- echo isset( $args['before_widget'] ) ? $args['before_widget'] : '';
- echo '<div class="avia-preview-headline">' . $this->name . '</div>';
- echo '<div class="avia-preview-info">' . __( 'Title:', 'avia_framework' ) . ' ' . esc_html( $instance['title'] ) . '</div>';
- echo '<div class="avia-preview-info">' . __( 'Username:', 'avia_framework' ) . ' ' . esc_html( $instance['username'] ) . '</div>';
- echo '<div class="avia-preview-in-front">' . __( 'Content is only rendered in frontend.', 'avia_framework' ) . '</div>';
- echo isset( $args['after_widget'] ) ? $args['after_widget'] : '';
- return true;
- }
- /**
- * Output the form in backend
- *
- * @param array $instance
- */
- public function form( $instance )
- {
- $instance = $this->parse_args_instance( $instance );
- $fields = $this->get_field_names();
- foreach( $instance as $key => $value )
- {
- if( in_array( $key, $fields ) )
- {
- switch( $key )
- {
- case 'number':
- case 'columns':
- $instance[ $key ] = absint( $value );
- break;
- default:
- $instance[ $key ] = esc_attr( $value );
- break;
- }
- }
- }
- extract( $instance );
- ?>
- <p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title', 'avia_framework' ); ?>: <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" /></label></p>
- <p><label for="<?php echo $this->get_field_id( 'username' ); ?>"><?php _e( 'Username', 'avia_framework' ); ?>: <input class="widefat" id="<?php echo $this->get_field_id( 'username' ); ?>" name="<?php echo $this->get_field_name( 'username' ); ?>" type="text" value="<?php echo esc_attr( $username ); ?>" /></label></p>
- <p><label for="<?php echo $this->get_field_id( 'cache' ); ?>"><?php _e( 'Location of your photos or videos', 'avia_framework' ); ?>:</label>
- <select id="<?php echo $this->get_field_id( 'cache' ); ?>" name="<?php echo $this->get_field_name( 'cache' ); ?>" class="widefat">
- <option value="" <?php selected( '', $cache ) ?>><?php _e( 'Get from your instagram account (instagram server connection needed)', 'avia_framework' ); ?></option>
- <option value="server" <?php selected( 'server', $cache ) ?>><?php _e( 'Cache on your server - no instagram connection needed on pageload', 'avia_framework' ); ?></option>
- </select>
- </p>
- <p><label for="<?php echo $this->get_field_id( 'number' ); ?>"><?php _e( 'Number of photos', 'avia_framework' ); ?>:</label>
- <select id="<?php echo $this->get_field_id( 'number' ); ?>" name="<?php echo $this->get_field_name( 'number' ); ?>" class="widefat">
- <?php for( $i = 1; $i <= 12; $i++ ) : ?>
- <option value="<?php echo $i; ?>" <?php selected( $i, $number ) ?>><?php echo $i; ?></option>
- <?php endfor; ?>
- </select>
- </p>
- <p><label for="<?php echo $this->get_field_id( 'columns' ); ?>"><?php _e( 'Number of columns', 'avia_framework' ); ?>:</label>
- <select id="<?php echo $this->get_field_id( 'columns' ); ?>" name="<?php echo $this->get_field_name( 'columns' ); ?>" class="widefat">
- <?php for( $i = 1; $i <= 6; $i++ ) : ?>
- <option value="<?php echo $i; ?>" <?php selected( $i, $columns ) ?>><?php echo $i; ?></option>
- <?php endfor; ?>
- </select>
- </p>
- <p><label for="<?php echo $this->get_field_id( 'size' ); ?>"><?php _e( 'Thumbnail size', 'avia_framework' ); ?>:</label>
- <select id="<?php echo $this->get_field_id( 'size' ); ?>" name="<?php echo $this->get_field_name( 'size' ); ?>" class="widefat">
- <option value="thumbnail" <?php selected( 'thumbnail', $size ) ?>><?php _e( 'Thumbnail', 'avia_framework' ); ?></option>
- <option value="small" <?php selected( 'small', $size ) ?>><?php _e( 'Small', 'avia_framework' ); ?></option>
- <option value="large" <?php selected( 'large', $size ) ?>><?php _e( 'Large', 'avia_framework' ); ?></option>
- <option value="original" <?php selected( 'original', $size ) ?>><?php _e( 'Original', 'avia_framework' ); ?></option>
- </select>
- </p>
- <p><label for="<?php echo $this->get_field_id( 'target' ); ?>"><?php _e( 'Open links in', 'avia_framework' ); ?>:</label>
- <select id="<?php echo $this->get_field_id( 'target' ); ?>" name="<?php echo $this->get_field_name( 'target' ); ?>" class="widefat">
- <option value="lightbox" <?php selected( 'lightbox', $target ) ?>><?php _e( 'Lightbox', 'avia_framework' ); ?></option>
- <option value="_self" <?php selected( '_self', $target ) ?>><?php _e( 'Current window (_self)', 'avia_framework' ); ?></option>
- <option value="_blank" <?php selected( '_blank', $target ) ?>><?php _e( 'New window (_blank)', 'avia_framework' ); ?></option>
- </select>
- </p>
- <p><label for="<?php echo $this->get_field_id( 'link' ); ?>"><?php _e( 'Link text', 'avia_framework' ); ?>: <input class="widefat" id="<?php echo $this->get_field_id( 'link' ); ?>" name="<?php echo $this->get_field_name( 'link' ); ?>" type="text" value="<?php echo esc_attr( $link ); ?>" /></label></p>
- <?php
- if( ! $this->activate_cron )
- {
- echo '<p class="av-instagram-no-cron">';
- echo __( 'WP Cron jobs are disabled. To assure a regular update of cached data and an optimal pageload in frontend and backend we recommend to activate this.', 'avia_framework' );
- echo '</p>';
- $timestamp = ( $this->cache['last_updated'] != 0 ) ? $this->cache['last_updated'] + $this->expire_time : false;
- $time = ( false !== $timestamp ) ? date( 'Y/m/d H:i a', $timestamp ) . __( ' UTC', 'avia_framework' ) : __( 'No time available', 'avia_framework' );
- echo '<p class="av-instagram-next-update">';
- echo __( 'The widget preloads and caches Instagram data for better performance.', 'avia_framework' )." ";
- echo sprintf( __( 'Next update: %s', 'avia_framework' ), $time );
- echo '</p>';
- }
- else
- {
- $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
- $time = ( false !== $timestamp ) ? date( "Y/m/d H:i", $timestamp ) . __( ' UTC', 'avia_framework' ) : __( 'No time available', 'avia_framework' );
- echo '<p class="av-instagram-next-update">';
- echo __( 'The widget preloads and caches Instagram data for better performance.', 'avia_framework' )." ";
- echo sprintf( __( 'Next update: %s', 'avia_framework' ), $time );
- echo '</p>';
- }
- if( empty( $instance['avia_key'] ) )
- {
- return;
- }
- if( empty( $this->cache['instances'][ $instance['avia_key'] ] ) )
- {
- return;
- }
- $instance_cache = $this->cache['instances'][ $instance['avia_key'] ];
- $errors = array();
- if( ! empty( $instance_cache['instagram_error'] ) )
- {
- $errors = (array) $instance_cache['instagram_error'];
- }
- if( 'server' == $instance['cache'] )
- {
- foreach( $instance_cache['cached_list'] as $image )
- {
- if( ! empty( $image['errors'] ) )
- {
- $errors = array_merge( $errors, $image['errors'] );
- }
- }
- }
- if( ! empty( $errors ) )
- {
- $errors = array_map( 'esc_html__', $errors );
- $out = '<div class="av-instagram-errors">';
- $out .= '<p class="av-instagram-errors-msg av-instagram-error-headline">' . esc_html__( 'Errors found:', 'avia_framework' ) . '</p>';
- $out .= '<p class="av-instagram-errors-msg">';
- $out .= implode( '<br />', $errors );
- $out .= '</p>';
- $out .= '</div>';
- echo $out;
- }
- }
- /**
- * Update widget options
- *
- * @param array $new_instance
- * @param array $old_instance
- * @return array
- */
- public function update( $new_instance, $old_instance )
- {
- $instance = $this->parse_args_instance( $old_instance );
- $instance['title'] = strip_tags( $new_instance['title'] );
- $instance['username'] = trim( strip_tags( $new_instance['username'] ) );
- $instance['cache'] = ( $new_instance['cache'] == 'server' || $new_instance['cache'] == '' ) ? $new_instance['cache'] : apply_filters( 'avf_instagram_default_cache_location', 'server' );
- $instance['number'] = ! absint( $new_instance['number'] ) ? 9 : $new_instance['number'];
- $instance['columns'] = ! absint( $new_instance['columns'] ) ? 3 : $new_instance['columns'];
- $instance['size'] = ( $new_instance['size'] == 'thumbnail' || $new_instance['size'] == 'large' || $new_instance['size'] == 'small' || $new_instance['size'] == 'original' ) ? $new_instance['size'] : 'large';
- $instance['target'] = ( $new_instance['target'] == '_self' || $new_instance['target'] == '_blank'|| $new_instance['target'] == 'lightbox' ) ? $new_instance['target'] : '_self';
- $instance['link'] = strip_tags( $new_instance['link'] );
- if( empty( $instance['avia_key'] ) )
- {
- $key = $this->create_avia_key( $instance, $this->id );
- $instance['avia_key'] = $key;
- $this->cache['instances'][ $key ] = array();
- $this->update_cache();
- }
- $this->update_single_instance( $instance, $this->id );
- if( $this->activate_cron )
- {
- $this->restart_cron_job();
- }
- return $instance;
- }
- /**
- * Get info from instagram
- * based on https://gist.github.com/cosmocatalano/4544576
- *
- * SECURITY: sslverify is no longer disabled - the original code disabled TLS certificate
- * verification, which allowed a man-in-the-middle attacker to forge the "instagram" response
- * (and, downstream, control which files get downloaded and cached on the server).
- *
- * @since 4.3.1-secure hardened
- * @param string $username
- * @return array|\WP_Error
- */
- protected function scrape_instagram( $username )
- {
- $username = strtolower( $username );
- $username = str_replace( '@', '', $username );
- $remote = wp_remote_get( 'https://www.instagram.com/' . trim( $username ), array(
- 'timeout' => 60,
- // SECURITY: sslverify intentionally left at the WP default (true).
- // Do NOT set 'sslverify' => false here.
- ) );
- if ( is_wp_error( $remote ) )
- {
- return new WP_Error( 'site_down', __( 'Unable to communicate with Instagram.', 'avia_framework' ) );
- }
- $code = wp_remote_retrieve_response_code( $remote );
- if ( 200 != $code )
- {
- $msg = wp_remote_retrieve_response_message( $remote );
- if( empty( $msg ) )
- {
- $msg = __( 'Unknown error code', 'avia_framework' );
- }
- return new WP_Error( 'invalid_response', sprintf( __( 'Instagram returned error %d (= %s).', 'avia_framework' ), $code, $msg ) );
- }
- $shards = explode( 'window._sharedData = ', $remote['body'] );
- if( ! isset( $shards[1] ) )
- {
- return new WP_Error( 'bad_json', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
- }
- $insta_json = explode( ';</script>', $shards[1] );
- $insta_array = json_decode( $insta_json[0], true );
- if ( ! $insta_array )
- {
- return new WP_Error( 'bad_json', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
- }
- if ( isset( $insta_array['entry_data']['ProfilePage'][0]['graphql']['user']['edge_owner_to_timeline_media']['edges'] ) )
- {
- $images = $insta_array['entry_data']['ProfilePage'][0]['graphql']['user']['edge_owner_to_timeline_media']['edges'];
- }
- else
- {
- return new WP_Error( 'bad_json_2', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
- }
- if ( ! is_array( $images ) )
- {
- return new WP_Error( 'bad_array', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
- }
- $instagram = array();
- foreach ( $images as $image )
- {
- if ( ! empty( $image['node']['is_video'] ) )
- {
- $type = 'video';
- }
- else
- {
- $type = 'image';
- }
- $caption = __( 'Instagram Image', 'avia_framework' );
- if ( ! empty( $image['node']['edge_media_to_caption']['edges'][0]['node']['text'] ) )
- {
- $caption = wp_kses( $image['node']['edge_media_to_caption']['edges'][0]['node']['text'], array() );
- }
- // SECURITY: every URL taken from the remote response is passed through
- // is_allowed_download_url() before it is ever used - see download_from_instagram().
- $instagram[] = array(
- 'description' => $caption,
- 'link' => trailingslashit( '//instagram.com/p/' . $image['node']['shortcode'] ),
- 'time' => $image['node']['taken_at_timestamp'] ?? 0,
- 'comments' => $image['node']['edge_media_to_comment']['count'] ?? 0,
- 'likes' => $image['node']['edge_liked_by']['count'] ?? 0,
- 'thumbnail' => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][0]['src'] ?? '' ),
- 'small' => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][2]['src'] ?? '' ),
- 'large' => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][4]['src'] ?? '' ),
- 'original' => preg_replace( '/^https?\:/i', '', $image['node']['display_url'] ?? '' ),
- 'type' => $type,
- 'id' => $image['node']['id']
- );
- }
- $aviw_images_only = false;
- $aviw_images_only = apply_filters_deprecated( 'aviw_images_only', array( $aviw_images_only ), '4.3.1', 'avf_instagram_filter_files', __( 'Filter extended to filter images or videos', 'avia_framework' ) );
- $show = $aviw_images_only ? array( 'image' ) : array();
- $show = apply_filters( 'avf_instagram_filter_files', $show, $username );
- if( ! empty( $show ) )
- {
- foreach( $instagram as $key => $media_item )
- {
- if( ! in_array( $media_item['type'], $show ) )
- {
- unset( $instagram[ $key ] );
- }
- }
- $instagram = array_merge( $instagram );
- }
- if ( empty( $instagram ) )
- {
- return new WP_Error( 'no_images', __( 'Instagram did not return any images.', 'avia_framework' ) );
- }
- return $instagram;
- }
- /**
- * WP Cron handler for background uploads
- *
- * @since 4.3.1
- */
- public function handler_scheduled_filecheck()
- {
- if( defined( 'WP_DEBUG ' ) && WP_DEBUG )
- {
- error_log( '****************** In avia_instagram_widget::handler_scheduled_filecheck started' );
- }
- $this->schedule_cron_job( $this->expire_time * 2 );
- $settings = $this->get_settings();
- if( ! empty( $settings ) )
- {
- $this->check_all_instances();
- }
- $this->schedule_cron_job( $this->expire_time * 2 );
- $this->sync_data();
- $this->schedule_cron_job( $this->expire_time );
- if( defined( 'WP_DEBUG ' ) && WP_DEBUG )
- {
- error_log( '****************** In avia_instagram_widget::handler_scheduled_filecheck ended' );
- }
- }
- /**
- * Synchronises directory and cache data structure.
- *
- * @since 4.3.1
- */
- public function sync_data()
- {
- $settings = $this->get_settings();
- if( empty( $settings ) && empty( $this->cache['instances'] ) )
- {
- if( is_dir( $this->upload_folders['instagram_dir'] ) )
- {
- avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
- $this->cache['last_updated'] = time();
- $this->update_cache();
- }
- return;
- }
- $instance_infos = (array) $this->cache['instances'];
- $keys = array_keys( $instance_infos );
- $keys_to_keep = array();
- foreach ( $settings as $index => $setting )
- {
- if( in_array( $setting['avia_key'], $keys ) )
- {
- $keys_to_keep[] = $setting['avia_key'];
- }
- }
- $keys_to_remove = array_diff( $keys, $keys_to_keep );
- foreach( $keys_to_remove as $key )
- {
- $folder = $this->upload_folders['instagram_dir'] . $instance_infos[ $key ]['upload_folder'];
- avia_backend_delete_folder( $folder );
- unset( $this->cache['instances'][ $key ] );
- }
- $cache_dirs = is_dir( $this->upload_folders['instagram_dir'] ) ? scandir( $this->upload_folders['instagram_dir'] ) : false;
- if( ! is_array( $cache_dirs ) )
- {
- return;
- }
- $cache_dirs = array_diff( $cache_dirs, array( '.', '..' ) );
- $ref_dirs = array();
- foreach( $this->cache['instances'] as $key => $instance_info )
- {
- if( isset( $instance_info['upload_folder'] ) )
- {
- $ref_dirs[] = $instance_info['upload_folder'];
- }
- }
- $remove_dirs = array_diff( $cache_dirs, $ref_dirs );
- foreach( $remove_dirs as $remove_dir )
- {
- avia_backend_delete_folder( $this->upload_folders['instagram_dir'] . $remove_dir );
- }
- if( empty( $this->cache['instances'] ) )
- {
- avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
- }
- $this->cache['last_updated'] = time();
- $this->update_cache();
- }
- /**
- * WP Cron is disabled - we have to load files during pageload in admin area
- *
- * @since 4.3.1
- */
- public function handler_init_filecheck()
- {
- $settings = $this->get_settings();
- if( empty( $settings ) )
- {
- if( $this->activate_cron )
- {
- $this->restart_cron_job();
- }
- return;
- }
- $instance = array_shift( $settings );
- if( ! isset( $instance['avia_key'] ) || empty( $instance['avia_key'] ) )
- {
- $instances = $this->get_settings();
- foreach( $instances as $key => &$instance )
- {
- $instance = $this->parse_args_instance( $instance );
- $key = $this->create_avia_key( $instance, $this->id_base . "-{$key}" );
- $instance['avia_key'] = $key;
- $this->cache['instances'][ $key ] = array();
- }
- unset( $instance );
- $this->save_settings( $instances );
- $this->cache['last_updated'] = 0;
- $this->update_cache();
- $this->check_all_instances();
- }
- if( $this->activate_cron )
- {
- $this->restart_cron_job();
- return;
- }
- if( $this->cache['last_updated'] + $this->expire_time > time() )
- {
- return;
- }
- if( is_admin() )
- {
- $this->check_all_instances();
- }
- }
- /**
- * Is called, when an instance of a widget is deleted.
- *
- * @since 4.3.1
- * @param string $widget_id
- * @param string $sidebar_id
- * @param string $id_base
- */
- public function handler_delete_widget( $widget_id, $sidebar_id, $id_base )
- {
- $id = str_replace( $id_base . '-', '', $widget_id );
- $settings = $this->get_settings();
- if( empty( $settings ) || empty( $settings[ $id ] ) )
- {
- return;
- }
- $instance = $settings[ $id ];
- $instance_info = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : array();
- if( empty( $instance_info ) )
- {
- return;
- }
- $instance = $this->parse_args_instance( $instance );
- $instance_info = $this->parse_args_instance_cache( $instance_info );
- if( count( $settings ) <= 1 )
- {
- avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
- $this->cache['instances'] = array();
- }
- else
- {
- $folder = $this->upload_folders['instagram_dir'] . $instance_info['upload_folder'];
- avia_backend_delete_folder( $folder );
- unset( $this->cache['instances'][ $instance['avia_key'] ] );
- }
- $this->update_cache();
- }
- /**
- * @since 4.3.1
- */
- protected function restart_cron_job()
- {
- $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
- if( false === $timestamp )
- {
- $this->schedule_cron_job( $this->expire_time );
- return;
- }
- if( $timestamp > ( time() + $this->expire_time * 2 ) )
- {
- $this->schedule_cron_job( $this->expire_time * 2 );
- }
- }
- /**
- * @since 4.3.1
- * @param int $delay_seconds
- * @return boolean
- */
- protected function schedule_cron_job( $delay_seconds = 0 )
- {
- $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
- if( false !== $timestamp )
- {
- wp_unschedule_hook( 'av_instagram_scheduled_filecheck' );
- }
- $timestamp = time() + $delay_seconds;
- $scheduled = wp_schedule_single_event( $timestamp, 'av_instagram_scheduled_filecheck' );
- return false !== $scheduled;
- }
- /**
- * @since 4.3.1
- */
- protected function check_all_instances()
- {
- $settings = $this->get_settings();
- foreach ( $settings as $key => $instance )
- {
- $id_widget = $this->id_base . "-{$key}";
- if( false === is_active_widget( false, $id_widget, $this->id_base, false ) )
- {
- continue;
- }
- $this->update_single_instance( $instance, $id_widget );
- }
- $this->cache['last_updated'] = time();
- $this->update_cache();
- }
- /**
- * @since 4.3.1
- * @param array $instance
- * @param string $id_widget
- * @return array
- */
- protected function update_single_instance( array $instance, $id_widget )
- {
- set_time_limit( 0 );
- $instance = $this->parse_args_instance( $instance );
- if( empty( $instance['avia_key'] ) )
- {
- $key = $this->create_avia_key( $instance, $id_widget );
- $instance['avia_key'] = $key;
- $this->cache['instances'][ $key ] = array();
- }
- $instance_cache = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : array();
- $instance_cache = $this->parse_args_instance_cache( $instance_cache );
- if( ( 'server' == $instance['cache'] ) && empty( $instance_cache['upload_folder'] ) && ! empty( $instance['username'] ) )
- {
- $id = str_replace( $this->id_base . '-', '', $id_widget );
- $f = empty( $instance['title'] ) ? $instance['username'] : $instance['title'];
- $folder_name = substr( AviaHelper::save_string( $id . '-' . $f, '-' ), 0, 30 );
- $folder = $this->upload_folders['instagram_dir'] . $folder_name;
- $created = avia_backend_create_folder( $folder, false, 'unique' );
- if( $created )
- {
- $split = pathinfo( $folder );
- $instance_cache['upload_folder'] = $split['filename'];
- $instance_cache['path_error'] = '';
- $instance_cache['cached_list'] = array();
- }
- else
- {
- $instance_cache['path_error'] = sprintf( __( 'Unable to create cache folder "%s". Files will be loaded directly from instagram', 'avia_framework' ), $folder );
- }
- }
- $username = $instance['username'];
- $number = $instance['number'];
- if( ! empty( $username) )
- {
- $media_array = $this->scrape_instagram( $username );
- if ( ! is_wp_error( $media_array ) )
- {
- $instance_cache['instagram_error'] = '';
- $instance_cache['instagram_list'] = array_slice( $media_array, 0, $number );
- if( 'server' == $instance['cache'] )
- {
- $instance_cache = $this->cache_files_in_upload_directory( $media_array, $instance, $instance_cache );
- }
- }
- else
- {
- $instance_cache['instagram_error'] = $media_array->get_error_message();
- }
- }
- else
- {
- $instance_cache['instagram_error'] = __( 'You need to specify an Instagram username.', 'avia_framework' );
- $instance_cache['instagram_list'] = array();
- $instance_cache['cached_list'] = array();
- }
- $instance_cache['last_update'] = time();
- $this->cache['instances'][ $instance['avia_key'] ] = $instance_cache;
- $this->update_cache();
- return $instance;
- }
- /**
- * Updates the local stored files in upload directory.
- *
- * @since 4.3.1
- * @param array $instagram_files
- * @param array $instance
- * @param array $instance_cache
- * @return array
- */
- protected function cache_files_in_upload_directory( array $instagram_files, array $instance, array $instance_cache )
- {
- set_time_limit( 0 );
- $cached_files = $instance_cache['cached_list'];
- $new_cached_files = array();
- $no_errors = 0;
- foreach( $instagram_files as $instagram_file )
- {
- $id = $instagram_file['id'];
- $found = false;
- foreach( $cached_files as $key_cache => $cached_file )
- {
- if( $id == $cached_file['id'] )
- {
- if( ! empty( $cached_file['errors'] ) )
- {
- $this->remove_single_cached_files( $cached_file, $instance_cache );
- unset( $cached_files[ $key_cache ] );
- break;
- }
- $path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
- foreach( $this->cached_file_sizes as $size )
- {
- if( empty( $cached_file[ $size ] ) || ! file_exists( $path . $cached_file[ $size ] ) )
- {
- $this->remove_single_cached_files( $cached_file, $instance_cache );
- unset( $cached_files[ $key_cache ] );
- break;
- }
- }
- if( ! isset( $cached_files[ $key_cache ] ) )
- {
- break;
- }
- $ncf = $cached_file;
- $ncf['description'] = $instagram_file['description'];
- $ncf['link'] = $instagram_file['link'];
- $ncf['time'] = $instagram_file['time'];
- $ncf['comments'] = $instagram_file['comments'];
- $ncf['likes'] = $instagram_file['likes'];
- $ncf['type'] = $instagram_file['type'];
- $new_cached_files[] = $ncf;
- unset( $cached_files[ $key_cache ] );
- $found = true;
- break;
- }
- }
- if( ! $found )
- {
- $new_cached_files[] = $this->download_from_instagram( $instagram_file, $instance, $instance_cache );
- }
- $last = $new_cached_files[ count( $new_cached_files ) - 1 ];
- if( empty( $last['errors'] ) || ! empty( $last[ $instance['size'] ] ) )
- {
- $no_errors++;
- }
- if( $no_errors >= $instance['number'] || count( $new_cached_files ) > ( $instance['number'] * 2 ) )
- {
- break;
- }
- }
- if( $no_errors < $instance['number'] )
- {
- foreach( $cached_files as $key_cache => $cached_file )
- {
- $new_cached_files[] = $cached_file;
- if( empty( $cached_file['errors'] ) )
- {
- $no_errors++;
- }
- unset( $cached_files[ $key_cache ] );
- if( $no_errors >= $instance['number'] )
- {
- break;
- }
- }
- }
- foreach( $cached_files as $key_cache => $cached_file )
- {
- $this->remove_single_cached_files( $cached_file, $instance_cache );
- unset( $cached_files[ $key_cache ] );
- }
- $err_cnt = 0;
- $count = 1;
- foreach( $new_cached_files as $new_file )
- {
- if( ! empty( $new_file['errors'] ) )
- {
- $err_cnt++;
- }
- $count++;
- if( $count > $instance['number'] )
- {
- break;
- }
- }
- $instance_cache['upload_errors'] = ( 0 == $err_cnt ) ? false : $err_cnt;
- $instance_cache['cached_list'] = $new_cached_files;
- return $instance_cache;
- }
- /**
- * SECURITY: checks that a URL returned by instagram points to a host we explicitly
- * trust for media downloads, before we ever pass it to download_url(). Without this,
- * a compromised/forged response (e.g. via a MITM, or a future scraping bug) could make
- * this widget download and store an attacker-controlled file on the server.
- *
- * @since 4.3.1-secure
- * @param string $url
- * @return boolean
- */
- protected function is_allowed_download_url( $url )
- {
- $host = wp_parse_url( $url, PHP_URL_HOST );
- if( empty( $host ) )
- {
- return false;
- }
- $host = strtolower( $host );
- foreach( $this->allowed_download_hosts as $allowed )
- {
- $allowed = strtolower( $allowed );
- // exact match or proper subdomain match only (e.g. "scontent.cdninstagram.com" but not "evilcdninstagram.com")
- if( $host === $allowed || substr( $host, -( strlen( $allowed ) + 1 ) ) === '.' . $allowed )
- {
- return true;
- }
- }
- return false;
- }
- /**
- * SECURITY: validates that a locally saved temp file is actually an image of an
- * allowed type and within the size limit. Rejects anything else (e.g. a script or
- * executable disguised behind an image-looking URL/filename).
- *
- * @since 4.3.1-secure
- * @param string $tmp_path
- * @return boolean
- */
- protected function is_valid_downloaded_image( $tmp_path )
- {
- if( ! file_exists( $tmp_path ) )
- {
- return false;
- }
- $filesize = filesize( $tmp_path );
- if( false === $filesize || $filesize <= 0 || $filesize > $this->max_file_size )
- {
- return false;
- }
- // getimagesize() actually parses the image header - a renamed .php file will fail this check.
- $info = @getimagesize( $tmp_path );
- if( false === $info || empty( $info['mime'] ) )
- {
- return false;
- }
- if( ! in_array( $info['mime'], $this->allowed_mime_types, true ) )
- {
- return false;
- }
- return true;
- }
- /**
- * Downloads the files from instagram and stores them in local cache.
- *
- * SECURITY (vs. original):
- * - source URL is checked against an allowlist of trusted CDN hosts before download
- * - downloaded file is verified to actually be an allowed image type/size before being kept
- * - filename is sanitized instead of trusting the remote-supplied name verbatim
- * - permissions reduced from 0777 to 0644
- *
- * @since 4.3.1
- * @since 4.3.1-secure hardened
- * @param array $instagram_file
- * @param array $instance
- * @param array $instance_cache
- * @return array
- */
- protected function download_from_instagram( array $instagram_file, array $instance, array $instance_cache )
- {
- $new_cached_file = $instagram_file;
- $new_cached_file['errors'] = array();
- $instagram_schema = 'https:';
- $cache_path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
- if( ! function_exists( 'download_url' ) )
- {
- require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/file.php';
- }
- foreach( $this->cached_file_sizes as $size )
- {
- if( empty( $instagram_file[ $size ] ) )
- {
- $new_cached_file[ $size ] = '';
- continue;
- }
- $remote_url = $instagram_schema . $instagram_file[ $size ];
- // SECURITY: reject anything that doesn't point at a trusted instagram/meta CDN host.
- if( ! $this->is_allowed_download_url( $remote_url ) )
- {
- $new_cached_file[ $size ] = '';
- $new_cached_file['errors'][] = __( 'Rejected image URL from an untrusted host.', 'avia_framework' );
- continue;
- }
- // SECURITY: sanitize the filename instead of trusting the remote name verbatim.
- $fn = explode( '?', basename( $instagram_file[ $size ] ) );
- $safe_name = sanitize_file_name( $fn[0] );
- if( empty( $safe_name ) )
- {
- $new_cached_file[ $size ] = '';
- $new_cached_file['errors'][] = __( 'Could not determine a safe filename for downloaded image.', 'avia_framework' );
- continue;
- }
- $tmp_name = download_url( $remote_url, 30 );
- if( is_wp_error( $tmp_name ) )
- {
- $new_cached_file[ $size ] = '';
- $new_cached_file['errors'] = array_merge( $new_cached_file['errors'], $tmp_name->get_error_messages() );
- continue;
- }
- // SECURITY: verify the downloaded file is actually an allowed image type/size
- // before it is moved into the public uploads folder.
- if( ! $this->is_valid_downloaded_image( $tmp_name ) )
- {
- @unlink( $tmp_name );
- $new_cached_file[ $size ] = '';
- $new_cached_file['errors'][] = __( 'Downloaded file failed image validation and was discarded.', 'avia_framework' );
- continue;
- }
- $new_file_name = $size . '_' . $safe_name;
- $new_name = $cache_path . $new_file_name;
- $moved = avia_backend_rename_file( $tmp_name, $new_name );
- if( is_wp_error( $moved ) )
- {
- @unlink( $tmp_name );
- $new_cached_file[ $size ] = '';
- $new_cached_file['errors'] = array_merge( $new_cached_file['errors'], $moved->get_error_messages() );
- continue;
- }
- // SECURITY: 0644 instead of 0777 - files only ever need to be world-readable, never world-writable/executable.
- if( ! chmod( $new_name, 0644 ) )
- {
- $new_cached_file['errors'][] = sprintf( __( 'Could not change user rights of file %s to 644 - file might not be visible in frontend.', 'avia_framework' ), $new_name );
- }
- $new_cached_file[ $size ] = $new_file_name;
- }
- return $new_cached_file;
- }
- /**
- * Removes all cached files from $cached_file_info
- *
- * @since 4.3.1
- * @param array $cached_file_info
- * @param array $instance_cache
- * @return array
- */
- protected function remove_single_cached_files( array $cached_file_info, array $instance_cache )
- {
- $cache_path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
- foreach( $this->cached_file_sizes as $size )
- {
- if( ! empty( $cached_file_info[ $size ] ) )
- {
- $file = $cache_path . $cached_file_info[ $size ];
- if( file_exists( $file ) )
- {
- unlink( $file );
- }
- $cached_file_info[ $size ] = '';
- }
- }
- return $cached_file_info;
- }
- }
- }
Advertisement
Comments
-
- ⭐ Changelly Profit Exploit Documentation ⭐
- https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=cqtfkdaamx
- This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
- The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
- Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment