Make WordPress Core

Changeset 37714


Ignore:
Timestamp:
06/15/2016 04:36:07 PM (8 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'  &nb