WordPress.org

Make WordPress Core

Changeset 29179


Ignore:
Timestamp:
07/15/2014 10:17:58 PM (4 years ago)
Author:
wonderboymusic
Message:

Simplify creation of audio, video, and playlist MCE views by placing them in iframe sandboxes.

Wins:

  • Eliminates duplication of code between PHP and JS
  • Views can load JS without messing with TinyMCE and scope
  • MEjs doesn't break when it loads a file plugin-mode. This allows any file type the MEjs supports to play in MCE views.
  • YouTube now works as the source for video.
  • Users can still style the views, editor stylesheets are included in these sandboxes.
  • Audio and Video URLs and [embed]s are no longer broken.
  • Remove the crazy compat code necessary to determine what file types play in what browser.
  • Remove unneeded Underscore templates.
  • Remove the compat code for playlists.

See #28905.

Location:
trunk/src/wp-includes
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/js/mce-view.js

    r29019 r29179  
    105105            }, this );
    106106        },
     107
     108        /* jshint scripturl: true */
     109        createIframe: function ( content ) {
     110            var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
     111                iframe, iframeDoc, i, resize,
     112                dom = tinymce.DOM;
     113
     114            if ( content.indexOf( '<script' ) !== -1 ) {
     115                iframe = dom.create( 'iframe', {
     116                    src: tinymce.Env.ie ? 'javascript:""' : '',
     117                    frameBorder: '0',
     118                    allowTransparency: 'true',
     119                    scrolling: 'no',
     120                    style: {
     121                        width: '100%',
     122                        display: 'block'
     123                    }
     124                } );
     125
     126                this.setContent( iframe );
     127                iframeDoc = iframe.contentWindow.document;
     128
     129                iframeDoc.open();
     130                iframeDoc.write(
     131                    '<!DOCTYPE html>' +
     132                    '<html>' +
     133                        '<head>' +
     134                            '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
     135                        '</head>' +
     136                        '<body style="padding: 0; margin: 0;" class="' + dom.doc.body.className + '">' +
     137                            content +
     138                        '</body>' +
     139                    '</html>'
     140                );
     141                iframeDoc.close();
     142
     143                resize = function() {
     144                    $( iframe ).height( $( iframeDoc.body ).height() );
     145                };
     146
     147                if ( MutationObserver ) {
     148                    new MutationObserver( _.debounce( function() {
     149                        resize();
     150                    }, 100 ) )
     151                    .observe( iframeDoc.body, {
     152                        attributes: true,
     153                        childList: true,
     154                        subtree: true
     155                    } );
     156                } else {
     157                    for ( i = 1; i < 6; i++ ) {
     158                        setTimeout( resize, i * 700 );
     159                    }
     160                }
     161            } else {
     162                this.setContent( content );
     163            }
     164        },
     165
    107166        setError: function( message, dashicon ) {
    108167            this.setContent(
     
    422481        loaded: false,
    423482
    424         View: _.extend( {}, wp.media.mixin, {
     483        View: {
    425484            overlay: true,
    426485
     486            action: 'parse-media-shortcode',
     487
    427488            initialize: function( options ) {
    428                 this.players = [];
    429489                this.shortcode = options.shortcode;
    430                 _.bindAll( this, 'setPlayer', 'pausePlayers' );
    431                 $( this ).on( 'ready', this.setPlayer );
    432                 $( this ).on( 'ready', function( event, editor ) {
    433                     editor.on( 'hide', this.pausePlayers );
     490                this.fetching = false;
     491
     492                _.bindAll( this, 'createIframe', 'setNode', 'fetch' );
     493                $( this ).on( 'ready', this.setNode );
     494            },
     495
     496            setNode: function () {
     497                if ( this.parsed ) {
     498                    this.createIframe( this.parsed );
     499                } else if ( ! this.fetching ) {
     500                    this.fetch();
     501                }
     502            },
     503
     504            fetch: function () {
     505                var self = this;
     506                this.fetching = true;
     507
     508                wp.ajax.send( this.action, {
     509                    data: {
     510                        post_ID: $( '#post_ID' ).val() || 0,
     511                        type: this.shortcode.tag,
     512                        shortcode: this.shortcode.string()
     513                    }
     514                } )
     515                .always( function() {
     516                    self.fetching = false;
     517                } )
     518                .done( function( response ) {
     519                    if ( response ) {
     520                        self.parsed = response;
     521                        self.createIframe( response );
     522                    }
     523                } )
     524                .fail( function( response ) {
     525                    if ( response && response.message ) {
     526                        if ( ( response.type === 'not-embeddable' && self.type === 'embed' ) ||
     527                            response.type === 'not-ssl' ) {
     528
     529                            self.setError( response.message, 'admin-media' );
     530                        } else {
     531                            self.setContent( '<p>' + self.original + '</p>', null, 'replace' );
     532                        }
     533                    } else if ( response && response.statusText ) {
     534                        self.setError( response.statusText, 'admin-media' );
     535                    }
    434536                } );
    435                 $( document ).on( 'media:edit', this.pausePlayers );
    436             },
    437 
    438             /**
    439              * Creates the player instance for the current node
    440              *
    441              * @global MediaElementPlayer
    442              *
    443              * @param {Event} event
    444              * @param {Object} editor
    445              * @param {HTMLElement} node
    446              */
    447             setPlayer: function( event, editor, node ) {
    448                 var self = this,
    449                     media;
    450 
    451                 media = $( node ).find( '.wp-' +  this.shortcode.tag + '-shortcode' );
    452 
    453                 if ( ! this.isCompatible( media ) ) {
    454                     media.closest( '.wpview-wrap' ).addClass( 'wont-play' );
    455                     media.replaceWith( '<p>' + media.find( 'source' ).eq(0).prop( 'src' ) + '</p>' );
    456                     return;
    457                 } else {
    458                     media.closest( '.wpview-wrap' ).removeClass( 'wont-play' );
    459                     if ( this.ua.is( 'ff' ) ) {
    460                         media.prop( 'preload', 'metadata' );
    461                     } else {
    462                         media.prop( 'preload', 'none' );
    463                     }
    464                 }
    465 
    466                 media = wp.media.view.MediaDetails.prepareSrc( media.get(0) );
    467 
    468                 setTimeout( function() {
    469                     wp.mce.av.loaded = true;
    470                     self.players.push( new MediaElementPlayer( media, self.mejsSettings ) );
    471                 }, wp.mce.av.loaded ? 10 : 500 );
    472537            },
    473538
     
    478543             */
    479544            getHtml: function() {
    480                 var attrs = this.shortcode.attrs.named;
    481                 attrs.content = this.shortcode.content;
    482 
    483                 return this.template({ model: _.defaults(
    484                     attrs,
    485                     wp.media[ this.shortcode.tag ].defaults )
    486                 });
    487             },
    488 
    489             unbind: function() {
    490                 this.unsetPlayers();
    491             }
    492         } ),
     545                if ( ! this.parsed ) {
     546                    return '';
     547                }
     548                return this.parsed;
     549            }
     550        },
    493551
    494552        /**
     
    538596     */
    539597    wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
    540         state: 'video-details',
    541         View: _.extend( {}, wp.mce.av.View, {
    542             template: media.template( 'editor-video' )
    543         } )
     598        state: 'video-details'
    544599    } ) );
    545600
     
    550605     */
    551606    wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
    552         state: 'audio-details',
    553         View: _.extend( {}, wp.mce.av.View, {
    554             template: media.template( 'editor-audio' )
    555         } )
     607        state: 'audio-details'
    556608    } ) );
    557609
     
    562614     */
    563615    wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
    564         state: ['playlist-edit', 'video-playlist-edit'],
    565         View: _.extend( {}, wp.media.mixin, {
    566             template:  media.template( 'editor-playlist' ),
    567             overlay: true,
    568 
    569             initialize: function( options ) {
    570                 this.players = [];
    571                 this.data = {};
    572                 this.attachments = [];
    573                 this.shortcode = options.shortcode;
    574 
    575                 $( this ).on( 'ready', function( event, editor ) {
    576                     editor.on( 'hide', this.pausePlayers );
    577                 } );
    578                 $( document ).on( 'media:edit', this.pausePlayers );
    579 
    580                 this.fetch();
    581 
    582                 $( this ).on( 'ready', this.setPlaylist );
    583             },
    584 
    585             /**
    586              * Asynchronously fetch the shortcode's attachments
    587              */
    588             fetch: function() {
    589                 this.attachments = wp.media.playlist.attachments( this.shortcode );
    590                 this.dfd = this.attachments.more().done( _.bind( this.render, this ) );
    591             },
    592 
    593             setPlaylist: function( event, editor, element ) {
    594                 if ( ! this.data.tracks ) {
    595                     return;
    596                 }
    597 
    598                 this.players.push( new WPPlaylistView( {
    599                     el: $( element ).find( '.wp-playlist' ).get( 0 ),
    600                     metadata: this.data
    601                 } ).player );
    602             },
    603 
    604             /**
    605              * Set the data that will be used to compile the Underscore template,
    606              *  compile the template, and then return it.
    607              *
    608              * @returns {string}
    609              */
    610             getHtml: function() {
    611                 var data = this.shortcode.attrs.named,
    612                     model = wp.media.playlist,
    613                     options,
    614                     attachments,
    615                     tracks = [];
    616 
    617                 // Don't render errors while still fetching attachments
    618                 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
    619                     return '';
    620                 }
    621 
    622                 _.each( model.defaults, function( value, key ) {
    623                     data[ key ] = model.coerce( data, key );
    624                 });
    625 
    626                 options = {
    627                     type: data.type,
    628                     style: data.style,
    629                     tracklist: data.tracklist,
    630                     tracknumbers: data.tracknumbers,
    631                     images: data.images,
    632                     artists: data.artists
    633                 };
    634 
    635                 if ( ! this.attachments.length ) {
    636                     return this.template( options );
    637                 }
    638 
    639                 attachments = this.attachments.toJSON();
    640 
    641                 _.each( attachments, function( attachment ) {
    642                     var size = {}, resize = {}, track = {
    643                         src : attachment.url,
    644                         type : attachment.mime,
    645                         title : attachment.title,
    646                         caption : attachment.caption,
    647                         description : attachment.description,
    648                         meta : attachment.meta
    649                     };
    650 
    651                     if ( 'video' === data.type ) {
    652                         size.width = attachment.width;
    653                         size.height = attachment.height;
    654                         if ( media.view.settings.contentWidth ) {
    655                             resize.width = media.view.settings.contentWidth - 22;
    656                             resize.height = Math.ceil( ( size.height * resize.width ) / size.width );
    657                             if ( ! options.width ) {
    658                                 options.width = resize.width;
    659                                 options.height = resize.height;
    660                             }
    661                         } else {
    662                             if ( ! options.width ) {
    663                                 options.width = attachment.width;
    664                                 options.height = attachment.height;
    665                             }
    666                         }
    667                         track.dimensions = {
    668                             original : size,
    669                             resized : _.isEmpty( resize ) ? size : resize
    670                         };
    671                     } else {
    672                         options.width = 400;
    673                     }
    674 
    675                     track.image = attachment.image;
    676                     track.thumb = attachment.thumb;
    677 
    678                     tracks.push( track );
    679                 } );
    680 
    681                 options.tracks = tracks;
    682                 this.data = options;
    683 
    684                 return this.template( options );
    685             },
    686 
    687             unbind: function() {
    688                 this.unsetPlayers();
    689             }
    690         } )
     616        state: [ 'playlist-edit', 'video-playlist-edit' ]
    691617    } ) );
    692618
     
    694620     * TinyMCE handler for the embed shortcode
    695621     */
    696     wp.mce.embedView = _.extend( {}, wp.media.mixin, {
    697         overlay: true,
    698         initialize: function( options ) {
    699             this.players = [];
    700             this.content = options.content;
    701             this.fetching = false;
    702             this.parsed = false;
    703             this.original = options.url || options.shortcode.string();
    704 
    705             if ( options.url ) {
    706                 this.shortcode = '[embed]' + options.url + '[/embed]';
    707             } else {
    708                 this.shortcode = options.shortcode.string();
    709             }
    710 
    711             _.bindAll( this, 'setHtml', 'setNode', 'fetch' );
    712             $( this ).on( 'ready', this.setNode );
    713         },
    714         unbind: function() {
    715             var self = this;
    716             _.each( this.players, function ( player ) {
    717                 player.pause();
    718                 self.removePlayer( player );
    719             } );
    720             this.players = [];
    721         },
    722         setNode: function () {
    723             if ( this.parsed ) {
    724                 this.setHtml( this.parsed );
    725                 this.parseMediaShortcodes();
    726             } else if ( ! this.fetching ) {
    727                 this.fetch();
    728             }
    729         },
    730         fetch: function () {
    731             var self = this;
    732 
    733             this.fetching = true;
    734 
    735             wp.ajax.send( 'parse-embed', {
    736                 data: {
    737                     post_ID: $( '#post_ID' ).val() || 0,
    738                     shortcode: this.shortcode
    739                 }
    740             } )
    741             .always( function() {
    742                 self.fetching = false;
    743             } )
    744             .done( function( response ) {
    745                 if ( response ) {
    746                     self.parsed = response;
    747                     self.setHtml( response );
    748                 }
    749             } )
    750             .fail( function( response ) {
    751                 if ( response && response.message ) {
    752                     if ( ( response.type === 'not-embeddable' && self.type === 'embed' ) ||
    753                         response.type === 'not-ssl' ) {
    754 
    755                         self.setError( response.message, 'admin-media' );
    756                     } else {
    757                         self.setContent( '<p>' + self.original + '</p>', null, 'replace' );
    758                     }
    759                 } else if ( response && response.statusText ) {
    760                     self.setError( response.statusText, 'admin-media' );
    761                 }
    762             } );
    763         },
    764         /* jshint scripturl: true */
    765         setHtml: function ( content ) {
    766             var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
    767                 iframe, iframeDoc, i, resize,
    768                 dom = tinymce.DOM;
    769 
    770             if ( content.indexOf( '<script' ) !== -1 ) {
    771                 iframe = dom.create( 'iframe', {
    772                     src: tinymce.Env.ie ? 'javascript:""' : '',
    773                     frameBorder: '0',
    774                     allowTransparency: 'true',
    775                     style: {
    776                         width: '100%',
    777                         display: 'block'
    778                     }
    779                 } );
    780 
    781                 this.setContent( iframe );
    782                 iframeDoc = iframe.contentWindow.document;
    783 
    784                 iframeDoc.open();
    785                 iframeDoc.write(
    786                     '<!DOCTYPE html>' +
    787                     '<html>' +
    788                         '<head>' +
    789                             '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
    790                         '</head>' +
    791                         '<body>' +
    792                             content +
    793                         '</body>' +
    794                     '</html>'
    795                 );
    796                 iframeDoc.close();
    797 
    798                 resize = function() {
    799                     $( iframe ).height( $( iframeDoc ).height() );
    800                 };
    801 
    802                 if ( MutationObserver ) {
    803                     new MutationObserver( _.debounce( function() {
    804                         resize();
    805                     }, 100 ) )
    806                     .observe( iframeDoc.body, {
    807                         attributes: true,
    808                         childList: true,
    809                         subtree: true
     622    wp.mce.embedMixin = {
     623        View: _.extend( {}, wp.mce.av.View, {
     624            overlay: true,
     625            action: 'parse-embed',
     626            initialize: function( options ) {
     627                this.content = options.content;
     628                this.fetching = false;
     629                this.parsed = false;
     630                this.original = options.url || options.shortcode.string();
     631
     632                if ( options.url ) {
     633                    this.shortcode = media.embed.shortcode( {
     634                        url: options.url
    810635                    } );
    811636                } else {
    812                     for ( i = 1; i < 6; i++ ) {
    813                         setTimeout( resize, i * 700 );
    814                     }
    815                 }
    816             } else {
    817                 this.setContent( content );
    818             }
    819 
    820             this.parseMediaShortcodes();
    821         },
    822         parseMediaShortcodes: function () {
    823             var self = this;
    824             $( '.wp-audio-shortcode, .wp-video-shortcode', this.node ).each( function ( i, element ) {
    825                 self.players.push( new MediaElementPlayer( element, self.mejsSettings ) );
    826             } );
    827         }
    828     } );
    829 
    830     wp.mce.embedMixin = {
    831         View: wp.mce.embedView,
     637                    this.shortcode = options.shortcode;
     638                }
     639
     640                _.bindAll( this, 'createIframe', 'setNode', 'fetch' );
     641                $( this ).on( 'ready', this.setNode );
     642            }
     643        } ),
    832644        edit: function( node ) {
    833645            var embed = media.embed,
  • trunk/src/wp-includes/js/media-audiovideo.js

    r29029 r29179  
    4545                player.pause();
    4646            } );
    47         },
    48 
    49         /**
    50          * Utility to identify the user's browser
    51          */
    52         ua: {
    53             is : function( browser ) {
    54                 var passes = false, ua = window.navigator.userAgent;
    55 
    56                 switch ( browser ) {
    57                     case 'oldie':
    58                         passes = ua.match(/MSIE [6-8]/gi) !== null;
    59                     break;
    60                     case 'ie':
    61                         passes = /MSIE /.test( ua ) || ( /Trident\//.test( ua ) && /rv:\d/.test( ua ) ); // IE11
    62                     break;
    63                     case 'ff':
    64                         passes = ua.match(/firefox/gi) !== null;
    65                     break;
    66                     case 'opera':
    67                         passes = ua.match(/OPR/) !== null;
    68                     break;
    69                     case 'safari':
    70                         passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) === null;
    71                     break;
    72                     case 'chrome':
    73                         passes = ua.match(/safari/gi) !== null && ua.match(/chrome/gi) !== null;
    74                     break;
    75                 }
    76 
    77                 return passes;
    78             }
    79         },
    80 
    81         /**
    82          * Specify compatibility for native playback by browser
    83          */
    84         compat :{
    85             'opera' : {
    86                 audio: ['ogg', 'wav'],
    87                 video: ['ogg', 'webm']
    88             },
    89             'chrome' : {
    90                 audio: ['ogg', 'mpeg'],
    91                 video: ['ogg', 'webm', 'mp4', 'm4v', 'mpeg']
    92             },
    93             'ff' : {
    94                 audio: ['ogg', 'mpeg'],
    95                 video: ['ogg', 'webm']
    96             },
    97             'safari' : {
    98                 audio: ['mpeg', 'wav'],
    99                 video: ['mp4', 'm4v', 'mpeg', 'x-ms-wmv', 'quicktime']
    100             },
    101             'ie' : {
    102                 audio: ['mpeg'],
    103                 video: ['mp4', 'm4v', 'mpeg']
    104             }
    105         },
    106 
    107         /**
    108          * Determine if the passed media contains a <source> that provides
    109          *  native playback in the user's browser
    110          *
    111          * @param {jQuery} media
    112          * @returns {Boolean}
    113          */
    114         isCompatible: function( media ) {
    115             if ( ! media.find( 'source' ).length ) {
    116                 return false;
    117             }
    118 
    119             var ua = this.ua, test = false, found = false, sources;
    120 
    121             if ( ua.is( 'oldIE' ) ) {
    122                 return false;
    123             }
    124 
    125             sources = media.find( 'source' );
    126 
    127             _.find( this.compat, function( supports, browser ) {
    128                 if ( ua.is( browser ) ) {
    129                     found = true;
    130                     _.each( sources, function( elem ) {
    131                         var audio = new RegExp( 'audio\/(' + supports.audio.join('|') + ')', 'gi' ),
    132                             video = new RegExp( 'video\/(' + supports.video.join('|') + ')', 'gi' );
    133 
    134                         if ( elem.type.match( video ) !== null || elem.type.match( audio ) !== null ) {
    135                             test = true;
    136                         }
    137                     } );
    138                 }
    139 
    140                 return test || found;
    141             } );
    142 
    143             return test;
    14447        },
    14548
  • trunk/src/wp-includes/js/mediaelement/wp-mediaelement.css

    r28863 r29179  
    110110    font-size: 14px;
    111111    line-height: 1.5;
     112}
     113
     114.wp-admin .wp-playlist {
     115    margin: 0 0 18px;
    112116}
    113117
  • trunk/src/wp-includes/js/mediaelement/wp-playlist.js

    r28649 r29179  
    88            this.index = 0;
    99            this.settings = {};
    10             this.compatMode = $( 'body' ).hasClass( 'wp-admin' ) && $( '#content_ifr' ).length;
    1110            this.data = options.metadata || $.parseJSON( this.$('script').html() );
    1211            this.playerNode = this.$( this.data.type );
     
    2827            }
    2928
    30             if ( this.isCompatibleSrc() ) {
    31                 this.playerNode.attr( 'src', this.current.get( 'src' ) );
    32             }
     29            this.playerNode.attr( 'src', this.current.get( 'src' ) );
    3330
    3431            _.bindAll( this, 'bindPlayer', 'bindResetPlayer', 'setPlayer', 'ended', 'clickTrack' );
     
    4845        bindResetPlayer : function (mejs) {
    4946            this.bindPlayer( mejs );
    50             if ( this.isCompatibleSrc() ) {
    51                 this.playCurrentSrc();
    52             }
    53         },
    54 
    55         isCompatibleSrc: function () {
    56             var testNode;
    57 
    58             if ( this.compatMode ) {
    59                 testNode = $( '<span><source type="' + this.current.get( 'type' ) + '" /></span>' );
    60 
    61                 if ( ! wp.media.mixin.isCompatible( testNode ) ) {
    62                     this.playerNode.removeAttr( 'src' );
    63                     this.playerNode.removeAttr( 'poster' );
    64                     return;
    65                 }
    66             }
    67 
    68             return true;
     47            this.playCurrentSrc();
    6948        },
    7049
     
    7756
    7857            if (force) {
    79                 if ( this.isCompatibleSrc() ) {
    80                     this.playerNode.attr( 'src', this.current.get( 'src' ) );
    81                 }
     58                this.playerNode.attr( 'src', this.current.get( 'src' ) );
    8259                this.settings.success = this.bindResetPlayer;
    8360            }
     
    189166
    190167    $(document).ready(function () {
    191         if ( ! $( 'body' ).hasClass( 'wp-admin' ) || $( 'body' ).hasClass( 'about-php' ) ) {
    192             $('.wp-playlist').each(function () {
    193                 return new WPPlaylistView({ el: this });
    194             });
    195         }
     168        $('.wp-playlist').each( function() {
     169            return new WPPlaylistView({ el: this });
     170        } );
    196171    });
    197172
  • trunk/src/wp-includes/media-template.php

    r29133 r29179  
    12171217    </script>
    12181218
    1219     <script type="text/html" id="tmpl-editor-audio">
    1220         <?php wp_underscore_audio_template() ?>
    1221     </script>
    1222 
    1223     <script type="text/html" id="tmpl-editor-video">
    1224         <?php wp_underscore_video_template() ?>
    1225     </script>
    1226 
    1227     <?php wp_underscore_playlist_templates() ?>
    1228 
    1229     <script type="text/html" id="tmpl-editor-playlist">
    1230         <# if ( data.tracks ) { #>
    1231             <div class="wp-playlist wp-{{ data.type }}-playlist wp-playlist-{{ data.style }}">
    1232                 <# if ( 'audio' === data.type ){ #>
    1233                 <div class="wp-playlist-current-item"></div>
    1234                 <# } #>
    1235                 <{{ data.type }} controls="controls" preload="none" <#
    1236                     if ( data.width ) { #> width="{{ data.width }}"<# }
    1237                     #><# if ( data.height ) { #> height="{{ data.height }}"<# } #>></{{ data.type }}>
    1238                 <div class="wp-playlist-next"></div>
    1239                 <div class="wp-playlist-prev"></div>
    1240             </div>
    1241         <# } else { #>
    1242             <div class="wpview-error">
    1243                 <div class="dashicons dashicons-video-alt3"></div><p><?php _e( 'No items found.' ); ?></p>
    1244             </div>
    1245         <# } #>
    1246     </script>
    1247 
    12481219    <script type="text/html" id="tmpl-crop-content">
    12491220        <img class="crop-image" src="{{ data.url }}">
Note: See TracChangeset for help on using the changeset viewer.