WordPress.org

Make WordPress Core

Ticket #31530: shiny.diff

File shiny.diff, 192.4 KB (added by obenland, 4 years ago)
  • src/wp-admin/admin-ajax.php

     
    6262        'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
    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
    6870// Deprecated
  • src/wp-admin/css/common.css

     
    13971397        background-color: #e5f5fa;
    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,
    14021417.wrap div.error,
     
    14051420        margin: 5px 0 15px;
    14061421}
    14071422
     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;
     1460}
     1461
    14081462#update-nag,
    14091463.update-nag {
    14101464        display: inline-block;
     
    14191473        box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1);
    14201474}
    14211475
    1422 .update-message {
    1423         color: #000;
    1424 }
    1425 
    14261476ul#dismissed-updates {
    14271477        display: none;
    14281478}
     
    14541504        margin-left: 2em;
    14551505}
    14561506
     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
     1550
    14571551/* @todo: this does not need its own section anymore */
    14581552/*------------------------------------------------------------------------------
    14591553  6.0 - Admin Header
  • src/wp-admin/css/forms.css

     
    10441044        display: inline;
    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
    10491086-------------------------------------------------------------- */
  • src/wp-admin/css/list-tables.css

     
    12711271        border-bottom: 0;
    12721272}
    12731273
    1274 .plugin-update-tr td {
    1275         border-top: 0;
    1276 }
    1277 
    12781274.plugins .inactive td,
    12791275.plugins .inactive th,
    12801276.plugins .active td,
     
    13091305        box-shadow: none;
    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;
    13211311}
    13221312
    1323 .plugins .active.update th.check-column,
    1324 .plugins .active.update + .plugin-update-tr .plugin-update {
    1325         border-left: 4px solid #d54e21;
    1326 }
    1327 
    13281313#wpbody-content .plugins .plugin-title,
    13291314#wpbody-content .plugins .theme-title {
    13301315        padding-right: 12px;
     
    13581343        border-top-width: 1px;
    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);
     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;
    13681351}
    13691352
    1370 .plugin-update-tr .update-message:before,
    1371 .plugin-card .update-now:before,
    1372 .plugin-card .install-now:before {
    1373         color: #d54e21;
     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;
    13781369        -moz-osx-font-smoothing: grayscale;
    13791370        vertical-align: top;
    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";
    13991375        -webkit-animation: rotation 2s infinite linear;
     
    14221398        }
    14231399}
    14241400
    1425 .plugin-update-tr .updated-message:before,
    14261401.plugin-card .updated-message:before {
    14271402        color: #79ba49;
    14281403        content: "\f147";
    14291404}
    14301405
    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;
    1445 }
    1446 
    14471406.plugin-install-php h2 {
    14481407        clear: both;
    14491408}
     
    21402099                margin-left: 0;
    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 {
    21452113                font-size: 1.4em;
  • src/wp-admin/css/themes.css

     
    1111        clear: both;
    1212}
    1313
    14 .themes-php .wrap h1 {
    15         float: left;
     14.themes-php:not(.network-admin) .wrap h1 {
    1615        margin-bottom: 15px;
    1716}
    1817
    19 .network-admin.themes-php .wrap h1 {
    20         margin-bottom: 0;
    21 }
    22 
    2318.themes-php .wrap h1 .button {
    2419        margin-left: 20px;
    2520}
     
    3732}
    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
    4744/**
     
    207204}
    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 */
    249209.theme-browser .theme.active .theme-name {
     
    951911}
    952912
    953913@media only screen and (max-width: 650px) {
    954         .theme-overlay .theme-update,
    955914        .theme-overlay .theme-description {
    956915                margin-left: 0;
    957916        }
     
    10411000.theme-browser .theme .theme-installed {
    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";
     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;
    10461011}
    1047 .theme-browser .theme.is-installed .theme-actions .button-primary {
    1048         display: none !important;
     1012
     1013.theme-install.updated-message:before {
     1014        content: '';
    10491015}
    10501016
    10511017.theme-install-php .wp-filter {
     
    13941360        pointer-events: none;
    13951361}
    13961362
     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;
     1376}
     1377
    13971378/* Collapse Button */
    13981379.wp-core-ui .wp-full-overlay .collapse-sidebar {
    13991380        position: fixed;
     
    17081689        max-width: 100%;
    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;
    17141695        /* For when .theme-install is a span rather than a.button-primary (already installed theme) */
     
    18031784                line-height: normal;
    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}
  • src/wp-admin/import.php

     
    4646
    4747add_thickbox();
    4848wp_enqueue_script( 'plugin-install' );
     49wp_enqueue_script( 'updates' );
    4950
    5051require_once( ABSPATH . 'wp-admin/admin-header.php' );
    5152$parent_file = 'tools.php';
     
    131132</div>
    132133
    133134<?php
     135wp_print_request_filesystem_credentials_modal();
     136wp_print_admin_notice_templates();
    134137
    135138include( ABSPATH . 'wp-admin/admin-footer.php' );
  • src/wp-admin/includes/ajax-actions.php

     
    28612861                        '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug )
    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 );
    28662888                $theme->version     = wp_kses( $theme->version, $themes_allowedtags );
     
    30693091        wp_send_json_success( array( 'message' => $message ) );
    30703092}
    30713093
    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         }
    3161 }
    3162 
    31633094/**
    31643095 * AJAX handler for saving a post from Press This.
    31653096 *
     
    33333264
    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}
  • src/wp-admin/includes/class-wp-filesystem-base.php

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

     
    149149                $this->has_items = ! empty( $themes['all'] );
    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 );
    154158                        $order = strtoupper( $order );
  • src/wp-admin/includes/class-wp-plugin-install-list-table.php

     
    461461                                                        /* translators: 1: Plugin name and version. */
    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'] ) {
    468468                                                        /* translators: 1: Plugin name and version */
    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                                }
    478502                        }
  • src/wp-admin/includes/class-wp-plugins-list-table.php

     
    246246
    247247                $total_this_page = $totals[ $status ];
    248248
     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                ) );
     257
    249258                if ( ! $orderby ) {
    250259                        $orderby = 'Name';
    251260                } else {
  • src/wp-admin/includes/class-wp-upgrader-skin.php

     
    1818        public $upgrader;
    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();
    2328
  • src/wp-admin/includes/class-wp-upgrader.php

     
    6161         *
    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;
    6767
  • src/wp-admin/includes/plugin-install.php

     
    540540        echo "</div>\n";
    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
    580589                if ( ! empty( $api->ratings ) && array_sum( (array) $api->ratings ) > 0 ) { ?>
     
    591600                                ) );
    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>
    601610                                <?php
     
    628637        </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                 }
     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        }
    636645
    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' );
     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' );
    640649
    641                         $san_section = esc_attr( $section_name );
     650                $san_section = esc_attr( $section_name );
    642651
    643                         $display = ( $section_name === $section ) ? 'block' : 'none';
     652                $display = ( $section_name === $section ) ? 'block' : 'none';
    644653
    645                         echo "\t<div id='section-{$san_section}' class='section' style='display: {$display};'>\n";
    646                         echo $content;
    647                         echo "\t</div>\n";
    648                 }
     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";
    651660        echo "</div>\n"; // #plugin-information-scrollable
     
    655664                switch ( $status['status'] ) {
    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;
    661670                        case 'update_available':
     
    664673                                }
    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;
    669679                        case 'latest_installed':
  • src/wp-admin/includes/theme.php

     
    172172                if ( !is_multisite() ) {
    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 ),
    178178                                        /* translators: 1: theme name, 2: version number */
     
    181181                                );
    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 ),
    187187                                        /* translators: 1: theme name, 2: version number */
     
    190190                                );
    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 ),
    196196                                        /* translators: 1: theme name, 2: version number */
     
    198198                                        $update['new_version'],
    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                        }
    204205                }
  • src/wp-admin/includes/update.php

     
    329329}
    330330
    331331/**
     332 * Displays update information for a plugin.
    332333 *
    333  * @param string $file
    334  * @param array  $plugin_data
     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        if ( ! isset( $current->response[ $file ] ) ) {
    340341                return false;
     342        }
    341343
    342         $r = $current->response[ $file ];
     344        $response = $current->response[ $file ];
    343345
    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        $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        );
    346354
    347         $details_url = self_admin_url('plugin-install.php?tab=plugin-information&plugin=' . $r->slug . '&section=changelog&TB_iframe=true&width=600&height=800');
     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' );
    348357
    349         $wp_list_table = _get_list_table('WP_Plugins_List_Table');
     358        /** @var WP_Plugins_List_Table $wp_list_table */
     359        $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
    350360
    351         if ( is_network_admin() || !is_multisite() ) {
     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' ) ) {
    361371                        /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number */
     
    363373                                $plugin_name,
    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>' ),
    372382                                $plugin_name,
    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 {
    379389                        /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */
     
    381391                                $plugin_name,
    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 */
    388398                                esc_attr( sprintf( __( 'Update %s now' ), $plugin_name ) )
    389399                        );
    390400                }
     401
    391402                /**
    392403                 * Fires at the end of the update message container in each
    393404                 * row of the plugins list table.
     
    400411                 * @param array $plugin_data {
    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 );
     437                do_action( "in_plugin_update_message-{$file}", $plugin_data, $response );
    427438
    428                 echo '</div></td></tr>';
     439                echo '</p></div></td></tr>';
    429440        }
    430441}
    431442
     
    466477}
    467478
    468479/**
     480 * Displays update information for a theme.
    469481 *
    470  * @param string   $theme_key
    471  * @param WP_Theme $theme
     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;
    478488
    479         $r = $current->response[ $theme_key ];
     489        if ( ! isset( $current->response[ $theme_key ] ) ) {
     490                return false;
     491        }
    480492
    481         $theme_name = $theme['Name'];
     493        $response = $current->response[ $theme_key ];
    482494
    483         $details_url = add_query_arg( array( 'TB_iframe' => 'true', 'width' => 1024, 'height' => 800 ), $current->response[ $theme_key ]['url'] );
     495        $details_url = add_query_arg( array(
     496                'TB_iframe' => 'true',
     497                'width'     => 1024,
     498                'height'    => 800,
     499        ), $current->response[ $theme_key ]['url'] );
    484500
    485         $wp_list_table = _get_list_table('WP_MS_Themes_List_Table');
     501        /** @var WP_MS_Themes_List_Table $wp_list_table */
     502        $wp_list_table = _get_list_table( 'WP_MS_Themes_List_Table' );
    486503
    487         $active = $theme->is_allowed( 'network' ) ? ' active': '';
     504        $active = $theme->is_allowed( 'network' ) ? ' active' : '';
    488505
    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') ) {
     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
    523541         * row of the themes list table.
     
    527545         *
    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         *
    534552         *     @type string $new_version New theme version.
     
    536554         *     @type string $package     Theme update package URL.
    537555         * }
    538556         */
    539         do_action( "in_theme_update_message-{$theme_key}", $theme, $r );
     557        do_action( "in_theme_update_message-{$theme_key}", $theme, $response );
    540558
    541         echo '</div></td></tr>';
     559        echo '</p></div></td></tr>';
    542560}
    543561
    544562/**
     
    577595
    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}
  • src/wp-admin/js/common.js

     
    421421                });
    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
    429427        screenMeta.init();
  • src/wp-admin/js/theme.js

     
    375375                'keydown': themes.isInstall ? 'preview': 'expand',
    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,
    382384
     385        initialize: function() {
     386                this.model.on( 'change', this.render, this );
     387        },
     388
    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
    391399                // Renders active theme styles
     
    394402                if ( this.model.get( 'displayAuthor' ) ) {
    395403                        this.$el.addClass( 'display-author' );
    396404                }
    397 
    398                 if ( this.model.get( 'installed' ) ) {
    399                         this.$el.addClass( 'is-installed' );
    400                 }
    401405        },
    402406
    403407        // Adds a class to the currently active theme
     
    439443                        return;
    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;
    444453
     
    461470                }
    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                }
    467476
     
    579588                if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
    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});
    584634
     
    593643                'click': 'collapse',
    594644                'click .delete-theme': 'deleteTheme',
    595645                'click .left': 'previousTheme',
    596                 'click .right': 'nextTheme'
     646                'click .right': 'nextTheme',
     647                'click #update-theme': 'updateTheme'
    597648        },
    598649
    599650        // The HTML template for the theme overlay
     
    713764                this.trigger( 'theme:collapse' );
    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
    721819        nextTheme: function() {
     
    759857                'click .devices button': 'previewDevice',
    760858                'click .previous-theme': 'previousTheme',
    761859                'click .next-theme': 'nextTheme',
    762                 'keyup': 'keyEvent'
     860                'keyup': 'keyEvent',
     861                'click .theme-install': 'installTheme'
    763862        },
    764863
    765864        // The HTML template for the theme preview
     
    859958                if ( event.keyCode === 37 ) {
    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});
    864983
     
    9291048                                return;
    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 ) {
    9341058                                self.overlay.nextTheme();
     
    10481172        // Renders the overlay with the ThemeDetails view
    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
    10541178                this.model = self.collection.get( id );
     
    10661190                });
    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
    10711211                // Bind to theme:next and theme:previous
  • src/wp-admin/js/updates.js

     
    1 /* global tb_remove */
    2 window.wp = window.wp || {};
     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 );
    331
    4 (function( $, wp, pagenow ) {
     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
    743        /**
     
    945         *
    1046         * @since 4.2.0
    1147         *
    12          * @var string
     48         * @type {string}
    1349         */
    14         wp.updates.ajaxNonce = window._wpUpdatesSettings.ajax_nonce;
     50        wp.updates.ajaxNonce = settings.ajax_nonce;
    1551
    1652        /**
    1753         * Localized strings.
    1854         *
    1955         * @since 4.2.0
    2056         *
    21          * @var object
     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}
    2267         */
    23         wp.updates.l10n = window._wpUpdatesSettings.l10n;
     68        wp.updates.searchTerm = '';
    2469
    2570        /**
    2671         * Whether filesystem credentials need to be requested from the user.
    2772         *
    2873         * @since 4.2.0
    2974         *
    30          * @var bool
     75         * @type {bool}
    3176         */
    32         wp.updates.shouldRequestFilesystemCredentials = null;
     77        wp.updates.shouldRequestFilesystemCredentials = false;
    3378
    3479        /**
    3580         * Filesystem credentials to be packaged along with the request.
    3681         *
    3782         * @since 4.2.0
     83         * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
    3884         *
    39          * @var object
     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                 }
     105                ssh:       {
     106                        publicKey:  '',
     107                        privateKey: ''
     108                },
     109                available: false
    52110        };
    53111
    54112        /**
    55          * Flag if we're waiting for an update to complete.
     113         * Whether we're waiting for an Ajax request to complete.
    56114         *
    57115         * @since 4.2.0
     116         * @since 4.6.0 More accurately named `ajaxLocked`.
    58117         *
    59          * @var bool
     118         * @type {bool}
    60119         */
    61         wp.updates.updateLock = false;
     120        wp.updates.ajaxLocked = false;
    62121
    63122        /**
    64          * * Flag if we've done an update successfully.
     123         * Admin notice template.
    65124         *
    66          * @since 4.2.0
     125         * @since 4.6.0
    67126         *
    68          * @var bool
     127         * @type {function} A function that lazily-compiles the template requested.
    69128         */
    70         wp.updates.updateDoneSuccessfully = false;
     129        wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
    71130
    72131        /**
     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
     138         * @since 4.6.0 More accurately named `queue`.
    77139         *
    78          * @var array
     140         * @type {Array.object}
    79141         */
    80         wp.updates.updateQueue = [];
     142        wp.updates.queue = [];
    81143
    82144        /**
    83          * Store a jQuery reference to return focus to when exiting the request credentials modal.
     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
     149         * @type {jQuery}
     150         */
     151        wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
     152
     153        /**
     154         * Adds or updates an admin notice.
     155         *
     156         * @since 4.6.0
     157         *
     158         * @param {object}  data
     159         * @param {*=}      data.selector      Optional. Selector of an element to be replaced with the admin notice.
     160         * @param {string=} data.id            Optional. Unique id that will be used as the notice's id attribute.
     161         * @param {string=} data.className     Optional. Class names that will be used in the admin notice.
     162         * @param {string=} data.message       Optional. The message displayed in the notice.
     163         * @param {number=} data.successes     Optional. The amount of successful operations.
     164         * @param {number=} data.errors        Optional. The amount of failed operations.
     165         * @param {Array=}  data.errorMessages Optional. Error messages of failed operations.
     166         *
     167         */
     168        wp.updates.addAdminNotice = function( data ) {
     169                var $notice = $( data.selector ), $adminNotice;
     170
     171                delete data.selector;
     172                $adminNotice = wp.updates.adminNotice( data );
     173
     174                // Check if this admin notice already exists.
     175                if ( ! $notice.length ) {
     176                        $notice = $( '#' + data.id );
     177                }
     178
     179                if ( $notice.length ) {
     180                        $notice.replaceWith( $adminNotice );
     181                } else {
     182                        $( '.wrap' ).find( '> h1' ).after( $adminNotice );
     183                }
     184
     185                $document.trigger( 'wp-updates-notice-added' );
     186        };
     187
     188        /**
     189         * Handles Ajax requests to WordPress.
     190         *
     191         * @since 4.6.0
     192         *
     193         * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
     194         * @param {object} data   Data that needs to be passed to the ajax callback.
     195         * @return {$.promise}    A jQuery promise that represents the request,
     196         *                        decorated with an abort() method.
     197         */
     198        wp.updates.ajax = function( action, data ) {
     199                var options = {};
     200
     201                if ( wp.updates.ajaxLocked ) {
     202                        wp.updates.queue.push( {
     203                                action: action,
     204                                data:   data
     205                        } );
     206
     207                        // Return a Deferred object so callbacks can always be registered.
     208                        return $.Deferred();
     209                }
     210
     211                wp.updates.ajaxLocked = true;
     212
     213                if ( data.success ) {
     214                        options.success = data.success;
     215                        delete data.success;
     216                }
     217
     218                if ( data.error ) {
     219                        options.error = data.error;
     220                        delete data.error;
     221                }
     222
     223                options.data = _.extend( data, {
     224                        action:          action,
     225                        _ajax_nonce:     wp.updates.ajaxNonce,
     226                        username:        wp.updates.filesystemCredentials.ftp.username,
     227                        password:        wp.updates.filesystemCredentials.ftp.password,
     228                        hostname:        wp.updates.filesystemCredentials.ftp.hostname,
     229                        connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
     230                        public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
     231                        private_key:     wp.updates.filesystemCredentials.ssh.privateKey
     232                } );
     233
     234                return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
     235        };
     236
     237        /**
     238         * Actions performed after every Ajax request.
     239         *
     240         * @since 4.6.0
     241         *
     242         * @param {object}  response
     243         * @param {array=}  response.debug     Optional. Debug information.
     244         * @param {string=} response.errorCode Optional. Error code for an error that occurred.
    88245         */
    89         wp.updates.$elToReturnFocusToFromCredentialsModal = null;
     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        };
    90258
    91259        /**
    92          * Decrement update counts throughout the various menus.
     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.
    93264         *
    94265         * @since 3.9.0
    95266         *
    96          * @param {string} upgradeType
     267         * @param {string} type The type of item that was updated or deleted.
     268         *                      Can be 'plugin', 'theme'.
    97269         */
    98         wp.updates.decrementCount = function( upgradeType ) {
    99                 var count,
    100                         pluginCount,
    101                         $adminBarUpdateCount = $( '#wp-admin-bar-updates .ab-label' ),
     270        wp.updates.decrementCount = function( type ) {
     271                var $adminBarUpdates             = $( '#wp-admin-bar-updates' ),
    102272                        $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
    103                         $pluginsMenuItem = $( '#menu-plugins' );
    104 
     273                        count                        = $adminBarUpdates.find( '.ab-label' ).text(),
     274                        $menuItem, $itemCount, itemCount;
    105275
    106                 count = $adminBarUpdateCount.text();
    107276                count = parseInt( count, 10 ) - 1;
     277
    108278                if ( count < 0 || isNaN( count ) ) {
    109279                        return;
    110280                }
    111                 $( '#wp-admin-bar-updates .ab-item' ).removeAttr( 'title' );
    112                 $adminBarUpdateCount.text( count );
    113281
     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                }
    114289
    115                 $dashboardNavMenuUpdateCount.each( function( index, elem ) {
    116                         elem.className = elem.className.replace( /count-\d+/, 'count-' + count );
     290                // Update the "Updates" menu item.
     291                $dashboardNavMenuUpdateCount.each( function( index, element ) {
     292                        element.className = element.className.replace( /count-\d+/, 'count-' + count );
    117293                } );
     294
    118295                $dashboardNavMenuUpdateCount.removeAttr( 'title' );
    119296                $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count );
    120297
    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 ) ) {
     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 );
    125311                                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 );
    130                         } );
     312                }
    131313
    132                         if (pluginCount > 0 ) {
    133                                 $( '.subsubsub .upgrade .count' ).text( '(' + pluginCount + ')' );
    134                         } else {
    135                                 $( '.subsubsub .upgrade' ).remove();
    136                         }
     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();
    137334                }
    138335        };
    139336
    140337        /**
    141          * Send an Ajax request to the server to update a plugin.
     338         * Sends an Ajax request to the server to update a plugin.
    142339         *
    143340         * @since 4.2.0
     341         * @since 4.6.0 More accurately named `updatePlugin`.
    144342         *
    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 );
     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 );
    151358
    152359                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 ) );
     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
    158368                        // Remove previous error messages, if any.
    159369                        $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
    160370                }
    161371
    162                 $message.addClass( 'updating-message' );
    163                 if ( $message.html() !== wp.updates.l10n.updating ){
     372                if ( $message.html() !== wp.updates.l10n.updating ) {
    164373                        $message.data( 'originaltext', $message.html() );
    165374                }
    166375
    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;
     376                $message
     377                        .attr( 'aria-label', message )
     378                        .text( wp.updates.l10n.updating );
    182379
    183                 var data = {
    184                         _ajax_nonce:     wp.updates.ajaxNonce,
    185                         plugin:          plugin,
    186                         slug:            slug,
    187                         username:        wp.updates.filesystemCredentials.ftp.username,
    188                         password:        wp.updates.filesystemCredentials.ftp.password,
    189                         hostname:        wp.updates.filesystemCredentials.ftp.hostname,
    190                         connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
    191                         public_key:      wp.updates.filesystemCredentials.ssh.publicKey,
    192                         private_key:     wp.updates.filesystemCredentials.ssh.privateKey
    193                 };
     380                $document.trigger( 'wp-plugin-updating' );
    194381
    195                 wp.ajax.post( 'update-plugin', data )
    196                         .done( wp.updates.updateSuccess )
    197                         .fail( wp.updates.updateError );
     382                return wp.updates.ajax( 'update-plugin', args );
    198383        };
    199384
    200385        /**
    201          * On a successful plugin update, update the UI with the result.
     386         * Updates the UI appropriately after a successful plugin update.
    202387         *
    203388         * @since 4.2.0
     389         * @since 4.6.0 More accurately named `updatePluginSuccess`.
    204390         *
    205          * @param {object} response
     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.
    206398         */
    207         wp.updates.updateSuccess = function( response ) {
    208                 var $updateMessage, name, $pluginRow, newText;
     399        wp.updates.updatePluginSuccess = function( response ) {
     400                var $pluginRow, $updateMessage, newText;
     401
    209402                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' );
     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 ) );
     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' );
    225417                }
    226418
    227                 $updateMessage.removeClass( 'updating-message' ).addClass( 'updated-message' );
    228                 $updateMessage.text( wp.updates.l10n.updated );
    229                 wp.a11y.speak( wp.updates.l10n.updatedMsg );
    230 
    231                 wp.updates.decrementCount( 'plugin' );
    232 
    233                 wp.updates.updateDoneSuccessfully = true;
     419                $updateMessage
     420                        .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
     421                        .text( wp.updates.l10n.updated );
    234422
    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;
     423                wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
    240424
    241                 $(document).trigger( 'wp-plugin-update-success', response );
     425                wp.updates.decrementCount( 'plugin' );
    242426
    243                 wp.updates.queueChecker();
     427                $document.trigger( 'wp-plugin-update-success', response );
    244428        };
    245429
    246 
    247430        /**
    248          * On a plugin update error, update the UI appropriately.
     431         * Updates the UI appropriately after a failed plugin update.
    249432         *
    250433         * @since 4.2.0
     434         * @since 4.6.0 More accurately named `updatePluginError`.
    251435         *
    252          * @param {object} response
     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.
    253443         */
    254         wp.updates.updateError = function( response ) {
    255                 var $card = $( '.plugin-card-' + response.slug ),
    256                         $message,
    257                         $button,
    258                         name,
    259                         error_message;
     444        wp.updates.updatePluginError = function( response ) {
     445                var $card, $message, errorMessage;
    260446
    261                 wp.updates.updateDoneSuccessfully = false;
     447                if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
     448                        return;
     449                }
    262450
    263                 if ( response.errorCode && response.errorCode == 'unable_to_connect_to_filesystem' && wp.updates.shouldRequestFilesystemCredentials ) {
    264                         wp.updates.credentialError( response, 'update-plugin' );
     451                if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
    265452                        return;
    266453                }
    267454
    268                 error_message = wp.updates.l10n.updateFailed.replace( '%s', response.error );
     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() {
    288476                                        $card
    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                         });
     484                        } );
    293485                }
    294486
    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 );
     487                wp.a11y.speak( errorMessage, 'assertive' );
    304488
    305                 wp.updates.queueChecker();
     489                $document.trigger( 'wp-plugin-update-error', response );
    306490        };
    307491
    308492        /**
    309          * Show an error message in the request for credentials form.
     493         * Sends an Ajax request to the server to install a plugin.
    310494         *
    311          * @param {string} message
    312          * @since 4.2.0
    313          */
    314         wp.updates.showErrorInCredentialsForm = function( message ) {
    315                 var $modal = $( '.notification-dialog' );
     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                }
    316518
    317                 // Remove any existing error.
    318                 $modal.find( '.error' ).remove();
     519                $message.addClass( 'updating-message' );
     520
     521                wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
    319522
    320                 $modal.find( 'h3' ).after( '<div class="error">' + message + '</div>' );
     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 );
    321527        };
    322528
    323529        /**
    324          * Events that need to happen when there is a credential error
     530         * Updates the UI appropriately after a successful plugin install.
    325531         *
    326          * @since 4.2.0
     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.
    327539         */
    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();
     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                }
    340561        };
    341562
    342563        /**
    343          * If an update job has been placed in the queue, queueChecker pulls it out and runs it.
     564         * Updates the UI appropriately after a failed plugin install.
    344565         *
    345          * @since 4.2.0
    346          */
    347         wp.updates.queueChecker = function() {
    348                 if ( wp.updates.updateLock || wp.updates.updateQueue.length <= 0 ) {
     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' ) ) {
    349581                        return;
    350582                }
    351583
    352                 var job = wp.updates.updateQueue.shift();
     584                if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
     585                        return;
     586                }
    353587
    354                 wp.updates.updatePlugin( job.data.plugin, job.data.slug );
    355         };
     588                errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
    356589
     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                } );
    357603
    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                         }
     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 );
    372608
    373                         wp.updates.updateLock = true;
     609                wp.a11y.speak( errorMessage, 'assertive' );
    374610
    375                         wp.updates.requestForCredentialsModalOpen();
    376                 }
     611                $document.trigger( 'wp-plugin-install-error', response );
    377612        };
    378613
    379614        /**
    380          * Keydown handler for the request for credentials modal.
     615         * Updates the UI appropriately after a successful importer install.
    381616         *
    382          * Close the modal when the escape key is pressed.
    383          * Constrain keyboard navigation to inside the modal.
     617         * @since 4.6.0
    384618         *
    385          * @since 4.2.0
    386          */
    387         wp.updates.keydown = function( event ) {
    388                 if ( 27 === event.keyCode ) {
    389                         wp.updates.requestForCredentialsModalCancel();
    390                 } 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 ) {
    393                                 $( '#hostname' ).focus();
    394                                 event.preventDefault();
    395                         } else if ( event.target.id === 'hostname' && event.shiftKey ) {
    396                                 $( '#upgrade' ).focus();
    397                                 event.preventDefault();
    398                         }
    399                 }
    400         };
     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                } );
    401631
    402         /**
    403          * Open the request for credentials modal.
    404          *
    405          * @since 4.2.0
    406          */
    407         wp.updates.requestForCredentialsModalOpen = function() {
    408                 var $modal = $( '#request-filesystem-credentials-dialog' );
    409                 $( 'body' ).addClass( 'modal-open' );
    410                 $modal.show();
     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 );
    411637
    412                 $modal.find( 'input:enabled:first' ).focus();
    413                 $modal.keydown( wp.updates.keydown );
    414         };
     638                wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
    415639
    416         /**
    417          * Close the request for credentials modal.
    418          *
    419          * @since 4.2.0
    420          */
    421         wp.updates.requestForCredentialsModalClose = function() {
    422                 $( '#request-filesystem-credentials-dialog' ).hide();
    423                 $( 'body' ).removeClass( 'modal-open' );
    424                 wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
     640                $document.trigger( 'wp-installer-install-success', response );
    425641        };
    426642
    427643        /**
    428          * The steps that need to happen when the modal is canceled out
     644         * Updates the UI appropriately after a failed importer install.
    429645         *
    430          * @since 4.2.0
     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.
    431654         */
    432         wp.updates.requestForCredentialsModalCancel = function() {
    433                 // no updateLock and no updateQueue means we already have cleared things up
    434                 var data, $message;
     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                }
    435661
    436                 if( wp.updates.updateLock === false && wp.updates.updateQueue.length === 0 ){
     662                if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
    437663                        return;
    438664                }
    439665
    440                 data = wp.updates.updateQueue[0].data;
     666                wp.updates.addAdminNotice( {
     667                        id:        response.errorCode,
     668                        className: 'notice-error is-dismissible',
     669                        message:   errorMessage
     670                } );
    441671
    442                 // remove the lock, and clear the queue
    443                 wp.updates.updateLock = false;
    444                 wp.updates.updateQueue = [];
     672                $( 'a[href*="' + response.slug + '"]' ).removeClass( 'updating-message' );
    445673
    446                 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                 }
     674                wp.a11y.speak( errorMessage, 'assertive' );
    452675
    453                 $message.removeClass( 'updating-message' );
    454                 $message.html( $message.data( 'originaltext' ) );
    455                 wp.a11y.speak( wp.updates.l10n.updateCancel );
     676                $document.trigger( 'wp-importer-install-error', response );
    456677        };
     678
    457679        /**
    458          * Potentially add an AYS to a user attempting to leave the page
     680         * Sends an Ajax request to the server to delete a plugin.
    459681         *
    460          * If an update is on-going and a user attempts to leave the page,
    461          * open an "Are you sure?" alert.
     682         * @since 4.6.0
    462683         *
    463          * @since 4.2.0
    464          */
     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 );
    465699
    466         wp.updates.beforeunload = function() {
    467                 if ( wp.updates.updateLock ) {
    468                         return wp.updates.l10n.beforeunload;
     700                if ( $message.html() !== wp.updates.l10n.updating ) {
     701                        $message.data( 'originaltext', $message.html() );
    469702                }
    470         };
    471703
     704                wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
    472705
    473         $( document ).ready( function() {
    474                 // Set initial focus on the first empty form field.
    475                 $( '#request-filesystem-credentials-form input[value=""]:first' ).focus();
     706                return wp.updates.ajax( 'delete-plugin', args );
     707        };
    476708
    477                 /*
    478                  * Check whether a user needs to submit filesystem credentials based on whether
    479                  * the form was output on the page server-side.
    480                  *
    481                  * @see {wp_print_request_filesystem_credentials_modal() in PHP}
    482                  */
    483                 wp.updates.shouldRequestFilesystemCredentials = ( $( '#request-filesystem-credentials-dialog' ).length <= 0 ) ? false : true;
     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                        }
    484743
    485                 // File system credentials form submit noop-er / handler.
    486                 $( '#request-filesystem-credentials-dialog form' ).on( 'submit', function() {
    487                         // 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();
     744                        $pluginRow.remove();
    494745
    495                         wp.updates.requestForCredentialsModalClose();
     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                        }
    496751
    497                         // Unlock and invoke the queue.
    498                         wp.updates.updateLock = false;
    499                         wp.updates.queueChecker();
     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                        }
    500761
    501                         return false;
    502                 });
     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                        }
    503770
    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                 });
     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                        }
    508779
    509                 // Hide SSH fields when not selected.
    510                 $( '#request-filesystem-credentials-form input[name="connection_type"]' ).on( 'change', function() {
    511                         $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
    512                 });
     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();
    513787
    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 );
     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                                }
    519791                        }
    520                         var updateRow = $( e.target ).parents( '.plugin-update-tr' );
    521                         // 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' ) );
    524792                } );
    525793
    526                 $( '.plugin-card' ).on( 'click', '.update-now', function( e ) {
    527                         e.preventDefault();
    528                         var $button = $( e.target );
     794                wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
    529795
    530                         // Do nothing while updating and when the button is disabled.
    531                         if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
    532                                 return;
    533                         }
     796                $document.trigger( 'wp-plugin-delete-success', response );
     797        };
    534798
    535                         if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.updateLock ) {
    536                                 wp.updates.requestFilesystemCredentials( e );
    537                         }
     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                        } );
    538820
    539                         wp.updates.updatePlugin( $button.data( 'plugin' ), $button.data( 'slug' ) );
    540                 } );
     821                if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
     822                        return;
     823                }
    541824
    542                 $( '#plugin_update_from_iframe' ).on( 'click' , function( e ) {
    543                         var target, job;
     825                if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
     826                        return;
     827                }
    544828
    545                         target = window.parent == window ? null : window.parent,
    546                         $.support.postMessage = !! window.postMessage;
     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 {
    547840
    548                         if ( $.support.postMessage === false || target === null || window.parent.location.pathname.indexOf( 'update-core.php' ) !== -1 )
    549                                 return;
     841                        // Remove previous error messages, if any.
     842                        $pluginUpdateRow.find( '.notice-error' ).remove();
    550843
    551                         e.preventDefault();
     844                        $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
     845                }
    552846
    553                         job = {
    554                                 action: 'updatePlugin',
    555                                 type: 'update-plugin',
    556                                 data: {
    557                                         plugin: $( this ).data( 'plugin' ),
    558                                         slug: $( this ).data( 'slug' )
     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                                        );
    5591177                                }
    560                         };
    5611178
    562                         target.postMessage( JSON.stringify( job ), window.location.origin );
    563                 });
     1179                                $themeRow.remove();
    5641180
    565         } );
     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        };
    5661206
    567         $( window ).on( 'message', function( e ) {
    568                 var event = e.originalEvent,
    569                         message,
    570                         loc = document.location,
    571                         expectedOrigin = loc.protocol + '//' + loc.hostname;
     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                        } );
    5721228
    573                 if ( event.origin !== expectedOrigin ) {
     1229                if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
    5741230                        return;
    5751231                }
    5761232
    577                 message = $.parseJSON( event.data );
     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         *
     1280         * @since 4.2.0
     1281         * @since 4.6.0 Can handle multiple job types.
     1282         */
     1283        wp.updates.queueChecker = function() {
     1284                var job;
    5781285
    579                 if ( typeof message.action === 'undefined' ) {
     1286                if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
    5801287                        return;
    5811288                }
    5821289
    583                 switch (message.action){
    584                         case 'decrementUpdateCount' :
    585                                 wp.updates.decrementCount( message.upgradeType );
     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 );
    5861316                                break;
    587                         case 'updatePlugin' :
    588                                 tb_remove();
    5891317
    590                                 wp.updates.updateQueue.push( message );
    591                                 wp.updates.queueChecker();
     1318                        default:
     1319                                window.console.error( 'Failed to execute queued update job.', job );
    5921320                                break;
    5931321                }
     1322        };
     1323
     1324        /**
     1325         * Requests the users filesystem credentials if they aren't already known.
     1326         *
     1327         * @since 4.2.0
     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        };
    5941345
    595         } );
     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.
     1364         *
     1365         * @since 4.2.0
     1366         *
     1367         * @param {Event} event Event interface.
     1368         */
     1369        wp.updates.keydown = function( event ) {
     1370                if ( 27 === event.keyCode ) {
     1371                        wp.updates.requestForCredentialsModalCancel();
     1372                } else if ( 9 === event.keyCode ) {
     1373
     1374                        // #upgrade button must always be the last focus-able element in the dialog.
     1375                        if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
     1376                                $( '#hostname' ).focus();
     1377
     1378                                event.preventDefault();
     1379                        } else if ( 'hostname' === event.target.id && event.shiftKey ) {
     1380                                $( '#upgrade' ).focus();
     1381
     1382                                event.preventDefault();
     1383                        }
     1384                }
     1385        };
     1386
     1387        /**
     1388         * Opens the request for credentials modal.
     1389         *
     1390         * @since 4.2.0
     1391         */
     1392        wp.updates.requestForCredentialsModalOpen = function() {
     1393                var $modal = $( '#request-filesystem-credentials-dialog' );
     1394
     1395                $( 'body' ).addClass( 'modal-open' );
     1396                $modal.show();
     1397                $modal.find( 'input:enabled:first' ).focus();
     1398                $modal.on( 'keydown', wp.updates.keydown );
     1399        };
     1400
     1401        /**
     1402         * Closes the request for credentials modal.
     1403         *
     1404         * @since 4.2.0
     1405         */
     1406        wp.updates.requestForCredentialsModalClose = function() {
     1407                $( '#request-filesystem-credentials-dialog' ).hide();
     1408                $( 'body' ).removeClass( 'modal-open' );
    5961409
    597         $( window ).on( 'beforeunload', wp.updates.beforeunload );
     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.
     1417         *
     1418         * @since 4.2.0
     1419         * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
     1420         */
     1421        wp.updates.requestForCredentialsModalCancel = function() {
     1422
     1423                // Not ajaxLocked and no queue means we already have cleared things up.
     1424                if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
     1425                        return;
     1426                }
     1427
     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 = [];
     1435
     1436                wp.updates.requestForCredentialsModalClose();
     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' );
    5981448
    599 })( jQuery, window.wp, window.pagenow, window.ajaxurl );
     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.
     1578         *
     1579         * If an update is on-going and a user attempts to leave the page,
     1580         * opens an "Are you sure?" alert.
     1581         *
     1582         * @since 4.2.0
     1583         */
     1584        wp.updates.beforeunload = function() {
     1585                if ( wp.updates.ajaxLocked ) {
     1586                        return wp.updates.l10n.beforeunload;
     1587                }
     1588        };
     1589
     1590        $( function() {
     1591                var $pluginFilter    = $( '#plugin-filter' ),
     1592                        $bulkActionForm  = $( '#bulk-action-form' ),
     1593                        $filesystemModal = $( '#request-filesystem-credentials-dialog' );
     1594
     1595                /*
     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.
     1599                 *
     1600                 * @see {wp_print_request_filesystem_credentials_modal() in PHP}
     1601                 */
     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
     1612                        // Persist the credentials input by the user for the duration of the page load.
     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();
     1624
     1625                        wp.updates.requestForCredentialsModalClose();
     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() {
     1641                        $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
     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
     1700                        // Return the user to the input box of the plugin's table row after closing the modal.
     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
     1719                        if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
     1720                                return;
     1721                        }
     1722
     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
     2088                        $.support.postMessage = !! window.postMessage;
     2089
     2090                        if ( false === $.support.postMessage || null === target ) {
     2091                                return;
     2092                        }
     2093
     2094                        event.preventDefault();
     2095
     2096                        update = {
     2097                                action: 'update-plugin',
     2098                                data:   {
     2099                                        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:   {
     2129                                        slug: $( this ).data( 'slug' )
     2130                                }
     2131                        };
     2132
     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 );
     2187        } );
     2188})( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) );
  • src/wp-admin/network/themes.php

     
    228228$title = __('Themes');
    229229$parent_file = 'themes.php';
    230230
     231wp_enqueue_script( 'updates' );
    231232wp_enqueue_script( 'theme-preview' );
    232233
    233234require_once(ABSPATH . 'wp-admin/admin-header.php');
     
    287288        echo '<p class="clear">' . __( 'The following themes are installed but incomplete.' ) . '</p>';
    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) ?>" />
    293294
     
    297298</div>
    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');
  • src/wp-admin/plugin-install.php

     
    148148 * @param int $paged The current page number of the plugins list table.
    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/**
    157160 * WordPress Administration Template Footer.
  • src/wp-admin/plugins.php

     
    507507
    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>
    513513
     
    519519<?php $wp_list_table->display(); ?>
    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');
  • src/wp-admin/theme-install.php

     
    5858) );
    5959
    6060wp_enqueue_script( 'theme' );
     61wp_enqueue_script( 'updates' );
    6162
    6263if ( $tab ) {
    6364        /**
     
    234235                <div class="theme-screenshot blank"></div>
    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>
    249266
    250267<script id="tmpl-theme-preview" type="text/template">
    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>
    293316                </div>
    294         </div>
    295         <div class="wp-full-overlay-main">
    296                 <iframe src="{{ data.preview_url }}" title="<?php esc_attr_e( 'Preview' ); ?>" />
     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');
  • src/wp-admin/themes.php

     
    145145
    146146add_thickbox();
    147147wp_enqueue_script( 'theme' );
     148wp_enqueue_script( 'updates' );
    148149wp_enqueue_script( 'customize-loader' );
    149150<