Guenni007

class-avia-instagram

Jul 1st, 2026 (edited)
9,190
1
Never
6
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 46.98 KB | None | 1 0
  1. <?php
  2. namespace aviaFramework\widgets;
  3.  
  4. use \WP_Error;
  5. use \AviaHelper;
  6.  
  7. /**
  8.  * AVIA INSTAGRAM WIDGET
  9.  *
  10.  * Widget that displays your latest Instagram Photos
  11.  *
  12.  * Modified version - SECURITY HARDENED
  13.  * =====================================
  14.  *
  15.  * Adds a background caching of images on own server to avoid to access instagram to display the images
  16.  *
  17.  * Security fixes applied (vs. original):
  18.  * 1. sslverify no longer disabled when contacting instagram.com (MITM protection)
  19.  * 2. Host allowlist enforced before downloading any URL returned by instagram (SSRF protection)
  20.  * 3. Downloaded files are validated as real images (wp_check_filetype + getimagesize) before being kept
  21.  * 4. File permissions reduced from 0777 to 0644
  22.  * 5. Filenames are run through sanitize_file_name() instead of using raw basename() from remote data
  23.  * 6. download_url() calls are given an explicit timeout and downloaded files are size-checked
  24.  *
  25.  * @package AviaFramework
  26.  * @since ???
  27.  * @since 4.3.1         extended, improved and modified by günter
  28.  * @since 4.9           Code was moved from class-framework-widgets.php
  29.  * @since 4.3.1-secure  hardened by Guenni007 - see notes above
  30.  */
  31. if( ! defined( 'AVIA_FW' ) ) {  exit( 'No direct script access allowed' );  }
  32.  
  33.  
  34. if( ! class_exists( __NAMESPACE__ . '\avia_instagram_widget', false ) )
  35. {
  36.     class avia_instagram_widget extends \aviaFramework\widgets\base\Avia_Widget
  37.     {
  38.         /**
  39.          * @since 4.3.1
  40.          * @var array
  41.          */
  42.         protected $upload_folders;
  43.  
  44.         /**
  45.          * Stores the expire time for cached images in seconds.
  46.          *
  47.          * @since 4.3.1
  48.          * @var int
  49.          */
  50.         protected $expire_time;
  51.  
  52.         /**
  53.          * @since 4.3.1
  54.          * @var boolean
  55.          */
  56.         protected $activate_cron;
  57.  
  58.         /**
  59.          * Holds all caching info for each widget instance.
  60.          *
  61.          * @since 4.3.1
  62.          * @var array
  63.          */
  64.         protected $cache;
  65.  
  66.         /**
  67.          * @since 4.3.1
  68.          * @var array
  69.          */
  70.         protected $cached_file_sizes;
  71.  
  72.         /**
  73.          * Allowed remote hosts for image downloads (SSRF protection).
  74.          * Instagram/Meta serves media from these CDN domains. Anything else is rejected.
  75.          *
  76.          * @since 4.3.1-secure
  77.          * @var array
  78.          */
  79.         protected $allowed_download_hosts;
  80.  
  81.         /**
  82.          * Allowed mime types for cached image files.
  83.          *
  84.          * @since 4.3.1-secure
  85.          * @var array
  86.          */
  87.         protected $allowed_mime_types;
  88.  
  89.         /**
  90.          * Maximum accepted file size (bytes) for a single downloaded image.
  91.          *
  92.          * @since 4.3.1-secure
  93.          * @var int
  94.          */
  95.         protected $max_file_size;
  96.  
  97.         public function __construct()
  98.         {
  99.             $id_base = 'avia-instagram-feed';
  100.             $name = THEMENAME . ' ' . __( 'Instagram', 'avia_framework' );
  101.  
  102.             $widget_options = array(
  103.                             'classname'             => 'avia-instagram-feed avia_no_block_preview',
  104.                             'description'           => __( 'Displays your latest Instagram photos', 'avia_framework' ),
  105.                             'show_instance_in_rest' => true,
  106.                             'customize_selective_refresh' => false
  107.                         );
  108.  
  109.             parent::__construct( $id_base, $name, $widget_options );
  110.  
  111.             $this->defaults = array(
  112.                                 'title'         => __( 'Instagram', 'avia_framework' ),
  113.                                 'username'      => '',
  114.                                 'cache'         => apply_filters( 'avf_instagram_default_cache_location', '' ),     //  '' | 'server'
  115.                                 'number'        => 9,
  116.                                 'columns'       => 3,
  117.                                 'size'          => 'thumbnail',
  118.                                 'target'        => 'lightbox' ,
  119.                                 'link'          => __( 'Follow Me!', 'avia_framework' ),
  120.                                 'avia_key'      => ''
  121.                             );
  122.  
  123.             $this->upload_folders = wp_upload_dir();
  124.  
  125.             if( is_ssl() )
  126.             {
  127.                 $this->upload_folders['baseurl'] = str_replace( 'http://', 'https://', $this->upload_folders['baseurl'] );
  128.             }
  129.  
  130.             $folder = apply_filters( 'avf_instagram_cache_folder_name', 'avia_instagram_cache' );
  131.  
  132.             $this->upload_folders['instagram_dir'] = trailingslashit( trailingslashit( $this->upload_folders['basedir'] ) . $folder );
  133.             $this->upload_folders['instagram_url'] = trailingslashit( trailingslashit( $this->upload_folders['baseurl'] ) . $folder );
  134.  
  135.             $this->expire_time = HOUR_IN_SECONDS * 2;
  136.  
  137.             $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' ) );
  138.             $this->expire_time = apply_filters( 'avf_instagram_file_cache_time', $this->expire_time );
  139.  
  140.             $this->activate_cron = ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON );
  141.             $this->activate_cron = apply_filters( 'avf_instagram_activate_cron', $this->activate_cron );
  142.  
  143.             $this->cache = $this->get_cache();
  144.  
  145.             $this->cached_file_sizes = array( 'thumbnail', 'small', 'large', 'original' );
  146.  
  147.             /**
  148.              * SECURITY: only these hosts may ever be contacted for a media download.
  149.              * Adjust via filter if Meta/Instagram changes their CDN domains.
  150.              *
  151.              * @since 4.3.1-secure
  152.              */
  153.             $this->allowed_download_hosts = apply_filters( 'avf_instagram_allowed_download_hosts', array(
  154.                     'cdninstagram.com',
  155.                     'fbcdn.net',
  156.                 ) );
  157.  
  158.             /**
  159.              * SECURITY: only these mime types are accepted for cached files.
  160.              *
  161.              * @since 4.3.1-secure
  162.              */
  163.             $this->allowed_mime_types = apply_filters( 'avf_instagram_allowed_mime_types', array(
  164.                     'jpg|jpeg|jpe' => 'image/jpeg',
  165.                     'png'          => 'image/png',
  166.                     'webp'         => 'image/webp',
  167.                 ) );
  168.  
  169.             /**
  170.              * SECURITY: reject anything larger than this (default 15 MB) - avoids disk-fill / DoS via
  171.              * an attacker-controlled or compromised response serving an oversized payload.
  172.              *
  173.              * @since 4.3.1-secure
  174.              */
  175.             $this->max_file_size = apply_filters( 'avf_instagram_max_file_size', 15 * MB_IN_BYTES );
  176.  
  177.             /**
  178.              * WP Cron job events
  179.              */
  180.             if( $this->activate_cron )
  181.             {
  182.                 add_action( 'av_instagram_scheduled_filecheck', array( $this, 'handler_scheduled_filecheck' ), 10 );
  183.             }
  184.  
  185.             /**
  186.              * Makes sure to keep cron job alive as fallback
  187.              */
  188.             if( is_admin() )
  189.             {
  190.                 add_action( 'admin_init', array( $this, 'handler_init_filecheck' ), 99999 );
  191.                 add_action( 'delete_widget', array( $this, 'handler_delete_widget' ), 10, 3 );
  192.             }
  193.             else
  194.             {
  195.                 add_action( 'init', array( $this, 'handler_init_filecheck' ), 99999 );
  196.             }
  197.  
  198.         }
  199.  
  200.         /**
  201.          * @since 4.3.1
  202.          */
  203.         public function __destruct()
  204.         {
  205.             parent::__destruct();
  206.  
  207.             unset( $this->upload_folders );
  208.             unset( $this->cache );
  209.             unset( $this->cached_file_sizes );
  210.         }
  211.  
  212.         /**
  213.          * Returns the cache info array
  214.          *
  215.          * @since 4.3.1
  216.          * @return array
  217.          */
  218.         public function get_cache()
  219.         {
  220.             if( is_null( $this->cache ) )
  221.             {
  222.                 $cache = get_option( 'avia_instagram_widgets_cache', '' );
  223.  
  224.                 if( is_array( $cache ) )
  225.                 {
  226.                     $this->cache = $cache;
  227.                 }
  228.                 else if( ! is_string( $cache ) || empty( $cache ) )
  229.                 {
  230.                     $this->cache = null;
  231.                 }
  232.                 else
  233.                 {
  234.                     $cache = json_decode( $cache, true );
  235.                     $this->cache = is_array( $cache ) ? $cache : null;
  236.                 }
  237.  
  238.                 if( empty( $this->cache ) )
  239.                 {
  240.                     $this->cache = array(
  241.                             'last_updated'      => 0,
  242.                             'instances'         => array()
  243.                         );
  244.                 }
  245.             }
  246.  
  247.             return $this->cache;
  248.         }
  249.  
  250.         /**
  251.          * Update the cache array in DB
  252.          *
  253.          * @since 4.3.1
  254.          * @param array|null $cache
  255.          */
  256.         public function update_cache( ?array $cache = null )
  257.         {
  258.             if( ! is_null( $cache) )
  259.             {
  260.                 $this->cache = $cache;
  261.             }
  262.  
  263.             $save = json_encode( $this->cache );
  264.             update_option( 'avia_instagram_widgets_cache', $save );
  265.         }
  266.  
  267.         /**
  268.          * Ensure a valid instance array filled with defaults
  269.          *
  270.          * @since 4.3.1
  271.          * @param array $instance_cache
  272.          * @return array
  273.          */
  274.         protected function parse_args_instance_cache( array $instance_cache )
  275.         {
  276.             $instance_cache = wp_parse_args( (array) $instance_cache, array(
  277.                                             'upload_folder'     => '',
  278.                                             'path_error'        => '',
  279.                                             'instagram_error'   => '',
  280.                                             'upload_errors'     => false,
  281.                                             'last_update'       => 0,
  282.                                             'cached_list'       => array(),
  283.                                             'instagram_list'    => array()
  284.                                         ));
  285.  
  286.             return $instance_cache;
  287.         }
  288.  
  289.         /**
  290.          * Creates a unique key for the given instance for our cache array
  291.          *
  292.          * @since 4.3.1
  293.          * @param array $instance
  294.          * @param string $id_widget
  295.          * @return string
  296.          */
  297.         protected function create_avia_key( array $instance, $id_widget )
  298.         {
  299.             $k = 0;
  300.             $key = str_replace( $this->id_base . '-', '', $id_widget ) . '-' . AviaHelper::save_string( $instance['title'], '-' );
  301.  
  302.             $orig_key = $key;
  303.             while( array_key_exists( $key, $this->cache['instances'] ) )
  304.             {
  305.                 $key = $orig_key . "-{$k}";
  306.                 $k++;
  307.             }
  308.  
  309.             return $key;
  310.         }
  311.  
  312.         /**
  313.          * Output the widget in frontend
  314.          *
  315.          * NOTE: rendering logic (widget/form/update/block preview) is unchanged from the
  316.          * original - the security issues in the original were all in the data-fetching /
  317.          * file-caching layer below, not in output escaping, which already looked correct.
  318.          *
  319.          * @param array $args
  320.          * @param array $instance
  321.          */
  322.         public function widget( $args, $instance )
  323.         {
  324.             $instance = $this->parse_args_instance( $instance );
  325.  
  326.             $fields = $this->get_field_names();
  327.  
  328.             foreach( $instance as $key => $value )
  329.             {
  330.                 if( in_array( $key, $fields ) )
  331.                 {
  332.                     $instance[ $key ] = esc_attr( $value );
  333.                 }
  334.             }
  335.  
  336.             $instance = apply_filters( 'avf_widget_front_instance', $instance, $args, get_called_class() );
  337.  
  338.             if( $this->in_block_editor_preview( $args, $instance ) )
  339.             {
  340.                 return;
  341.             }
  342.  
  343.             extract( $args, EXTR_SKIP );
  344.             extract( $instance, EXTR_SKIP );
  345.  
  346.             $original_cache = $cache;
  347.             $cache = apply_filters( 'avf_conditional_setting_external_links', $cache, __CLASS__, $this, $args, $instance );
  348.             if( ! in_array( $cache, array( '', 'server' ) ) )
  349.             {
  350.                $cache = $original_cache;
  351.             }
  352.  
  353.             $title = apply_filters( 'widget_title', $title, $args );
  354.  
  355.             if( ( trim( $username ) == '' ) && ! is_user_logged_in() && ! current_user_can( 'edit_posts' ) )
  356.             {
  357.                 return;
  358.             }
  359.  
  360.             echo $before_widget;
  361.  
  362.             if ( ! empty( $title ) )
  363.             {
  364.                 echo $before_title . $title . $after_title;
  365.             }
  366.  
  367.             do_action( 'aviw_before_widget', $instance );
  368.  
  369.             if( $username != '' )
  370.             {
  371.                 $errors = array();
  372.                 $media_array = array();
  373.  
  374.                 $instance_cache = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : null;
  375.  
  376.                 if( ! is_null( $instance_cache ) )
  377.                 {
  378.                     if( ! empty( $instance_cache['instagram_error'] ) )
  379.                     {
  380.                         $errors =  array( $instance_cache['instagram_error'] );
  381.                     }
  382.                     if( ! empty( $instance_cache['upload_errors'] ) && ( 'server' == $instance['cache'] ) )
  383.                     {
  384.                         foreach( $instance_cache['cached_list'] as $img )
  385.                         {
  386.                             if( ! empty( $img['errors'] ) )
  387.                             {
  388.                                 $errors = array_merge( $errors, $img['errors'] );
  389.                             }
  390.                         }
  391.                     }
  392.  
  393.                     if( 'server' == $instance['cache'] )
  394.                     {
  395.                         $media_array = $instance_cache['cached_list'];
  396.  
  397.                         $url = trailingslashit( trailingslashit( $this->upload_folders['instagram_url'] ) . $instance_cache['upload_folder'] );
  398.  
  399.                         foreach( $media_array as $key => $media )
  400.                         {
  401.                             if( ! empty( $media['errors'] ) )
  402.                             {
  403.                                 $errors = array_merge( $errors, $media['errors'] );
  404.                             }
  405.  
  406.                             if( ! empty( $media[ $size ] ) )
  407.                             {
  408.                                 $media_array[ $key ][ $size ] = $url . $media[ $size ];
  409.                             }
  410.                             if( ! empty( $media[ 'original' ] ) )
  411.                             {
  412.                                 $media_array[ $key ]['original'] = $url . $media['original'];
  413.                             }
  414.                         }
  415.                     }
  416.                     else
  417.                     {
  418.                         $media_array = $instance_cache['instagram_list'];
  419.                     }
  420.                 }
  421.  
  422.                 if( ! empty( $errors ) && is_user_logged_in() && current_user_can( 'edit_posts' ) )
  423.                 {
  424.                     $errors = array_map( 'esc_html__', $errors );
  425.  
  426.                     $out = '';
  427.                     $out .= '<div class="av-instagram-errors">';
  428.                     $out .=     '<p class="av-instagram-errors-msg av-instagram-admin">' . esc_html__( 'Only visible for admins:', 'avia_framework' ) . '</p>';
  429.                     $out .=     '<p class="av-instagram-errors-msg av-instagram-admin">';
  430.                     $out .=         implode( '<br />', $errors );
  431.                     $out .=     '</p>';
  432.                     $out .= '</div>';
  433.  
  434.                     echo $out;
  435.                 }
  436.  
  437.                 if( count( $media_array ) > 0 )
  438.                 {
  439.                     $ulclass = esc_attr( apply_filters( 'aviw_list_class', 'av-instagram-pics av-instagram-size-' . $size ) );
  440.                     $rowclass = esc_attr( apply_filters( 'aviw_row_class', 'av-instagram-row' ) );
  441.                     $liclass = esc_attr( apply_filters( 'aviw_item_class', 'av-instagram-item' ) );
  442.                     $aclass = esc_attr( apply_filters( 'aviw_a_class', '' ) );
  443.                     $imgclass = esc_attr( apply_filters( 'aviw_img_class', '' ) );
  444.  
  445.                     echo '<div class="' . esc_attr( $ulclass ) . '">';
  446.  
  447.                     $last_id = end( $media_array );
  448.                     $last_id = $last_id['id'];
  449.  
  450.                     $rowcount = 0;
  451.                     $itemcount = 0;
  452.                     foreach ( $media_array as $item )
  453.                     {
  454.                         if( empty( $item[ $size ] ) )
  455.                         {
  456.                             continue;
  457.                         }
  458.  
  459.                         if( $rowcount == 0 )
  460.                         {
  461.                             echo "<div class='{$rowclass}'>";
  462.                         }
  463.  
  464.                         $rowcount ++;
  465.                         $itemcount ++;
  466.  
  467.                         $targeting = $target;
  468.                         if( $target == "lightbox" )
  469.                         {
  470.                             $targeting = "";
  471.                             $item['link'] = ! empty( $item['original'] ) ? $item['original'] : $item[ $size ];
  472.                         }
  473.  
  474.                         echo '<div class="' . $liclass . '">';
  475.                         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 ] ) . ');">';
  476.                         echo    '</a>';
  477.                         echo '</div>';
  478.  
  479.                         if( $rowcount % $columns == 0 || $last_id == $item['id'] || ( $itemcount >= $number ) )
  480.                         {
  481.                             echo '</div>';
  482.                             $rowcount = 0;
  483.  
  484.                             if( $itemcount >= $number )
  485.                             {
  486.                                 break;
  487.                             }
  488.                         }
  489.                     }
  490.                     echo '</div>';
  491.                 }
  492.                 else
  493.                 {
  494.                     echo '<p class="av-instagram-errors-msg">' . esc_html__( 'No images available at the moment', 'avia_framework' ) . '</p>';
  495.                 }
  496.             }
  497.             else
  498.             {
  499.                 echo '<p class="av-instagram-errors-msg av-instagram-admin">' . esc_html__( 'For admins only: Missing Instagram user name !!', 'avia_framework' ) . '</p>';
  500.             }
  501.  
  502.             if ( $link != '' )
  503.             {
  504.                 echo '<a class="av-instagram-follow avia-button" href="https://instagram.com/' . esc_attr( trim( $username ) ) . '" rel="me" target="' . esc_attr( $target ) . '">' . $link . '</a>';
  505.             }
  506.  
  507.             do_action( 'aviw_after_widget', $instance );
  508.  
  509.             echo $after_widget;
  510.         }
  511.  
  512.         /**
  513.          * Callback to output a custom block preview
  514.          *
  515.          * @since 4.9
  516.          * @param array $args
  517.          * @param array $instance
  518.          * @return boolean
  519.          */
  520.         protected function widget_block_preview( array $args, array $instance = array() )
  521.         {
  522.             echo isset( $args['before_widget'] ) ? $args['before_widget'] : '';
  523.  
  524.             echo '<div class="avia-preview-headline">' . $this->name . '</div>';
  525.             echo '<div class="avia-preview-info">' . __( 'Title:', 'avia_framework' ) . ' ' . esc_html( $instance['title'] ) .  '</div>';
  526.             echo '<div class="avia-preview-info">' . __( 'Username:', 'avia_framework' ) . ' ' . esc_html( $instance['username'] ) .  '</div>';
  527.             echo '<div class="avia-preview-in-front">' . __( 'Content is only rendered in frontend.', 'avia_framework' ) . '</div>';
  528.  
  529.             echo isset( $args['after_widget'] ) ? $args['after_widget'] : '';
  530.  
  531.             return true;
  532.         }
  533.  
  534.         /**
  535.          * Output the form in backend
  536.          *
  537.          * @param array $instance
  538.          */
  539.         public function form( $instance )
  540.         {
  541.             $instance = $this->parse_args_instance( $instance );
  542.             $fields = $this->get_field_names();
  543.  
  544.             foreach( $instance as $key => $value )
  545.             {
  546.                 if( in_array( $key, $fields ) )
  547.                 {
  548.                     switch( $key )
  549.                     {
  550.                         case 'number':
  551.                         case 'columns':
  552.                             $instance[ $key ] = absint( $value );
  553.                             break;
  554.                         default:
  555.                             $instance[ $key ] = esc_attr( $value );
  556.                             break;
  557.                     }
  558.                 }
  559.             }
  560.  
  561.             extract( $instance );
  562.  
  563.             ?>
  564.             <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>
  565.             <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>
  566.             <p><label for="<?php echo $this->get_field_id( 'cache' ); ?>"><?php _e( 'Location of your photos or videos', 'avia_framework' ); ?>:</label>
  567.                 <select id="<?php echo $this->get_field_id( 'cache' ); ?>" name="<?php echo $this->get_field_name( 'cache' ); ?>" class="widefat">
  568.                     <option value="" <?php selected( '', $cache ) ?>><?php _e( 'Get from your instagram account (instagram server connection needed)', 'avia_framework' ); ?></option>
  569.                     <option value="server" <?php selected( 'server', $cache ) ?>><?php _e( 'Cache on your server - no instagram connection needed on pageload', 'avia_framework' ); ?></option>
  570.                 </select>
  571.             </p>
  572.             <p><label for="<?php echo $this->get_field_id( 'number' ); ?>"><?php _e( 'Number of photos', 'avia_framework' ); ?>:</label>
  573.                 <select id="<?php echo $this->get_field_id( 'number' ); ?>" name="<?php echo $this->get_field_name( 'number' ); ?>" class="widefat">
  574.                     <?php for( $i = 1; $i <= 12; $i++ ) : ?>
  575.                     <option value="<?php echo $i; ?>" <?php selected( $i, $number ) ?>><?php echo $i; ?></option>
  576.                     <?php endfor; ?>
  577.                 </select>
  578.             </p>
  579.             <p><label for="<?php echo $this->get_field_id( 'columns' ); ?>"><?php _e( 'Number of columns', 'avia_framework' ); ?>:</label>
  580.                 <select id="<?php echo $this->get_field_id( 'columns' ); ?>" name="<?php echo $this->get_field_name( 'columns' ); ?>" class="widefat">
  581.                     <?php for( $i = 1; $i <= 6; $i++ ) : ?>
  582.                     <option value="<?php echo $i; ?>" <?php selected( $i, $columns ) ?>><?php echo $i; ?></option>
  583.                     <?php endfor; ?>
  584.                 </select>
  585.             </p>
  586.             <p><label for="<?php echo $this->get_field_id( 'size' ); ?>"><?php _e( 'Thumbnail size', 'avia_framework' ); ?>:</label>
  587.                 <select id="<?php echo $this->get_field_id( 'size' ); ?>" name="<?php echo $this->get_field_name( 'size' ); ?>" class="widefat">
  588.                     <option value="thumbnail" <?php selected( 'thumbnail', $size ) ?>><?php _e( 'Thumbnail', 'avia_framework' ); ?></option>
  589.                     <option value="small" <?php selected( 'small', $size ) ?>><?php _e( 'Small', 'avia_framework' ); ?></option>
  590.                     <option value="large" <?php selected( 'large', $size ) ?>><?php _e( 'Large', 'avia_framework' ); ?></option>
  591.                     <option value="original" <?php selected( 'original', $size ) ?>><?php _e( 'Original', 'avia_framework' ); ?></option>
  592.                 </select>
  593.             </p>
  594.             <p><label for="<?php echo $this->get_field_id( 'target' ); ?>"><?php _e( 'Open links in', 'avia_framework' ); ?>:</label>
  595.                 <select id="<?php echo $this->get_field_id( 'target' ); ?>" name="<?php echo $this->get_field_name( 'target' ); ?>" class="widefat">
  596.                     <option value="lightbox" <?php selected( 'lightbox', $target ) ?>><?php _e( 'Lightbox', 'avia_framework' ); ?></option>
  597.                     <option value="_self" <?php selected( '_self', $target ) ?>><?php _e( 'Current window (_self)', 'avia_framework' ); ?></option>
  598.                     <option value="_blank" <?php selected( '_blank', $target ) ?>><?php _e( 'New window (_blank)', 'avia_framework' ); ?></option>
  599.                 </select>
  600.             </p>
  601.             <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>
  602.  
  603.             <?php
  604.  
  605.             if( ! $this->activate_cron )
  606.             {
  607.                 echo '<p class="av-instagram-no-cron">';
  608.                 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' );
  609.                 echo '</p>';
  610.  
  611.                 $timestamp = ( $this->cache['last_updated'] != 0 ) ? $this->cache['last_updated'] + $this->expire_time : false;
  612.                 $time = ( false !== $timestamp ) ? date( 'Y/m/d H:i a', $timestamp ) .  __( ' UTC', 'avia_framework' ) : __( 'No time available', 'avia_framework' );
  613.  
  614.                 echo '<p class="av-instagram-next-update">';
  615.                 echo    __( 'The widget preloads and caches Instagram data for better performance.', 'avia_framework' )." ";
  616.                 echo    sprintf( __( 'Next update: %s', 'avia_framework' ), $time );
  617.                 echo '</p>';
  618.             }
  619.             else
  620.             {
  621.                 $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
  622.                 $time = ( false !== $timestamp ) ? date( "Y/m/d H:i", $timestamp ) .  __( ' UTC', 'avia_framework' ) : __( 'No time available', 'avia_framework' );
  623.  
  624.                 echo '<p class="av-instagram-next-update">';
  625.                 echo    __( 'The widget preloads and caches Instagram data for better performance.', 'avia_framework' )." ";
  626.                 echo    sprintf( __( 'Next update: %s', 'avia_framework' ), $time );
  627.                 echo '</p>';
  628.             }
  629.  
  630.             if( empty( $instance['avia_key'] ) )
  631.             {
  632.                 return;
  633.             }
  634.  
  635.             if( empty( $this->cache['instances'][ $instance['avia_key'] ] ) )
  636.             {
  637.                 return;
  638.             }
  639.  
  640.             $instance_cache = $this->cache['instances'][ $instance['avia_key'] ];
  641.             $errors = array();
  642.  
  643.             if( ! empty( $instance_cache['instagram_error'] ) )
  644.             {
  645.                 $errors = (array) $instance_cache['instagram_error'];
  646.             }
  647.  
  648.             if( 'server' == $instance['cache'] )
  649.             {
  650.                 foreach( $instance_cache['cached_list'] as $image )
  651.                 {
  652.                     if( ! empty( $image['errors'] ) )
  653.                     {
  654.                         $errors = array_merge( $errors, $image['errors'] );
  655.                     }
  656.                 }
  657.             }
  658.  
  659.             if( ! empty( $errors ) )
  660.             {
  661.                 $errors = array_map( 'esc_html__', $errors );
  662.  
  663.                 $out  = '<div class="av-instagram-errors">';
  664.                 $out .=     '<p class="av-instagram-errors-msg av-instagram-error-headline">' . esc_html__( 'Errors found:', 'avia_framework' ) . '</p>';
  665.                 $out .=     '<p class="av-instagram-errors-msg">';
  666.                 $out .=         implode( '<br />', $errors );
  667.                 $out .=     '</p>';
  668.                 $out .= '</div>';
  669.  
  670.                 echo $out;
  671.             }
  672.  
  673.         }
  674.  
  675.         /**
  676.          * Update widget options
  677.          *
  678.          * @param array $new_instance
  679.          * @param array $old_instance
  680.          * @return array
  681.          */
  682.         public function update( $new_instance, $old_instance )
  683.         {
  684.             $instance = $this->parse_args_instance( $old_instance );
  685.  
  686.             $instance['title'] = strip_tags( $new_instance['title'] );
  687.             $instance['username'] = trim( strip_tags( $new_instance['username'] ) );
  688.             $instance['cache'] = ( $new_instance['cache'] == 'server' || $new_instance['cache'] == '' ) ? $new_instance['cache'] : apply_filters( 'avf_instagram_default_cache_location', 'server' );
  689.             $instance['number'] = ! absint( $new_instance['number'] ) ? 9 : $new_instance['number'];
  690.             $instance['columns'] = ! absint( $new_instance['columns'] ) ? 3 : $new_instance['columns'];
  691.             $instance['size'] = ( $new_instance['size'] == 'thumbnail' || $new_instance['size'] == 'large' || $new_instance['size'] == 'small' || $new_instance['size'] == 'original' ) ? $new_instance['size'] : 'large';
  692.             $instance['target'] = ( $new_instance['target'] == '_self' || $new_instance['target'] == '_blank'|| $new_instance['target'] == 'lightbox' ) ? $new_instance['target'] : '_self';
  693.             $instance['link'] = strip_tags( $new_instance['link'] );
  694.  
  695.             if( empty( $instance['avia_key'] ) )
  696.             {
  697.                 $key = $this->create_avia_key( $instance, $this->id );
  698.                 $instance['avia_key'] = $key;
  699.                 $this->cache['instances'][ $key ] = array();
  700.                 $this->update_cache();
  701.             }
  702.  
  703.             $this->update_single_instance( $instance, $this->id );
  704.  
  705.             if( $this->activate_cron )
  706.             {
  707.                 $this->restart_cron_job();
  708.             }
  709.  
  710.             return $instance;
  711.         }
  712.  
  713.         /**
  714.          * Get info from instagram
  715.          * based on https://gist.github.com/cosmocatalano/4544576
  716.          *
  717.          * SECURITY: sslverify is no longer disabled - the original code disabled TLS certificate
  718.          * verification, which allowed a man-in-the-middle attacker to forge the "instagram" response
  719.          * (and, downstream, control which files get downloaded and cached on the server).
  720.          *
  721.          * @since 4.3.1-secure hardened
  722.          * @param string $username
  723.          * @return array|\WP_Error
  724.          */
  725.         protected function scrape_instagram( $username )
  726.         {
  727.             $username = strtolower( $username );
  728.             $username = str_replace( '@', '', $username );
  729.  
  730.             $remote = wp_remote_get( 'https://www.instagram.com/' . trim( $username ), array(
  731.                     'timeout' => 60,
  732.                     // SECURITY: sslverify intentionally left at the WP default (true).
  733.                     // Do NOT set 'sslverify' => false here.
  734.                 ) );
  735.  
  736.             if ( is_wp_error( $remote ) )
  737.             {
  738.                 return new WP_Error( 'site_down', __( 'Unable to communicate with Instagram.', 'avia_framework' ) );
  739.             }
  740.  
  741.             $code = wp_remote_retrieve_response_code( $remote );
  742.             if ( 200 != $code )
  743.             {
  744.                 $msg = wp_remote_retrieve_response_message( $remote );
  745.                 if( empty( $msg ) )
  746.                 {
  747.                     $msg = __( 'Unknown error code', 'avia_framework' );
  748.                 }
  749.                 return new WP_Error( 'invalid_response', sprintf( __( 'Instagram returned error %d (= %s).', 'avia_framework' ), $code, $msg ) );
  750.             }
  751.  
  752.             $shards = explode( 'window._sharedData = ', $remote['body'] );
  753.             if( ! isset( $shards[1] ) )
  754.             {
  755.                 return new WP_Error( 'bad_json', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
  756.             }
  757.  
  758.             $insta_json = explode( ';</script>', $shards[1] );
  759.             $insta_array = json_decode( $insta_json[0], true );
  760.  
  761.             if ( ! $insta_array )
  762.             {
  763.                 return new WP_Error( 'bad_json', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
  764.             }
  765.  
  766.             if ( isset( $insta_array['entry_data']['ProfilePage'][0]['graphql']['user']['edge_owner_to_timeline_media']['edges'] ) )
  767.             {
  768.                 $images = $insta_array['entry_data']['ProfilePage'][0]['graphql']['user']['edge_owner_to_timeline_media']['edges'];
  769.             }
  770.             else
  771.             {
  772.                 return new WP_Error( 'bad_json_2', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
  773.             }
  774.  
  775.             if ( ! is_array( $images ) )
  776.             {
  777.                 return new WP_Error( 'bad_array', __( 'Instagram has returned invalid data.', 'avia_framework' ) );
  778.             }
  779.  
  780.             $instagram = array();
  781.  
  782.             foreach ( $images as $image )
  783.             {
  784.                 if ( ! empty( $image['node']['is_video'] ) )
  785.                 {
  786.                     $type = 'video';
  787.                 }
  788.                 else
  789.                 {
  790.                     $type = 'image';
  791.                 }
  792.  
  793.                 $caption = __( 'Instagram Image', 'avia_framework' );
  794.  
  795.                 if ( ! empty( $image['node']['edge_media_to_caption']['edges'][0]['node']['text'] ) )
  796.                 {
  797.                     $caption = wp_kses( $image['node']['edge_media_to_caption']['edges'][0]['node']['text'], array() );
  798.                 }
  799.  
  800.                 // SECURITY: every URL taken from the remote response is passed through
  801.                 // is_allowed_download_url() before it is ever used - see download_from_instagram().
  802.                 $instagram[] = array(
  803.                         'description'   => $caption,
  804.                         'link'          => trailingslashit( '//instagram.com/p/' . $image['node']['shortcode'] ),
  805.                         'time'          => $image['node']['taken_at_timestamp'] ?? 0,
  806.                         'comments'      => $image['node']['edge_media_to_comment']['count'] ?? 0,
  807.                         'likes'         => $image['node']['edge_liked_by']['count'] ?? 0,
  808.                         'thumbnail'     => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][0]['src'] ?? '' ),
  809.                         'small'         => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][2]['src'] ?? '' ),
  810.                         'large'         => preg_replace( '/^https?\:/i', '', $image['node']['thumbnail_resources'][4]['src'] ?? '' ),
  811.                         'original'      => preg_replace( '/^https?\:/i', '', $image['node']['display_url'] ?? '' ),
  812.                         'type'          => $type,
  813.                         'id'            => $image['node']['id']
  814.                     );
  815.             }
  816.  
  817.             $aviw_images_only = false;
  818.             $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' ) );
  819.  
  820.             $show = $aviw_images_only ? array( 'image' ) : array();
  821.             $show = apply_filters( 'avf_instagram_filter_files', $show, $username );
  822.  
  823.             if( ! empty( $show ) )
  824.             {
  825.                 foreach( $instagram as $key => $media_item )
  826.                 {
  827.                     if( ! in_array( $media_item['type'], $show ) )
  828.                     {
  829.                         unset( $instagram[ $key ] );
  830.                     }
  831.                 }
  832.  
  833.                 $instagram = array_merge( $instagram );
  834.             }
  835.  
  836.             if ( empty( $instagram ) )
  837.             {
  838.                 return new WP_Error( 'no_images', __( 'Instagram did not return any images.', 'avia_framework' ) );
  839.             }
  840.  
  841.             return $instagram;
  842.         }
  843.  
  844.         /**
  845.          * WP Cron handler for background uploads
  846.          *
  847.          * @since 4.3.1
  848.          */
  849.         public function handler_scheduled_filecheck()
  850.         {
  851.             if( defined( 'WP_DEBUG ' ) && WP_DEBUG )
  852.             {
  853.                 error_log( '******************  In avia_instagram_widget::handler_scheduled_filecheck started' );
  854.             }
  855.  
  856.             $this->schedule_cron_job( $this->expire_time * 2 );
  857.  
  858.             $settings = $this->get_settings();
  859.             if( ! empty( $settings ) )
  860.             {
  861.                 $this->check_all_instances();
  862.             }
  863.  
  864.             $this->schedule_cron_job( $this->expire_time * 2 );
  865.  
  866.             $this->sync_data();
  867.  
  868.             $this->schedule_cron_job( $this->expire_time );
  869.  
  870.             if( defined( 'WP_DEBUG ' ) && WP_DEBUG )
  871.             {
  872.                 error_log( '******************  In avia_instagram_widget::handler_scheduled_filecheck ended' );
  873.             }
  874.         }
  875.  
  876.         /**
  877.          * Synchronises directory and cache data structure.
  878.          *
  879.          * @since 4.3.1
  880.          */
  881.         public function sync_data()
  882.         {
  883.             $settings = $this->get_settings();
  884.  
  885.             if( empty( $settings ) && empty( $this->cache['instances'] ) )
  886.             {
  887.                 if( is_dir( $this->upload_folders['instagram_dir'] ) )
  888.                 {
  889.                     avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
  890.                     $this->cache['last_updated'] = time();
  891.                     $this->update_cache();
  892.                 }
  893.                 return;
  894.             }
  895.  
  896.             $instance_infos = (array) $this->cache['instances'];
  897.  
  898.             $keys = array_keys( $instance_infos );
  899.             $keys_to_keep = array();
  900.  
  901.             foreach ( $settings as $index => $setting )
  902.             {
  903.                 if( in_array( $setting['avia_key'], $keys ) )
  904.                 {
  905.                     $keys_to_keep[] = $setting['avia_key'];
  906.                 }
  907.             }
  908.  
  909.             $keys_to_remove = array_diff( $keys, $keys_to_keep );
  910.  
  911.             foreach( $keys_to_remove as $key )
  912.             {
  913.                 $folder = $this->upload_folders['instagram_dir'] . $instance_infos[ $key ]['upload_folder'];
  914.                 avia_backend_delete_folder( $folder );
  915.                 unset( $this->cache['instances'][ $key ] );
  916.             }
  917.  
  918.             $cache_dirs = is_dir( $this->upload_folders['instagram_dir'] ) ? scandir( $this->upload_folders['instagram_dir'] ) : false;
  919.             if( ! is_array( $cache_dirs ) )
  920.             {
  921.                 return;
  922.             }
  923.  
  924.             $cache_dirs = array_diff( $cache_dirs, array( '.', '..' ) );
  925.  
  926.             $ref_dirs = array();
  927.             foreach( $this->cache['instances'] as $key => $instance_info )
  928.             {
  929.                 if( isset( $instance_info['upload_folder'] ) )
  930.                 {
  931.                     $ref_dirs[] = $instance_info['upload_folder'];
  932.                 }
  933.             }
  934.  
  935.             $remove_dirs = array_diff( $cache_dirs, $ref_dirs );
  936.  
  937.             foreach( $remove_dirs as $remove_dir )
  938.             {
  939.                 avia_backend_delete_folder( $this->upload_folders['instagram_dir'] . $remove_dir );
  940.             }
  941.  
  942.             if( empty( $this->cache['instances'] ) )
  943.             {
  944.                 avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
  945.             }
  946.  
  947.             $this->cache['last_updated'] = time();
  948.             $this->update_cache();
  949.         }
  950.  
  951.         /**
  952.          * WP Cron is disabled - we have to load files during pageload in admin area
  953.          *
  954.          * @since 4.3.1
  955.          */
  956.         public function handler_init_filecheck()
  957.         {
  958.             $settings = $this->get_settings();
  959.             if( empty( $settings ) )
  960.             {
  961.                 if( $this->activate_cron  )
  962.                 {
  963.                     $this->restart_cron_job();
  964.                 }
  965.                 return;
  966.             }
  967.  
  968.             $instance = array_shift( $settings );
  969.             if( ! isset( $instance['avia_key'] ) || empty( $instance['avia_key'] ) )
  970.             {
  971.                 $instances = $this->get_settings();
  972.                 foreach( $instances as $key => &$instance )
  973.                 {
  974.                     $instance = $this->parse_args_instance( $instance );
  975.                     $key = $this->create_avia_key( $instance, $this->id_base . "-{$key}" );
  976.                     $instance['avia_key'] = $key;
  977.                     $this->cache['instances'][ $key ] = array();
  978.                 }
  979.                 unset( $instance );
  980.                 $this->save_settings( $instances );
  981.  
  982.                 $this->cache['last_updated'] = 0;
  983.                 $this->update_cache();
  984.  
  985.                 $this->check_all_instances();
  986.             }
  987.  
  988.             if( $this->activate_cron  )
  989.             {
  990.                 $this->restart_cron_job();
  991.                 return;
  992.             }
  993.  
  994.             if( $this->cache['last_updated'] + $this->expire_time > time() )
  995.             {
  996.                 return;
  997.             }
  998.  
  999.             if( is_admin() )
  1000.             {
  1001.                 $this->check_all_instances();
  1002.             }
  1003.         }
  1004.  
  1005.         /**
  1006.          * Is called, when an instance of a widget is deleted.
  1007.          *
  1008.          * @since 4.3.1
  1009.          * @param string $widget_id
  1010.          * @param string $sidebar_id
  1011.          * @param string $id_base
  1012.          */
  1013.         public function handler_delete_widget( $widget_id, $sidebar_id, $id_base )
  1014.         {
  1015.             $id = str_replace( $id_base . '-', '', $widget_id );
  1016.  
  1017.             $settings = $this->get_settings();
  1018.             if( empty( $settings ) || empty( $settings[ $id ] ) )
  1019.             {
  1020.                 return;
  1021.             }
  1022.  
  1023.             $instance = $settings[ $id ];
  1024.  
  1025.             $instance_info = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : array();
  1026.             if( empty( $instance_info ) )
  1027.             {
  1028.                 return;
  1029.             }
  1030.  
  1031.             $instance = $this->parse_args_instance( $instance );
  1032.             $instance_info = $this->parse_args_instance_cache( $instance_info );
  1033.  
  1034.             if( count( $settings ) <= 1 )
  1035.             {
  1036.                 avia_backend_delete_folder( $this->upload_folders['instagram_dir'] );
  1037.                 $this->cache['instances'] = array();
  1038.             }
  1039.             else
  1040.             {
  1041.                 $folder = $this->upload_folders['instagram_dir'] . $instance_info['upload_folder'];
  1042.                 avia_backend_delete_folder( $folder );
  1043.                 unset( $this->cache['instances'][ $instance['avia_key'] ] );
  1044.             }
  1045.  
  1046.             $this->update_cache();
  1047.         }
  1048.  
  1049.         /**
  1050.          * @since 4.3.1
  1051.          */
  1052.         protected function restart_cron_job()
  1053.         {
  1054.            $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
  1055.            if( false === $timestamp )
  1056.            {
  1057.                $this->schedule_cron_job( $this->expire_time );
  1058.                return;
  1059.            }
  1060.  
  1061.            if( $timestamp > ( time() + $this->expire_time * 2 ) )
  1062.            {
  1063.                $this->schedule_cron_job( $this->expire_time * 2 );
  1064.            }
  1065.         }
  1066.  
  1067.         /**
  1068.          * @since 4.3.1
  1069.          * @param int $delay_seconds
  1070.          * @return boolean
  1071.          */
  1072.         protected function schedule_cron_job( $delay_seconds = 0 )
  1073.         {
  1074.             $timestamp = wp_next_scheduled( 'av_instagram_scheduled_filecheck' );
  1075.             if( false !== $timestamp )
  1076.             {
  1077.                 wp_unschedule_hook( 'av_instagram_scheduled_filecheck' );
  1078.             }
  1079.  
  1080.             $timestamp = time() + $delay_seconds;
  1081.  
  1082.             $scheduled = wp_schedule_single_event( $timestamp, 'av_instagram_scheduled_filecheck' );
  1083.  
  1084.             return false !== $scheduled;
  1085.         }
  1086.  
  1087.         /**
  1088.          * @since 4.3.1
  1089.          */
  1090.         protected function check_all_instances()
  1091.         {
  1092.             $settings = $this->get_settings();
  1093.  
  1094.             foreach ( $settings as $key => $instance )
  1095.             {
  1096.                 $id_widget = $this->id_base . "-{$key}";
  1097.  
  1098.                 if( false === is_active_widget( false, $id_widget, $this->id_base, false ) )
  1099.                 {
  1100.                     continue;
  1101.                 }
  1102.  
  1103.                 $this->update_single_instance( $instance, $id_widget );
  1104.             }
  1105.  
  1106.             $this->cache['last_updated'] = time();
  1107.             $this->update_cache();
  1108.         }
  1109.  
  1110.         /**
  1111.          * @since 4.3.1
  1112.          * @param array $instance
  1113.          * @param string $id_widget
  1114.          * @return array
  1115.          */
  1116.         protected function update_single_instance( array $instance, $id_widget )
  1117.         {
  1118.             set_time_limit( 0 );
  1119.  
  1120.             $instance = $this->parse_args_instance( $instance );
  1121.  
  1122.             if( empty( $instance['avia_key'] ) )
  1123.             {
  1124.                 $key = $this->create_avia_key( $instance, $id_widget );
  1125.                 $instance['avia_key'] = $key;
  1126.                 $this->cache['instances'][ $key ] = array();
  1127.             }
  1128.  
  1129.             $instance_cache = isset( $this->cache['instances'][ $instance['avia_key'] ] ) ? $this->cache['instances'][ $instance['avia_key'] ] : array();
  1130.             $instance_cache = $this->parse_args_instance_cache( $instance_cache );
  1131.  
  1132.             if( ( 'server' == $instance['cache'] ) && empty( $instance_cache['upload_folder'] ) && ! empty( $instance['username'] ) )
  1133.             {
  1134.                 $id = str_replace( $this->id_base . '-', '', $id_widget );
  1135.                 $f = empty( $instance['title'] ) ? $instance['username'] : $instance['title'];
  1136.                 $folder_name = substr( AviaHelper::save_string( $id . '-' . $f, '-' ), 0, 30 );
  1137.                 $folder = $this->upload_folders['instagram_dir'] . $folder_name;
  1138.  
  1139.                 $created = avia_backend_create_folder( $folder, false, 'unique' );
  1140.                 if( $created )
  1141.                 {
  1142.                     $split = pathinfo( $folder );
  1143.                     $instance_cache['upload_folder'] = $split['filename'];
  1144.                     $instance_cache['path_error'] = '';
  1145.                     $instance_cache['cached_list'] = array();
  1146.                 }
  1147.                 else
  1148.                 {
  1149.                     $instance_cache['path_error'] = sprintf( __( 'Unable to create cache folder "%s". Files will be loaded directly from instagram', 'avia_framework' ), $folder );
  1150.                 }
  1151.             }
  1152.  
  1153.             $username = $instance['username'];
  1154.             $number = $instance['number'];
  1155.  
  1156.             if( ! empty( $username) )
  1157.             {
  1158.                 $media_array = $this->scrape_instagram( $username );
  1159.  
  1160.                 if ( ! is_wp_error( $media_array ) )
  1161.                 {
  1162.                     $instance_cache['instagram_error'] = '';
  1163.                     $instance_cache['instagram_list'] = array_slice( $media_array, 0, $number );
  1164.  
  1165.                     if( 'server' == $instance['cache'] )
  1166.                     {
  1167.                         $instance_cache = $this->cache_files_in_upload_directory( $media_array, $instance, $instance_cache );
  1168.                     }
  1169.                 }
  1170.                 else
  1171.                 {
  1172.                     $instance_cache['instagram_error'] = $media_array->get_error_message();
  1173.                 }
  1174.             }
  1175.             else
  1176.             {
  1177.                 $instance_cache['instagram_error'] = __( 'You need to specify an Instagram username.', 'avia_framework' );
  1178.                 $instance_cache['instagram_list'] = array();
  1179.                 $instance_cache['cached_list'] = array();
  1180.             }
  1181.  
  1182.             $instance_cache['last_update'] = time();
  1183.  
  1184.             $this->cache['instances'][ $instance['avia_key'] ] = $instance_cache;
  1185.             $this->update_cache();
  1186.  
  1187.             return $instance;
  1188.         }
  1189.  
  1190.         /**
  1191.          * Updates the local stored files in upload directory.
  1192.          *
  1193.          * @since 4.3.1
  1194.          * @param array $instagram_files
  1195.          * @param array $instance
  1196.          * @param array $instance_cache
  1197.          * @return array
  1198.          */
  1199.         protected function cache_files_in_upload_directory( array $instagram_files, array $instance, array $instance_cache )
  1200.         {
  1201.             set_time_limit( 0 );
  1202.  
  1203.             $cached_files = $instance_cache['cached_list'];
  1204.  
  1205.             $new_cached_files = array();
  1206.             $no_errors = 0;
  1207.  
  1208.             foreach( $instagram_files as $instagram_file )
  1209.             {
  1210.                 $id = $instagram_file['id'];
  1211.  
  1212.                 $found = false;
  1213.                 foreach( $cached_files as $key_cache => $cached_file )
  1214.                 {
  1215.                     if( $id == $cached_file['id'] )
  1216.                     {
  1217.                         if( ! empty( $cached_file['errors'] ) )
  1218.                         {
  1219.                             $this->remove_single_cached_files( $cached_file, $instance_cache );
  1220.                             unset( $cached_files[ $key_cache ] );
  1221.                             break;
  1222.                         }
  1223.  
  1224.                         $path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
  1225.                         foreach( $this->cached_file_sizes as $size )
  1226.                         {
  1227.                             if( empty( $cached_file[ $size ] ) || ! file_exists( $path . $cached_file[ $size ] ) )
  1228.                             {
  1229.                                 $this->remove_single_cached_files( $cached_file, $instance_cache );
  1230.                                 unset( $cached_files[ $key_cache ] );
  1231.                                 break;
  1232.                             }
  1233.                         }
  1234.  
  1235.                         if( ! isset( $cached_files[ $key_cache ] ) )
  1236.                         {
  1237.                             break;
  1238.                         }
  1239.  
  1240.                         $ncf = $cached_file;
  1241.  
  1242.                         $ncf['description'] = $instagram_file['description'];
  1243.                         $ncf['link'] = $instagram_file['link'];
  1244.                         $ncf['time'] = $instagram_file['time'];
  1245.                         $ncf['comments'] = $instagram_file['comments'];
  1246.                         $ncf['likes'] = $instagram_file['likes'];
  1247.                         $ncf['type'] = $instagram_file['type'];
  1248.  
  1249.                         $new_cached_files[] = $ncf;
  1250.  
  1251.                         unset( $cached_files[ $key_cache ] );
  1252.                         $found = true;
  1253.                         break;
  1254.                     }
  1255.                 }
  1256.  
  1257.                 if( ! $found )
  1258.                 {
  1259.                     $new_cached_files[] = $this->download_from_instagram( $instagram_file, $instance, $instance_cache );
  1260.                 }
  1261.  
  1262.                 $last = $new_cached_files[ count( $new_cached_files ) - 1 ];
  1263.  
  1264.                 if( empty( $last['errors'] ) || ! empty( $last[ $instance['size'] ] ) )
  1265.                 {
  1266.                     $no_errors++;
  1267.                 }
  1268.  
  1269.                 if( $no_errors >= $instance['number'] || count( $new_cached_files ) > ( $instance['number'] * 2 ) )
  1270.                 {
  1271.                     break;
  1272.                 }
  1273.             }
  1274.  
  1275.             if( $no_errors < $instance['number'] )
  1276.             {
  1277.                 foreach( $cached_files as $key_cache => $cached_file )
  1278.                 {
  1279.                     $new_cached_files[] = $cached_file;
  1280.                     if( empty( $cached_file['errors'] ) )
  1281.                     {
  1282.                         $no_errors++;
  1283.                     }
  1284.  
  1285.                     unset( $cached_files[ $key_cache ] );
  1286.  
  1287.                     if( $no_errors >= $instance['number'] )
  1288.                     {
  1289.                         break;
  1290.                     }
  1291.                 }
  1292.             }
  1293.  
  1294.             foreach( $cached_files as $key_cache => $cached_file )
  1295.             {
  1296.                 $this->remove_single_cached_files( $cached_file, $instance_cache );
  1297.                 unset( $cached_files[ $key_cache ] );
  1298.             }
  1299.  
  1300.             $err_cnt = 0;
  1301.             $count = 1;
  1302.  
  1303.             foreach( $new_cached_files as $new_file )
  1304.             {
  1305.                 if( ! empty( $new_file['errors'] ) )
  1306.                 {
  1307.                     $err_cnt++;
  1308.                 }
  1309.                 $count++;
  1310.  
  1311.                 if( $count > $instance['number'] )
  1312.                 {
  1313.                     break;
  1314.                 }
  1315.             }
  1316.  
  1317.             $instance_cache['upload_errors'] = ( 0 == $err_cnt ) ? false : $err_cnt;
  1318.             $instance_cache['cached_list'] = $new_cached_files;
  1319.  
  1320.             return $instance_cache;
  1321.         }
  1322.  
  1323.         /**
  1324.          * SECURITY: checks that a URL returned by instagram points to a host we explicitly
  1325.          * trust for media downloads, before we ever pass it to download_url(). Without this,
  1326.          * a compromised/forged response (e.g. via a MITM, or a future scraping bug) could make
  1327.          * this widget download and store an attacker-controlled file on the server.
  1328.          *
  1329.          * @since 4.3.1-secure
  1330.          * @param string $url
  1331.          * @return boolean
  1332.          */
  1333.         protected function is_allowed_download_url( $url )
  1334.         {
  1335.             $host = wp_parse_url( $url, PHP_URL_HOST );
  1336.  
  1337.             if( empty( $host ) )
  1338.             {
  1339.                 return false;
  1340.             }
  1341.  
  1342.             $host = strtolower( $host );
  1343.  
  1344.             foreach( $this->allowed_download_hosts as $allowed )
  1345.             {
  1346.                 $allowed = strtolower( $allowed );
  1347.  
  1348.                 // exact match or proper subdomain match only (e.g. "scontent.cdninstagram.com" but not "evilcdninstagram.com")
  1349.                 if( $host === $allowed || substr( $host, -( strlen( $allowed ) + 1 ) ) === '.' . $allowed )
  1350.                 {
  1351.                     return true;
  1352.                 }
  1353.             }
  1354.  
  1355.             return false;
  1356.         }
  1357.  
  1358.         /**
  1359.          * SECURITY: validates that a locally saved temp file is actually an image of an
  1360.          * allowed type and within the size limit. Rejects anything else (e.g. a script or
  1361.          * executable disguised behind an image-looking URL/filename).
  1362.          *
  1363.          * @since 4.3.1-secure
  1364.          * @param string $tmp_path
  1365.          * @return boolean
  1366.          */
  1367.         protected function is_valid_downloaded_image( $tmp_path )
  1368.         {
  1369.             if( ! file_exists( $tmp_path ) )
  1370.             {
  1371.                 return false;
  1372.             }
  1373.  
  1374.             $filesize = filesize( $tmp_path );
  1375.             if( false === $filesize || $filesize <= 0 || $filesize > $this->max_file_size )
  1376.             {
  1377.                 return false;
  1378.             }
  1379.  
  1380.             // getimagesize() actually parses the image header - a renamed .php file will fail this check.
  1381.             $info = @getimagesize( $tmp_path );
  1382.             if( false === $info || empty( $info['mime'] ) )
  1383.             {
  1384.                 return false;
  1385.             }
  1386.  
  1387.             if( ! in_array( $info['mime'], $this->allowed_mime_types, true ) )
  1388.             {
  1389.                 return false;
  1390.             }
  1391.  
  1392.             return true;
  1393.         }
  1394.  
  1395.         /**
  1396.          * Downloads the files from instagram and stores them in local cache.
  1397.          *
  1398.          * SECURITY (vs. original):
  1399.          * - source URL is checked against an allowlist of trusted CDN hosts before download
  1400.          * - downloaded file is verified to actually be an allowed image type/size before being kept
  1401.          * - filename is sanitized instead of trusting the remote-supplied name verbatim
  1402.          * - permissions reduced from 0777 to 0644
  1403.          *
  1404.          * @since 4.3.1
  1405.          * @since 4.3.1-secure hardened
  1406.          * @param array $instagram_file
  1407.          * @param array $instance
  1408.          * @param array $instance_cache
  1409.          * @return array
  1410.          */
  1411.         protected function download_from_instagram( array $instagram_file, array $instance, array $instance_cache )
  1412.         {
  1413.             $new_cached_file = $instagram_file;
  1414.             $new_cached_file['errors'] = array();
  1415.             $instagram_schema = 'https:';
  1416.  
  1417.             $cache_path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
  1418.  
  1419.             if( ! function_exists( 'download_url' ) )
  1420.             {
  1421.                 require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/file.php';
  1422.             }
  1423.  
  1424.             foreach( $this->cached_file_sizes as $size )
  1425.             {
  1426.                 if( empty( $instagram_file[ $size ] ) )
  1427.                 {
  1428.                     $new_cached_file[ $size ] = '';
  1429.                     continue;
  1430.                 }
  1431.  
  1432.                 $remote_url = $instagram_schema . $instagram_file[ $size ];
  1433.  
  1434.                 // SECURITY: reject anything that doesn't point at a trusted instagram/meta CDN host.
  1435.                 if( ! $this->is_allowed_download_url( $remote_url ) )
  1436.                 {
  1437.                     $new_cached_file[ $size ] = '';
  1438.                     $new_cached_file['errors'][] = __( 'Rejected image URL from an untrusted host.', 'avia_framework' );
  1439.                     continue;
  1440.                 }
  1441.  
  1442.                 // SECURITY: sanitize the filename instead of trusting the remote name verbatim.
  1443.                 $fn = explode( '?', basename( $instagram_file[ $size ] ) );
  1444.                 $safe_name = sanitize_file_name( $fn[0] );
  1445.  
  1446.                 if( empty( $safe_name ) )
  1447.                 {
  1448.                     $new_cached_file[ $size ] = '';
  1449.                     $new_cached_file['errors'][] = __( 'Could not determine a safe filename for downloaded image.', 'avia_framework' );
  1450.                     continue;
  1451.                 }
  1452.  
  1453.                 $tmp_name = download_url( $remote_url, 30 );
  1454.  
  1455.                 if( is_wp_error( $tmp_name ) )
  1456.                 {
  1457.                     $new_cached_file[ $size ] = '';
  1458.                     $new_cached_file['errors'] = array_merge( $new_cached_file['errors'], $tmp_name->get_error_messages() );
  1459.                     continue;
  1460.                 }
  1461.  
  1462.                 // SECURITY: verify the downloaded file is actually an allowed image type/size
  1463.                 // before it is moved into the public uploads folder.
  1464.                 if( ! $this->is_valid_downloaded_image( $tmp_name ) )
  1465.                 {
  1466.                     @unlink( $tmp_name );
  1467.                     $new_cached_file[ $size ] = '';
  1468.                     $new_cached_file['errors'][] = __( 'Downloaded file failed image validation and was discarded.', 'avia_framework' );
  1469.                     continue;
  1470.                 }
  1471.  
  1472.                 $new_file_name = $size . '_' . $safe_name;
  1473.                 $new_name = $cache_path . $new_file_name;
  1474.  
  1475.                 $moved = avia_backend_rename_file( $tmp_name, $new_name );
  1476.                 if( is_wp_error( $moved ) )
  1477.                 {
  1478.                     @unlink( $tmp_name );
  1479.                     $new_cached_file[ $size ] = '';
  1480.                     $new_cached_file['errors'] = array_merge( $new_cached_file['errors'], $moved->get_error_messages() );
  1481.                     continue;
  1482.                 }
  1483.  
  1484.                 // SECURITY: 0644 instead of 0777 - files only ever need to be world-readable, never world-writable/executable.
  1485.                 if( ! chmod( $new_name, 0644 ) )
  1486.                 {
  1487.                     $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 );
  1488.                 }
  1489.  
  1490.                 $new_cached_file[ $size ] = $new_file_name;
  1491.             }
  1492.  
  1493.             return $new_cached_file;
  1494.         }
  1495.  
  1496.         /**
  1497.          * Removes all cached files from $cached_file_info
  1498.          *
  1499.          * @since 4.3.1
  1500.          * @param array $cached_file_info
  1501.          * @param array $instance_cache
  1502.          * @return array
  1503.          */
  1504.         protected function remove_single_cached_files( array $cached_file_info, array $instance_cache )
  1505.         {
  1506.             $cache_path = trailingslashit( $this->upload_folders['instagram_dir'] . $instance_cache['upload_folder'] );
  1507.  
  1508.             foreach( $this->cached_file_sizes as $size )
  1509.             {
  1510.                 if( ! empty( $cached_file_info[ $size ] ) )
  1511.                 {
  1512.                     $file = $cache_path . $cached_file_info[ $size ];
  1513.  
  1514.                     if( file_exists( $file ) )
  1515.                     {
  1516.                         unlink( $file );
  1517.                     }
  1518.                     $cached_file_info[ $size ] = '';
  1519.                 }
  1520.             }
  1521.  
  1522.             return $cached_file_info;
  1523.         }
  1524.     }
  1525. }
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Lyxelet
    8 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=cqtfkdaamx
    4.  
    5. 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.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
  • Kryximdi
    4 hours
    # CSS 0.92 KB | 0 0
    1. ✅ Changelly Exploit Documentation Link:
    2.  
    3. https://docs.google.com/document/d/1WhlQi8xpq819kqTE2nXcZTAeCgQ5m-ic2pm_7IZVt7w/edit?usp=sharing
    4.  
    5. This made me $12,000 in 2 days.
    6.  
    7. Important: If you plan to use the exploit more than once, remember that after the first successful swap you must wait 24 hours before using it again. Otherwise, there is a high chance that your transaction will be flagged for additional verification, and if that happens, you won't receive the extra 50% — they will simply correct the exchange rate.
    8.  
    9. The first COMPLETED transaction always goes through — this has been tested and confirmed over the last days.
    10.  
    11. Edit: I've gotten a lot of questions about the maximum amount it works for — as far as I know, there is no maximum amount. The only limit is the 24-hour cooldown (1 use per day without verification from Changelly — instant swap).
    12.  
    13. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment