Make WordPress Core

Changeset 37714 for trunk


Ignore:
Timestamp:
06/15/2016 04:36:07 PM (10 years ago)
Author:
obenland
Message:

Update/Install: Shiny Updates v2.

Gone are the days of isolation and feelings of "meh", brought on by The Bleak Screen of Sadness. For a shiny knight has arrived to usher our plugins and themes along their arduous journey of installation, updates, and the inevitable fate of ultimate deletion.

Props swissspidy, adamsilverstein, mapk, afragen, ocean90, ryelle, j-falk, michael-arestad, melchoyce, DrewAPicture, AdamSoucie, ethitter, pento, dd32, kraftbj, Ipstenu, jorbin, afercia, stephdau, paulwilde, jipmoors, khag7, svovaf, jipmoors, obenland.
Fixes #22029, #25828, #31002, #31529, #31530, #31773, #33637, #35032.

Location:
trunk
Files:
2 added
28 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/admin-ajax.php

    r36709 r37714  
    6363        'save-user-color-scheme', 'update-widget', 'query-themes', 'parse-embed', 'set-attachment-thumbnail',
    6464        'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post',
    65         'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username',
     65        'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin',
     66        'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme',
     67        'install-theme',
    6668);
    6769
  • trunk/src/wp-admin/css/common.css

    r37439 r37714  
    13981398}
    13991399
     1400.update-message p:before,
     1401.updating-message p:before,
     1402.updated-message p:before,
     1403.import-php .updating-message:before,
     1404.button.updating-message:before,
     1405.button.updated-message:before,
     1406.button.installed:before,
     1407.button.installing:before {
     1408        display: inline-block;
     1409        font: normal 20px/1 'dashicons';
     1410        -webkit-font-smoothing: antialiased;
     1411        -moz-osx-font-smoothing: grayscale;
     1412        vertical-align: top;
     1413}
     1414
    14001415.wrap .notice,
    14011416.wrap div.updated,
     
    14041419.media-upload-form div.error {
    14051420        margin: 5px 0 15px;
     1421}
     1422
     1423/* Update icon. */
     1424.update-message p:before,
     1425.updating-message p:before,
     1426.import-php .updating-message:before,
     1427.button.updating-message:before,
     1428.button.installing:before {
     1429        color: #f56e28;
     1430        content: "\f463";
     1431}
     1432
     1433/* Spins the update icon. */
     1434.updating-message p:before,
     1435.import-php .updating-message:before,
     1436.button.updating-message:before,
     1437.button.installing:before {
     1438        -webkit-animation: rotation 2s infinite linear;
     1439        animation: rotation 2s infinite linear;
     1440}
     1441
     1442/* Updated icon (check mark). */
     1443.updated-message p:before,
     1444.installed p:before,
     1445.button.updated-message:before {
     1446        color: #79ba49;
     1447        content: '\f147';
     1448}
     1449
     1450/* Error icon. */
     1451.update-message.notice-error p:before {
     1452         color: #dc3232;
     1453         content: "\f534";
     1454}
     1455
     1456.wrap .notice p:before,
     1457.import-php .updating-message:before {
     1458        margin-right: 6px;
     1459        vertical-align: bottom;
    14061460}
    14071461
     
    14201474}
    14211475
    1422 .update-message {
    1423         color: #000;
    1424 }
    1425 
    14261476ul#dismissed-updates {
    14271477        display: none;
     
    14541504        margin-left: 2em;
    14551505}
     1506
     1507.button.updating-message:before,
     1508.button.updated-message:before,
     1509.button.installed:before,
     1510.button.installing:before {
     1511        margin: 3px 5px 0 -2px;
     1512}
     1513
     1514.button-primary.updating-message:before {
     1515        color: #fff;
     1516}
     1517
     1518.button-primary.updated-message:before {
     1519        color: #66c6e4;
     1520}
     1521
     1522.button.updated-message,
     1523.notice .button-link {
     1524        -webkit-transition-property: border, background, color;
     1525        transition-property: border, background, color;
     1526        -webkit-transition-duration: .05s;
     1527        transition-duration: .05s;
     1528        -webkit-transition-timing-function: ease-in-out;
     1529        transition-timing-function: ease-in-out;
     1530}
     1531
     1532.notice .button-link {
     1533        color: #0073aa;
     1534}
     1535
     1536.notice .button-link:hover,
     1537.notice .button-link:active {
     1538        color: #00a0d2;
     1539}
     1540
     1541@media aural {
     1542        .wrap .notice p:before,
     1543        .button.installing:before,
     1544        .button.installed:before,
     1545        .update-message p:before {
     1546                speak: none;
     1547        }
     1548}
     1549
    14561550
    14571551/* @todo: this does not need its own section anymore */
  • trunk/src/wp-admin/css/forms.css

    r37693 r37714  
    10451045}
    10461046
     1047.request-filesystem-credentials-dialog .ftp-username,
     1048.request-filesystem-credentials-dialog .ftp-password {
     1049        float: none;
     1050        width: auto;
     1051}
     1052
     1053.request-filesystem-credentials-dialog .ftp-username {
     1054        margin-bottom: 1em;
     1055}
     1056
     1057.request-filesystem-credentials-dialog .ftp-password {
     1058        margin: 0;
     1059}
     1060
     1061.request-filesystem-credentials-dialog .ftp-password em {
     1062        color: #888;
     1063}
     1064
     1065.request-filesystem-credentials-dialog label {
     1066        display: block;
     1067        line-height: 1.5;
     1068        margin-bottom: 1em;
     1069}
     1070
     1071.request-filesystem-credentials-form legend {
     1072        padding-bottom: 0;
     1073}
     1074
     1075.request-filesystem-credentials-form #ssh-keys legend {
     1076        font-size: 1.3em;
     1077}
     1078
     1079.request-filesystem-credentials-form .notice {
     1080        margin: 0 0 20px 0;
     1081        clear: both;
     1082}
     1083
    10471084
    10481085/* =Media Queries
  • trunk/src/wp-admin/css/list-tables.css

    r36959 r37714  
    12721272}
    12731273
    1274 .plugin-update-tr td {
    1275         border-top: 0;
    1276 }
    1277 
    12781274.plugins .inactive td,
    12791275.plugins .inactive th,
     
    13101306}
    13111307
    1312 .plugins .active.update td,
    1313 .plugins .active.update th,
    1314 tr.active.update + tr.plugin-update-tr .plugin-update {
    1315         background-color: #fef7f1;
    1316 }
    1317 
    13181308.plugins .active th.check-column,
    13191309.plugin-update-tr.active td {
    13201310        border-left: 4px solid #00a0d2;
    1321 }
    1322 
    1323 .plugins .active.update th.check-column,
    1324 .plugins .active.update + .plugin-update-tr .plugin-update {
    1325         border-left: 4px solid #d54e21;
    13261311}
    13271312
     
    13591344}
    13601345
    1361 .plugin-update-tr .update-message {
    1362         font-size: 13px;
    1363         font-weight: normal;
    1364         margin: 0 10px 8px 31px;
    1365         padding: 6px 12px 8px 40px;
    1366         background-color: #f7f7f7;
    1367         background-color: rgba(0,0,0,0.03);
    1368 }
    1369 
    1370 .plugin-update-tr .update-message:before,
    1371 .plugin-card .update-now:before,
    1372 .plugin-card .install-now:before {
    1373         color: #d54e21;
     1346.plugins .plugin-update-tr .plugin-update {
     1347        -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1);
     1348        box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1);
     1349        overflow: hidden; /* clearfix */
     1350        padding: 0;
     1351}
     1352
     1353.plugins .plugin-update-tr .notice {
     1354        margin: 5px 20px 15px 40px;
     1355}
     1356
     1357.plugins .notice p {
     1358        margin: 0.5em 0;
     1359}
     1360
     1361.plugin-card .update-now:before {
     1362        color: #f56e28;
     1363        content: "\f463";
    13741364        display: inline-block;
    13751365        font: normal 20px/1 dashicons;
     1366        margin: 3px 5px 0 -2px;
    13761367        speak: none;
    13771368        -webkit-font-smoothing: antialiased;
     
    13801371}
    13811372
    1382 .plugin-update-tr .update-message:before,
    1383 .plugin-card .update-now:before {
    1384         content: "\f463";
    1385 }
    1386 
    1387 .plugin-update-tr .update-message:before {
    1388         margin: 0 10px 0 -30px;
    1389 }
    1390 
    1391 .plugin-card .update-now:before,
    1392 .plugin-card .install-now:before {
    1393         margin: 3px 5px 0 -2px;
    1394 }
    1395 
    1396 .plugin-update-tr .updating-message:before,
    13971373.plugin-card .updating-message:before {
    13981374        content: "\f463";
     
    14231399}
    14241400
    1425 .plugin-update-tr .updated-message:before,
    14261401.plugin-card .updated-message:before {
    14271402        color: #79ba49;
    14281403        content: "\f147";
    1429 }
    1430 
    1431 .wp-list-table.plugins tbody tr.plugin-update-tr td.plugin-update {
    1432         overflow: hidden; /* clearfix */
    1433         padding: 0;
    1434         -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1);
    1435         box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1);
    1436 }
    1437 
    1438 /* update notices for active plugins */
    1439 tr.active + tr.plugin-update-tr .plugin-update {
    1440         background-color: #f7fcfe;
    1441 }
    1442 
    1443 tr.active + tr.plugin-update-tr:not(.updated) .plugin-update .update-message {
    1444         background-color: #fcf3ef;
    14451404}
    14461405
     
    21412100        }
    21422101
     2102        .plugins .active.update + .plugin-update-tr:before {
     2103                background-color: #f7fcfe;
     2104                border-left: 4px solid #00a0d2;
     2105        }
     2106
     2107        .plugins .plugin-update-tr .update-message {
     2108                margin-left: 0;
     2109        }
     2110
    21432111        .wp-list-table.plugins .plugin-title strong,
    21442112        .wp-list-table.plugins .theme-title strong {
  • trunk/src/wp-admin/css/themes.css

    r37341 r37714  
    1212}
    1313
    14 .themes-php .wrap h1 {
    15         float: left;
     14.themes-php:not(.network-admin) .wrap h1 {
    1615        margin-bottom: 15px;
    17 }
    18 
    19 .network-admin.themes-php .wrap h1 {
    20         margin-bottom: 0;
    2116}
    2217
     
    3833
    3934/* Position admin messages */
    40 .themes-php div.updated,
    41 .themes-php div.error,
    42 .themes-php div.notice {
    43         margin: 0 0 20px 0;
    44         clear: both;
     35.theme .notice,
     36.theme .notice.is-dismissible {
     37        left: 0;
     38        margin: 0;
     39        position: absolute;
     40        right: 0;
     41        top: 0;
    4542}
    4643
     
    208205
    209206/**
    210  * Displays a theme update notice
    211  * when an update is available.
    212  */
    213 .theme-browser .theme .theme-update,
    214 .theme-browser .theme .theme-installed {
    215         background: #d54e21;
    216         background: rgba(213, 78, 33, 0.95);
    217         color: #fff;
    218         display: block;
    219         font-size: 13px;
    220         font-weight: 400;
    221         height: 48px;
    222         line-height: 48px;
    223         padding: 0 10px;
    224         position: absolute;
    225         top: 0;
    226         right: 0;
    227         left: 0;
    228         border-bottom: 1px solid rgba(0,0,0,0.25);
    229         overflow: hidden;
    230 }
    231 
    232 .theme-browser .theme .theme-update:before,
    233 .theme-browser .theme .theme-installed:before {
    234         content: "\f463";
    235         display: inline-block;
    236         font: normal 20px/1 dashicons;
    237         margin: 0 6px 0 0;
    238         opacity: 0.8;
    239         position: relative;
    240         top: 5px;
    241         speak: none;
    242         -webkit-font-smoothing: antialiased;
    243 }
    244 
    245 
    246 /**
    247207 * The currently active theme
    248208 */
     
    952912
    953913@media only screen and (max-width: 650px) {
    954         .theme-overlay .theme-update,
    955914        .theme-overlay .theme-description {
    956915                margin-left: 0;
     
    10421001        background: #0073aa;
    10431002}
    1044 .theme-browser .theme .theme-installed:before {
     1003.theme-browser .theme .notice-success p:before {
     1004        color: #79ba49;
    10451005        content: "\f147";
    1046 }
    1047 .theme-browser .theme.is-installed .theme-actions .button-primary {
    1048         display: none !important;
     1006        display: inline-block;
     1007        font: normal 20px/1 'dashicons';
     1008        -webkit-font-smoothing: antialiased;
     1009        -moz-osx-font-smoothing: grayscale;
     1010        vertical-align: top;
     1011}
     1012
     1013.theme-install.updated-message:before {
     1014        content: '';
    10491015}
    10501016
     
    13931359        cursor: default;
    13941360        pointer-events: none;
     1361}
     1362
     1363.theme-install-overlay .close-full-overlay,
     1364.theme-install-overlay .previous-theme,
     1365.theme-install-overlay .next-theme {
     1366        border-left: 0;
     1367        border-top: 0;
     1368        border-bottom: 0;
     1369}
     1370
     1371.theme-install-overlay .close-full-overlay:before,
     1372.theme-install-overlay .previous-theme:before,
     1373.theme-install-overlay .next-theme:before {
     1374        top: 2px;
     1375        left: 0;
    13951376}
    13961377
     
    17091690}
    17101691
    1711 .theme-install-overlay .wp-full-overlay-header .theme-install {
     1692.theme-install-overlay .wp-full-overlay-header .button {
    17121693        float: right;
    17131694        margin: 8px 10px 0 0;
     
    18041785        }
    18051786}
     1787
     1788@media aural {
     1789        .theme .notice:before,
     1790        .theme-info .updating-message:before,
     1791        .theme-info .updated-message:before,
     1792        .theme-install.updating-message:before {
     1793                speak: none;
     1794        }
     1795}
  • trunk/src/wp-admin/import.php

    r36964 r37714  
    4747add_thickbox();
    4848wp_enqueue_script( 'plugin-install' );
     49wp_enqueue_script( 'updates' );
    4950
    5051require_once( ABSPATH . 'wp-admin/admin-header.php' );
     
    132133
    133134<?php
     135wp_print_request_filesystem_credentials_modal();
     136wp_print_admin_notice_templates();
    134137
    135138include( ABSPATH . 'wp-admin/admin-footer.php' );
  • trunk/src/wp-admin/includes/ajax-actions.php

    r37674 r37714  
    28622862                ), $update_php );
    28632863
     2864                if ( current_user_can( 'switch_themes' ) ) {
     2865                        if ( is_multisite() ) {
     2866                                $theme->activate_url = add_query_arg( array(
     2867                                        'action'   => 'enable',
     2868                                        '_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ),
     2869                                        'theme'    => $theme->slug,
     2870                                ), network_admin_url( 'themes.php' ) );
     2871                        } else {
     2872                                $theme->activate_url = add_query_arg( array(
     2873                                        'action'     => 'activate',
     2874                                        '_wpnonce'   => wp_create_nonce( 'switch-theme_' . $theme->slug ),
     2875                                        'stylesheet' => $theme->slug,
     2876                                ), admin_url( 'themes.php' ) );
     2877                        }
     2878                }
     2879
     2880                if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
     2881                        $theme->customize_url = add_query_arg( array(
     2882                                'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
     2883                        ), wp_customize_url( $theme->slug ) );
     2884                }
     2885
    28642886                $theme->name        = wp_kses( $theme->name, $themes_allowedtags );
    28652887                $theme->author      = wp_kses( $theme->author, $themes_allowedtags );
     
    30683090
    30693091        wp_send_json_success( array( 'message' => $message ) );
    3070 }
    3071 
    3072 
    3073 /**
    3074  * AJAX handler for updating a plugin.
    3075  *
    3076  * @since 4.2.0
    3077  *
    3078  * @see Plugin_Upgrader
    3079  */
    3080 function wp_ajax_update_plugin() {
    3081         global $wp_filesystem;
    3082 
    3083         $plugin = urldecode( $_POST['plugin'] );
    3084 
    3085         $status = array(
    3086                 'update'     => 'plugin',
    3087                 'plugin'     => $plugin,
    3088                 'slug'       => sanitize_key( $_POST['slug'] ),
    3089                 'oldVersion' => '',
    3090                 'newVersion' => '',
    3091         );
    3092 
    3093         $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
    3094         if ( $plugin_data['Version'] ) {
    3095                 $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
    3096         }
    3097 
    3098         if ( ! current_user_can( 'update_plugins' ) ) {
    3099                 $status['error'] = __( 'You do not have sufficient permissions to update plugins for this site.' );
    3100                 wp_send_json_error( $status );
    3101         }
    3102 
    3103         check_ajax_referer( 'updates' );
    3104 
    3105         include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
    3106 
    3107         wp_update_plugins();
    3108 
    3109         $skin = new Automatic_Upgrader_Skin();
    3110         $upgrader = new Plugin_Upgrader( $skin );
    3111         $result = $upgrader->bulk_upgrade( array( $plugin ) );
    3112 
    3113         if ( is_array( $result ) && empty( $result[$plugin] ) && is_wp_error( $skin->result ) ) {
    3114                 $result = $skin->result;
    3115         }
    3116 
    3117         if ( is_array( $result ) && !empty( $result[ $plugin ] ) ) {
    3118                 $plugin_update_data = current( $result );
    3119 
    3120                 /*
    3121                  * If the `update_plugins` site transient is empty (e.g. when you update
    3122                  * two plugins in quick succession before the transient repopulates),
    3123                  * this may be the return.
    3124                  *
    3125                  * Preferably something can be done to ensure `update_plugins` isn't empty.
    3126                  * For now, surface some sort of error here.
    3127                  */
    3128                 if ( $plugin_update_data === true ) {
    3129                         $status['error'] = __( 'Plugin update failed.' );
    3130                         wp_send_json_error( $status );
    3131                 }
    3132 
    3133                 $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] );
    3134                 $plugin_data = reset( $plugin_data );
    3135 
    3136                 if ( $plugin_data['Version'] ) {
    3137                         $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
    3138                 }
    3139 
    3140                 wp_send_json_success( $status );
    3141         } else if ( is_wp_error( $result ) ) {
    3142                 $status['error'] = $result->get_error_message();
    3143                 wp_send_json_error( $status );
    3144 
    3145         } else if ( is_bool( $result ) && ! $result ) {
    3146                 $status['errorCode'] = 'unable_to_connect_to_filesystem';
    3147                 $status['error'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
    3148 
    3149                 // Pass through the error from WP_Filesystem if one was raised
    3150                 if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
    3151                         $status['error'] = $wp_filesystem->errors->get_error_message();
    3152                 }
    3153 
    3154                 wp_send_json_error( $status );
    3155 
    3156         } else {
    3157                 // An unhandled error occured
    3158                 $status['error'] = __( 'Plugin update failed.' );
    3159                 wp_send_json_error( $status );
    3160         }
    31613092}
    31623093
     
    33343265        wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) );
    33353266}
     3267
     3268/**
     3269 * AJAX handler for installing a theme.
     3270 *
     3271 * @since 4.6.0
     3272 */
     3273function wp_ajax_install_theme() {
     3274        check_ajax_referer( 'updates' );
     3275
     3276        if ( empty( $_POST['slug'] ) ) {
     3277                wp_send_json_error( array(
     3278                        'slug'         => '',
     3279                        'errorCode'    => 'no_theme_specified',
     3280                        'errorMessage' => __( 'No theme specified.' ),
     3281                ) );
     3282        }
     3283
     3284        $slug = sanitize_key( wp_unslash( $_POST['slug'] ) );
     3285
     3286        $status = array(
     3287                'install' => 'theme',
     3288                'slug'    => $slug,
     3289        );
     3290
     3291        if ( ! current_user_can( 'install_themes' ) ) {
     3292                $status['errorMessage'] = __( 'You do not have sufficient permissions to install themes on this site.' );
     3293                wp_send_json_error( $status );
     3294        }
     3295
     3296        include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
     3297        include_once( ABSPATH . 'wp-admin/includes/theme.php' );
     3298
     3299        $api = themes_api( 'theme_information', array(
     3300                'slug'   => $slug,
     3301                'fields' => array( 'sections' => false ),
     3302        ) );
     3303
     3304        if ( is_wp_error( $api ) ) {
     3305                $status['errorMessage'] = $api->get_error_message();
     3306                wp_send_json_error( $status );
     3307        }
     3308
     3309        $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() );
     3310        $result   = $upgrader->install( $api->download_link );
     3311
     3312        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     3313                $status['debug'] = $upgrader->skin->get_upgrade_messages();
     3314        }
     3315
     3316        if ( is_wp_error( $result ) ) {
     3317                $status['errorMessage'] = $result->get_error_message();
     3318                wp_send_json_error( $status );
     3319        } elseif ( is_null( $result ) ) {
     3320                global $wp_filesystem;
     3321
     3322                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3323                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3324
     3325                // Pass through the error from WP_Filesystem if one was raised.
     3326                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3327                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3328                }
     3329
     3330                wp_send_json_error( $status );
     3331        }
     3332
     3333        if ( current_user_can( 'switch_themes' ) ) {
     3334                if ( is_multisite() ) {
     3335                        $status['activateUrl'] = add_query_arg( array(
     3336                                'action'   => 'enable',
     3337                                '_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ),
     3338                                'theme'    => $slug,
     3339                        ), network_admin_url( 'themes.php' ) );
     3340                } else {
     3341                        $status['activateUrl'] = add_query_arg( array(
     3342                                'action'     => 'activate',
     3343                                '_wpnonce'   => wp_create_nonce( 'switch-theme_' . $slug ),
     3344                                'stylesheet' => $slug,
     3345                        ), admin_url( 'themes.php' ) );
     3346                }
     3347        }
     3348
     3349        if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
     3350                $status['customizeUrl'] = add_query_arg( array(
     3351                        'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
     3352                ), wp_customize_url( $slug ) );
     3353        }
     3354
     3355        /*
     3356         * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check
     3357         * on post-install status.
     3358         */
     3359        wp_send_json_success( $status );
     3360}
     3361
     3362/**
     3363 * AJAX handler for updating a theme.
     3364 *
     3365 * @since 4.6.0
     3366 *
     3367 * @see Theme_Upgrader
     3368 */
     3369function wp_ajax_update_theme() {
     3370        check_ajax_referer( 'updates' );
     3371
     3372        if ( empty( $_POST['slug'] ) ) {
     3373                wp_send_json_error( array(
     3374                        'slug'         => '',
     3375                        'errorCode'    => 'no_theme_specified',
     3376                        'errorMessage' => __( 'No theme specified.' ),
     3377                ) );
     3378        }
     3379
     3380        $stylesheet = sanitize_key( wp_unslash( $_POST['slug'] ) );
     3381        $status     = array(
     3382                'update'     => 'theme',
     3383                'slug'       => $stylesheet,
     3384                'newVersion' => '',
     3385        );
     3386
     3387        if ( ! current_user_can( 'update_themes' ) ) {
     3388                $status['errorMessage'] = __( 'You do not have sufficient permissions to update themes on this site.' );
     3389                wp_send_json_error( $status );
     3390        }
     3391
     3392        include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
     3393
     3394        $current = get_site_transient( 'update_themes' );
     3395        if ( empty( $current ) ) {
     3396                wp_update_themes();
     3397        }
     3398
     3399        $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() );
     3400        $result   = $upgrader->bulk_upgrade( array( $stylesheet ) );
     3401
     3402        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     3403                $status['debug'] = $upgrader->skin->get_upgrade_messages();
     3404        }
     3405
     3406        if ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) {
     3407
     3408                // Theme is already at the latest version.
     3409                if ( true === $result[ $stylesheet ] ) {
     3410                        $status['errorMessage'] = $upgrader->strings['up_to_date'];
     3411                        wp_send_json_error( $status );
     3412                }
     3413
     3414                $theme = wp_get_theme( $stylesheet );
     3415                if ( $theme->get( 'Version' ) ) {
     3416                        $status['newVersion'] = $theme->get( 'Version' );
     3417                }
     3418
     3419                wp_send_json_success( $status );
     3420        } elseif ( is_wp_error( $upgrader->skin->result ) ) {
     3421                $status['errorCode']    = $upgrader->skin->result->get_error_code();
     3422                $status['errorMessage'] = $upgrader->skin->result->get_error_message();
     3423                wp_send_json_error( $status );
     3424        } elseif ( false === $result ) {
     3425                global $wp_filesystem;
     3426
     3427                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3428                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3429
     3430                // Pass through the error from WP_Filesystem if one was raised.
     3431                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3432                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3433                }
     3434
     3435                wp_send_json_error( $status );
     3436        }
     3437
     3438        // An unhandled error occurred.
     3439        $status['errorMessage'] = __( 'Update failed.' );
     3440        wp_send_json_error( $status );
     3441}
     3442
     3443/**
     3444 * AJAX handler for deleting a theme.
     3445 *
     3446 * @since 4.6.0
     3447 */
     3448function wp_ajax_delete_theme() {
     3449        check_ajax_referer( 'updates' );
     3450
     3451        if ( empty( $_POST['slug'] ) ) {
     3452                wp_send_json_error( array(
     3453                        'slug'         => '',
     3454                        'errorCode'    => 'no_theme_specified',
     3455                        'errorMessage' => __( 'No theme specified.' ),
     3456                ) );
     3457        }
     3458
     3459        $stylesheet = sanitize_key( wp_unslash( $_POST['slug'] ) );
     3460        $status     = array(
     3461                'delete' => 'theme',
     3462                'slug'   => $stylesheet,
     3463        );
     3464
     3465        if ( ! current_user_can( 'delete_themes' ) ) {
     3466                $status['errorMessage'] = __( 'You do not have sufficient permissions to delete themes on this site.' );
     3467                wp_send_json_error( $status );
     3468        }
     3469
     3470        if ( ! wp_get_theme( $stylesheet )->exists() ) {
     3471                $status['errorMessage'] = __( 'The requested theme does not exist.' );
     3472                wp_send_json_error( $status );
     3473        }
     3474
     3475        // Check filesystem credentials. `delete_plugins()` will bail otherwise.
     3476        ob_start();
     3477        $url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
     3478        if ( false === ( $credentials = request_filesystem_credentials( $url ) ) || ! WP_Filesystem( $credentials ) ) {
     3479                global $wp_filesystem;
     3480                ob_end_clean();
     3481
     3482                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3483                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3484
     3485                // Pass through the error from WP_Filesystem if one was raised.
     3486                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3487                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3488                }
     3489
     3490                wp_send_json_error( $status );
     3491        }
     3492
     3493        include_once( ABSPATH . 'wp-admin/includes/theme.php' );
     3494
     3495        $result = delete_theme( $stylesheet );
     3496
     3497        if ( is_wp_error( $result ) ) {
     3498                $status['errorMessage'] = $result->get_error_message();
     3499                wp_send_json_error( $status );
     3500        } elseif ( false === $result ) {
     3501                $status['errorMessage'] = __( 'Theme could not be deleted.' );
     3502                wp_send_json_error( $status );
     3503        }
     3504
     3505        wp_send_json_success( $status );
     3506}
     3507
     3508/**
     3509 * AJAX handler for installing a plugin.
     3510 *
     3511 * @since 4.6.0
     3512 */
     3513function wp_ajax_install_plugin() {
     3514        check_ajax_referer( 'updates' );
     3515
     3516        if ( empty( $_POST['slug'] ) ) {
     3517                wp_send_json_error( array(
     3518                        'slug'         => '',
     3519                        'errorCode'    => 'no_plugin_specified',
     3520                        'errorMessage' => __( 'No plugin specified.' ),
     3521                ) );
     3522        }
     3523
     3524        $status = array(
     3525                'install' => 'plugin',
     3526                'slug'    => sanitize_key( wp_unslash( $_POST['slug'] ) ),
     3527        );
     3528
     3529        if ( ! current_user_can( 'install_plugins' ) ) {
     3530                $status['errorMessage'] = __( 'You do not have sufficient permissions to install plugins on this site.' );
     3531                wp_send_json_error( $status );
     3532        }
     3533
     3534        include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
     3535        include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' );
     3536
     3537        $api = plugins_api( 'plugin_information', array(
     3538                'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
     3539                'fields' => array(
     3540                        'sections' => false,
     3541                ),
     3542        ) );
     3543
     3544        if ( is_wp_error( $api ) ) {
     3545                $status['errorMessage'] = $api->get_error_message();
     3546                wp_send_json_error( $status );
     3547        }
     3548
     3549        $status['pluginName'] = $api->name;
     3550
     3551        $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
     3552        $result   = $upgrader->install( $api->download_link );
     3553
     3554        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     3555                $status['debug'] = $upgrader->skin->get_upgrade_messages();
     3556        }
     3557
     3558        if ( is_wp_error( $result ) ) {
     3559                $status['errorMessage'] = $result->get_error_message();
     3560                wp_send_json_error( $status );
     3561        } elseif ( is_null( $result ) ) {
     3562                global $wp_filesystem;
     3563
     3564                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3565                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3566
     3567                // Pass through the error from WP_Filesystem if one was raised.
     3568                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3569                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3570                }
     3571
     3572                wp_send_json_error( $status );
     3573        }
     3574
     3575        $install_status = install_plugin_install_status( $api );
     3576
     3577        if ( current_user_can( 'activate_plugins' ) && is_plugin_inactive( $install_status['file'] ) ) {
     3578                $status['activateUrl'] = add_query_arg( array(
     3579                        '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ),
     3580                        'action'   => 'activate',
     3581                        'plugin'   => $install_status['file'],
     3582                ), network_admin_url( 'plugins.php' ) );
     3583        }
     3584
     3585        if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
     3586                $status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
     3587        }
     3588
     3589        wp_send_json_success( $status );
     3590}
     3591
     3592/**
     3593 * AJAX handler for updating a plugin.
     3594 *
     3595 * @since 4.2.0
     3596 *
     3597 * @see Plugin_Upgrader
     3598 */
     3599function wp_ajax_update_plugin() {
     3600        check_ajax_referer( 'updates' );
     3601
     3602        if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) {
     3603                wp_send_json_error( array(
     3604                        'slug'         => '',
     3605                        'errorCode'    => 'no_plugin_specified',
     3606                        'errorMessage' => __( 'No plugin specified.' ),
     3607                ) );
     3608        }
     3609
     3610        $plugin      = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
     3611        $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
     3612
     3613        $status = array(
     3614                'update'     => 'plugin',
     3615                'plugin'     => $plugin,
     3616                'slug'       => sanitize_key( wp_unslash( $_POST['slug'] ) ),
     3617                'pluginName' => $plugin_data['Name'],
     3618                'oldVersion' => '',
     3619                'newVersion' => '',
     3620        );
     3621
     3622        if ( $plugin_data['Version'] ) {
     3623                /* translators: %s: Plugin version */
     3624                $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
     3625        }
     3626
     3627        if ( ! current_user_can( 'update_plugins' ) ) {
     3628                $status['errorMessage'] = __( 'You do not have sufficient permissions to update plugins for this site.' );
     3629                wp_send_json_error( $status );
     3630        }
     3631
     3632        include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
     3633
     3634        wp_update_plugins();
     3635
     3636        $skin     = new Automatic_Upgrader_Skin();
     3637        $upgrader = new Plugin_Upgrader( $skin );
     3638        $result   = $upgrader->bulk_upgrade( array( $plugin ) );
     3639
     3640        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     3641                $status['debug'] = $upgrader->skin->get_upgrade_messages();
     3642        }
     3643
     3644        if ( is_array( $result ) && empty( $result[ $plugin ] ) && is_wp_error( $skin->result ) ) {
     3645                $result = $skin->result;
     3646        }
     3647
     3648        if ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) {
     3649                $plugin_update_data = current( $result );
     3650
     3651                /*
     3652                 * If the `update_plugins` site transient is empty (e.g. when you update
     3653                 * two plugins in quick succession before the transient repopulates),
     3654                 * this may be the return.
     3655                 *
     3656                 * Preferably something can be done to ensure `update_plugins` isn't empty.
     3657                 * For now, surface some sort of error here.
     3658                 */
     3659                if ( true === $plugin_update_data ) {
     3660                        $status['errorMessage'] = __( 'Plugin update failed.' );
     3661                        wp_send_json_error( $status );
     3662                }
     3663
     3664                $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] );
     3665                $plugin_data = reset( $plugin_data );
     3666
     3667                if ( $plugin_data['Version'] ) {
     3668                        /* translators: %s: Plugin version */
     3669                        $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
     3670                }
     3671                wp_send_json_success( $status );
     3672        } elseif ( is_wp_error( $result ) ) {
     3673                $status['errorMessage'] = $result->get_error_message();
     3674                wp_send_json_error( $status );
     3675        } elseif ( false === $result ) {
     3676                global $wp_filesystem;
     3677
     3678                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3679                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3680
     3681                // Pass through the error from WP_Filesystem if one was raised.
     3682                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3683                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3684                }
     3685
     3686                wp_send_json_error( $status );
     3687        }
     3688
     3689        // An unhandled error occurred.
     3690        $status['errorMessage'] = __( 'Plugin update failed.' );
     3691        wp_send_json_error( $status );
     3692}
     3693
     3694/**
     3695 * AJAX handler for deleting a plugin.
     3696 *
     3697 * @since 4.6.0
     3698 */
     3699function wp_ajax_delete_plugin() {
     3700        check_ajax_referer( 'updates' );
     3701
     3702        if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
     3703                wp_send_json_error( array( 'errorCode' => 'no_plugin_specified' ) );
     3704        }
     3705
     3706        $plugin      = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );
     3707        $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
     3708
     3709        $status = array(
     3710                'delete'     => 'plugin',
     3711                'slug'       => sanitize_key( wp_unslash( $_POST['slug'] ) ),
     3712                'plugin'     => $plugin,
     3713                'pluginName' => $plugin_data['Name'],
     3714        );
     3715
     3716        if ( ! current_user_can( 'delete_plugins' ) ) {
     3717                $status['errorMessage'] = __( 'You do not have sufficient permissions to delete plugins for this site.' );
     3718                wp_send_json_error( $status );
     3719        }
     3720
     3721        if ( is_plugin_active( $plugin ) ) {
     3722                $status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' );
     3723                wp_send_json_error( $status );
     3724        }
     3725
     3726        // Check filesystem credentials. `delete_plugins()` will bail otherwise.
     3727        ob_start();
     3728        $url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' );
     3729        if ( false === ( $credentials = request_filesystem_credentials( $url ) ) || ! WP_Filesystem( $credentials ) ) {
     3730                global $wp_filesystem;
     3731                ob_end_clean();
     3732
     3733                $status['errorCode']    = 'unable_to_connect_to_filesystem';
     3734                $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     3735
     3736                // Pass through the error from WP_Filesystem if one was raised.
     3737                if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
     3738                        $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
     3739                }
     3740
     3741                wp_send_json_error( $status );
     3742        }
     3743
     3744        $result = delete_plugins( array( $plugin ) );
     3745
     3746        if ( is_wp_error( $result ) ) {
     3747                $status['errorMessage'] = $result->get_error_message();
     3748                wp_send_json_error( $status );
     3749        } elseif ( false === $result ) {
     3750                $status['errorMessage'] = __( 'Plugin could not be deleted.' );
     3751                wp_send_json_error( $status );
     3752        }
     3753
     3754        wp_send_json_success( $status );
     3755}
     3756
     3757/**
     3758 * AJAX handler for searching plugins.
     3759 *
     3760 * @since 4.6.0
     3761 *
     3762 * @global WP_List_Table $wp_list_table Current list table instance.
     3763 * @global string        $hook_suffix   Current admin page.
     3764 * @global string        $s             Search term.
     3765 */
     3766function wp_ajax_search_plugins() {
     3767        check_ajax_referer( 'updates' );
     3768
     3769        global $wp_list_table, $hook_suffix, $s;
     3770        $hook_suffix = 'plugins.php';
     3771
     3772        /** @var WP_Plugins_List_Table $wp_list_table */
     3773        $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
     3774        $status        = array();
     3775
     3776        if ( ! $wp_list_table->ajax_user_can() ) {
     3777                $status['errorMessage'] = __( 'You do not have sufficient permissions to manage plugins on this site.' );
     3778                wp_send_json_error( $status );
     3779        }
     3780
     3781        // Set the correct requester, so pagination works.
     3782        $_SERVER['REQUEST_URI'] = add_query_arg( array_diff_key( $_POST, array(
     3783                '_ajax_nonce' => null,
     3784                'action'      => null,
     3785        ) ), network_admin_url( 'plugins.php', 'relative' ) );
     3786
     3787        $s = sanitize_text_field( $_POST['s'] );
     3788
     3789        $wp_list_table->prepare_items();
     3790
     3791        ob_start();
     3792        $wp_list_table->display();
     3793        $status['items'] = ob_get_clean();
     3794
     3795        wp_send_json_success( $status );
     3796}
     3797
     3798/**
     3799 * AJAX handler for searching plugins to install.
     3800 *
     3801 * @since 4.6.0
     3802 *
     3803 * @global WP_List_Table $wp_list_table Current list table instance.
     3804 * @global string        $hook_suffix   Current admin page.
     3805 */
     3806function wp_ajax_search_install_plugins() {
     3807        check_ajax_referer( 'updates' );
     3808
     3809        global $wp_list_table, $hook_suffix;
     3810        $hook_suffix = 'plugin-install.php';
     3811
     3812        /** @var WP_Plugin_Install_List_Table $wp_list_table */
     3813        $wp_list_table = _get_list_table( 'WP_Plugin_Install_List_Table' );
     3814        $status        = array();
     3815
     3816        if ( ! $wp_list_table->ajax_user_can() ) {
     3817                $status['errorMessage'] = __( 'You do not have sufficient permissions to manage plugins on this site.' );
     3818                wp_send_json_error( $status );
     3819        }
     3820
     3821        // Set the correct requester, so pagination works.
     3822        $_SERVER['REQUEST_URI'] = add_query_arg( array_diff_key( $_POST, array(
     3823                '_ajax_nonce' => null,
     3824                'action'      => null,
     3825        ) ), network_admin_url( 'plugin-install.php', 'relative' ) );
     3826
     3827        $wp_list_table->prepare_items();
     3828
     3829        ob_start();
     3830        $wp_list_table->display();
     3831        $status['items'] = ob_get_clean();
     3832
     3833        wp_send_json_success( $status );
     3834}
  • trunk/src/wp-admin/includes/class-wp-filesystem-base.php

    r37674 r37714  
    4242        /**
    4343         * @access public
     44         * @var WP_Error
    4445         */
    4546        public $errors = null;
  • trunk/src/wp-admin/includes/class-wp-ms-themes-list-table.php

    r37488 r37714  
    150150                $total_this_page = $totals[ $status ];
    151151
     152                wp_localize_script( 'updates', '_wpUpdatesItemCounts', array(
     153                        'totals' => $totals,
     154                ) );
     155
    152156                if ( $orderby ) {
    153157                        $orderby = ucfirst( $orderby );
  • trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php

    r37488 r37714  
    462462                                                        $action_links[] = '<a class="install-now button" data-slug="' . esc_attr( $plugin['slug'] ) . '" href="' . esc_url( $status['url'] ) . '" aria-label="' . esc_attr( sprintf( __( 'Install %s now' ), $name ) ) . '" data-name="' . esc_attr( $name ) . '">' . __( 'Install Now' ) . '</a>';
    463463                                                }
    464 
    465464                                                break;
     465
    466466                                        case 'update_available':
    467467                                                if ( $status['url'] ) {
     
    469469                                                        $action_links[] = '<a class="update-now button aria-button-if-js" data-plugin="' . esc_attr( $status['file'] ) . '" data-slug="' . esc_attr( $plugin['slug'] ) . '" href="' . esc_url( $status['url'] ) . '" aria-label="' . esc_attr( sprintf( __( 'Update %s now' ), $name ) ) . '" data-name="' . esc_attr( $name ) . '">' . __( 'Update Now' ) . '</a>';
    470470                                                }
    471 
    472471                                                break;
     472
    473473                                        case 'latest_installed':
    474474                                        case 'newer_installed':
    475                                                 $action_links[] = '<span class="button button-disabled">' . _x( 'Installed', 'plugin' ) . '</span>';
     475                                                if ( is_plugin_active( $status['file'] ) ) {
     476                                                        $action_links[] = '<button type="button" class="button button-disabled" disabled="disabled">' . _x( 'Active', 'plugin' ) . '</button>';
     477                                                } elseif ( current_user_can( 'activate_plugins' ) ) {
     478                                                        $button_text  = __( 'Activate' );
     479                                                        $activate_url = add_query_arg( array(
     480                                                                '_wpnonce'    => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
     481                                                                'action'      => 'activate',
     482                                                                'plugin'      => $status['file'],
     483                                                        ), network_admin_url( 'plugins.php' ) );
     484
     485                                                        if ( is_network_admin() ) {
     486                                                                $button_text  = __( 'Network Activate' );
     487                                                                $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
     488                                                        }
     489
     490                                                        $action_links[] = sprintf(
     491                                                                '<a href="%1$s" class="button activate-now button-secondary" aria-label="%2$s">%3$s</a>',
     492                                                                esc_url( $activate_url ),
     493                                                                /* translators: %s: Plugin name */
     494                                                                esc_attr( sprintf( __( 'Activate %s' ), $plugin['name'] ) ),
     495                                                                $button_text
     496                                                        );
     497                                                } else {
     498                                                        $action_links[] = '<button type="button" class="button button-disabled" disabled="disabled">' . _x( 'Installed', 'plugin' ) . '</button>';
     499                                                }
    476500                                                break;
    477501                                }
  • trunk/src/wp-admin/includes/class-wp-plugins-list-table.php

    r37488 r37714  
    246246
    247247                $total_this_page = $totals[ $status ];
     248
     249                $js_plugins = array();
     250                foreach ( $plugins as $key => $list ) {
     251                        $js_plugins[ $key ] = array_keys( (array) $list );
     252                }
     253
     254                wp_localize_script( 'updates', '_wpUpdatesItemCounts', array(
     255                        'plugins' => $js_plugins,
     256                ) );
    248257
    249258                if ( ! $orderby ) {
  • trunk/src/wp-admin/includes/class-wp-upgrader-skin.php

    r37432 r37714  
    1919        public $done_header = false;
    2020        public $done_footer = false;
     21
     22        /**
     23         *
     24         * @var string|false|WP_Error
     25         */
    2126        public $result = false;
    2227        public $options = array();
  • trunk/src/wp-admin/includes/class-wp-upgrader.php

    r37687 r37714  
    6262         * @since 2.8.0
    6363         * @access public
    64          * @var WP_Upgrader_Skin $skin
     64         * @var Automatic_Upgrader_Skin|WP_Upgrader_Skin $skin
    6565         */
    6666        public $skin = null;
  • trunk/src/wp-admin/includes/plugin-install.php

    r37488 r37714  
    541541
    542542        ?>
    543         <div id="<?php echo $_tab; ?>-content" class='<?php echo $_with_banner; ?>'>
     543<div id="<?php echo $_tab; ?>-content" class='<?php echo $_with_banner; ?>'>
    544544        <div class="fyi">
    545545                <ul>
    546                 <?php if ( ! empty( $api->version ) ) { ?>
    547                         <li><strong><?php _e( 'Version:' ); ?></strong> <?php echo $api->version; ?></li>
    548                 <?php } if ( ! empty( $api->author ) ) { ?>
    549                         <li><strong><?php _e( 'Author:' ); ?></strong> <?php echo links_add_target( $api->author, '_blank' ); ?></li>
    550                 <?php } if ( ! empty( $api->last_updated ) ) { ?>
    551                         <li><strong><?php _e( 'Last Updated:' ); ?></strong>
    552                                 <?php printf( __( '%s ago' ), human_time_diff( strtotime( $api->last_updated ) ) ); ?>
    553                         </li>
    554                 <?php } if ( ! empty( $api->requires ) ) { ?>
    555                         <li><strong><?php _e( 'Requires WordPress Version:' ); ?></strong> <?php printf( __( '%s or higher' ), $api->requires ); ?></li>
    556                 <?php } if ( ! empty( $api->tested ) ) { ?>
    557                         <li><strong><?php _e( 'Compatible up to:' ); ?></strong> <?php echo $api->tested; ?></li>
    558                 <?php } if ( ! empty( $api->active_installs ) ) { ?>
    559                         <li><strong><?php _e( 'Active Installs:' ); ?></strong> <?php
    560                                 if ( $api->active_installs >= 1000000 ) {
    561                                         _ex( '1+ Million', 'Active plugin installs' );
    562                                 } else {
    563                                         echo number_format_i18n( $api->active_installs ) . '+';
    564                                 }
    565                         ?></li>
    566                 <?php } if ( ! empty( $api->slug ) && empty( $api->external ) ) { ?>
    567                         <li><a target="_blank" href="https://wordpress.org/plugins/<?php echo $api->slug; ?>/"><?php _e( 'WordPress.org Plugin Page &#187;' ); ?></a></li>
    568                 <?php } if ( ! empty( $api->homepage ) ) { ?>
    569                         <li><a target="_blank" href="<?php echo esc_url( $api->homepage ); ?>"><?php _e( 'Plugin Homepage &#187;' ); ?></a></li>
    570                 <?php } if ( ! empty( $api->donate_link ) && empty( $api->contributors ) ) { ?>
    571                         <li><a target="_blank" href="<?php echo esc_url( $api->donate_link ); ?>"><?php _e( 'Donate to this plugin &#187;' ); ?></a></li>
    572                 <?php } ?>
     546                        <?php if ( ! empty( $api->version ) ) { ?>
     547                                <li><strong><?php _e( 'Version:' ); ?></strong> <?php echo $api->version; ?></li>
     548                        <?php } if ( ! empty( $api->author ) ) { ?>
     549                                <li><strong><?php _e( 'Author:' ); ?></strong> <?php echo links_add_target( $api->author, '_blank' ); ?></li>
     550                        <?php } if ( ! empty( $api->last_updated ) ) { ?>
     551                                <li><strong><?php _e( 'Last Updated:' ); ?></strong>
     552                                        <?php
     553                                        /* translators: %s: Time since the last update */
     554                                        printf( __( '%s ago' ), human_time_diff( strtotime( $api->last_updated ) ) );
     555                                        ?>
     556                                </li>
     557                        <?php } if ( ! empty( $api->requires ) ) { ?>
     558                                <li>
     559                                        <strong><?php _e( 'Requires WordPress Version:' ); ?></strong>
     560                                        <?php
     561                                        /* translators: %s: WordPress version */
     562                                        printf( __( '%s or higher' ), $api->requires );
     563                                        ?>
     564                                </li>
     565                        <?php } if ( ! empty( $api->tested ) ) { ?>
     566                                <li><strong><?php _e( 'Compatible up to:' ); ?></strong> <?php echo $api->tested; ?></li>
     567                        <?php } if ( ! empty( $api->active_installs ) ) { ?>
     568                                <li><strong><?php _e( 'Active Installs:' ); ?></strong> <?php
     569                                        if ( $api->active_installs >= 1000000 ) {
     570                                                _ex( '1+ Million', 'Active plugin installs' );
     571                                        } else {
     572                                                echo number_format_i18n( $api->active_installs ) . '+';
     573                                        }
     574                                        ?></li>
     575                        <?php } if ( ! empty( $api->slug ) && empty( $api->external ) ) { ?>
     576                                <li><a target="_blank" href="https://wordpress.org/plugins/<?php echo $api->slug; ?>/"><?php _e( 'WordPress.org Plugin Page &#187;' ); ?></a></li>
     577                        <?php } if ( ! empty( $api->homepage ) ) { ?>
     578                                <li><a target="_blank" href="<?php echo esc_url( $api->homepage ); ?>"><?php _e( 'Plugin Homepage &#187;' ); ?></a></li>
     579                        <?php } if ( ! empty( $api->donate_link ) && empty( $api->contributors ) ) { ?>
     580                                <li><a target="_blank" href="<?php echo esc_url( $api->donate_link ); ?>"><?php _e( 'Donate to this plugin &#187;' ); ?></a></li>
     581                        <?php } ?>
    573582                </ul>
    574583                <?php if ( ! empty( $api->rating ) ) { ?>
    575                 <h3><?php _e( 'Average Rating' ); ?></h3>
    576                 <?php wp_star_rating( array( 'rating' => $api->rating, 'type' => 'percent', 'number' => $api->num_ratings ) ); ?>
    577                 <p aria-hidden="true" class="fyi-description"><?php printf( _n( '(based on %s rating)', '(based on %s ratings)', $api->num_ratings ), number_format_i18n( $api->num_ratings ) ); ?></p>
     584                        <h3><?php _e( 'Average Rating' ); ?></h3>
     585                        <?php wp_star_rating( array( 'rating' => $api->rating, 'type' => 'percent', 'number' => $api->num_ratings ) ); ?>
     586                        <p aria-hidden="true" class="fyi-description"><?php printf( _n( '(based on %s rating)', '(based on %s ratings)', $api->num_ratings ), number_format_i18n( $api->num_ratings ) ); ?></p>
    578587                <?php }
    579588
     
    592601                                ?>
    593602                                <div class="counter-container">
    594                                         <span class="counter-label"><a href="https://wordpress.org/support/view/plugin-reviews/<?php echo $api->slug; ?>?filter=<?php echo $key; ?>"
    595                                                 target="_blank" aria-label="<?php echo $aria_label; ?>"><?php printf( _n( '%d star', '%d stars', $key ), $key ); ?></a></span>
    596                                         <span class="counter-back">
    597                                                 <span class="counter-bar" style="width: <?php echo 92 * $_rating; ?>px;"></span>
    598                                         </span>
     603                                                <span class="counter-label"><a href="https://wordpress.org/support/view/plugin-reviews/<?php echo $api->slug; ?>?filter=<?php echo $key; ?>"
     604                                                                               target="_blank" aria-label="<?php echo $aria_label; ?>"><?php printf( _n( '%d star', '%d stars', $key ), $key ); ?></a></span>
     605                                                <span class="counter-back">
     606                                                        <span class="counter-bar" style="width: <?php echo 92 * $_rating; ?>px;"></span>
     607                                                </span>
    599608                                        <span class="counter-count" aria-hidden="true"><?php echo number_format_i18n( $ratecount ); ?></span>
    600609                                </div>
     
    629638        <div id="section-holder" class="wrap">
    630639        <?php
    631                 if ( ! empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) ) {
    632                         echo '<div class="notice notice-warning notice-alt"><p>' . __( '<strong>Warning:</strong> This plugin has <strong>not been tested</strong> with your current version of WordPress.' ) . '</p></div>';
    633                 } elseif ( ! empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) ) {
    634                         echo '<div class="notice notice-warning notice-alt"><p>' . __( '<strong>Warning:</strong> This plugin has <strong>not been marked as compatible</strong> with your version of WordPress.' ) . '</p></div>';
    635                 }
    636 
    637                 foreach ( (array) $api->sections as $section_name => $content ) {
    638                         $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' );
    639                         $content = links_add_target( $content, '_blank' );
    640 
    641                         $san_section = esc_attr( $section_name );
    642 
    643                         $display = ( $section_name === $section ) ? 'block' : 'none';
    644 
    645                         echo "\t<div id='section-{$san_section}' class='section' style='display: {$display};'>\n";
    646                         echo $content;
    647                         echo "\t</div>\n";
    648                 }
     640        if ( ! empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) ) {
     641                echo '<div class="notice notice-warning notice-alt"><p>' . __( '<strong>Warning:</strong> This plugin has <strong>not been tested</strong> with your current version of WordPress.' ) . '</p></div>';
     642        } elseif ( ! empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) ) {
     643                echo '<div class="notice notice-warning notice-alt"><p>' . __( '<strong>Warning:</strong> This plugin has <strong>not been marked as compatible</strong> with your version of WordPress.' ) . '</p></div>';
     644        }
     645
     646        foreach ( (array) $api->sections as $section_name => $content ) {
     647                $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' );
     648                $content = links_add_target( $content, '_blank' );
     649
     650                $san_section = esc_attr( $section_name );
     651
     652                $display = ( $section_name === $section ) ? 'block' : 'none';
     653
     654                echo "\t<div id='section-{$san_section}' class='section' style='display: {$display};'>\n";
     655                echo $content;
     656                echo "\t</div>\n";
     657        }
    649658        echo "</div>\n";
    650659        echo "</div>\n";
     
    656665                        case 'install':
    657666                                if ( $status['url'] ) {
    658                                         echo '<a class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Now' ) . '</a>';
     667                                        echo '<a data-slug="' . esc_attr( $api->slug ) . '" id="plugin_install_from_iframe" class="button button-primary right" href="' . $status['url'] . '" target="_parent">' . __( 'Install Now' ) . '</a>';
    659668                                }
    660669                                break;
     
    665674                                break;
    666675                        case 'newer_installed':
     676                                /* translators: %s: Plugin version */
    667677                                echo '<a class="button button-primary right disabled">' . sprintf( __( 'Newer Version (%s) Installed'), $status['version'] ) . '</a>';
    668678                                break;
  • trunk/src/wp-admin/includes/theme.php

    r37488 r37714  
    173173                        if ( ! current_user_can('update_themes') ) {
    174174                                /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number */
    175                                 $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox" aria-label="%3$s">View version %4$s details</a>.' ) . '</strong></p>',
     175                                $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a>.' ) . '</strong></p>',
    176176                                        $theme_name,
    177177                                        esc_url( $details_url ),
     
    182182                        } elseif ( empty( $update['package'] ) ) {
    183183                                /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number */
    184                                 $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox" aria-label="%3$s">View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ) . '</strong></p>',
     184                                $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ) . '</strong></p>',
    185185                                        $theme_name,
    186186                                        esc_url( $details_url ),
     
    191191                        } else {
    192192                                /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */
    193                                 $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox" aria-label="%3$s">View version %4$s details</a> or <a href="%5$s" aria-label="%6$s">update now</a>.' ) . '</strong></p>',
     193                                $html = sprintf( '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a> or <a href="%5$s" aria-label="%6$s" id="update-theme" data-slug="%7$s">update now</a>.' ) . '</strong></p>',
    194194                                        $theme_name,
    195195                                        esc_url( $details_url ),
     
    199199                                        $update_url,
    200200                                        /* translators: %s: theme name */
    201                                         esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) )
     201                                        esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) ),
     202                                        $stylesheet
    202203                                );
    203204                        }
  • trunk/src/wp-admin/includes/update.php

    r37675 r37714  
    330330
    331331/**
    332  *
    333  * @param string $file
    334  * @param array  $plugin_data
     332 * Displays update information for a plugin.
     333 *
     334 * @param string $file        Plugin basename.
     335 * @param array  $plugin_data Plugin information.
    335336 * @return false|void
    336337 */
    337338function wp_plugin_update_row( $file, $plugin_data ) {
    338339        $current = get_site_transient( 'update_plugins' );
    339         if ( !isset( $current->response[ $file ] ) )
    340                 return false;
    341 
    342         $r = $current->response[ $file ];
    343 
    344         $plugins_allowedtags = array('a' => array('href' => array(),'title' => array()),'abbr' => array('title' => array()),'acronym' => array('title' => array()),'code' => array(),'em' => array(),'strong' => array());
    345         $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags );
    346 
    347         $details_url = self_admin_url('plugin-install.php?tab=plugin-information&plugin=' . $r->slug . '&section=changelog&TB_iframe=true&width=600&height=800');
    348 
    349         $wp_list_table = _get_list_table('WP_Plugins_List_Table');
    350 
    351         if ( is_network_admin() || !is_multisite() ) {
     340        if ( ! isset( $current->response[ $file ] ) ) {
     341                return false;
     342        }
     343
     344        $response = $current->response[ $file ];
     345
     346        $plugins_allowedtags = array(
     347                'a'       => array( 'href' => array(), 'title' => array() ),
     348                'abbr'    => array( 'title' => array() ),
     349                'acronym' => array( 'title' => array() ),
     350                'code'    => array(),
     351                'em'      => array(),
     352                'strong'  => array(),
     353        );
     354
     355        $plugin_name   = wp_kses( $plugin_data['Name'], $plugins_allowedtags );
     356        $details_url   = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '&section=changelog&TB_iframe=true&width=600&height=800' );
     357
     358        /** @var WP_Plugins_List_Table $wp_list_table */
     359        $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
     360
     361        if ( is_network_admin() || ! is_multisite() ) {
    352362                if ( is_network_admin() ) {
    353                         $active_class = is_plugin_active_for_network( $file ) ? ' active': '';
     363                        $active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
    354364                } else {
    355365                        $active_class = is_plugin_active( $file ) ? ' active' : '';
    356366                }
    357367
    358                 echo '<tr class="plugin-update-tr' . $active_class . '" id="' . esc_attr( $r->slug . '-update' ) . '" data-slug="' . esc_attr( $r->slug ) . '" data-plugin="' . esc_attr( $file ) . '"><td colspan="' . esc_attr( $wp_list_table->get_column_count() ) . '" class="plugin-update colspanchange"><div class="update-message">';
     368                echo '<tr class="plugin-update-tr' . $active_class . '" id="' . esc_attr( $response->slug . '-update' ) . '" data-slug="' . esc_attr( $response->slug ) . '" data-plugin="' . esc_attr( $file ) . '"><td colspan="' . esc_attr( $wp_list_table->get_column_count() ) . '" class="plugin-update colspanchange"><div class="update-message notice inline notice-warning notice-alt"><p>';
    359369
    360370                if ( ! current_user_can( 'update_plugins' ) ) {
     
    364374                                esc_url( $details_url ),
    365375                                /* translators: 1: plugin name, 2: version number */
    366                                 esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ),
    367                                 $r->new_version
     376                                esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ),
     377                                $response->new_version
    368378                        );
    369                 } elseif ( empty( $r->package ) ) {
     379                } elseif ( empty( $response->package ) ) {
    370380                        /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number */
    371381                        printf( __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a>. <em>Automatic update is unavailable for this plugin.</em>' ),
     
    373383                                esc_url( $details_url ),
    374384                                /* translators: 1: plugin name, 2: version number */
    375                                 esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ),
    376                                 $r->new_version
     385                                esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ),
     386                                $response->new_version
    377387                        );
    378388                } else {
     
    382392                                esc_url( $details_url ),
    383393                                /* translators: 1: plugin name, 2: version number */
    384                                 esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ),
    385                                 $r->new_version,
     394                                esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ),
     395                                $response->new_version,
    386396                                wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $file, 'upgrade-plugin_' . $file ),
    387397                                /* translators: %s: plugin name */
     
    389399                        );
    390400                }
     401
    391402                /**
    392403                 * Fires at the end of the update message container in each
     
    401412                 *     An array of plugin metadata.
    402413                 *
    403                  *     @type string $name         The human-readable name of the plugin.
    404                  *     @type string $plugin_uri   Plugin URI.
    405                  *     @type string $version      Plugin version.
    406                  *     @type string $description  Plugin description.
    407                  *     @type string $author       Plugin author.
    408                  *     @type string $author_uri   Plugin author URI.
    409                  *     @type string $text_domain  Plugin text domain.
    410                  *     @type string $domain_path  Relative path to the plugin's .mo file(s).
    411                  *     @type bool   $network      Whether the plugin can only be activated network wide.
    412                  *     @type string $title        The human-readable title of the plugin.
    413                  *     @type string $author_name  Plugin author's name.
    414                  *     @type bool   $update       Whether there's an available update. Default null.
    415                  * }
    416                  * @param array $r {
    417                  *     An array of metadata about the available plugin update.
    418                  *
    419                  *     @type int    $id           Plugin ID.
    420                  *     @type string $slug         Plugin slug.
    421                  *     @type string $new_version New plugin version.
    422                  *     @type string $url          Plugin URL.
    423                  *     @type string $package      Plugin update package URL.
    424                  * }
     414                 *     @type string $name        The human-readable name of the plugin.
     415                 *     @type string $plugin_uri  Plugin URI.
     416                 *     @type string $version     Plugin version.
     417                 *     @type string $description Plugin description.
     418                 *     @type string $author      Plugin author.
     419                 *     @type string $author_uri  Plugin author URI.
     420                 *     @type string $text_domain Plugin text domain.
     421                 *     @type string $domain_path Relative path to the plugin's .mo file(s).
     422                 *     @type bool   $network     Whether the plugin can only be activated network wide.
     423                 *     @type string $title       The human-readable title of the plugin.
     424                 *     @type string $author_name Plugin author's name.
     425                 *     @type bool   $update      Whether there's an available update. Default null.
     426                 * }
     427                 * @param array $response {
     428                 *     An array of metadata about the available plugin update.
     429                 *
     430                 *     @type int    $id          Plugin ID.
     431                 *     @type string $slug        Plugin slug.
     432                 *     @type string $new_version New plugin version.
     433                 *     @type string $url         Plugin URL.
     434                 *     @type string $package     Plugin update package URL.
     435                 * }
    425436                 */
    426                 do_action( "in_plugin_update_message-{$file}", $plugin_data, $r );
    427 
    428                 echo '</div></td></tr>';
     437                do_action( "in_plugin_update_message-{$file}", $plugin_data, $response );
     438
     439                echo '</p></div></td></tr>';
    429440        }
    430441}
     
    467478
    468479/**
    469  *
    470  * @param string   $theme_key
    471  * @param WP_Theme $theme
     480 * Displays update information for a theme.
     481 *
     482 * @param string   $theme_key Theme stylesheet.
     483 * @param WP_Theme $theme     Theme object.
    472484 * @return false|void
    473485 */
    474486function wp_theme_update_row( $theme_key, $theme ) {
    475487        $current = get_site_transient( 'update_themes' );
    476         if ( !isset( $current->response[ $theme_key ] ) )
    477                 return false;
    478 
    479         $r = $current->response[ $theme_key ];
    480 
    481         $theme_name = $theme['Name'];
    482 
    483         $details_url = add_query_arg( array( 'TB_iframe' => 'true', 'width' => 1024, 'height' => 800 ), $current->response[ $theme_key ]['url'] );
    484 
    485         $wp_list_table = _get_list_table('WP_MS_Themes_List_Table');
    486 
    487         $active = $theme->is_allowed( 'network' ) ? ' active': '';
    488 
    489         echo '<tr class="plugin-update-tr' . $active . '" id="' . esc_attr( $theme->get_stylesheet() . '-update' ) . '" data-slug="' . esc_attr( $theme->get_stylesheet() ) . '"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange"><div class="update-message">';
    490         if ( ! current_user_can('update_themes') ) {
     488
     489        if ( ! isset( $current->response[ $theme_key ] ) ) {
     490                return false;
     491        }
     492
     493        $response = $current->response[ $theme_key ];
     494
     495        $details_url = add_query_arg( array(
     496                'TB_iframe' => 'true',
     497                'width'     => 1024,
     498                'height'    => 800,
     499        ), $current->response[ $theme_key ]['url'] );
     500
     501        /** @var WP_MS_Themes_List_Table $wp_list_table */
     502        $wp_list_table = _get_list_table( 'WP_MS_Themes_List_Table' );
     503
     504        $active = $theme->is_allowed( 'network' ) ? ' active' : '';
     505
     506        echo '<tr class="plugin-update-tr' . $active . '" id="' . esc_attr( $theme->get_stylesheet() . '-update' ) . '" data-slug="' . esc_attr( $theme->get_stylesheet() ) . '"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange"><div class="update-message notice inline notice-warning notice-alt"><p>';
     507        if ( ! current_user_can( 'update_themes' ) ) {
    491508                /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number */
    492509                printf( __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a>.'),
    493                         $theme_name,
     510                        $theme['Name'],
    494511                        esc_url( $details_url ),
    495512                        /* translators: 1: theme name, 2: version number */
    496                         esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ),
    497                         $r['new_version']
     513                        esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ),
     514                        $response['new_version']
    498515                );
    499         } elseif ( empty( $r['package'] ) ) {
     516        } elseif ( empty( $response['package'] ) ) {
    500517                /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number */
    501518                printf( __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ),
    502                         $theme_name,
     519                        $theme['Name'],
    503520                        esc_url( $details_url ),
    504521                        /* translators: 1: theme name, 2: version number */
    505                         esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ),
    506                         $r['new_version']
     522                        esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ),
     523                        $response['new_version']
    507524                );
    508525        } else {
    509526                /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */
    510527                printf( __( 'There is a new version of %1$s available. <a href="%2$s" class="thickbox open-plugin-details-modal" aria-label="%3$s">View version %4$s details</a> or <a href="%5$s" class="update-link" aria-label="%6$s">update now</a>.' ),
    511                         $theme_name,
     528                        $theme['Name'],
    512529                        esc_url( $details_url ),
    513530                        /* translators: 1: theme name, 2: version number */
    514                         esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ),
    515                         $r['new_version'],
     531                        esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ),
     532                        $response['new_version'],
    516533                        wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' ) . $theme_key, 'upgrade-theme_' . $theme_key ),
    517534                        /* translators: %s: theme name */
    518                         esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) )
     535                        esc_attr( sprintf( __( 'Update %s now' ), $theme['Name'] ) )
    519536                );
    520537        }
     538
    521539        /**
    522540         * Fires at the end of the update message container in each
     
    528546         * @since 3.1.0
    529547         *
    530          * @param WP_Theme $theme The WP_Theme object.
    531          * @param array    $r {
     548         * @param WP_Theme $theme    The WP_Theme object.
     549         * @param array    $response {
    532550         *     An array of metadata about the available theme update.
    533551         *
     
    537555         * }
    538556         */
    539         do_action( "in_theme_update_message-{$theme_key}", $theme, $r );
    540 
    541         echo '</div></td></tr>';
     557        do_action( "in_theme_update_message-{$theme_key}", $theme, $response );
     558
     559        echo '</p></div></td></tr>';
    542560}
    543561
     
    578596        echo "<div class='update-nag'>$msg</div>";
    579597}
     598
     599/**
     600 * Prints the JavaScript templates for update admin notices.
     601 *
     602 * Template takes one argument with four values:
     603 *
     604 *     param {object} data {
     605 *         Arguments for admin notice.
     606 *
     607 *         @type string id        ID of the notice.
     608 *         @type string className Class names for the notice.
     609 *         @type string message   The notice's message.
     610 *         @type string type      The type of update the notice is for. Either 'plugin' or 'theme'.
     611 *     }
     612 *
     613 * @since 4.6.0
     614 */
     615function wp_print_admin_notice_templates() {
     616        ?>
     617        <script id="tmpl-wp-updates-admin-notice" type="text/html">
     618                <div <# if ( data.id ) { #>id="{{ data.id }}"<# } #> class="notice {{ data.className }}"><p>{{{ data.message }}}</p></div>
     619        </script>
     620        <script id="tmpl-wp-bulk-updates-admin-notice" type="text/html">
     621                <div id="{{ data.id }}" class="notice <# if ( data.errors ) { #>notice-error<# } else { #>notice-success<# } #>">
     622                        <p>
     623                                <# if ( data.successes ) { #>
     624                                        <# if ( 1 === data.successes ) { #>
     625                                                <# if ( 'plugin' === data.type ) { #>
     626                                                        <?php
     627                                                        /* translators: %s: Number of plugins */
     628                                                        printf( __( '%s plugin successfully updated.' ), '{{ data.successes }}' );
     629                                                        ?>
     630                                                <# } else { #>
     631                                                        <?php
     632                                                        /* translators: %s: Number of themes */
     633                                                        printf( __( '%s theme successfully updated.' ), '{{ data.successes }}' );
     634                                                        ?>
     635                                                <# } #>
     636                                        <# } else { #>
     637                                                <# if ( 'plugin' === data.type ) { #>
     638                                                        <?php
     639                                                        /* translators: %s: Number of plugins */
     640                                                        printf( __( '%s plugins successfully updated.' ), '{{ data.successes }}' );
     641                                                        ?>
     642                                                <# } else { #>
     643                                                        <?php
     644                                                        /* translators: %s: Number of themes */
     645                                                        printf( __( '%s themes successfully updated.' ), '{{ data.successes }}' );
     646                                                        ?>
     647                                                <# } #>
     648                                        <# } #>
     649                                <# } #>
     650                                <# if ( data.errors ) { #>
     651                                        <# if ( 1 === data.errors ) { #>
     652                                                <button class="button-link">
     653                                                        <?php
     654                                                        /* translators: %s: Number of failures */
     655                                                        printf( __( '%s failure.' ), '{{ data.errors }}' );
     656                                                        ?>
     657                                                </button>
     658                                        <# } else { #>
     659                                                <button class="button-link">
     660                                                        <?php
     661                                                        /* translators: %s: Number of failures */
     662                                                        printf( __( '%s failures.' ), '{{ data.errors }}' );
     663                                                        ?>
     664                                                </button>
     665                                        <# } #>
     666                                <# } #>
     667                        </p>
     668                        <# if ( data.errors ) { #>
     669                                <ul class="hidden">
     670                                        <# _.each( data.errorMessages, function( errorMessage ) { #>
     671                                                <li>{{ errorMessage }}</li>
     672                                        <# } ); #>
     673                                </ul>
     674                        <# } #>
     675                </div>
     676        </script>
     677        <?php
     678}
     679
     680/**
     681 * Prints the JavaScript templates for update and deletion rows in list tables.
     682 *
     683 * The update template takes one argument with four values:
     684 *
     685 *     param {object} data {
     686 *         Arguments for the update row
     687 *
     688 *         @type string slug    Plugin slug.
     689 *         @type string plugin  Plugin base name.
     690 *         @type string colspan The number of table columns this row spans.
     691 *         @type string content The row content.
     692 *     }
     693 *             
     694 * The delete template takes one argument with four values:
     695 *
     696 *     param {object} data {
     697 *         Arguments for the update row
     698 *
     699 *         @type string slug    Plugin slug.
     700 *         @type string plugin  Plugin base name.
     701 *         @type string name    Plugin name.
     702 *         @type string colspan The number of table columns this row spans.
     703 *     }
     704 *
     705 * @since 4.6.0
     706 */
     707function wp_print_update_row_templates() {
     708        ?>
     709        <script id="tmpl-item-update-row" type="text/template">
     710                <tr class="plugin-update-tr update" id="{{ data.slug }}-update" data-slug="{{ data.slug }}" <# if ( data.plugin ) { #>data-plugin="{{ data.plugin }}"<# } #>>
     711                        <td colspan="{{ data.colspan }}" class="plugin-update colspanchange">
     712                                {{{ data.content }}}
     713                        </td>
     714                </tr>
     715        </script>
     716        <script id="tmpl-item-deleted-row" type="text/template">
     717                <tr class="plugin-deleted-tr inactive deleted" id="{{ data.slug }}-deleted" data-slug="{{ data.slug }}" <# if ( data.plugin ) { #>data-plugin="{{ data.plugin }}"<# } #>>
     718                        <td colspan="{{ data.colspan }}" class="plugin-update colspanchange">
     719                                <?php
     720                                printf(
     721                                        /* translators: %s: Plugin or Theme name */
     722                                        __( '%s was successfully deleted.' ),
     723                                        '<strong>{{{ data.name }}}</strong>'
     724                                );
     725                                ?>
     726                        </td>
     727                </tr>
     728        </script>
     729        <?php
     730}
  • trunk/src/wp-admin/js/common.js

    r37431 r37714  
    422422        }
    423423
    424         $document.on( 'wp-plugin-update-error', function() {
    425                 makeNoticesDismissible();
    426         });
     424        $document.on( 'wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error', makeNoticesDismissible );
    427425
    428426        // Init screen meta
  • trunk/src/wp-admin/js/theme.js

    r37221 r37714  
    376376                'touchend': themes.isInstall ? 'preview': 'expand',
    377377                'keyup': 'addFocus',
    378                 'touchmove': 'preventExpand'
     378                'touchmove': 'preventExpand',
     379                'click .theme-install': 'installTheme',
     380                'click .update-message': 'updateTheme'
    379381        },
    380382
    381383        touchDrag: false,
     384
     385        initialize: function() {
     386                this.model.on( 'change', this.render, this );
     387        },
    382388
    383389        render: function() {
    384390                var data = this.model.toJSON();
     391
    385392                // Render themes using the html template
    386393                this.$el.html( this.html( data ) ).attr({
    387394                        tabindex: 0,
    388                         'aria-describedby' : data.id + '-action ' + data.id + '-name'
     395                        'aria-describedby' : data.id + '-action ' + data.id + '-name',
     396                        'data-slug': data.id
    389397                });
    390398
     
    394402                if ( this.model.get( 'displayAuthor' ) ) {
    395403                        this.$el.addClass( 'display-author' );
    396                 }
    397 
    398                 if ( this.model.get( 'installed' ) ) {
    399                         this.$el.addClass( 'is-installed' );
    400404                }
    401405        },
     
    440444                }
    441445
     446                // Prevent the modal from showing when the user clicks one of the direct action buttons.
     447                if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
     448                        return;
     449                }
     450
    442451                // Set focused theme to current element
    443452                themes.focusedTheme = this.$el;
     
    462471
    463472                // Allow direct link path to installing a theme.
    464                 if ( $( event.target ).hasClass( 'button-primary' ) ) {
     473                if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
    465474                        return;
    466475                }
     
    580589                        $themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
    581590                }
     591        },
     592
     593        installTheme: function( event ) {
     594                var _this = this;
     595
     596                event.preventDefault();
     597
     598                wp.updates.maybeRequestFilesystemCredentials( event );
     599
     600                $( document ).on( 'wp-install-theme-success', function( event, response ) {
     601                        if ( _this.model.get( 'id' ) === response.slug ) {
     602                                _this.model.set( { 'installed': true } );
     603                        }
     604                } );
     605
     606                wp.updates.installTheme( {
     607                        slug: $( event.target ).data( 'slug' )
     608                } );
     609        },
     610
     611        updateTheme: function( event ) {
     612                var _this = this;
     613                event.preventDefault();
     614                this.$el.off( 'click', '.update-message' );
     615
     616                wp.updates.maybeRequestFilesystemCredentials( event );
     617
     618                $( document ).on( 'wp-theme-update-success', function( event, response ) {
     619                        _this.model.off( 'change', _this.render, _this );
     620                        if ( _this.model.get( 'id' ) === response.slug ) {
     621                                _this.model.set( {
     622                                        hasUpdate: false,
     623                                        version: response.newVersion
     624                                } );
     625                        }
     626                        _this.model.on( 'change', _this.render, _this );
     627                } );
     628
     629                wp.updates.updateTheme( {
     630                        slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
     631                } );
    582632        }
    583633});
     
    594644                'click .delete-theme': 'deleteTheme',
    595645                'click .left': 'previousTheme',
    596                 'click .right': 'nextTheme'
     646                'click .right': 'nextTheme',
     647                'click #update-theme': 'updateTheme'
    597648        },
    598649
     
    714765        },
    715766
    716         // Confirmation dialog for deleting a theme
    717         deleteTheme: function() {
    718                 return confirm( themes.data.settings.confirmDelete );
     767        updateTheme: function( event ) {
     768                var _this = this;
     769                event.preventDefault();
     770
     771                wp.updates.maybeRequestFilesystemCredentials( event );
     772
     773                $( document ).on( 'wp-theme-update-success', function( event, response ) {
     774                        if ( _this.model.get( 'id' ) === response.slug ) {
     775                                _this.model.set( {
     776                                        hasUpdate: false,
     777                                        version: response.newVersion
     778                                } );
     779                        }
     780                        _this.render();
     781                } );
     782
     783                wp.updates.updateTheme( {
     784                        slug: $( event.target ).data( 'slug' )
     785                } );
     786        },
     787
     788        deleteTheme: function( event ) {
     789                var _this = this,
     790                    _collection = _this.model.collection,
     791                    _themes = themes;
     792                event.preventDefault();
     793
     794                // Confirmation dialog for deleting a theme.
     795                if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
     796                        return;
     797                }
     798
     799                wp.updates.maybeRequestFilesystemCredentials( event );
     800
     801                $( document ).one( 'wp-delete-theme-success', function( event, response ) {
     802                        _this.$el.find( '.close' ).trigger( 'click' );
     803                        $( '[data-slug="' + response.slug + '"' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
     804                                $( this ).remove();
     805                                _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
     806
     807                                $( '.wp-filter-search' ).val( '' );
     808                                _collection.doSearch( '' );
     809                                _collection.remove( _this.model );
     810                                _collection.trigger( 'themes:update' );
     811                        } );
     812                } );
     813
     814                wp.updates.deleteTheme( {
     815                        slug: this.model.get( 'id' )
     816                } );
    719817        },
    720818
     
    760858                'click .previous-theme': 'previousTheme',
    761859                'click .next-theme': 'nextTheme',
    762                 'keyup': 'keyEvent'
     860                'keyup': 'keyEvent',
     861                'click .theme-install': 'installTheme'
    763862        },
    764863
     
    860959                        this.previousTheme();
    861960                }
     961        },
     962
     963        installTheme: function( event ) {
     964                var _this   = this,
     965                    $target = $( event.target );
     966                event.preventDefault();
     967
     968                if ( $target.hasClass( 'disabled' ) ) {
     969                        return;
     970                }
     971
     972                wp.updates.maybeRequestFilesystemCredentials( event );
     973
     974                $( document ).on( 'wp-install-theme-success', function() {
     975                        _this.model.set( { 'installed': true } );
     976                } );
     977
     978                wp.updates.installTheme( {
     979                        slug: $target.data( 'slug' )
     980                } );
    862981        }
    863982});
     
    9301049                        }
    9311050
     1051                        // Bail if the filesystem credentials dialog is shown.
     1052                        if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
     1053                                return;
     1054                        }
     1055
    9321056                        // Pressing the right arrow key fires a theme:next event
    9331057                        if ( event.keyCode === 39 ) {
     
    10491173        // Uses the current model data
    10501174        expand: function( id ) {
    1051                 var self = this;
     1175                var self = this, $card, $modal;
    10521176
    10531177                // Set the current theme model
     
    10671191
    10681192                this.overlay.render();
     1193
     1194                if ( this.model.get( 'hasUpdate' ) ) {
     1195                        $card  = $( '[data-slug="' + this.model.id + '"]' );
     1196                        $modal = $( this.overlay.el );
     1197
     1198                        if ( $card.find( '.updating-message' ).length ) {
     1199                                $modal.find( '.notice-warning h3' ).remove();
     1200                                $modal.find( '.notice-warning' )
     1201                                        .removeClass( 'notice-large' )
     1202                                        .addClass( 'updating-message' )
     1203                                        .find( 'p' ).text( wp.updates.l10n.updating );
     1204                        } else if ( $card.find( '.notice-error' ).length ) {
     1205                                $modal.find( '.notice-warning' ).remove();
     1206                        }
     1207                }
     1208
    10691209                this.$overlay.html( this.overlay.el );
    10701210
  • trunk/src/wp-admin/js/updates.js

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

    r37202 r37714  
    229229$parent_file = 'themes.php';
    230230
     231wp_enqueue_script( 'updates' );
    231232wp_enqueue_script( 'theme-preview' );
    232233
     
    288289?>
    289290
    290 <form method="post">
     291<form id="bulk-action-form" method="post">
    291292<input type="hidden" name="theme_status" value="<?php echo esc_attr($status) ?>" />
    292293<input type="hidden" name="paged" value="<?php echo esc_attr($page) ?>" />
     
    298299
    299300<?php
     301wp_print_request_filesystem_credentials_modal();
     302wp_print_admin_notice_templates();
     303wp_print_update_row_templates();
     304
    300305include(ABSPATH . 'wp-admin/admin-footer.php');
  • trunk/src/wp-admin/plugin-install.php

    r37680 r37714  
    149149 */
    150150do_action( "install_plugins_$tab", $paged ); ?>
     151
     152        <span class="spinner"></span>
    151153</div>
    152154
    153155<?php
    154156wp_print_request_filesystem_credentials_modal();
     157wp_print_admin_notice_templates();
    155158
    156159/**
  • trunk/src/wp-admin/plugins.php

    r37221 r37714  
    508508<?php $wp_list_table->views(); ?>
    509509
    510 <form method="get">
     510<form class="search-form search-plugins" method="get">
    511511<?php $wp_list_table->search_box( __( 'Search Installed Plugins' ), 'plugin' ); ?>
    512512</form>
     
    520520</form>
    521521
     522        <span class="spinner"></span>
    522523</div>
    523524
    524525<?php
    525526wp_print_request_filesystem_credentials_modal();
     527wp_print_admin_notice_templates();
     528wp_print_update_row_templates();
    526529
    527530include(ABSPATH . 'wp-admin/admin-footer.php');
  • trunk/src/wp-admin/theme-install.php

    r37488 r37714  
    5959
    6060wp_enqueue_script( 'theme' );
     61wp_enqueue_script( 'updates' );
    6162
    6263if ( $tab ) {
     
    235236        <# } #>
    236237        <span class="more-details"><?php _ex( 'Details &amp; Preview', 'theme' ); ?></span>
    237         <div class="theme-author"><?php printf( __( 'By %s' ), '{{ data.author }}' ); ?></div>
     238        <div class="theme-author">
     239                <?php
     240                /* translators: %s: Theme author name */
     241                printf( __( 'By %s' ), '{{ data.author }}' );
     242                ?>
     243        </div>
    238244        <h3 class="theme-name">{{ data.name }}</h3>
    239245
    240246        <div class="theme-actions">
    241                 <a class="button button-primary" href="{{ data.install_url }}"><?php esc_html_e( 'Install' ); ?></a>
    242                 <a class="button button-secondary preview install-theme-preview" href="#"><?php esc_html_e( 'Preview' ); ?></a>
     247                <# if ( data.installed ) { #>
     248                        <# if ( data.activate_url ) { #>
     249                                <a class="button button-primary activate" href="{{ data.activate_url }}"><?php esc_html_e( 'Activate' ); ?></a>
     250                        <# } #>
     251                        <# if ( data.customize_url ) { #>
     252                                <a class="button button-secondary load-customize" href="{{ data.customize_url }}"><?php esc_html_e( 'Live Preview' ); ?></a>
     253                        <# } else { #>
     254                                <button class="button-secondary preview install-theme-preview"><?php esc_html_e( 'Preview' ); ?></button>
     255                        <# } #>
     256                <# } else { #>
     257                        <a class="button button-primary theme-install" data-slug="{{ data.id }}" href="{{ data.install_url }}"><?php esc_html_e( 'Install' ); ?></a>
     258                        <button class="button-secondary preview install-theme-preview"><?php esc_html_e( 'Preview' ); ?></button>
     259                <# } #>
    243260        </div>
    244261
    245262        <# if ( data.installed ) { #>
    246                 <div class="theme-installed"><?php _ex( 'Already Installed', 'theme' ); ?></div>
     263                <div class="notice notice-success notice-alt"><p><?php _ex( 'Installed', 'theme' ); ?></p></div>
    247264        <# } #>
    248265</script>
     
    251268        <div class="wp-full-overlay-sidebar">
    252269                <div class="wp-full-overlay-header">
    253                         <a href="#" class="close-full-overlay"><span class="screen-reader-text"><?php _e( 'Close' ); ?></span></a>
    254                         <a href="#" class="previous-theme"><span class="screen-reader-text"><?php _ex( 'Previous', 'Button label for a theme' ); ?></span></a>
    255                         <a href="#" class="next-theme"><span class="screen-reader-text"><?php _ex( 'Next', 'Button label for a theme' ); ?></span></a>
    256                 <# if ( data.installed ) { #>
    257                         <a href="#" class="button button-primary theme-install disabled"><?php _ex( 'Installed', 'theme' ); ?></a>
    258                 <# } else { #>
    259                         <a href="{{ data.install_url }}" class="button button-primary theme-install"><?php _e( 'Install' ); ?></a>
    260                 <# } #>
     270                        <button class="close-full-overlay"><span class="screen-reader-text"><?php _e( 'Close' ); ?></span></button>
     271                        <button class="previous-theme"><span class="screen-reader-text"><?php _ex( 'Previous', 'Button label for a theme' ); ?></span></button>
     272                        <button class="next-theme"><span class="screen-reader-text"><?php _ex( 'Next', 'Button label for a theme' ); ?></span></button>
     273                        <# if ( data.installed ) { #>
     274                                <a class="button button-primary activate" href="{{ data.activate_url }}"><?php esc_html_e( 'Activate' ); ?></a>
     275                        <# } else { #>
     276                                <a href="{{ data.install_url }}" class="button button-primary theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></a>
     277                        <# } #>
    261278                </div>
    262279                <div class="wp-full-overlay-sidebar-content">
    263280                        <div class="install-theme-info">
    264281                                <h3 class="theme-name">{{ data.name }}</h3>
    265                                 <span class="theme-by"><?php printf( __( 'By %s' ), '{{ data.author }}' ); ?></span>
    266 
    267                                 <img class="theme-screenshot" src="{{ data.screenshot_url }}" alt="" />
    268 
    269                                 <div class="theme-details">
    270                                         <# if ( data.rating ) { #>
    271                                                 <div class="theme-rating">
    272                                                         {{{ data.stars }}}
    273                                                         <span class="num-ratings" aria-hidden="true">({{ data.num_ratings }})</span>
     282                                        <span class="theme-by">
     283                                                <?php
     284                                                /* translators: %s: Theme author name */
     285                                                printf( __( 'By %s' ), '{{ data.author }}' );
     286                                                ?>
     287                                        </span>
     288
     289                                        <img class="theme-screenshot" src="{{ data.screenshot_url }}" alt="" />
     290
     291                                        <div class="theme-details">
     292                                                <# if ( data.rating ) { #>
     293                                                        <div class="theme-rating">
     294                                                                {{{ data.stars }}}
     295                                                                <span class="num-ratings">({{ data.num_ratings }})</span>
     296                                                        </div>
     297                                                <# } else { #>
     298                                                        <span class="no-rating"><?php _e( 'This theme has not been rated yet.' ); ?></span>
     299                                                <# } #>
     300                                                <div class="theme-version">
     301                                                        <?php
     302                                                        /* translators: %s: Theme version */
     303                                                        printf( __( 'Version: %s' ), '{{ data.version }}' );
     304                                                        ?>
    274305                                                </div>
    275                                         <# } else { #>
    276                                                 <span class="no-rating"><?php _e( 'This theme has not been rated yet.' ); ?></span>
    277                                         <# } #>
    278                                         <div class="theme-version"><?php printf( __( 'Version: %s' ), '{{ data.version }}' ); ?></div>
    279                                         <div class="theme-description">{{{ data.description }}}</div>
     306                                                <div class="theme-description">{{{ data.description }}}</div>
     307                                        </div>
    280308                                </div>
    281309                        </div>
    282                 </div>
    283                 <div class="wp-full-overlay-footer">
    284                         <div class="devices">
    285                                 <button type="button" class="preview-desktop active" aria-pressed="true" data-device="desktop"><span class="screen-reader-text"><?php _e( 'Enter desktop preview mode' ); ?></span></button>
    286                                 <button type="button" class="preview-tablet" aria-pressed="false" data-device="tablet"><span class="screen-reader-text"><?php _e( 'Enter tablet preview mode' ); ?></span></button>
    287                                 <button type="button" class="preview-mobile" aria-pressed="false" data-device="mobile"><span class="screen-reader-text"><?php _e( 'Enter mobile preview mode' ); ?></span></button>
     310                        <div class="wp-full-overlay-footer">
     311                                <button type="button" class="collapse-sidebar button-secondary" aria-expanded="true" aria-label="<?php esc_attr_e( 'Collapse Sidebar' ); ?>">
     312                                        <span class="collapse-sidebar-arrow"></span>
     313                                        <span class="collapse-sidebar-label"><?php _e( 'Collapse' ); ?></span>
     314                                </button>
    288315                        </div>
    289                         <button type="button" class="collapse-sidebar button-secondary" aria-expanded="true" aria-label="<?php esc_attr_e( 'Collapse Sidebar' ); ?>">
    290                                 <span class="collapse-sidebar-arrow"></span>
    291                                 <span class="collapse-sidebar-label"><?php _e( 'Collapse' ); ?></span>
    292                         </button>
    293                 </div>
    294         </div>
    295         <div class="wp-full-overlay-main">
    296                 <iframe src="{{ data.preview_url }}" title="<?php esc_attr_e( 'Preview' ); ?>" />
     316                </div>
     317                <div class="wp-full-overlay-main">
     318                <iframe src="{{ data.preview_url }}" title="<?php esc_attr_e( 'Preview' ); ?>"></iframe>
    297319        </div>
    298320</script>
    299321
    300322<?php
     323wp_print_request_filesystem_credentials_modal();
     324wp_print_admin_notice_templates();
     325
    301326include(ABSPATH . 'wp-admin/admin-footer.php');
  • trunk/src/wp-admin/themes.php

    r37297 r37714  
    146146add_thickbox();
    147147wp_enqueue_script( 'theme' );
     148wp_enqueue_script( 'updates' );
    148149wp_enqueue_script( 'customize-loader' );
    149150
     
    249250                <div class="theme-screenshot blank"></div>
    250251        <?php } ?>
     252
     253        <?php if ( $theme['hasUpdate'] ) : ?>
     254                <div class="update-message notice inline notice-warning notice-alt">
     255                        <p><?php _e( 'New version available. <button class="button-link" type="button">Update now</button>' ); ?></p>
     256                </div>
     257        <?php endif; ?>
     258
    251259        <span class="more-details" id="<?php echo $aria_action; ?>"><?php _e( 'Theme Details' ); ?></span>
    252260        <div class="theme-author"><?php printf( __( 'By %s' ), $theme['author'] ); ?></div>
     
    277285
    278286        </div>
    279 
    280         <?php if ( $theme['hasUpdate'] ) { ?>
    281                 <div class="theme-update"><?php _e( 'Update Available' ); ?></div>
    282         <?php } ?>
    283287</div>
    284288<?php endforeach; ?>
     
    369373                <div class="theme-screenshot blank"></div>
    370374        <# } #>
     375
     376        <# if ( data.hasUpdate ) { #>
     377                <div class="update-message notice inline notice-warning notice-alt"><p><?php _e( 'New version available. <button class="button-link" type="button">Update now</button>' ); ?></p></div>
     378        <# } #>
     379
    371380        <span class="more-details" id="{{ data.id }}-action"><?php _e( 'Theme Details' ); ?></span>
    372         <div class="theme-author"><?php printf( __( 'By %s' ), '{{{ data.author }}}' ); ?></div>
     381        <div class="theme-author">
     382                <?php
     383                /* translators: %s: Theme author name */
     384                printf( __( 'By %s' ), '{{{ data.author }}}' );
     385                ?>
     386        </div>
    373387
    374388        <# if ( data.active ) { #>
    375389                <h2 class="theme-name" id="{{ data.id }}-name">
    376390                        <?php
    377                         /* translators: %s: theme name */
     391                        /* translators: %s: Theme name */
    378392                        printf( __( '<span>Active:</span> %s' ), '{{{ data.name }}}' );
    379393                        ?>
     
    384398
    385399        <div class="theme-actions">
    386 
    387         <# if ( data.active ) { #>
    388                 <# if ( data.actions.customize ) { #>
    389                         <a class="button button-primary customize load-customize hide-if-no-customize" href="{{{ data.actions.customize }}}"><?php _e( 'Customize' ); ?></a>
     400                <# if ( data.active ) { #>
     401                        <# if ( data.actions.customize ) { #>
     402                                <a class="button button-primary customize load-customize hide-if-no-customize" href="{{{ data.actions.customize }}}"><?php _e( 'Customize' ); ?></a>
     403                        <# } #>
     404                <# } else { #>
     405                        <a class="button button-secondary activate" href="{{{ data.actions.activate }}}"><?php _e( 'Activate' ); ?></a>
     406                        <a class="button button-primary load-customize hide-if-no-customize" href="{{{ data.actions.customize }}}"><?php _e( 'Live Preview' ); ?></a>
    390407                <# } #>
    391         <# } else { #>
    392                 <a class="button button-secondary activate" href="{{{ data.actions.activate }}}"><?php _e( 'Activate' ); ?></a>
    393                 <a class="button button-primary load-customize hide-if-no-customize" href="{{{ data.actions.customize }}}"><?php _e( 'Live Preview' ); ?></a>
    394         <# } #>
    395 
    396408        </div>
    397 
    398         <# if ( data.hasUpdate ) { #>
    399                 <div class="theme-update"><?php _e( 'Update Available' ); ?></div>
    400         <# } #>
    401409</script>
    402410
     
    462470</script>
    463471
    464 <?php require( ABSPATH . 'wp-admin/admin-footer.php' );
     472<?php
     473wp_print_request_filesystem_credentials_modal();
     474wp_print_admin_notice_templates();
     475wp_print_update_row_templates();
     476
     477require( ABSPATH . 'wp-admin/admin-footer.php' );
  • trunk/src/wp-content/themes/twentyten/languages/twentyten.pot

    r37168 r37714  
    1 # Copyright (C) 2016 the WordPress team
     1# Copyright (C) 2015 the WordPress team
    22# This file is distributed under the GNU General Public License v2 or later.
    33msgid ""
     
    55"Project-Id-Version: Twenty Ten 2.1\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/theme/twentyten\n"
    7 "POT-Creation-Date: 2016-04-05 09:48:39+00:00\n"
     7"POT-Creation-Date: 2015-12-08 15:15:12+00:00\n"
    88"MIME-Version: 1.0\n"
    99"Content-Type: text/plain; charset=UTF-8\n"
    1010"Content-Transfer-Encoding: 8bit\n"
    11 "PO-Revision-Date: 2016-MO-DA HO:MI+ZONE\n"
     11"PO-Revision-Date: 2015-MO-DA HO:MI+ZONE\n"
    1212"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    1313"Language-Team: LANGUAGE <LL@li.org>\n"
  • trunk/src/wp-includes/js/wp-util.js

    r37431 r37714  
    4949                 * Sends a POST request to WordPress.
    5050                 *
    51                  * @param  {string} action The slug of the action to fire in WordPress.
    52                  * @param  {object} data   The data to populate $_POST with.
     51                 * @param  {(string|object)} action  The slug of the action to fire in WordPress or options passed
     52                 *                                   to jQuery.ajax.
     53                 * @param  {object=}         data    Optional. The data to populate $_POST with.
    5354                 * @return {$.promise}     A jQuery promise that represents the request,
    5455                 *                         decorated with an abort() method.
     
    6566                 * Sends a POST request to WordPress.
    6667                 *
    67                  * @param  {string} action  The slug of the action to fire in WordPress.
    68                  * @param  {object} options The options passed to jQuery.ajax.
     68                 * @param  {(string|object)} action  The slug of the action to fire in WordPress or options passed
     69                 *                                   to jQuery.ajax.
     70                 * @param  {object=}         options Optional. The options passed to jQuery.ajax.
    6971                 * @return {$.promise}      A jQuery promise that represents the request,
    7072                 *                          decorated with an abort() method.
  • trunk/src/wp-includes/script-loader.php

    r37526 r37714  
    596596                        'ajax_nonce' => wp_create_nonce( 'updates' ),
    597597                        'l10n'       => array(
    598                                 'updating'          => __( 'Updating...' ), // no ellipsis
    599                                 'updated'           => __( 'Updated!' ),
    600                                 'updateFailedShort' => __( 'Update Failed!' ),
     598                                /* translators: %s: Search string */
     599                                'searchResults'              => __( 'Search results for &#8220;%s&#8221;' ),
     600                                'noPlugins'                  => __( 'You do not appear to have any plugins available at this time.' ),
     601                                'noItemsSelected'            => __( 'Please select at least one item to perform this action on.' ),
     602                                'updating'                   => __( 'Updating...' ), // No ellipsis.
     603                                'updated'                    => __( 'Updated!' ),
     604                                'update'                     => __( 'Update' ),
     605                                'updateNow'                  => __( 'Update Now' ),
     606                                'updateFailedShort'          => __( 'Update Failed!' ),
    601607                                /* translators: Error string for a failed update */
    602                                 'updateFailed'      => __( 'Update Failed: %s' ),
     608                                'updateFailed'               => __( 'Update Failed: %s' ),
    603609                                /* translators: Plugin name and version */
    604                                 'updatingLabel'     => __( 'Updating %s...' ), // no ellipsis
     610                                'updatingLabel'              => __( 'Updating %s...' ), // No ellipsis.
    605611                                /* translators: Plugin name and version */
    606                                 'updatedLabel'      => __( '%s updated!' ),
     612                                'updatedLabel'               => __( '%s updated!' ),
    607613                                /* translators: Plugin name and version */
    608                                 'updateFailedLabel' => __( '%s update failed' ),
     614                                'updateFailedLabel'          => __( '%s update failed' ),
    609615                                /* translators: JavaScript accessible string */
    610                                 'updatingMsg'       => __( 'Updating... please wait.' ), // no ellipsis
     616                                'updatingMsg'                => __( 'Updating... please wait.' ), // No ellipsis.
    611617                                /* translators: JavaScript accessible string */
    612                                 'updatedMsg'        => __( 'Update completed successfully.' ),
     618                                'updatedMsg'                 => __( 'Update completed successfully.' ),
    613619                                /* translators: JavaScript accessible string */
    614                                 'updateCancel'      => __( 'Update canceled.' ),
    615                                 'beforeunload'      => __( 'Plugin updates may not complete if you navigate away from this page.' ),
    616                         )
     620                                'updateCancel'               => __( 'Update canceled.' ),
     621                                'beforeunload'               => __( 'Updates may not complete if you navigate away from this page.' ),
     622                                'installNow'                 => __( 'Install Now' ),
     623                                'installing'                 => __( 'Installing...' ),
     624                                'installed'                  => __( 'Installed!' ),
     625                                'installFailedShort'         => __( 'Install Failed!' ),
     626                                /* translators: Error string for a failed installation */
     627                                'installFailed'              => __( 'Installation failed: %s' ),
     628                                /* translators: Plugin/Theme name and version */
     629                                'installingLabel'            => __( 'Installing %s...' ), // no ellipsis
     630                                /* translators: Plugin/Theme name and version */
     631                                'installedLabel'             => __( '%s installed!' ),
     632                                /* translators: Plugin/Theme name and version */
     633                                'installFailedLabel'         => __( '%s installation failed' ),
     634                                'installingMsg'              => __( 'Installing... please wait.' ),
     635                                'installedMsg'               => __( 'Installation completed successfully.' ),
     636                                /* translators: Activation URL */
     637                                'importerInstalledMsg'       => __( 'Importer installed successfully. <a href="%s">Activate plugin &#38; run importer</a>' ),
     638                                /* translators: %s: Theme name */
     639                                'aysDelete'                  => __( 'Are you sure you want to delete %s?' ),
     640                                /* translators: %s: Plugin name */
     641                                'aysDeleteUninstall'         => __( 'Are you sure you want to delete %s and its data?' ),
     642                                'aysBulkDelete'              => __( 'Are you sure you want to delete the selected plugins and their data?' ),
     643                                'aysBulkDeleteThemes'        => __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ),
     644                                'deleting'                   => __( 'Deleting...' ),
     645                                /* translators: %s: Error string for a failed deletion */
     646                                'deleteFailed'               => __( 'Deletion failed: %s' ),
     647                                'deleted'                    => __( 'Deleted!' ),
     648                                'livePreview'                => __( 'Live Preview' ),
     649                                'activatePlugin'             => is_network_admin() ? __( 'Network Activate' ) : __( 'Activate' ),
     650                                'activateTheme'              => is_network_admin() ? __( 'Network Enable' ) : __( 'Activate' ),
     651                                'activateImporter'           => __( 'Activate importer' ),
     652                                'unknownError'               => __( 'An unknown error occured' ),
     653                        ),
    617654                ) );
    618655
  • trunk/tests/qunit/index.html

    r37476 r37714  
    1111                <script src="../../src/wp-includes/js/wp-backbone.js"></script>
    1212                <script src="../../src/wp-includes/js/zxcvbn.min.js"></script>
     13                <script>
     14                        window._wpUtilSettings = {
     15                                'ajax': {
     16                                        'url': '\/wp-admin\/admin-ajax.php'
     17                                }
     18                        };
     19                </script>
    1320                <script src="../../src/wp-includes/js/wp-util.js"></script>
    1421                <script src="../../src/wp-includes/js/wp-a11y.js"></script>
     
    483490                <script src="editor/js/utils.js"></script>
    484491                <script src="wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
     492
     493                <!-- Updates templates and HTML fixtures -->
     494                <script id="tmpl-wp-updates-admin-notice" type="text/html">
     495                        <div <# if ( data.id ) { #>id="{{ data.id }}"<# } #> class="notice {{ data.className }}"><p>{{{ data.message }}}</p></div>
     496                </script>
     497                <div hidden>
     498                        <li id="wp-admin-bar-updates">
     499                                <a class="ab-item" href="wp-admin/update-core.php" title="2 Plugin Updates">
     500                                        <span class="ab-icon"></span>
     501                                        <span class="ab-label">2</span>
     502                                        <span class="screen-reader-text">2 Plugin Updates</span>
     503                                </a>
     504                        </li>
     505                        <li class="wp-has-submenu wp-not-current-submenu menu-top menu-icon-plugins" id="menu-plugins">
     506                                <a href="plugins.php" class="wp-has-submenu wp-not-current-submenu menu-top menu-icon-plugins" aria-haspopup="true">
     507                                        <div class="wp-menu-arrow"><div></div></div>
     508                                        <div class="wp-menu-image dashicons-before dashicons-admin-plugins"><br></div>
     509                                        <div class="wp-menu-name">Plugins
     510                                <span class="update-plugins count-2">
     511                                        <span class="plugin-count">2</span>
     512                                </span>
     513                                        </div>
     514                                </a>
     515                                <ul class="wp-submenu wp-submenu-wrap">
     516                                        <li class="wp-submenu-head" aria-hidden="true">Plugins
     517                                <span class="update-plugins count-2">
     518                                        <span class="plugin-count">2</span>
     519                                </span>
     520                                        </li>
     521                                        <li class="wp-first-item">
     522                                                <a href="plugins.php" class="wp-first-item">Installed Plugins</a></li><li><a href="plugin-install.php">Add New</a>
     523                                </li><li>
     524                                        <a href="plugin-editor.php">Editor</a>
     525                                </li>
     526                                </ul>
     527                        </li>
     528                </div>
    485529        </body>
    486530</html>
Note: See TracChangeset for help on using the changeset viewer.