Make WordPress Core

Ticket #37230: 37230.2.diff

File 37230.2.diff, 79.0 KB (added by swissspidy, 8 years ago)
  • src/wp-admin/css/forms.css

    diff --git src/wp-admin/css/forms.css src/wp-admin/css/forms.css
    index f219831..15be606 100644
    p.search-box { 
    564564        margin: 0 4px 0 0;
    565565}
    566566
     567.js.plugins-php .search-box .wp-filter-search {
     568        margin: 0;
     569        width: 280px;
     570        font-size: 16px;
     571        font-weight: 300;
     572        line-height: 1.5;
     573        padding: 3px 5px;
     574        height: 32px;
     575}
     576
    567577input[type="text"].ui-autocomplete-loading,
    568578input[type="email"].ui-autocomplete-loading {
    569579        background-image: url(../images/loading.gif);
  • src/wp-admin/includes/ajax-actions.php

    diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
    index e5b3262..10aaa9c 100644
    function wp_ajax_search_plugins() { 
    38023802
    38033803        ob_start();
    38043804        $wp_list_table->display();
     3805        $status['count'] = count( $wp_list_table->items );
    38053806        $status['items'] = ob_get_clean();
    38063807
    38073808        wp_send_json_success( $status );
  • src/wp-admin/includes/class-wp-plugins-list-table.php

    diff --git src/wp-admin/includes/class-wp-plugins-list-table.php src/wp-admin/includes/class-wp-plugins-list-table.php
    index c36b77f..b04d93f 100644
    class WP_Plugins_List_Table extends WP_List_Table { 
    342342        }
    343343
    344344        /**
     345         * Display the search box.
     346         *
     347         * @since 3.1.0
     348         * @access public
     349         *
     350         * @param string $text The search button text
     351         * @param string $input_id The search input id
     352         */
     353        public function search_box( $text, $input_id ) {
     354                if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
     355                        return;
     356                }
     357
     358                $input_id = $input_id . '-search-input';
     359
     360                if ( ! empty( $_REQUEST['orderby'] ) ) {
     361                        echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
     362                }
     363                if ( ! empty( $_REQUEST['order'] ) ) {
     364                        echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
     365                }
     366                ?>
     367                <p class="search-box">
     368                        <label class="screen-reader-text" for="<?php echo $input_id ?>"><?php echo $text; ?>:</label>
     369                        <input type="search" id="<?php echo $input_id ?>" class="wp-filter-search" name="s" value="<?php _admin_search_query(); ?>" placeholder="<?php echo esc_attr( 'Search installed plugins...' ); ?>"/>
     370                        <input type="submit" id="search-submit" class="button hide-if-js" value="<?php echo esc_attr( $text ); ?>">
     371                </p>
     372                <?php
     373        }
     374
     375        /**
    345376         *
    346377         * @global string $status
    347378         * @return array
  • src/wp-admin/includes/update.php

    diff --git src/wp-admin/includes/update.php src/wp-admin/includes/update.php
    index 8845219..64d1acb 100644
    function wp_print_admin_notice_templates() { 
    706706 */
    707707function wp_print_update_row_templates() {
    708708        ?>
    709         <script id="tmpl-item-update-row" type="text/template">
     709        <script id="tmpl-item-updatte-row" type="text/template">
    710710                <tr class="plugin-update-tr update" id="{{ data.slug }}-update" data-slug="{{ data.slug }}" <# if ( data.plugin ) { #>data-plugin="{{ data.plugin }}"<# } #>>
    711711                        <td colspan="{{ data.colspan }}" class="plugin-update colspanchange">
    712712                                {{{ data.content }}}
  • src/wp-admin/js/updates.js

    diff --git src/wp-admin/js/updates.js src/wp-admin/js/updates.js
    index c97c0bb..0de60b5 100644
     
    15841584        $( function() {
    15851585                var $pluginFilter    = $( '#plugin-filter' ),
    15861586                        $bulkActionForm  = $( '#bulk-action-form' ),
    1587                         $filesystemModal = $( '#request-filesystem-credentials-dialog' );
     1587                        $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
     1588                        $pluginSearch    = $( '.plugins-php .wp-filter-search' );
    15881589
    15891590                /*
    15901591                 * Whether a user needs to submit filesystem credentials.
     
    19721973                 *
    19731974                 * @since 4.6.0
    19741975                 */
    1975                 $( 'input.wp-filter-search, .wp-filter input[name="s"]' ).on( 'keyup search', _.debounce( function() {
     1976                $( '.plugin-install-php .wp-filter-search' ).on( 'keyup search', _.debounce( function() {
    19761977                        var $form = $( '#plugin-filter' ).empty(),
    19771978                                data  = _.extend( {
    19781979                                        _ajax_nonce: wp.updates.ajaxNonce,
     
    19871988                                wp.updates.searchTerm = data.s;
    19881989                        }
    19891990
    1990                         history.pushState( null, '', location.href.split( '?' )[0] + '?' + $.param( _.omit( data, '_ajax_nonce' ) ) );
     1991                        if ( history.pushState ) {
     1992                                history.pushState( null, '', location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, '_ajax_nonce' ) ) );
     1993                        }
    19911994
    19921995                        if ( 'undefined' !== typeof wp.updates.searchRequest ) {
    19931996                                wp.updates.searchRequest.abort();
     
    20012004                        } );
    20022005                }, 500 ) );
    20032006
     2007                if ( $pluginSearch.length > 0 ) {
     2008                        $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
     2009                }
     2010
    20042011                /**
    20052012                 * Handles changes to the plugin search box on the Installed Plugins screen,
    20062013                 * searching the plugin list dynamically.
    20072014                 *
    20082015                 * @since 4.6.0
    20092016                 */
    2010                 $( '#plugin-search-input' ).on( 'keyup search', _.debounce( function() {
     2017                $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
    20112018                        var data = {
    20122019                                _ajax_nonce: wp.updates.ajaxNonce,
    2013                                 s:           $( '<p />' ).html( $( this ).val() ).text()
     2020                                s:           event.target.value
    20142021                        };
    20152022
     2023                        // Clear on escape.
     2024                        if ( 'keyup' === event.type && 27 === event.which ) {
     2025                                event.target.value = '';
     2026                        }
     2027
    20162028                        if ( wp.updates.searchTerm === data.s ) {
    20172029                                return;
    20182030                        } else {
    20192031                                wp.updates.searchTerm = data.s;
    20202032                        }
    20212033
    2022                         history.pushState( null, '', location.href.split( '?' )[0] + '?s=' + data.s );
     2034                        if ( history.pushState ) {
     2035                                history.pushState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s );
     2036                        }
    20232037
    20242038                        if ( 'undefined' !== typeof wp.updates.searchRequest ) {
    20252039                                wp.updates.searchRequest.abort();
     
    20452059                                $( 'body' ).removeClass( 'loading-content' );
    20462060                                $bulkActionForm.append( response.items );
    20472061                                delete wp.updates.searchRequest;
     2062
     2063                                if ( 0 === response.count ) {
     2064                                        wp.a11y.speak( wp.updates.l10n.noPluginsFound );
     2065                                } else {
     2066                                        wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
     2067                                }
    20482068                        } );
    20492069                }, 500 ) );
    20502070
  • new file src/wp-admin/js/updates.js.orig

    diff --git src/wp-admin/js/updates.js.orig src/wp-admin/js/updates.js.orig
    new file mode 100644
    index 0000000..c97c0bb
    - +  
     1/**
     2 * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
     3 *
     4 * @version 4.2.0
     5 *
     6 * @package WordPress
     7 * @subpackage Administration
     8 */
     9
     10/* global pagenow */
     11
     12/**
     13 * @param {jQuery}  $                                   jQuery object.
     14 * @param {object}  wp                                  WP object.
     15 * @param {object}  settings                            WP Updates settings.
     16 * @param {string}  settings.ajax_nonce                 AJAX nonce.
     17 * @param {object}  settings.l10n                       Translation strings.
     18 * @param {object=} settings.plugins                    Base names of plugins in their different states.
     19 * @param {Array}   settings.plugins.all                Base names of all plugins.
     20 * @param {Array}   settings.plugins.active             Base names of active plugins.
     21 * @param {Array}   settings.plugins.inactive           Base names of inactive plugins.
     22 * @param {Array}   settings.plugins.upgrade            Base names of plugins with updates available.
     23 * @param {Array}   settings.plugins.recently_activated Base names of recently activated plugins.
     24 * @param {object=} settings.totals                     Plugin/theme status information or null.
     25 * @param {number}  settings.totals.all                 Amount of all plugins or themes.
     26 * @param {number}  settings.totals.upgrade             Amount of plugins or themes with updates available.
     27 * @param {number}  settings.totals.disabled            Amount of disabled themes.
     28 */
     29(function( $, wp, settings ) {
     30        var $document = $( document );
     31
     32        wp = wp || {};
     33
     34        /**
     35         * The WP Updates object.
     36         *
     37         * @since 4.2.0
     38         *
     39         * @type {object}
     40         */
     41        wp.updates = {};
     42
     43        /**
     44         * User nonce for ajax calls.
     45         *
     46         * @since 4.2.0
     47         *
     48         * @type {string}
     49         */
     50        wp.updates.ajaxNonce = settings.ajax_nonce;
     51
     52        /**
     53         * Localized strings.
     54         *
     55         * @since 4.2.0
     56         *
     57         * @type {object}
     58         */
     59        wp.updates.l10n = settings.l10n;
     60
     61        /**
     62         * Current search term.
     63         *
     64         * @since 4.6.0
     65         *
     66         * @type {string}
     67         */
     68        wp.updates.searchTerm = '';
     69
     70        /**
     71         * Whether filesystem credentials need to be requested from the user.
     72         *
     73         * @since 4.2.0
     74         *
     75         * @type {bool}
     76         */
     77        wp.updates.shouldRequestFilesystemCredentials = false;
     78
     79        /**
     80         * Filesystem credentials to be packaged along with the request.
     81         *
     82         * @since 4.2.0
     83         * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
     84         *
     85         * @type {object} filesystemCredentials                    Holds filesystem credentials.
     86         * @type {object} filesystemCredentials.ftp                Holds FTP credentials.
     87         * @type {string} filesystemCredentials.ftp.host           FTP host. Default empty string.
     88         * @type {string} filesystemCredentials.ftp.username       FTP user name. Default empty string.
     89         * @type {string} filesystemCredentials.ftp.password       FTP password. Default empty string.
     90         * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
     91         *                                                         Default empty string.
     92         * @type {object} filesystemCredentials.ssh                Holds SSH credentials.
     93         * @type {string} filesystemCredentials.ssh.publicKey      The public key. Default empty string.
     94         * @type {string} filesystemCredentials.ssh.privateKey     The private key. Default empty string.
     95         * @type {bool}   filesystemCredentials.available          Whether filesystem credentials have been provided.
     96         *                                                         Default 'false'.
     97         */
     98        wp.updates.filesystemCredentials = {
     99                ftp:       {
     100                        host:           '',
     101                        username:       '',
     102                        password:       '',
     103                        connectionType: ''
     104                },
     105                ssh:       {
     106                        publicKey:  '',
     107                        privateKey: ''
     108                },
     109                available: false
     110        };
     111
     112        /**
     113         * Whether we're waiting for an Ajax request to complete.
     114         *
     115         * @since 4.2.0
     116         * @since 4.6.0 More accurately named `ajaxLocked`.
     117         *
     118         * @type {bool}
     119         */
     120        wp.updates.ajaxLocked = false;
     121
     122        /**
     123         * Admin notice template.
     124         *
     125         * @since 4.6.0
     126         *
     127         * @type {function} A function that lazily-compiles the template requested.
     128         */
     129        wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
     130
     131        /**
     132         * Update queue.
     133         *
     134         * If the user tries to update a plugin while an update is
     135         * already happening, it can be placed in this queue to perform later.
     136         *
     137         * @since 4.2.0
     138         * @since 4.6.0 More accurately named `queue`.
     139         *
     140         * @type {Array.object}
     141         */
     142        wp.updates.queue = [];
     143
     144        /**
     145         * Holds a jQuery reference to return focus to when exiting the request credentials modal.
     146         *
     147         * @since 4.2.0
     148         *
     149         * @type {jQuery}
     150         */
     151        wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
     152
     153        /**
     154         * Adds or updates an admin notice.
     155         *
     156         * @since 4.6.0
     157         *
     158         * @param {object}  data
     159         * @param {*=}      data.selector      Optional. Selector of an element to be replaced with the admin notice.
     160         * @param {string=} data.id            Optional. Unique id that will be used as the notice's id attribute.
     161         * @param {string=} data.className     Optional. Class names that will be used in the admin notice.
     162         * @param {string=} data.message       Optional. The message displayed in the notice.
     163         * @param {number=} data.successes     Optional. The amount of successful operations.
     164         * @param {number=} data.errors        Optional. The amount of failed operations.
     165         * @param {Array=}  data.errorMessages Optional. Error messages of failed operations.
     166         *
     167         */
     168        wp.updates.addAdminNotice = function( data ) {
     169                var $notice = $( data.selector ), $adminNotice;
     170
     171                delete data.selector;
     172                $adminNotice = wp.updates.adminNotice( data );
     173
     174                // Check if this admin notice already exists.
     175                if ( ! $notice.length ) {
     176                        $notice = $( '#' + data.id );
     177                }
     178
     179                if ( $notice.length ) {
     180                        $notice.replaceWith( $adminNotice );
     181                } else {
     182                        $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     183                }
     184
     185                $document.trigger( 'wp-updates-notice-added' );
     186        };
     187
     188        /**
     189         * Handles Ajax requests to WordPress.
     190         *
     191         * @since 4.6.0
     192         *
     193         * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
     194         * @param {object} data   Data that needs to be passed to the ajax callback.
     195         * @return {$.promise}    A jQuery promise that represents the request,
     196         *                        decorated with an abort() method.
     197         */
     198        wp.updates.ajax = function( action, data ) {
     199                var options = {};
     200
     201                if ( wp.updates.ajaxLocked ) {
     202                        wp.updates.queue.push( {
     203                                action: action,
     204                                data:   data
     205                        } );
     206
     207                        // Return a Deferred object so callbacks can always be registered.
     208                        return $.Deferred();
     209                }
     210
     211                wp.updates.ajaxLocked = true;
     212
     213                if ( data.success ) {
     214                        options.success = data.success;
     215                        delete data.success;
     216                }
     217
     218                if ( data.error ) {
     219                        options.error = data.error;
     220                        delete data.error;
     221                }
     222
     223                options.data = _.extend( data, {
     224                        action:          action,
     225                        _ajax_nonce:     wp.updates.ajaxNonce,
     226                        username:        wp.updates.filesystemCredentials.ftp.username,
     227                        password:        wp.updates.filesystemCredentials.ftp.password,
     228                        hostname:        wp.updates.filesystemCredentials.ftp.hostname,
     229                        connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
     230                        public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
     231                        private_key:     wp.updates.filesystemCredentials.ssh.privateKey
     232                } );
     233
     234                return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
     235        };
     236
     237        /**
     238         * Actions performed after every Ajax request.
     239         *
     240         * @since 4.6.0
     241         *
     242         * @param {object}  response
     243         * @param {array=}  response.debug     Optional. Debug information.
     244         * @param {string=} response.errorCode Optional. Error code for an error that occurred.
     245         */
     246        wp.updates.ajaxAlways = function( response ) {
     247                if ( ! response.errorCode && 'unable_to_connect_to_filesystem' !== response.errorCode ) {
     248                        wp.updates.ajaxLocked = false;
     249                        wp.updates.queueChecker();
     250                }
     251
     252                if ( 'undefined' !== typeof response.debug ) {
     253                        _.map( response.debug, function( message ) {
     254                                window.console.log( $( '<p />' ).html( message ).text() );
     255                        } );
     256                }
     257        };
     258
     259        /**
     260         * Decrements the update counts throughout the various menus.
     261         *
     262         * This includes the toolbar, the "Updates" menu item and the menu items
     263         * for plugins and themes.
     264         *
     265         * @since 3.9.0
     266         *
     267         * @param {string} type The type of item that was updated or deleted.
     268         *                      Can be 'plugin', 'theme'.
     269         */
     270        wp.updates.decrementCount = function( type ) {
     271                var $adminBarUpdates             = $( '#wp-admin-bar-updates' ),
     272                        $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
     273                        count                        = $adminBarUpdates.find( '.ab-label' ).text(),
     274                        $menuItem, $itemCount, itemCount;
     275
     276                count = parseInt( count, 10 ) - 1;
     277
     278                if ( count < 0 || isNaN( count ) ) {
     279                        return;
     280                }
     281
     282                $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
     283                $adminBarUpdates.find( '.ab-label' ).text( count );
     284
     285                // Remove the update count from the toolbar if it's zero.
     286                if ( ! count ) {
     287                        $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
     288                }
     289
     290                // Update the "Updates" menu item.
     291                $dashboardNavMenuUpdateCount.each( function( index, element ) {
     292                        element.className = element.className.replace( /count-\d+/, 'count-' + count );
     293                } );
     294
     295                $dashboardNavMenuUpdateCount.removeAttr( 'title' );
     296                $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count );
     297
     298                if ( 'plugin' === type ) {
     299                        $menuItem  = $( '#menu-plugins' );
     300                        $itemCount = $menuItem.find( '.plugin-count' );
     301                } else if ( 'theme' === type ) {
     302                        $menuItem  = $( '#menu-appearance' );
     303                        $itemCount = $menuItem.find( '.theme-count' );
     304                }
     305
     306                // Decrement the counter of the other menu items.
     307                if ( $itemCount ) {
     308                        itemCount = $itemCount.eq( 0 ).text();
     309                        itemCount = parseInt( itemCount, 10 ) - 1;
     310                }
     311
     312                if ( itemCount < 0 || isNaN( itemCount ) ) {
     313                        return;
     314                }
     315
     316                if ( itemCount > 0 ) {
     317                        $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
     318
     319                        $itemCount.text( itemCount );
     320                        $menuItem.find( '.update-plugins' ).each( function( index, element ) {
     321                                element.className = element.className.replace( /count-\d+/, 'count-' + itemCount );
     322                        } );
     323                } else {
     324                        $( '.subsubsub .upgrade' ).remove();
     325                        $menuItem.find( '.update-plugins' ).remove();
     326                }
     327        };
     328
     329        /**
     330         * Sends an Ajax request to the server to update a plugin.
     331         *
     332         * @since 4.2.0
     333         * @since 4.6.0 More accurately named `updatePlugin`.
     334         *
     335         * @param {object}               args         Arguments.
     336         * @param {string}               args.plugin  Plugin basename.
     337         * @param {string}               args.slug    Plugin slug.
     338         * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
     339         * @param {updatePluginError=}   args.error   Optional. Error callback. Default: wp.updates.updatePluginError
     340         * @return {$.promise} A jQuery promise that represents the request,
     341         *                     decorated with an abort() method.
     342         */
     343        wp.updates.updatePlugin = function( args ) {
     344                var $updateRow, $card, $message, message;
     345
     346                args = _.extend( {
     347                        success: wp.updates.updatePluginSuccess,
     348                        error: wp.updates.updatePluginError
     349                }, args );
     350
     351                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
     352                        $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
     353                        $message   = $updateRow.find( '.update-message' ).addClass( 'updating-message' ).find( 'p' );
     354                        message    = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() );
     355                } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
     356                        $card    = $( '.plugin-card-' + args.slug );
     357                        $message = $card.find( '.update-now' ).addClass( 'updating-message' );
     358                        message  = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) );
     359
     360                        // Remove previous error messages, if any.
     361                        $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
     362                }
     363
     364                if ( $message.html() !== wp.updates.l10n.updating ) {
     365                        $message.data( 'originaltext', $message.html() );
     366                }
     367
     368                $message
     369                        .attr( 'aria-label', message )
     370                        .text( wp.updates.l10n.updating );
     371
     372                $document.trigger( 'wp-plugin-updating' );
     373
     374                return wp.updates.ajax( 'update-plugin', args );
     375        };
     376
     377        /**
     378         * Updates the UI appropriately after a successful plugin update.
     379         *
     380         * @since 4.2.0
     381         * @since 4.6.0 More accurately named `updatePluginSuccess`.
     382         *
     383         * @typedef {object} updatePluginSuccess
     384         * @param {object} response            Response from the server.
     385         * @param {string} response.slug       Slug of the plugin to be updated.
     386         * @param {string} response.plugin     Basename of the plugin to be updated.
     387         * @param {string} response.pluginName Name of the plugin to be updated.
     388         * @param {string} response.oldVersion Old version of the plugin.
     389         * @param {string} response.newVersion New version of the plugin.
     390         */
     391        wp.updates.updatePluginSuccess = function( response ) {
     392                var $pluginRow, $updateMessage, newText;
     393
     394                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
     395                        $pluginRow     = $( 'tr[data-plugin="' + response.plugin + '"]' )
     396                                .removeClass( 'update' )
     397                                .addClass( 'updated' );
     398                        $updateMessage = $pluginRow.find( '.update-message' )
     399                                .removeClass( 'updating-message notice-warning' )
     400                                .addClass( 'updated-message notice-success' ).find( 'p' );
     401
     402                        // Update the version number in the row.
     403                        newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
     404                        $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
     405                } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
     406                        $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
     407                                .removeClass( 'updating-message' )
     408                                .addClass( 'button-disabled updated-message' );
     409                }
     410
     411                $updateMessage
     412                        .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
     413                        .text( wp.updates.l10n.updated );
     414
     415                wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
     416
     417                wp.updates.decrementCount( 'plugin' );
     418
     419                $document.trigger( 'wp-plugin-update-success', response );
     420        };
     421
     422        /**
     423         * Updates the UI appropriately after a failed plugin update.
     424         *
     425         * @since 4.2.0
     426         * @since 4.6.0 More accurately named `updatePluginError`.
     427         *
     428         * @typedef {object} updatePluginError
     429         * @param {object}  response              Response from the server.
     430         * @param {string}  response.slug         Slug of the plugin to be updated.
     431         * @param {string}  response.plugin       Basename of the plugin to be updated.
     432         * @param {string=} response.pluginName   Optional. Name of the plugin to be updated.
     433         * @param {string}  response.errorCode    Error code for the error that occurred.
     434         * @param {string}  response.errorMessage The error that occurred.
     435         */
     436        wp.updates.updatePluginError = function( response ) {
     437                var $card, $message, errorMessage;
     438
     439                if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
     440                        return;
     441                }
     442
     443                if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
     444                        return;
     445                }
     446
     447                errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
     448
     449                if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
     450                        $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
     451                        $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
     452                } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
     453                        $card = $( '.plugin-card-' + response.slug )
     454                                .addClass( 'plugin-card-update-failed' )
     455                                .append( wp.updates.adminNotice( {
     456                                        className: 'update-message notice-error notice-alt is-dismissible',
     457                                        message:   errorMessage
     458                                } ) );
     459
     460                        $card.find( '.update-now' )
     461                                .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) )
     462                                .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
     463
     464                        $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
     465
     466                                // Use same delay as the total duration of the notice fadeTo + slideUp animation.
     467                                setTimeout( function() {
     468                                        $card
     469                                                .removeClass( 'plugin-card-update-failed' )
     470                                                .find( '.column-name a' ).focus();
     471
     472                                        $card.find( '.update-now' )
     473                                                .attr( 'aria-label', false )
     474                                                .text( wp.updates.l10n.updateNow );
     475                                }, 200 );
     476                        } );
     477                }
     478
     479                wp.a11y.speak( errorMessage, 'assertive' );
     480
     481                $document.trigger( 'wp-plugin-update-error', response );
     482        };
     483
     484        /**
     485         * Sends an Ajax request to the server to install a plugin.
     486         *
     487         * @since 4.6.0
     488         *
     489         * @param {object}                args         Arguments.
     490         * @param {string}                args.slug    Plugin identifier in the WordPress.org Plugin repository.
     491         * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
     492         * @param {installPluginError=}   args.error   Optional. Error callback. Default: wp.updates.installPluginError
     493         * @return {$.promise} A jQuery promise that represents the request,
     494         *                     decorated with an abort() method.
     495         */
     496        wp.updates.installPlugin = function( args ) {
     497                var $card    = $( '.plugin-card-' + args.slug ),
     498                        $message = $card.find( '.install-now' );
     499
     500                args = _.extend( {
     501                        success: wp.updates.installPluginSuccess,
     502                        error: wp.updates.installPluginError
     503                }, args );
     504
     505                if ( 'import' === pagenow ) {
     506                        $message = $( 'a[href*="' + args.slug + '"]' );
     507                } else {
     508                        $message.text( wp.updates.l10n.installing );
     509                }
     510
     511                $message.addClass( 'updating-message' );
     512
     513                wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
     514
     515                // Remove previous error messages, if any.
     516                $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
     517
     518                return wp.updates.ajax( 'install-plugin', args );
     519        };
     520
     521        /**
     522         * Updates the UI appropriately after a successful plugin install.
     523         *
     524         * @since 4.6.0
     525         *
     526         * @typedef {object} installPluginSuccess
     527         * @param {object} response             Response from the server.
     528         * @param {string} response.slug        Slug of the installed plugin.
     529         * @param {string} response.pluginName  Name of the installed plugin.
     530         * @param {string} response.activateUrl URL to activate the just installed plugin.
     531         */
     532        wp.updates.installPluginSuccess = function( response ) {
     533                var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
     534
     535                $message
     536                        .removeClass( 'updating-message' )
     537                        .addClass( 'updated-message installed button-disabled' )
     538                        .text( wp.updates.l10n.installed );
     539
     540                wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
     541
     542                $document.trigger( 'wp-plugin-install-success', response );
     543
     544                if ( response.activateUrl ) {
     545                        setTimeout( function() {
     546
     547                                // Transform the 'Install' button into an 'Activate' button.
     548                                $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' )
     549                                        .attr( 'href', response.activateUrl )
     550                                        .text( wp.updates.l10n.activatePlugin );
     551                        }, 1000 );
     552                }
     553        };
     554
     555        /**
     556         * Updates the UI appropriately after a failed plugin install.
     557         *
     558         * @since 4.6.0
     559         *
     560         * @typedef {object} installPluginError
     561         * @param {object}  response              Response from the server.
     562         * @param {string}  response.slug         Slug of the plugin to be installed.
     563         * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
     564         * @param {string}  response.errorCode    Error code for the error that occurred.
     565         * @param {string}  response.errorMessage The error that occurred.
     566         */
     567        wp.updates.installPluginError = function( response ) {
     568                var $card   = $( '.plugin-card-' + response.slug ),
     569                        $button = $card.find( '.install-now' ),
     570                        errorMessage;
     571
     572                if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
     573                        return;
     574                }
     575
     576                if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
     577                        return;
     578                }
     579
     580                errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
     581
     582                $card
     583                        .addClass( 'plugin-card-update-failed' )
     584                        .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
     585
     586                $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
     587
     588                        // Use same delay as the total duration of the notice fadeTo + slideUp animation.
     589                        setTimeout( function() {
     590                                $card
     591                                        .removeClass( 'plugin-card-update-failed' )
     592                                        .find( '.column-name a' ).focus();
     593                        }, 200 );
     594                } );
     595
     596                $button
     597                        .removeClass( 'updating-message' ).addClass( 'button-disabled' )
     598                        .attr( 'aria-label', wp.updates.l10n.installFailedLabel.replace( '%s', response.pluginName ) )
     599                        .text( wp.updates.l10n.installFailedShort );
     600
     601                wp.a11y.speak( errorMessage, 'assertive' );
     602
     603                $document.trigger( 'wp-plugin-install-error', response );
     604        };
     605
     606        /**
     607         * Updates the UI appropriately after a successful importer install.
     608         *
     609         * @since 4.6.0
     610         *
     611         * @typedef {object} installImporterSuccess
     612         * @param {object} response             Response from the server.
     613         * @param {string} response.slug        Slug of the installed plugin.
     614         * @param {string} response.pluginName  Name of the installed plugin.
     615         * @param {string} response.activateUrl URL to activate the just installed plugin.
     616         */
     617        wp.updates.installImporterSuccess = function( response ) {
     618                wp.updates.addAdminNotice( {
     619                        id:        'install-success',
     620                        className: 'notice-success is-dismissible',
     621                        message:   wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' )
     622                } );
     623
     624                $( 'a[href*="' + response.slug + '"]' )
     625                        .removeClass( 'thickbox open-plugin-details-modal updating-message' )
     626                        .off( 'click' )
     627                        .attr( 'href', response.activateUrl + '&from=import' )
     628                        .attr( 'title', wp.updates.l10n.activateImporter );
     629
     630                wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
     631
     632                $document.trigger( 'wp-importer-install-success', response );
     633        };
     634
     635        /**
     636         * Updates the UI appropriately after a failed importer install.
     637         *
     638         * @since 4.6.0
     639         *
     640         * @typedef {object} installImporterError
     641         * @param {object}  response              Response from the server.
     642         * @param {string}  response.slug         Slug of the plugin to be installed.
     643         * @param {string=} response.pluginName   Optional. Name of the plugin to be installed.
     644         * @param {string}  response.errorCode    Error code for the error that occurred.
     645         * @param {string}  response.errorMessage The error that occurred.
     646         */
     647        wp.updates.installImporterError = function( response ) {
     648                var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
     649
     650                if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
     651                        return;
     652                }
     653
     654                if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
     655                        return;
     656                }
     657
     658                wp.updates.addAdminNotice( {
     659                        id:        response.errorCode,
     660                        className: 'notice-error is-dismissible',
     661                        message:   errorMessage
     662                } );
     663
     664                $( 'a[href*="' + response.slug + '"]' ).removeClass( 'updating-message' );
     665
     666                wp.a11y.speak( errorMessage, 'assertive' );
     667
     668                $document.trigger( 'wp-importer-install-error', response );
     669        };
     670
     671        /**
     672         * Sends an Ajax request to the server to delete a plugin.
     673         *
     674         * @since 4.6.0
     675         *
     676         * @param {object}               args         Arguments.
     677         * @param {string}               args.plugin  Basename of the plugin to be deleted.
     678         * @param {string}               args.slug    Slug of the plugin to be deleted.
     679         * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
     680         * @param {deletePluginError=}   args.error   Optional. Error callback. Default: wp.updates.deletePluginError
     681         * @return {$.promise} A jQuery promise that represents the request,
     682         *                     decorated with an abort() method.
     683         */
     684        wp.updates.deletePlugin = function( args ) {
     685                var $message = $( '[data-plugin="' + args.plugin + '"]' ).find( '.update-message p' );
     686
     687                args = _.extend( {
     688                        success: wp.updates.deletePluginSuccess,
     689                        error: wp.updates.deletePluginError
     690                }, args );
     691
     692                if ( $message.html() !== wp.updates.l10n.updating ) {
     693                        $message.data( 'originaltext', $message.html() );
     694                }
     695
     696                wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
     697
     698                return wp.updates.ajax( 'delete-plugin', args );
     699        };
     700
     701        /**
     702         * Updates the UI appropriately after a successful plugin deletion.
     703         *
     704         * @since 4.6.0
     705         *
     706         * @typedef {object} deletePluginSuccess
     707         * @param {object} response            Response from the server.
     708         * @param {string} response.slug       Slug of the plugin that was deleted.
     709         * @param {string} response.plugin     Base name of the plugin that was deleted.
     710         * @param {string} response.pluginName Name of the plugin that was deleted.
     711         */
     712        wp.updates.deletePluginSuccess = function( response ) {
     713
     714                // Removes the plugin and updates rows.
     715                $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
     716                        var $form            = $( '#bulk-action-form' ),
     717                                $views           = $( '.subsubsub' ),
     718                                $pluginRow       = $( this ),
     719                                columnCount      = $form.find( 'thead th:not(.hidden), thead td' ).length,
     720                                pluginDeletedRow = wp.template( 'item-deleted-row' ),
     721                                /** @type {object} plugins Base names of plugins in their different states. */
     722                                plugins          = settings.plugins;
     723
     724                        // Add a success message after deleting a plugin.
     725                        if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
     726                                $pluginRow.after(
     727                                        pluginDeletedRow( {
     728                                                slug:    response.slug,
     729                                                plugin:  response.plugin,
     730                                                colspan: columnCount,
     731                                                name:    response.pluginName
     732                                        } )
     733                                );
     734                        }
     735
     736                        $pluginRow.remove();
     737
     738                        // Remove plugin from update count.
     739                        if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
     740                                plugins.upgrade = _.without( plugins.upgrade, response.plugin );
     741                                wp.updates.decrementCount( 'plugin' );
     742                        }
     743
     744                        // Remove from views.
     745                        if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
     746                                plugins.inactive = _.without( plugins.inactive, response.plugin );
     747                                if ( plugins.inactive.length ) {
     748                                        $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
     749                                } else {
     750                                        $views.find( '.inactive' ).remove();
     751                                }
     752                        }
     753
     754                        if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
     755                                plugins.active = _.without( plugins.active, response.plugin );
     756                                if ( plugins.active.length ) {
     757                                        $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
     758                                } else {
     759                                        $views.find( '.active' ).remove();
     760                                }
     761                        }
     762
     763                        if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
     764                                plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
     765                                if ( plugins.recently_activated.length ) {
     766                                        $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
     767                                } else {
     768                                        $views.find( '.recently_activated' ).remove();
     769                                }
     770                        }
     771
     772                        plugins.all = _.without( plugins.all, response.plugin );
     773
     774                        if ( plugins.all.length ) {
     775                                $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
     776                        } else {
     777                                $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
     778                                $views.find( '.all' ).remove();
     779
     780                                if ( ! $form.find( 'tr.no-items' ).length ) {
     781                                        $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + wp.updates.l10n.noPlugins + '</td></tr>' );
     782                                }
     783                        }
     784                } );
     785
     786                wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
     787
     788                $document.trigger( 'wp-plugin-delete-success', response );
     789        };
     790
     791        /**
     792         * Updates the UI appropriately after a failed plugin deletion.
     793         *
     794         * @since 4.6.0
     795         *
     796         * @typedef {object} deletePluginError
     797         * @param {object}  response              Response from the server.
     798         * @param {string}  response.slug         Slug of the plugin to be deleted.
     799         * @param {string}  response.plugin       Base name of the plugin to be deleted
     800         * @param {string=} response.pluginName   Optional. Name of the plugin to be deleted.
     801         * @param {string}  response.errorCode    Error code for the error that occurred.
     802         * @param {string}  response.errorMessage The error that occurred.
     803         */
     804        wp.updates.deletePluginError = function( response ) {
     805                var $plugin          = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ),
     806                        pluginUpdateRow  = wp.template( 'item-update-row' ),
     807                        $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ),
     808                        noticeContent    = wp.updates.adminNotice( {
     809                                className: 'update-message notice-error notice-alt',
     810                                message:   response.errorMessage
     811                        } );
     812
     813                if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
     814                        return;
     815                }
     816
     817                if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
     818                        return;
     819                }
     820
     821                // Add a plugin update row if it doesn't exist yet.
     822                if ( ! $pluginUpdateRow.length ) {
     823                        $plugin.addClass( 'update' ).after(
     824                                pluginUpdateRow( {
     825                                        slug:    response.slug,
     826                                        plugin:  response.plugin,
     827                                        colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
     828                                        content: noticeContent
     829                                } )
     830                        );
     831                } else {
     832
     833                        // Remove previous error messages, if any.
     834                        $pluginUpdateRow.find( '.notice-error' ).remove();
     835
     836                        $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
     837                }
     838
     839                $document.trigger( 'wp-plugin-delete-error', response );
     840        };
     841
     842        /**
     843         * Sends an Ajax request to the server to update a theme.
     844         *
     845         * @since 4.6.0
     846         *
     847         * @param {object}              args         Arguments.
     848         * @param {string}              args.slug    Theme stylesheet.
     849         * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
     850         * @param {updateThemeError=}   args.error   Optional. Error callback. Default: wp.updates.updateThemeError
     851         * @return {$.promise} A jQuery promise that represents the request,
     852         *                     decorated with an abort() method.
     853         */
     854        wp.updates.updateTheme = function( args ) {
     855                var $notice;
     856
     857                args = _.extend( {
     858                        success: wp.updates.updateThemeSuccess,
     859                        error: wp.updates.updateThemeError
     860                }, args );
     861
     862                if ( 'themes-network' === pagenow ) {
     863                        $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).addClass( 'updating-message' ).find( 'p' );
     864
     865                } else {
     866                        $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
     867
     868                        $notice.find( 'h3' ).remove();
     869
     870                        $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
     871                        $notice = $notice.addClass( 'updating-message' ).find( 'p' );
     872                }
     873
     874                if ( $notice.html() !== wp.updates.l10n.updating ) {
     875                        $notice.data( 'originaltext', $notice.html() );
     876                }
     877
     878                wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
     879                $notice.text( wp.updates.l10n.updating );
     880
     881                $document.trigger( 'wp-theme-updating' );
     882
     883                return wp.updates.ajax( 'update-theme', args );
     884        };
     885
     886        /**
     887         * Updates the UI appropriately after a successful theme update.
     888         *
     889         * @since 4.6.0
     890         *
     891         * @typedef {object} updateThemeSuccess
     892         * @param {object} response
     893         * @param {string} response.slug       Slug of the theme to be updated.
     894         * @param {object} response.theme      Updated theme.
     895         * @param {string} response.oldVersion Old version of the theme.
     896         * @param {string} response.newVersion New version of the theme.
     897         */
     898        wp.updates.updateThemeSuccess = function( response ) {
     899                var isModalOpen    = $( 'body.modal-open' ).length,
     900                        $theme         = $( '[data-slug="' + response.slug + '"]' ),
     901                        updatedMessage = {
     902                                className: 'updated-message notice-success notice-alt',
     903                                message:   wp.updates.l10n.updated
     904                        },
     905                        $notice, newText;
     906
     907                if ( 'themes-network' === pagenow ) {
     908                        $notice = $theme.find( '.update-message' );
     909
     910                        // Update the version number in the row.
     911                        newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
     912                        $theme.find( '.theme-version-author-uri' ).html( newText );
     913                } else {
     914                        $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
     915
     916                        // Focus on Customize button after updating.
     917                        if ( isModalOpen ) {
     918                                $( '.load-customize:visible' ).focus();
     919                        } else {
     920                                $theme.find( '.load-customize' ).focus();
     921                        }
     922                }
     923
     924                wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
     925                wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
     926
     927                wp.updates.decrementCount( 'theme' );
     928
     929                $document.trigger( 'wp-theme-update-success', response );
     930
     931                // Show updated message after modal re-rendered.
     932                if ( isModalOpen ) {
     933                        $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
     934                }
     935        };
     936
     937        /**
     938         * Updates the UI appropriately after a failed theme update.
     939         *
     940         * @since 4.6.0
     941         *
     942         * @typedef {object} updateThemeError
     943         * @param {object} response              Response from the server.
     944         * @param {string} response.slug         Slug of the theme to be updated.
     945         * @param {string} response.errorCode    Error code for the error that occurred.
     946         * @param {string} response.errorMessage The error that occurred.
     947         */
     948        wp.updates.updateThemeError = function( response ) {
     949                var $theme       = $( '[data-slug="' + response.slug + '"]' ),
     950                        errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
     951                        $notice;
     952
     953                if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
     954                        return;
     955                }
     956
     957                if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
     958                        return;
     959                }
     960
     961                if ( 'themes-network' === pagenow ) {
     962                        $notice = $theme.find( '.update-message ' );
     963                } else {
     964                        $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
     965
     966                        $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
     967                }
     968
     969                wp.updates.addAdminNotice( {
     970                        selector:  $notice,
     971                        className: 'update-message notice-error notice-alt is-dismissible',
     972                        message:   errorMessage
     973                } );
     974
     975                wp.a11y.speak( errorMessage, 'polite' );
     976
     977                $document.trigger( 'wp-theme-update-error', response );
     978        };
     979
     980        /**
     981         * Sends an Ajax request to the server to install a theme.
     982         *
     983         * @since 4.6.0
     984         *
     985         * @param {object}               args
     986         * @param {string}               args.slug    Theme stylesheet.
     987         * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
     988         * @param {installThemeError=}   args.error   Optional. Error callback. Default: wp.updates.installThemeError
     989         * @return {$.promise} A jQuery promise that represents the request,
     990         *                     decorated with an abort() method.
     991         */
     992        wp.updates.installTheme = function( args ) {
     993                var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
     994
     995                args = _.extend( {
     996                        success: wp.updates.installThemeSuccess,
     997                        error: wp.updates.installThemeError
     998                }, args );
     999
     1000                $message.addClass( 'updating-message' );
     1001                $message.parents( '.theme' ).addClass( 'focus' );
     1002                if ( $message.html() !== wp.updates.l10n.installing ) {
     1003                        $message.data( 'originaltext', $message.html() );
     1004                }
     1005
     1006                $message.text( wp.updates.l10n.installing );
     1007                wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
     1008
     1009                // Remove previous error messages, if any.
     1010                $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
     1011
     1012                return wp.updates.ajax( 'install-theme', args );
     1013        };
     1014
     1015        /**
     1016         * Updates the UI appropriately after a successful theme install.
     1017         *
     1018         * @since 4.6.0
     1019         *
     1020         * @typedef {object} installThemeSuccess
     1021         * @param {object} response              Response from the server.
     1022         * @param {string} response.slug         Slug of the theme to be installed.
     1023         * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
     1024         * @param {string} response.activateUrl  URL to activate the just installed theme.
     1025         */
     1026        wp.updates.installThemeSuccess = function( response ) {
     1027                var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
     1028                        $message;
     1029
     1030                $document.trigger( 'wp-install-theme-success', response );
     1031
     1032                $message = $card.find( '.button-primary' )
     1033                        .removeClass( 'updating-message' )
     1034                        .addClass( 'updated-message disabled' )
     1035                        .text( wp.updates.l10n.installed );
     1036
     1037                wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
     1038
     1039                setTimeout( function() {
     1040
     1041                        if ( response.activateUrl ) {
     1042
     1043                                // Transform the 'Install' button into an 'Activate' button.
     1044                                $message
     1045                                        .attr( 'href', response.activateUrl )
     1046                                        .removeClass( 'theme-install updated-message disabled' )
     1047                                        .addClass( 'activate' )
     1048                                        .text( wp.updates.l10n.activateTheme );
     1049                        }
     1050
     1051                        if ( response.customizeUrl ) {
     1052
     1053                                // Transform the 'Preview' button into a 'Live Preview' button.
     1054                                $message.siblings( '.preview' ).replaceWith( function () {
     1055                                        return $( '<a>' )
     1056                                                .attr( 'href', response.customizeUrl )
     1057                                                .addClass( 'button button-secondary load-customize' )
     1058                                                .text( wp.updates.l10n.livePreview );
     1059                                } );
     1060                        }
     1061                }, 1000 );
     1062        };
     1063
     1064        /**
     1065         * Updates the UI appropriately after a failed theme install.
     1066         *
     1067         * @since 4.6.0
     1068         *
     1069         * @typedef {object} installThemeError
     1070         * @param {object} response              Response from the server.
     1071         * @param {string} response.slug         Slug of the theme to be installed.
     1072         * @param {string} response.errorCode    Error code for the error that occurred.
     1073         * @param {string} response.errorMessage The error that occurred.
     1074         */
     1075        wp.updates.installThemeError = function( response ) {
     1076                var $card, $button,
     1077                        errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
     1078                        $message     = wp.updates.adminNotice( {
     1079                                className: 'update-message notice-error notice-alt',
     1080                                message:   errorMessage
     1081                        } );
     1082
     1083                if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
     1084                        return;
     1085                }
     1086
     1087                if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
     1088                        return;
     1089                }
     1090
     1091                if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
     1092                        $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
     1093                        $card   = $( '.install-theme-info' ).prepend( $message );
     1094                } else {
     1095                        $card   = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
     1096                        $button = $card.find( '.theme-install' );
     1097                }
     1098
     1099                $button
     1100                        .removeClass( 'updating-message' )
     1101                        .attr( 'aria-label', wp.updates.l10n.installFailedLabel.replace( '%s', $card.find( '.theme-name' ).text() ) )
     1102                        .text( wp.updates.l10n.installFailedShort );
     1103
     1104                wp.a11y.speak( errorMessage, 'assertive' );
     1105
     1106                $document.trigger( 'wp-theme-install-error', response );
     1107        };
     1108
     1109        /**
     1110         * Sends an Ajax request to the server to install a theme.
     1111         *
     1112         * @since 4.6.0
     1113         *
     1114         * @param {object}              args
     1115         * @param {string}              args.slug    Theme stylesheet.
     1116         * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
     1117         * @param {deleteThemeError=}   args.error   Optional. Error callback. Default: wp.updates.deleteThemeError
     1118         * @return {$.promise} A jQuery promise that represents the request,
     1119         *                     decorated with an abort() method.
     1120         */
     1121        wp.updates.deleteTheme = function( args ) {
     1122                var $button = $( '.theme-actions .delete-theme' );
     1123
     1124                args = _.extend( {
     1125                        success: wp.updates.deleteThemeSuccess,
     1126                        error: wp.updates.deleteThemeError
     1127                }, args );
     1128
     1129                if ( $button.html() !== wp.updates.l10n.deleting ) {
     1130                        $button.data( 'originaltext', $button.html() );
     1131                }
     1132
     1133                $button.text( wp.updates.l10n.deleting );
     1134                wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
     1135
     1136                // Remove previous error messages, if any.
     1137                $( '.theme-info .update-message' ).remove();
     1138
     1139                return wp.updates.ajax( 'delete-theme', args );
     1140        };
     1141
     1142        /**
     1143         * Updates the UI appropriately after a successful theme deletion.
     1144         *
     1145         * @since 4.6.0
     1146         *
     1147         * @typedef {object} deleteThemeSuccess
     1148         * @param {object} response      Response from the server.
     1149         * @param {string} response.slug Slug of the theme that was deleted.
     1150         */
     1151        wp.updates.deleteThemeSuccess = function( response ) {
     1152                var $themeRows = $( '[data-slug="' + response.slug + '"]' );
     1153
     1154                if ( 'themes-network' === pagenow ) {
     1155
     1156                        // Removes the theme and updates rows.
     1157                        $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
     1158                                var $views     = $( '.subsubsub' ),
     1159                                        $themeRow  = $( this ),
     1160                                        totals     = settings.totals,
     1161                                        deletedRow = wp.template( 'item-deleted-row' );
     1162
     1163                                if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
     1164                                        $themeRow.after(
     1165                                                deletedRow( {
     1166                                                        slug:    response.slug,
     1167                                                        colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
     1168                                                        name:    $themeRow.find( '.theme-title strong' ).text()
     1169                                                } )
     1170                                        );
     1171                                }
     1172
     1173                                $themeRow.remove();
     1174
     1175                                // Remove theme from update count.
     1176                                if ( $themeRow.hasClass( 'update' ) ) {
     1177                                        totals.upgrade--;
     1178                                        wp.updates.decrementCount( 'theme' );
     1179                                }
     1180
     1181                                // Remove from views.
     1182                                if ( $themeRow.hasClass( 'inactive' ) ) {
     1183                                        totals.disabled--;
     1184                                        if ( totals.disabled ) {
     1185                                                $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
     1186                                        } else {
     1187                                                $views.find( '.disabled' ).remove();
     1188                                        }
     1189                                }
     1190
     1191                                // There is always at least one theme available.
     1192                                $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
     1193                        } );
     1194                }
     1195
     1196                wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
     1197
     1198                $document.trigger( 'wp-delete-theme-success', response );
     1199        };
     1200
     1201        /**
     1202         * Updates the UI appropriately after a failed theme deletion.
     1203         *
     1204         * @since 4.6.0
     1205         *
     1206         * @typedef {object} deleteThemeError
     1207         * @param {object} response              Response from the server.
     1208         * @param {string} response.slug         Slug of the theme to be deleted.
     1209         * @param {string} response.errorCode    Error code for the error that occurred.
     1210         * @param {string} response.errorMessage The error that occurred.
     1211         */
     1212        wp.updates.deleteThemeError = function( response ) {
     1213                var $themeRow    = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
     1214                        $button      = $( '.theme-actions .delete-theme' ),
     1215                        updateRow    = wp.template( 'item-update-row' ),
     1216                        $updateRow   = $themeRow.siblings( '#' + response.slug + '-update' ),
     1217                        errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ),
     1218                        $message     = wp.updates.adminNotice( {
     1219                                className: 'update-message notice-error notice-alt',
     1220                                message:   errorMessage
     1221                        } );
     1222
     1223                if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
     1224                        return;
     1225                }
     1226
     1227                if ( 'themes-network' === pagenow ) {
     1228                        if ( ! $updateRow.length ) {
     1229                                $themeRow.addClass( 'update' ).after(
     1230                                        updateRow( {
     1231                                                slug: response.slug,
     1232                                                colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
     1233                                                content: $message
     1234                                        } )
     1235                                );
     1236                        } else {
     1237                                // Remove previous error messages, if any.
     1238                                $updateRow.find( '.notice-error' ).remove();
     1239                                $updateRow.find( '.plugin-update' ).append( $message );
     1240                        }
     1241                } else {
     1242                        $( '.theme-info .theme-description' ).before( $message );
     1243                }
     1244
     1245                $button.html( $button.data( 'originaltext' ) );
     1246
     1247                wp.a11y.speak( errorMessage, 'assertive' );
     1248
     1249                $document.trigger( 'wp-theme-delete-error', response );
     1250        };
     1251
     1252        /**
     1253         * Adds the appropriate callback based on the type of action and the current page.
     1254         *
     1255         * @since 4.6.0
     1256         * @private
     1257         *
     1258         * @param {object} data   AJAX payload.
     1259         * @param {string} action The type of request to perform.
     1260         * @return {object} The AJAX payload with the appropriate callbacks.
     1261         */
     1262        wp.updates._addCallbacks = function( data, action ) {
     1263                if ( 'import' === pagenow && 'install-plugin' === action ) {
     1264                        data.success = wp.updates.installImporterSuccess;
     1265                        data.error   = wp.updates.installImporterError;
     1266                }
     1267
     1268                return data;
     1269        };
     1270
     1271        /**
     1272         * Pulls available jobs from the queue and runs them.
     1273         *
     1274         * @since 4.2.0
     1275         * @since 4.6.0 Can handle multiple job types.
     1276         */
     1277        wp.updates.queueChecker = function() {
     1278                var job;
     1279
     1280                if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
     1281                        return;
     1282                }
     1283
     1284                job = wp.updates.queue.shift();
     1285
     1286                // Handle a queue job.
     1287                switch ( job.action ) {
     1288                        case 'install-plugin':
     1289                                wp.updates.installPlugin( job.data );
     1290                                break;
     1291
     1292                        case 'update-plugin':
     1293                                wp.updates.updatePlugin( job.data );
     1294                                break;
     1295
     1296                        case 'delete-plugin':
     1297                                wp.updates.deletePlugin( job.data );
     1298                                break;
     1299
     1300                        case 'install-theme':
     1301                                wp.updates.installTheme( job.data );
     1302                                break;
     1303
     1304                        case 'update-theme':
     1305                                wp.updates.updateTheme( job.data );
     1306                                break;
     1307
     1308                        case 'delete-theme':
     1309                                wp.updates.deleteTheme( job.data );
     1310                                break;
     1311
     1312                        default:
     1313                                window.console.error( 'Failed to execute queued update job.', job );
     1314                                break;
     1315                }
     1316        };
     1317
     1318        /**
     1319         * Requests the users filesystem credentials if they aren't already known.
     1320         *
     1321         * @since 4.2.0
     1322         *
     1323         * @param {Event=} event Optional. Event interface.
     1324         */
     1325        wp.updates.requestFilesystemCredentials = function( event ) {
     1326                if ( false === wp.updates.filesystemCredentials.available ) {
     1327                        /*
     1328                         * After exiting the credentials request modal,
     1329                         * return the focus to the element triggering the request.
     1330                         */
     1331                        if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
     1332                                wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
     1333                        }
     1334
     1335                        wp.updates.ajaxLocked = true;
     1336                        wp.updates.requestForCredentialsModalOpen();
     1337                }
     1338        };
     1339
     1340        /**
     1341         * Requests the users filesystem credentials if needed and there is no lock.
     1342         *
     1343         * @since 4.6.0
     1344         *
     1345         * @param {Event=} event Optional. Event interface.
     1346         */
     1347        wp.updates.maybeRequestFilesystemCredentials = function( event ) {
     1348                if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
     1349                        wp.updates.requestFilesystemCredentials( event );
     1350                }
     1351        };
     1352
     1353        /**
     1354         * Keydown handler for the request for credentials modal.
     1355         *
     1356         * Closes the modal when the escape key is pressed and
     1357         * constrains keyboard navigation to inside the modal.
     1358         *
     1359         * @since 4.2.0
     1360         *
     1361         * @param {Event} event Event interface.
     1362         */
     1363        wp.updates.keydown = function( event ) {
     1364                if ( 27 === event.keyCode ) {
     1365                        wp.updates.requestForCredentialsModalCancel();
     1366                } else if ( 9 === event.keyCode ) {
     1367
     1368                        // #upgrade button must always be the last focus-able element in the dialog.
     1369                        if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
     1370                                $( '#hostname' ).focus();
     1371
     1372                                event.preventDefault();
     1373                        } else if ( 'hostname' === event.target.id && event.shiftKey ) {
     1374                                $( '#upgrade' ).focus();
     1375
     1376                                event.preventDefault();
     1377                        }
     1378                }
     1379        };
     1380
     1381        /**
     1382         * Opens the request for credentials modal.
     1383         *
     1384         * @since 4.2.0
     1385         */
     1386        wp.updates.requestForCredentialsModalOpen = function() {
     1387                var $modal = $( '#request-filesystem-credentials-dialog' );
     1388
     1389                $( 'body' ).addClass( 'modal-open' );
     1390                $modal.show();
     1391                $modal.find( 'input:enabled:first' ).focus();
     1392                $modal.on( 'keydown', wp.updates.keydown );
     1393        };
     1394
     1395        /**
     1396         * Closes the request for credentials modal.
     1397         *
     1398         * @since 4.2.0
     1399         */
     1400        wp.updates.requestForCredentialsModalClose = function() {
     1401                $( '#request-filesystem-credentials-dialog' ).hide();
     1402                $( 'body' ).removeClass( 'modal-open' );
     1403
     1404                if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
     1405                        wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
     1406                }
     1407        };
     1408
     1409        /**
     1410         * Takes care of the steps that need to happen when the modal is canceled out.
     1411         *
     1412         * @since 4.2.0
     1413         * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
     1414         */
     1415        wp.updates.requestForCredentialsModalCancel = function() {
     1416
     1417                // Not ajaxLocked and no queue means we already have cleared things up.
     1418                if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
     1419                        return;
     1420                }
     1421
     1422                _.each( wp.updates.queue, function( job ) {
     1423                        $document.trigger( 'credential-modal-cancel', job );
     1424                } );
     1425
     1426                // Remove the lock, and clear the queue.
     1427                wp.updates.ajaxLocked = false;
     1428                wp.updates.queue = [];
     1429
     1430                wp.updates.requestForCredentialsModalClose();
     1431        };
     1432
     1433        /**
     1434         * Displays an error message in the request for credentials form.
     1435         *
     1436         * @since 4.2.0
     1437         *
     1438         * @param {string} message Error message.
     1439         */
     1440        wp.updates.showErrorInCredentialsForm = function( message ) {
     1441                var $modal = $( '#request-filesystem-credentials-form' );
     1442
     1443                // Remove any existing error.
     1444                $modal.find( '.notice' ).remove();
     1445                $modal.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
     1446        };
     1447
     1448        /**
     1449         * Handles credential errors and runs events that need to happen in that case.
     1450         *
     1451         * @since 4.2.0
     1452         *
     1453         * @param {object} response Ajax response.
     1454         * @param {string} action   The type of request to perform.
     1455         */
     1456        wp.updates.credentialError = function( response, action ) {
     1457
     1458                // Restore callbacks.
     1459                response = wp.updates._addCallbacks( response, action );
     1460
     1461                wp.updates.queue.push( {
     1462                        action: action,
     1463
     1464                        /*
     1465                         * Not cool that we're depending on response for this data.
     1466                         * This would feel more whole in a view all tied together.
     1467                         */
     1468                        data: response
     1469                } );
     1470
     1471                wp.updates.filesystemCredentials.available = false;
     1472                wp.updates.showErrorInCredentialsForm( response.errorMessage );
     1473                wp.updates.requestFilesystemCredentials();
     1474        };
     1475
     1476        /**
     1477         * Handles credentials errors if it could not connect to the filesystem.
     1478         *
     1479         * @since 4.6.0
     1480         *
     1481         * @typedef {object} maybeHandleCredentialError
     1482         * @param {object} response              Response from the server.
     1483         * @param {string} response.errorCode    Error code for the error that occurred.
     1484         * @param {string} response.errorMessage The error that occurred.
     1485         * @param {string} action                The type of request to perform.
     1486         * @returns {boolean} Whether there is an error that needs to be handled or not.
     1487         */
     1488        wp.updates.maybeHandleCredentialError = function( response, action ) {
     1489                if ( response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
     1490                        wp.updates.credentialError( response, action );
     1491                        return true;
     1492                }
     1493
     1494                return false;
     1495        };
     1496
     1497        /**
     1498         * Validates an AJAX response to ensure it's a proper object.
     1499         *
     1500         * If the response deems to be invalid, an admin notice is being displayed.
     1501         *
     1502         * @param {(object|string)} response              Response from the server.
     1503         * @param {function=}       response.always       Optional. Callback for when the Deferred is resolved or rejected.
     1504         * @param {string=}         response.statusText   Optional. Status message corresponding to the status code.
     1505         * @param {string=}         response.responseText Optional. Request response as text.
     1506         * @param {string}          action                Type of action the response is referring to. Can be 'delete',
     1507         *                                                'update' or 'install'.
     1508         */
     1509        wp.updates.isValidResponse = function( response, action ) {
     1510                var error = wp.updates.l10n.unknownError,
     1511                    errorMessage;
     1512
     1513                // Make sure the response is a valid data object and not a Promise object.
     1514                if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
     1515                        return true;
     1516                }
     1517
     1518                if ( _.isString( response ) ) {
     1519                        error = response;
     1520                } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
     1521                        error = response.responseText;
     1522                } else if ( _.isString( response.statusText ) ) {
     1523                        error = response.statusText;
     1524                }
     1525
     1526                switch ( action ) {
     1527                        case 'update':
     1528                                errorMessage = wp.updates.l10n.updateFailed;
     1529                                break;
     1530
     1531                        case 'install':
     1532                                errorMessage = wp.updates.l10n.installFailed;
     1533                                break;
     1534
     1535                        case 'delete':
     1536                                errorMessage = wp.updates.l10n.deleteFailed;
     1537                                break;
     1538                }
     1539
     1540                errorMessage = errorMessage.replace( '%s', error );
     1541
     1542                // Add admin notice.
     1543                wp.updates.addAdminNotice( {
     1544                        id:        'unknown_error',
     1545                        className: 'notice-error is-dismissible',
     1546                        message:   errorMessage
     1547                } );
     1548
     1549                // Remove the lock, and clear the queue.
     1550                wp.updates.ajaxLocked = false;
     1551                wp.updates.queue      = [];
     1552
     1553                // Change buttons of all running updates.
     1554                $( '.button.updating-message' )
     1555                        .removeClass( 'updating-message' )
     1556                        .attr( 'aria-label', wp.updates.l10n.updateFailedShort )
     1557                        .prop( 'disabled', true )
     1558                        .text( wp.updates.l10n.updateFailedShort );
     1559
     1560                $( '.updating-message:not(.button):not(.thickbox)' )
     1561                        .removeClass( 'updating-message notice-warning' )
     1562                        .addClass( 'notice-error' )
     1563                        .find( 'p' ).text( errorMessage );
     1564
     1565                wp.a11y.speak( errorMessage, 'assertive' );
     1566
     1567                return false;
     1568        };
     1569
     1570        /**
     1571         * Potentially adds an AYS to a user attempting to leave the page.
     1572         *
     1573         * If an update is on-going and a user attempts to leave the page,
     1574         * opens an "Are you sure?" alert.
     1575         *
     1576         * @since 4.2.0
     1577         */
     1578        wp.updates.beforeunload = function() {
     1579                if ( wp.updates.ajaxLocked ) {
     1580                        return wp.updates.l10n.beforeunload;
     1581                }
     1582        };
     1583
     1584        $( function() {
     1585                var $pluginFilter    = $( '#plugin-filter' ),
     1586                        $bulkActionForm  = $( '#bulk-action-form' ),
     1587                        $filesystemModal = $( '#request-filesystem-credentials-dialog' );
     1588
     1589                /*
     1590                 * Whether a user needs to submit filesystem credentials.
     1591                 *
     1592                 * This is based on whether the form was output on the page server-side.
     1593                 *
     1594                 * @see {wp_print_request_filesystem_credentials_modal() in PHP}
     1595                 */
     1596                wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
     1597
     1598                /**
     1599                 * File system credentials form submit noop-er / handler.
     1600                 *
     1601                 * @since 4.2.0
     1602                 */
     1603                $filesystemModal.on( 'submit', 'form', function( event ) {
     1604                        event.preventDefault();
     1605
     1606                        // Persist the credentials input by the user for the duration of the page load.
     1607                        wp.updates.filesystemCredentials.ftp.hostname       = $( '#hostname' ).val();
     1608                        wp.updates.filesystemCredentials.ftp.username       = $( '#username' ).val();
     1609                        wp.updates.filesystemCredentials.ftp.password       = $( '#password' ).val();
     1610                        wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
     1611                        wp.updates.filesystemCredentials.ssh.publicKey      = $( '#public_key' ).val();
     1612                        wp.updates.filesystemCredentials.ssh.privateKey     = $( '#private_key' ).val();
     1613                        wp.updates.filesystemCredentials.available          = true;
     1614
     1615                        // Unlock and invoke the queue.
     1616                        wp.updates.ajaxLocked = false;
     1617                        wp.updates.queueChecker();
     1618
     1619                        wp.updates.requestForCredentialsModalClose();
     1620                } );
     1621
     1622                /**
     1623                 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
     1624                 *
     1625                 * @since 4.2.0
     1626                 */
     1627                $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
     1628
     1629                /**
     1630                 * Hide SSH fields when not selected.
     1631                 *
     1632                 * @since 4.2.0
     1633                 */
     1634                $filesystemModal.on( 'change', 'input[name="connection_type"]', function() {
     1635                        $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
     1636                } ).change();
     1637
     1638                /**
     1639                 * Handles events after the credential modal was closed.
     1640                 *
     1641                 * @since 4.6.0
     1642                 *
     1643                 * @param {Event}  event Event interface.
     1644                 * @param {string} job   The install/update.delete request.
     1645                 */
     1646                $document.on( 'credential-modal-cancel', function( event, job ) {
     1647                        var $updatingMessage = $( '.updating-message' ),
     1648                                $message, originalText;
     1649
     1650                        if ( 'import' === pagenow ) {
     1651                                $updatingMessage.removeClass( 'updating-message' );
     1652                        } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
     1653                                $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
     1654                        } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
     1655                                $message = $( '.update-now.updating-message' );
     1656                        } else {
     1657                                $message = $updatingMessage;
     1658                        }
     1659
     1660                        if ( $message ) {
     1661                                originalText = $message.data( 'originaltext' );
     1662
     1663                                if ( 'undefined' === typeof originalText ) {
     1664                                        originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
     1665                                }
     1666
     1667                                $message
     1668                                        .removeClass( 'updating-message' )
     1669                                        .html( originalText );
     1670                        }
     1671
     1672                        wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
     1673                } );
     1674
     1675                /**
     1676                 * Click handler for plugin updates in List Table view.
     1677                 *
     1678                 * @since 4.2.0
     1679                 *
     1680                 * @param {Event} event Event interface.
     1681                 */
     1682                $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
     1683                        var $message   = $( event.target ),
     1684                                $pluginRow = $message.parents( 'tr' );
     1685
     1686                        event.preventDefault();
     1687
     1688                        if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
     1689                                return;
     1690                        }
     1691
     1692                        wp.updates.maybeRequestFilesystemCredentials( event );
     1693
     1694                        // Return the user to the input box of the plugin's table row after closing the modal.
     1695                        wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
     1696                        wp.updates.updatePlugin( {
     1697                                plugin: $pluginRow.data( 'plugin' ),
     1698                                slug:   $pluginRow.data( 'slug' )
     1699                        } );
     1700                } );
     1701
     1702                /**
     1703                 * Click handler for plugin updates in plugin install view.
     1704                 *
     1705                 * @since 4.2.0
     1706                 *
     1707                 * @param {Event} event Event interface.
     1708                 */
     1709                $pluginFilter.on( 'click', '.update-now', function( event ) {
     1710                        var $button = $( event.target );
     1711                        event.preventDefault();
     1712
     1713                        if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
     1714                                return;
     1715                        }
     1716
     1717                        wp.updates.maybeRequestFilesystemCredentials( event );
     1718
     1719                        wp.updates.updatePlugin( {
     1720                                plugin: $button.data( 'plugin' ),
     1721                                slug:   $button.data( 'slug' )
     1722                        } );
     1723                } );
     1724
     1725                /**
     1726                 * Click handler for plugin installs in plugin install view.
     1727                 *
     1728                 * @since 4.6.0
     1729                 *
     1730                 * @param {Event} event Event interface.
     1731                 */
     1732                $pluginFilter.on( 'click', '.install-now', function( event ) {
     1733                        var $button = $( event.target );
     1734                        event.preventDefault();
     1735
     1736                        if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
     1737                                return;
     1738                        }
     1739
     1740                        if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
     1741                                wp.updates.requestFilesystemCredentials( event );
     1742
     1743                                $document.on( 'credential-modal-cancel', function() {
     1744                                        var $message = $( '.install-now.updating-message' );
     1745
     1746                                        $message
     1747                                                .removeClass( 'updating-message' )
     1748                                                .text( wp.updates.l10n.installNow );
     1749
     1750                                        wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
     1751                                } );
     1752                        }
     1753
     1754                        wp.updates.installPlugin( {
     1755                                slug: $button.data( 'slug' )
     1756                        } );
     1757                } );
     1758
     1759                /**
     1760                 * Click handler for plugin deletions.
     1761                 *
     1762                 * @since 4.6.0
     1763                 *
     1764                 * @param {Event} event Event interface.
     1765                 */
     1766                $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
     1767                        var $pluginRow = $( event.target ).parents( 'tr' );
     1768
     1769                        event.preventDefault();
     1770
     1771                        if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
     1772                                return;
     1773                        }
     1774
     1775                        wp.updates.maybeRequestFilesystemCredentials( event );
     1776
     1777                        wp.updates.deletePlugin( {
     1778                                plugin: $pluginRow.data( 'plugin' ),
     1779                                slug:   $pluginRow.data( 'slug' )
     1780                        } );
     1781
     1782                } );
     1783
     1784                /**
     1785                 * Click handler for theme updates.
     1786                 *
     1787                 * @since 4.6.0
     1788                 *
     1789                 * @param {Event} event Event interface.
     1790                 */
     1791                $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
     1792                        var $message  = $( event.target ),
     1793                                $themeRow = $message.parents( 'tr' );
     1794
     1795                        event.preventDefault();
     1796
     1797                        if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
     1798                                return;
     1799                        }
     1800
     1801                        wp.updates.maybeRequestFilesystemCredentials( event );
     1802
     1803                        // Return the user to the input box of the theme's table row after closing the modal.
     1804                        wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
     1805                        wp.updates.updateTheme( {
     1806                                slug: $themeRow.data( 'slug' )
     1807                        } );
     1808                } );
     1809
     1810                /**
     1811                 * Click handler for theme deletions.
     1812                 *
     1813                 * @since 4.6.0
     1814                 *
     1815                 * @param {Event} event Event interface.
     1816                 */
     1817                $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
     1818                        var $themeRow = $( event.target ).parents( 'tr' );
     1819
     1820                        event.preventDefault();
     1821
     1822                        if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
     1823                                return;
     1824                        }
     1825
     1826                        wp.updates.maybeRequestFilesystemCredentials( event );
     1827
     1828                        wp.updates.deleteTheme( {
     1829                                slug: $themeRow.data( 'slug' )
     1830                        } );
     1831                } );
     1832
     1833                /**
     1834                 * Bulk action handler for plugins and themes.
     1835                 *
     1836                 * Handles both deletions and updates.
     1837                 *
     1838                 * @since 4.6.0
     1839                 *
     1840                 * @param {Event} event Event interface.
     1841                 */
     1842                $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
     1843                        var bulkAction    = $( event.target ).siblings( 'select' ).val(),
     1844                                itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
     1845                                success       = 0,
     1846                                error         = 0,
     1847                                errorMessages = [],
     1848                                type, action;
     1849
     1850                        // Determine which type of item we're dealing with.
     1851                        switch ( pagenow ) {
     1852                                case 'plugins':
     1853                                case 'plugins-network':
     1854                                        type = 'plugin';
     1855                                        break;
     1856
     1857                                case 'themes-network':
     1858                                        type = 'theme';
     1859                                        break;
     1860
     1861                                default:
     1862                                        window.console.error( 'The page "%s" is not white-listed for bulk action handling.', pagenow );
     1863                                        return;
     1864                        }
     1865
     1866                        // Bail if there were no items selected.
     1867                        if ( ! itemsSelected.length ) {
     1868                                event.preventDefault();
     1869                                $( 'html, body' ).animate( { scrollTop: 0 } );
     1870
     1871                                return wp.updates.addAdminNotice( {
     1872                                        id:        'no-items-selected',
     1873                                        className: 'notice-error is-dismissible',
     1874                                        message:   wp.updates.l10n.noItemsSelected
     1875                                } );
     1876                        }
     1877
     1878                        // Determine the type of request we're dealing with.
     1879                        switch ( bulkAction ) {
     1880                                case 'update-selected':
     1881                                        action = bulkAction.replace( 'selected', type );
     1882                                        break;
     1883
     1884                                case 'delete-selected':
     1885                                        if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
     1886                                                event.preventDefault();
     1887                                                return;
     1888                                        }
     1889
     1890                                        action = bulkAction.replace( 'selected', type );
     1891                                        break;
     1892
     1893                                default:
     1894                                        window.console.error( 'Failed to identify bulk action: %s', bulkAction );
     1895                                        return;
     1896                        }
     1897
     1898                        wp.updates.maybeRequestFilesystemCredentials( event );
     1899
     1900                        event.preventDefault();
     1901
     1902                        // Un-check the bulk checkboxes.
     1903                        $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
     1904
     1905                        // Find all the checkboxes which have been checked.
     1906                        itemsSelected.each( function( index, element ) {
     1907                                var $checkbox  = $( element ),
     1908                                        $itemRow = $checkbox.parents( 'tr' );
     1909
     1910                                // Un-check the box.
     1911                                $checkbox.prop( 'checked', false );
     1912
     1913                                // Only add update-able items to the update queue.
     1914                                if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
     1915                                        return;
     1916                                }
     1917
     1918                                // Add it to the queue.
     1919                                wp.updates.queue.push( {
     1920                                        action: action,
     1921                                        data:   {
     1922                                                plugin: $itemRow.data( 'plugin' ),
     1923                                                slug:   $itemRow.data( 'slug' )
     1924                                        }
     1925                                } );
     1926                        } );
     1927
     1928                        // Display bulk notification for updates of any kind.
     1929                        $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
     1930                                var $bulkActionNotice, itemName;
     1931
     1932                                if ( 'wp-' + response.update + '-update-success' === event.type ) {
     1933                                        success++;
     1934                                } else {
     1935                                        itemName = response.pluginName ? response.pluginName : $( '[data-slug="' + response.slug + '"]' ).find( '.theme-title strong' ).text();
     1936
     1937                                        error++;
     1938                                        errorMessages.push( itemName + ': ' + response.errorMessage );
     1939                                }
     1940
     1941                                wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
     1942
     1943                                wp.updates.addAdminNotice( {
     1944                                        id:            'bulk-action-notice',
     1945                                        successes:     success,
     1946                                        errors:        error,
     1947                                        errorMessages: errorMessages,
     1948                                        type:          response.update
     1949                                } );
     1950
     1951                                $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
     1952                                        $bulkActionNotice.find( 'ul' ).toggleClass( 'hidden' );
     1953                                } );
     1954
     1955                                if ( error > 0 && ! wp.updates.queue.length ) {
     1956                                        $( 'html, body' ).animate( { scrollTop: 0 } );
     1957                                }
     1958                        } );
     1959
     1960                        // Reset admin notice template after #bulk-action-notice was added.
     1961                        $document.on( 'wp-updates-notice-added', function() {
     1962                                wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
     1963                        } );
     1964
     1965                        // Check the queue, now that the event handlers have been added.
     1966                        wp.updates.queueChecker();
     1967                } );
     1968
     1969                /**
     1970                 * Handles changes to the plugin search box on the new-plugin page,
     1971                 * searching the repository dynamically.
     1972                 *
     1973                 * @since 4.6.0
     1974                 */
     1975                $( 'input.wp-filter-search, .wp-filter input[name="s"]' ).on( 'keyup search', _.debounce( function() {
     1976                        var $form = $( '#plugin-filter' ).empty(),
     1977                                data  = _.extend( {
     1978                                        _ajax_nonce: wp.updates.ajaxNonce,
     1979                                        s:           $( '<p />' ).html( $( this ).val() ).text(),
     1980                                        tab:         'search',
     1981                                        type:        $( '#typeselector' ).val()
     1982                                }, { type: 'term' } );
     1983
     1984                        if ( wp.updates.searchTerm === data.s ) {
     1985                                return;
     1986                        } else {
     1987                                wp.updates.searchTerm = data.s;
     1988                        }
     1989
     1990                        history.pushState( null, '', location.href.split( '?' )[0] + '?' + $.param( _.omit( data, '_ajax_nonce' ) ) );
     1991
     1992                        if ( 'undefined' !== typeof wp.updates.searchRequest ) {
     1993                                wp.updates.searchRequest.abort();
     1994                        }
     1995                        $( 'body' ).addClass( 'loading-content' );
     1996
     1997                        wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
     1998                                $( 'body' ).removeClass( 'loading-content' );
     1999                                $form.append( response.items );
     2000                                delete wp.updates.searchRequest;
     2001                        } );
     2002                }, 500 ) );
     2003
     2004                /**
     2005                 * Handles changes to the plugin search box on the Installed Plugins screen,
     2006                 * searching the plugin list dynamically.
     2007                 *
     2008                 * @since 4.6.0
     2009                 */
     2010                $( '#plugin-search-input' ).on( 'keyup search', _.debounce( function() {
     2011                        var data = {
     2012                                _ajax_nonce: wp.updates.ajaxNonce,
     2013                                s:           $( '<p />' ).html( $( this ).val() ).text()
     2014                        };
     2015
     2016                        if ( wp.updates.searchTerm === data.s ) {
     2017                                return;
     2018                        } else {
     2019                                wp.updates.searchTerm = data.s;
     2020                        }
     2021
     2022                        history.pushState( null, '', location.href.split( '?' )[0] + '?s=' + data.s );
     2023
     2024                        if ( 'undefined' !== typeof wp.updates.searchRequest ) {
     2025                                wp.updates.searchRequest.abort();
     2026                        }
     2027
     2028                        $bulkActionForm.empty();
     2029                        $( 'body' ).addClass( 'loading-content' );
     2030
     2031                        wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
     2032
     2033                                // Can we just ditch this whole subtitle business?
     2034                                var $subTitle    = $( '<span />' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', data.s ) ),
     2035                                        $oldSubTitle = $( '.wrap .subtitle' );
     2036
     2037                                if ( ! data.s.length ) {
     2038                                        $oldSubTitle.remove();
     2039                                } else if ( $oldSubTitle.length ) {
     2040                                        $oldSubTitle.replaceWith( $subTitle );
     2041                                } else {
     2042                                        $( '.wrap h1' ).append( $subTitle );
     2043                                }
     2044
     2045                                $( 'body' ).removeClass( 'loading-content' );
     2046                                $bulkActionForm.append( response.items );
     2047                                delete wp.updates.searchRequest;
     2048                        } );
     2049                }, 500 ) );
     2050
     2051                /**
     2052                 * Trigger a search event when the search form gets submitted.
     2053                 *
     2054                 * @since 4.6.0
     2055                 */
     2056                $document.on( 'submit', '.search-plugins', function( event ) {
     2057                        event.preventDefault();
     2058
     2059                        $( 'input.wp-filter-search' ).trigger( 'search' );
     2060                } );
     2061
     2062                /**
     2063                 * Trigger a search event when the search type gets changed.
     2064                 *
     2065                 * @since 4.6.0
     2066                 */
     2067                $( '#typeselector' ).on( 'change', function() {
     2068                        $( 'input[name="s"]' ).trigger( 'search' );
     2069                } );
     2070
     2071                /**
     2072                 * Click handler for updating a plugin from the details modal on `plugin-install.php`.
     2073                 *
     2074                 * @since 4.2.0
     2075                 *
     2076                 * @param {Event} event Event interface.
     2077                 */
     2078                $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
     2079                        var target = window.parent === window ? null : window.parent,
     2080                                update;
     2081
     2082                        $.support.postMessage = !! window.postMessage;
     2083
     2084                        if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
     2085                                return;
     2086                        }
     2087
     2088                        event.preventDefault();
     2089
     2090                        update = {
     2091                                action: 'update-plugin',
     2092                                data:   {
     2093                                        plugin: $( this ).data( 'plugin' ),
     2094                                        slug:   $( this ).data( 'slug' )
     2095                                }
     2096                        };
     2097
     2098                        target.postMessage( JSON.stringify( update ), window.location.origin );
     2099                } );
     2100
     2101                /**
     2102                 * Click handler for installing a plugin from the details modal on `plugin-install.php`.
     2103                 *
     2104                 * @since 4.6.0
     2105                 *
     2106                 * @param {Event} event Event interface.
     2107                 */
     2108                $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
     2109                        var target = window.parent === window ? null : window.parent,
     2110                                install;
     2111
     2112                        $.support.postMessage = !! window.postMessage;
     2113
     2114                        if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
     2115                                return;
     2116                        }
     2117
     2118                        event.preventDefault();
     2119
     2120                        install = {
     2121                                action: 'install-plugin',
     2122                                data:   {
     2123                                        slug: $( this ).data( 'slug' )
     2124                                }
     2125                        };
     2126
     2127                        target.postMessage( JSON.stringify( install ), window.location.origin );
     2128                } );
     2129
     2130                /**
     2131                 * Handles postMessage events.
     2132                 *
     2133                 * @since 4.2.0
     2134                 * @since 4.6.0 Switched `update-plugin` action to use the queue.
     2135                 *
     2136                 * @param {Event} event Event interface.
     2137                 */
     2138                $( window ).on( 'message', function( event ) {
     2139                        var originalEvent  = event.originalEvent,
     2140                                expectedOrigin = document.location.protocol + '//' + document.location.hostname,
     2141                                message;
     2142
     2143                        if ( originalEvent.origin !== expectedOrigin ) {
     2144                                return;
     2145                        }
     2146
     2147                        try {
     2148                                message = $.parseJSON( originalEvent.data );
     2149                        } catch ( e ) {
     2150                                return;
     2151                        }
     2152
     2153                        if ( 'undefined' === typeof message.action ) {
     2154                                return;
     2155                        }
     2156
     2157                        switch ( message.action ) {
     2158
     2159                                // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
     2160                                case 'decrementUpdateCount':
     2161                                        /** @property {string} message.upgradeType */
     2162                                        wp.updates.decrementCount( message.upgradeType );
     2163                                        break;
     2164
     2165                                case 'install-plugin':
     2166                                case 'update-plugin':
     2167                                        /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
     2168                                        window.tb_remove();
     2169                                        /* jscs:enable */
     2170
     2171                                        message.data = wp.updates._addCallbacks( message.data, message.action );
     2172
     2173                                        wp.updates.queue.push( message );
     2174                                        wp.updates.queueChecker();
     2175                                        break;
     2176                        }
     2177                } );
     2178
     2179                /**
     2180                 * Adds a callback to display a warning before leaving the page.
     2181                 *
     2182                 * @since 4.2.0
     2183                 */
     2184                $( window ).on( 'beforeunload', wp.updates.beforeunload );
     2185        } );
     2186})( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );
  • src/wp-admin/plugins.php

    diff --git src/wp-admin/plugins.php src/wp-admin/plugins.php
    index 6010784..fd90515 100644
    get_current_screen()->add_help_tab( array( 
    371371'title'         => __('Overview'),
    372372'content'       =>
    373373        '<p>' . __('Plugins extend and expand the functionality of WordPress. Once a plugin is installed, you may activate it or deactivate it here.') . '</p>' .
     374        '<p>' . __( 'The search for installed plugins will search for terms in their name, description, or author.' ) . ' <span id="live-search-desc" class="hide-if-no-js">' . __( 'The search results will be updated as you type.' ) . '</span></p>' .
    374375        '<p>' . sprintf(
    375376                /* translators: %s: WordPress Plugin Directory URL */
    376377                __( 'If you would like to see more plugins to choose from, click on the &#8220;Add New&#8221; button and you will be able to browse or search for additional plugins from the <a href="%s" target="_blank">WordPress.org Plugin Directory</a>. Plugins in the WordPress.org Plugin Directory are designed and developed by third parties, and are compatible with the license WordPress uses. Oh, and they&#8217;re free!' ),
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 3b1e8fa..2945b48 100644
    function wp_default_scripts( &$scripts ) { 
    653653                                'activateTheme'              => is_network_admin() ? __( 'Network Enable' ) : __( 'Activate' ),
    654654                                'activateImporter'           => __( 'Activate importer' ),
    655655                                'unknownError'               => __( 'An unknown error occured' ),
     656                                'pluginsFound'               => __( 'Number of plugins found: %d' ),
     657                                'noPluginsFound'             => __( 'No plugins found. Try a different search.' ),
    656658                        ),
    657659                ) );
    658660
  • tests/qunit/fixtures/updates.js

    diff --git tests/qunit/fixtures/updates.js tests/qunit/fixtures/updates.js
    index 657ef73..d570aa6 100644
    window._wpUpdatesSettings = { 
    3939                'activatePlugin': 'Activate',
    4040                'activateTheme': 'Activate',
    4141                'activateImporter': 'Activate importer',
    42                 'unknownError': 'An unknown error occured'
     42                'unknownError': 'An unknown error occured',
     43                'pluginsFound': 'Number of plugins found: %d',
     44                'noPluginsFound': 'No plugins found. Try a different search.'
    4345        }
    4446};
    4547window._wpUpdatesItemCounts = {