WordPress.org

Make WordPress Core

Changeset 37714


Ignore:
Timestamp:
06/15/16 16:36:07 (4 months 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 <[email protected]>\n" 
  • trunk/src/wp-includes/js/wp-util.js

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

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

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