From 4dd1d9bef9b240f5f31369fdd39a66d1334ca838 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 15 Jun 2016 16:36:07 +0000 Subject: [PATCH] Update/Install: Shiny Updates v2. Gone are the days of isolation and feelings of "meh", brought on by The Bleak Screen of Sadness. For a shiny knight has arrived to usher our plugins and themes along their arduous journey of installation, updates, and the inevitable fate of ultimate deletion. Props swissspidy, adamsilverstein, mapk, afragen, ocean90, ryelle, j-falk, michael-arestad, melchoyce, DrewAPicture, AdamSoucie, ethitter, pento, dd32, kraftbj, Ipstenu, jorbin, afercia, stephdau, paulwilde, jipmoors, khag7, svovaf, jipmoors, obenland. Fixes #22029, #25828, #31002, #31529, #31530, #31773, #33637, #35032. git-svn-id: https://develop.svn.wordpress.org/trunk@37714 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/admin-ajax.php | 4 +- src/wp-admin/css/common.css | 102 +- src/wp-admin/css/forms.css | 37 + src/wp-admin/css/list-tables.css | 84 +- src/wp-admin/css/themes.css | 96 +- src/wp-admin/import.php | 3 + src/wp-admin/includes/ajax-actions.php | 681 ++++- .../includes/class-wp-filesystem-base.php | 1 + .../class-wp-ms-themes-list-table.php | 4 + .../class-wp-plugin-install-list-table.php | 30 +- .../includes/class-wp-plugins-list-table.php | 9 + .../includes/class-wp-upgrader-skin.php | 5 + src/wp-admin/includes/class-wp-upgrader.php | 2 +- src/wp-admin/includes/plugin-install.php | 112 +- src/wp-admin/includes/theme.php | 9 +- src/wp-admin/includes/update.php | 287 ++- src/wp-admin/js/common.js | 4 +- src/wp-admin/js/theme.js | 166 +- src/wp-admin/js/updates.js | 2271 ++++++++++++++--- src/wp-admin/network/themes.php | 7 +- src/wp-admin/plugin-install.php | 3 + src/wp-admin/plugins.php | 5 +- src/wp-admin/theme-install.php | 99 +- src/wp-admin/themes.php | 53 +- .../themes/twentyten/languages/twentyten.pot | 6 +- src/wp-includes/js/wp-util.js | 10 +- src/wp-includes/script-loader.php | 61 +- tests/qunit/fixtures/updates.js | 48 + tests/qunit/index.html | 44 + tests/qunit/wp-admin/js/updates.js | 179 ++ 30 files changed, 3653 insertions(+), 769 deletions(-) create mode 100644 tests/qunit/fixtures/updates.js create mode 100644 tests/qunit/wp-admin/js/updates.js diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index f3a816acab..58719dc05f 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -62,7 +62,9 @@ $core_actions_post = array( 'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs', 'save-user-color-scheme', 'update-widget', 'query-themes', 'parse-embed', 'set-attachment-thumbnail', 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post', - 'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', + 'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin', + 'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', + 'install-theme', ); // Deprecated diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index e392d8a7fe..66b5ef685f 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1397,6 +1397,21 @@ div.error { background-color: #e5f5fa; } +.update-message p:before, +.updating-message p:before, +.updated-message p:before, +.import-php .updating-message:before, +.button.updating-message:before, +.button.updated-message:before, +.button.installed:before, +.button.installing:before { + display: inline-block; + font: normal 20px/1 'dashicons'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + vertical-align: top; +} + .wrap .notice, .wrap div.updated, .wrap div.error, @@ -1405,6 +1420,45 @@ div.error { margin: 5px 0 15px; } +/* Update icon. */ +.update-message p:before, +.updating-message p:before, +.import-php .updating-message:before, +.button.updating-message:before, +.button.installing:before { + color: #f56e28; + content: "\f463"; +} + +/* Spins the update icon. */ +.updating-message p:before, +.import-php .updating-message:before, +.button.updating-message:before, +.button.installing:before { + -webkit-animation: rotation 2s infinite linear; + animation: rotation 2s infinite linear; +} + +/* Updated icon (check mark). */ +.updated-message p:before, +.installed p:before, +.button.updated-message:before { + color: #79ba49; + content: '\f147'; +} + +/* Error icon. */ +.update-message.notice-error p:before { + color: #dc3232; + content: "\f534"; +} + +.wrap .notice p:before, +.import-php .updating-message:before { + margin-right: 6px; + vertical-align: bottom; +} + #update-nag, .update-nag { display: inline-block; @@ -1419,10 +1473,6 @@ div.error { box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1); } -.update-message { - color: #000; -} - ul#dismissed-updates { display: none; } @@ -1454,6 +1504,50 @@ form.upgrade .hint { margin-left: 2em; } +.button.updating-message:before, +.button.updated-message:before, +.button.installed:before, +.button.installing:before { + margin: 3px 5px 0 -2px; +} + +.button-primary.updating-message:before { + color: #fff; +} + +.button-primary.updated-message:before { + color: #66c6e4; +} + +.button.updated-message, +.notice .button-link { + -webkit-transition-property: border, background, color; + transition-property: border, background, color; + -webkit-transition-duration: .05s; + transition-duration: .05s; + -webkit-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; +} + +.notice .button-link { + color: #0073aa; +} + +.notice .button-link:hover, +.notice .button-link:active { + color: #00a0d2; +} + +@media aural { + .wrap .notice p:before, + .button.installing:before, + .button.installed:before, + .update-message p:before { + speak: none; + } +} + + /* @todo: this does not need its own section anymore */ /*------------------------------------------------------------------------------ 6.0 - Admin Header diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 8c02e01d52..a42a948c34 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -1044,6 +1044,43 @@ table.form-table td .updated p { display: inline; } +.request-filesystem-credentials-dialog .ftp-username, +.request-filesystem-credentials-dialog .ftp-password { + float: none; + width: auto; +} + +.request-filesystem-credentials-dialog .ftp-username { + margin-bottom: 1em; +} + +.request-filesystem-credentials-dialog .ftp-password { + margin: 0; +} + +.request-filesystem-credentials-dialog .ftp-password em { + color: #888; +} + +.request-filesystem-credentials-dialog label { + display: block; + line-height: 1.5; + margin-bottom: 1em; +} + +.request-filesystem-credentials-form legend { + padding-bottom: 0; +} + +.request-filesystem-credentials-form #ssh-keys legend { + font-size: 1.3em; +} + +.request-filesystem-credentials-form .notice { + margin: 0 0 20px 0; + clear: both; +} + /* =Media Queries -------------------------------------------------------------- */ diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index d9d2d4595d..88517875b9 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -1271,10 +1271,6 @@ ul.cat-checklist { border-bottom: 0; } -.plugin-update-tr td { - border-top: 0; -} - .plugins .inactive td, .plugins .inactive th, .plugins .active td, @@ -1309,22 +1305,11 @@ ul.cat-checklist { box-shadow: none; } -.plugins .active.update td, -.plugins .active.update th, -tr.active.update + tr.plugin-update-tr .plugin-update { - background-color: #fef7f1; -} - .plugins .active th.check-column, .plugin-update-tr.active td { border-left: 4px solid #00a0d2; } -.plugins .active.update th.check-column, -.plugins .active.update + .plugin-update-tr .plugin-update { - border-left: 4px solid #d54e21; -} - #wpbody-content .plugins .plugin-title, #wpbody-content .plugins .theme-title { padding-right: 12px; @@ -1358,42 +1343,33 @@ tr.active.update + tr.plugin-update-tr .plugin-update { border-top-width: 1px; } -.plugin-update-tr .update-message { - font-size: 13px; - font-weight: normal; - margin: 0 10px 8px 31px; - padding: 6px 12px 8px 40px; - background-color: #f7f7f7; - background-color: rgba(0,0,0,0.03); +.plugins .plugin-update-tr .plugin-update { + -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); + box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); + overflow: hidden; /* clearfix */ + padding: 0; } -.plugin-update-tr .update-message:before, -.plugin-card .update-now:before, -.plugin-card .install-now:before { - color: #d54e21; +.plugins .plugin-update-tr .notice { + margin: 5px 20px 15px 40px; +} + +.plugins .notice p { + margin: 0.5em 0; +} + +.plugin-card .update-now:before { + color: #f56e28; + content: "\f463"; display: inline-block; font: normal 20px/1 dashicons; + margin: 3px 5px 0 -2px; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; vertical-align: top; } -.plugin-update-tr .update-message:before, -.plugin-card .update-now:before { - content: "\f463"; -} - -.plugin-update-tr .update-message:before { - margin: 0 10px 0 -30px; -} - -.plugin-card .update-now:before, -.plugin-card .install-now:before { - margin: 3px 5px 0 -2px; -} - -.plugin-update-tr .updating-message:before, .plugin-card .updating-message:before { content: "\f463"; -webkit-animation: rotation 2s infinite linear; @@ -1422,28 +1398,11 @@ tr.active.update + tr.plugin-update-tr .plugin-update { } } -.plugin-update-tr .updated-message:before, .plugin-card .updated-message:before { color: #79ba49; content: "\f147"; } -.wp-list-table.plugins tbody tr.plugin-update-tr td.plugin-update { - overflow: hidden; /* clearfix */ - padding: 0; - -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); - box-shadow: inset 0 -1px 0 rgba(0,0,0,0.1); -} - -/* update notices for active plugins */ -tr.active + tr.plugin-update-tr .plugin-update { - background-color: #f7fcfe; -} - -tr.active + tr.plugin-update-tr:not(.updated) .plugin-update .update-message { - background-color: #fcf3ef; -} - .plugin-install-php h2 { clear: both; } @@ -2140,6 +2099,15 @@ div.action-links, margin-left: 0; } + .plugins .active.update + .plugin-update-tr:before { + background-color: #f7fcfe; + border-left: 4px solid #00a0d2; + } + + .plugins .plugin-update-tr .update-message { + margin-left: 0; + } + .wp-list-table.plugins .plugin-title strong, .wp-list-table.plugins .theme-title strong { font-size: 1.4em; diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index 98d5a5e519..9f04e6354a 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -11,15 +11,10 @@ clear: both; } -.themes-php .wrap h1 { - float: left; +.themes-php:not(.network-admin) .wrap h1 { margin-bottom: 15px; } -.network-admin.themes-php .wrap h1 { - margin-bottom: 0; -} - .themes-php .wrap h1 .button { margin-left: 20px; } @@ -37,11 +32,13 @@ } /* Position admin messages */ -.themes-php div.updated, -.themes-php div.error, -.themes-php div.notice { - margin: 0 0 20px 0; - clear: both; +.theme .notice, +.theme .notice.is-dismissible { + left: 0; + margin: 0; + position: absolute; + right: 0; + top: 0; } /** @@ -206,43 +203,6 @@ opacity: 1; } -/** - * Displays a theme update notice - * when an update is available. - */ -.theme-browser .theme .theme-update, -.theme-browser .theme .theme-installed { - background: #d54e21; - background: rgba(213, 78, 33, 0.95); - color: #fff; - display: block; - font-size: 13px; - font-weight: 400; - height: 48px; - line-height: 48px; - padding: 0 10px; - position: absolute; - top: 0; - right: 0; - left: 0; - border-bottom: 1px solid rgba(0,0,0,0.25); - overflow: hidden; -} - -.theme-browser .theme .theme-update:before, -.theme-browser .theme .theme-installed:before { - content: "\f463"; - display: inline-block; - font: normal 20px/1 dashicons; - margin: 0 6px 0 0; - opacity: 0.8; - position: relative; - top: 5px; - speak: none; - -webkit-font-smoothing: antialiased; -} - - /** * The currently active theme */ @@ -951,7 +911,6 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { } @media only screen and (max-width: 650px) { - .theme-overlay .theme-update, .theme-overlay .theme-description { margin-left: 0; } @@ -1041,11 +1000,18 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { .theme-browser .theme .theme-installed { background: #0073aa; } -.theme-browser .theme .theme-installed:before { +.theme-browser .theme .notice-success p:before { + color: #79ba49; content: "\f147"; + display: inline-block; + font: normal 20px/1 'dashicons'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + vertical-align: top; } -.theme-browser .theme.is-installed .theme-actions .button-primary { - display: none !important; + +.theme-install.updated-message:before { + content: ''; } .theme-install-php .wp-filter { @@ -1394,6 +1360,21 @@ body.full-overlay-active { pointer-events: none; } +.theme-install-overlay .close-full-overlay, +.theme-install-overlay .previous-theme, +.theme-install-overlay .next-theme { + border-left: 0; + border-top: 0; + border-bottom: 0; +} + +.theme-install-overlay .close-full-overlay:before, +.theme-install-overlay .previous-theme:before, +.theme-install-overlay .next-theme:before { + top: 2px; + left: 0; +} + /* Collapse Button */ .wp-core-ui .wp-full-overlay .collapse-sidebar { position: fixed; @@ -1708,7 +1689,7 @@ body.full-overlay-active { max-width: 100%; } -.theme-install-overlay .wp-full-overlay-header .theme-install { +.theme-install-overlay .wp-full-overlay-header .button { float: right; margin: 8px 10px 0 0; /* For when .theme-install is a span rather than a.button-primary (already installed theme) */ @@ -1803,3 +1784,12 @@ body.full-overlay-active { line-height: normal; } } + +@media aural { + .theme .notice:before, + .theme-info .updating-message:before, + .theme-info .updated-message:before, + .theme-install.updating-message:before { + speak: none; + } +} diff --git a/src/wp-admin/import.php b/src/wp-admin/import.php index a1ce80e2b3..e05e9ce1f2 100644 --- a/src/wp-admin/import.php +++ b/src/wp-admin/import.php @@ -46,6 +46,7 @@ if ( ! empty( $_GET['invalid'] ) && isset( $popular_importers[ $_GET['invalid'] add_thickbox(); wp_enqueue_script( 'plugin-install' ); +wp_enqueue_script( 'updates' ); require_once( ABSPATH . 'wp-admin/admin-header.php' ); $parent_file = 'tools.php'; @@ -131,5 +132,7 @@ if ( current_user_can('install_plugins') ) wp_create_nonce( 'install-theme_' . $theme->slug ) ), $update_php ); + if ( current_user_can( 'switch_themes' ) ) { + if ( is_multisite() ) { + $theme->activate_url = add_query_arg( array( + 'action' => 'enable', + '_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ), + 'theme' => $theme->slug, + ), network_admin_url( 'themes.php' ) ); + } else { + $theme->activate_url = add_query_arg( array( + 'action' => 'activate', + '_wpnonce' => wp_create_nonce( 'switch-theme_' . $theme->slug ), + 'stylesheet' => $theme->slug, + ), admin_url( 'themes.php' ) ); + } + } + + if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $theme->customize_url = add_query_arg( array( + 'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ), + ), wp_customize_url( $theme->slug ) ); + } + $theme->name = wp_kses( $theme->name, $themes_allowedtags ); $theme->author = wp_kses( $theme->author, $themes_allowedtags ); $theme->version = wp_kses( $theme->version, $themes_allowedtags ); @@ -3069,97 +3091,6 @@ function wp_ajax_destroy_sessions() { wp_send_json_success( array( 'message' => $message ) ); } - -/** - * AJAX handler for updating a plugin. - * - * @since 4.2.0 - * - * @see Plugin_Upgrader - */ -function wp_ajax_update_plugin() { - global $wp_filesystem; - - $plugin = urldecode( $_POST['plugin'] ); - - $status = array( - 'update' => 'plugin', - 'plugin' => $plugin, - 'slug' => sanitize_key( $_POST['slug'] ), - 'oldVersion' => '', - 'newVersion' => '', - ); - - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); - if ( $plugin_data['Version'] ) { - $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); - } - - if ( ! current_user_can( 'update_plugins' ) ) { - $status['error'] = __( 'You do not have sufficient permissions to update plugins for this site.' ); - wp_send_json_error( $status ); - } - - check_ajax_referer( 'updates' ); - - include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); - - wp_update_plugins(); - - $skin = new Automatic_Upgrader_Skin(); - $upgrader = new Plugin_Upgrader( $skin ); - $result = $upgrader->bulk_upgrade( array( $plugin ) ); - - if ( is_array( $result ) && empty( $result[$plugin] ) && is_wp_error( $skin->result ) ) { - $result = $skin->result; - } - - if ( is_array( $result ) && !empty( $result[ $plugin ] ) ) { - $plugin_update_data = current( $result ); - - /* - * If the `update_plugins` site transient is empty (e.g. when you update - * two plugins in quick succession before the transient repopulates), - * this may be the return. - * - * Preferably something can be done to ensure `update_plugins` isn't empty. - * For now, surface some sort of error here. - */ - if ( $plugin_update_data === true ) { - $status['error'] = __( 'Plugin update failed.' ); - wp_send_json_error( $status ); - } - - $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] ); - $plugin_data = reset( $plugin_data ); - - if ( $plugin_data['Version'] ) { - $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); - } - - wp_send_json_success( $status ); - } else if ( is_wp_error( $result ) ) { - $status['error'] = $result->get_error_message(); - wp_send_json_error( $status ); - - } else if ( is_bool( $result ) && ! $result ) { - $status['errorCode'] = 'unable_to_connect_to_filesystem'; - $status['error'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); - - // Pass through the error from WP_Filesystem if one was raised - if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { - $status['error'] = $wp_filesystem->errors->get_error_message(); - } - - wp_send_json_error( $status ); - - } else { - // An unhandled error occured - $status['error'] = __( 'Plugin update failed.' ); - wp_send_json_error( $status ); - } -} - /** * AJAX handler for saving a post from Press This. * @@ -3333,3 +3264,571 @@ function wp_ajax_save_wporg_username() { wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) ); } + +/** + * AJAX handler for installing a theme. + * + * @since 4.6.0 + */ +function wp_ajax_install_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) ); + } + + $slug = sanitize_key( wp_unslash( $_POST['slug'] ) ); + + $status = array( + 'install' => 'theme', + 'slug' => $slug, + ); + + if ( ! current_user_can( 'install_themes' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to install themes on this site.' ); + wp_send_json_error( $status ); + } + + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + include_once( ABSPATH . 'wp-admin/includes/theme.php' ); + + $api = themes_api( 'theme_information', array( + 'slug' => $slug, + 'fields' => array( 'sections' => false ), + ) ); + + if ( is_wp_error( $api ) ) { + $status['errorMessage'] = $api->get_error_message(); + wp_send_json_error( $status ); + } + + $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $upgrader->skin->get_upgrade_messages(); + } + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( is_null( $result ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + if ( current_user_can( 'switch_themes' ) ) { + if ( is_multisite() ) { + $status['activateUrl'] = add_query_arg( array( + 'action' => 'enable', + '_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ), + 'theme' => $slug, + ), network_admin_url( 'themes.php' ) ); + } else { + $status['activateUrl'] = add_query_arg( array( + 'action' => 'activate', + '_wpnonce' => wp_create_nonce( 'switch-theme_' . $slug ), + 'stylesheet' => $slug, + ), admin_url( 'themes.php' ) ); + } + } + + if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) { + $status['customizeUrl'] = add_query_arg( array( + 'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ), + ), wp_customize_url( $slug ) ); + } + + /* + * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check + * on post-install status. + */ + wp_send_json_success( $status ); +} + +/** + * AJAX handler for updating a theme. + * + * @since 4.6.0 + * + * @see Theme_Upgrader + */ +function wp_ajax_update_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) ); + } + + $stylesheet = sanitize_key( wp_unslash( $_POST['slug'] ) ); + $status = array( + 'update' => 'theme', + 'slug' => $stylesheet, + 'newVersion' => '', + ); + + if ( ! current_user_can( 'update_themes' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to update themes on this site.' ); + wp_send_json_error( $status ); + } + + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + + $current = get_site_transient( 'update_themes' ); + if ( empty( $current ) ) { + wp_update_themes(); + } + + $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->bulk_upgrade( array( $stylesheet ) ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $upgrader->skin->get_upgrade_messages(); + } + + if ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) { + + // Theme is already at the latest version. + if ( true === $result[ $stylesheet ] ) { + $status['errorMessage'] = $upgrader->strings['up_to_date']; + wp_send_json_error( $status ); + } + + $theme = wp_get_theme( $stylesheet ); + if ( $theme->get( 'Version' ) ) { + $status['newVersion'] = $theme->get( 'Version' ); + } + + wp_send_json_success( $status ); + } elseif ( is_wp_error( $upgrader->skin->result ) ) { + $status['errorCode'] = $upgrader->skin->result->get_error_code(); + $status['errorMessage'] = $upgrader->skin->result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + // An unhandled error occurred. + $status['errorMessage'] = __( 'Update failed.' ); + wp_send_json_error( $status ); +} + +/** + * AJAX handler for deleting a theme. + * + * @since 4.6.0 + */ +function wp_ajax_delete_theme() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_theme_specified', + 'errorMessage' => __( 'No theme specified.' ), + ) ); + } + + $stylesheet = sanitize_key( wp_unslash( $_POST['slug'] ) ); + $status = array( + 'delete' => 'theme', + 'slug' => $stylesheet, + ); + + if ( ! current_user_can( 'delete_themes' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to delete themes on this site.' ); + wp_send_json_error( $status ); + } + + if ( ! wp_get_theme( $stylesheet )->exists() ) { + $status['errorMessage'] = __( 'The requested theme does not exist.' ); + wp_send_json_error( $status ); + } + + // Check filesystem credentials. `delete_plugins()` will bail otherwise. + ob_start(); + $url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet ); + if ( false === ( $credentials = request_filesystem_credentials( $url ) ) || ! WP_Filesystem( $credentials ) ) { + global $wp_filesystem; + ob_end_clean(); + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + include_once( ABSPATH . 'wp-admin/includes/theme.php' ); + + $result = delete_theme( $stylesheet ); + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + $status['errorMessage'] = __( 'Theme could not be deleted.' ); + wp_send_json_error( $status ); + } + + wp_send_json_success( $status ); +} + +/** + * AJAX handler for installing a plugin. + * + * @since 4.6.0 + */ +function wp_ajax_install_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) ) { + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) ); + } + + $status = array( + 'install' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + ); + + if ( ! current_user_can( 'install_plugins' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to install plugins on this site.' ); + wp_send_json_error( $status ); + } + + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + + $api = plugins_api( 'plugin_information', array( + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + 'fields' => array( + 'sections' => false, + ), + ) ); + + if ( is_wp_error( $api ) ) { + $status['errorMessage'] = $api->get_error_message(); + wp_send_json_error( $status ); + } + + $status['pluginName'] = $api->name; + + $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $upgrader->skin->get_upgrade_messages(); + } + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( is_null( $result ) ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + $install_status = install_plugin_install_status( $api ); + + if ( current_user_can( 'activate_plugins' ) && is_plugin_inactive( $install_status['file'] ) ) { + $status['activateUrl'] = add_query_arg( array( + '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ), + 'action' => 'activate', + 'plugin' => $install_status['file'], + ), network_admin_url( 'plugins.php' ) ); + } + + if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) { + $status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] ); + } + + wp_send_json_success( $status ); +} + +/** + * AJAX handler for updating a plugin. + * + * @since 4.2.0 + * + * @see Plugin_Upgrader + */ +function wp_ajax_update_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) { + wp_send_json_error( array( + 'slug' => '', + 'errorCode' => 'no_plugin_specified', + 'errorMessage' => __( 'No plugin specified.' ), + ) ); + } + + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + + $status = array( + 'update' => 'plugin', + 'plugin' => $plugin, + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + 'pluginName' => $plugin_data['Name'], + 'oldVersion' => '', + 'newVersion' => '', + ); + + if ( $plugin_data['Version'] ) { + /* translators: %s: Plugin version */ + $status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); + } + + if ( ! current_user_can( 'update_plugins' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to update plugins for this site.' ); + wp_send_json_error( $status ); + } + + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + + wp_update_plugins(); + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new Plugin_Upgrader( $skin ); + $result = $upgrader->bulk_upgrade( array( $plugin ) ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $status['debug'] = $upgrader->skin->get_upgrade_messages(); + } + + if ( is_array( $result ) && empty( $result[ $plugin ] ) && is_wp_error( $skin->result ) ) { + $result = $skin->result; + } + + if ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) { + $plugin_update_data = current( $result ); + + /* + * If the `update_plugins` site transient is empty (e.g. when you update + * two plugins in quick succession before the transient repopulates), + * this may be the return. + * + * Preferably something can be done to ensure `update_plugins` isn't empty. + * For now, surface some sort of error here. + */ + if ( true === $plugin_update_data ) { + $status['errorMessage'] = __( 'Plugin update failed.' ); + wp_send_json_error( $status ); + } + + $plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] ); + $plugin_data = reset( $plugin_data ); + + if ( $plugin_data['Version'] ) { + /* translators: %s: Plugin version */ + $status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); + } + wp_send_json_success( $status ); + } elseif ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + global $wp_filesystem; + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + // An unhandled error occurred. + $status['errorMessage'] = __( 'Plugin update failed.' ); + wp_send_json_error( $status ); +} + +/** + * AJAX handler for deleting a plugin. + * + * @since 4.6.0 + */ +function wp_ajax_delete_plugin() { + check_ajax_referer( 'updates' ); + + if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) { + wp_send_json_error( array( 'errorCode' => 'no_plugin_specified' ) ); + } + + $plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) ); + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + + $status = array( + 'delete' => 'plugin', + 'slug' => sanitize_key( wp_unslash( $_POST['slug'] ) ), + 'plugin' => $plugin, + 'pluginName' => $plugin_data['Name'], + ); + + if ( ! current_user_can( 'delete_plugins' ) ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to delete plugins for this site.' ); + wp_send_json_error( $status ); + } + + if ( is_plugin_active( $plugin ) ) { + $status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' ); + wp_send_json_error( $status ); + } + + // Check filesystem credentials. `delete_plugins()` will bail otherwise. + ob_start(); + $url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' ); + if ( false === ( $credentials = request_filesystem_credentials( $url ) ) || ! WP_Filesystem( $credentials ) ) { + global $wp_filesystem; + ob_end_clean(); + + $status['errorCode'] = 'unable_to_connect_to_filesystem'; + $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) { + $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + + $result = delete_plugins( array( $plugin ) ); + + if ( is_wp_error( $result ) ) { + $status['errorMessage'] = $result->get_error_message(); + wp_send_json_error( $status ); + } elseif ( false === $result ) { + $status['errorMessage'] = __( 'Plugin could not be deleted.' ); + wp_send_json_error( $status ); + } + + wp_send_json_success( $status ); +} + +/** + * AJAX handler for searching plugins. + * + * @since 4.6.0 + * + * @global WP_List_Table $wp_list_table Current list table instance. + * @global string $hook_suffix Current admin page. + * @global string $s Search term. + */ +function wp_ajax_search_plugins() { + check_ajax_referer( 'updates' ); + + global $wp_list_table, $hook_suffix, $s; + $hook_suffix = 'plugins.php'; + + /** @var WP_Plugins_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); + $status = array(); + + if ( ! $wp_list_table->ajax_user_can() ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to manage plugins on this site.' ); + wp_send_json_error( $status ); + } + + // Set the correct requester, so pagination works. + $_SERVER['REQUEST_URI'] = add_query_arg( array_diff_key( $_POST, array( + '_ajax_nonce' => null, + 'action' => null, + ) ), network_admin_url( 'plugins.php', 'relative' ) ); + + $s = sanitize_text_field( $_POST['s'] ); + + $wp_list_table->prepare_items(); + + ob_start(); + $wp_list_table->display(); + $status['items'] = ob_get_clean(); + + wp_send_json_success( $status ); +} + +/** + * AJAX handler for searching plugins to install. + * + * @since 4.6.0 + * + * @global WP_List_Table $wp_list_table Current list table instance. + * @global string $hook_suffix Current admin page. + */ +function wp_ajax_search_install_plugins() { + check_ajax_referer( 'updates' ); + + global $wp_list_table, $hook_suffix; + $hook_suffix = 'plugin-install.php'; + + /** @var WP_Plugin_Install_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( 'WP_Plugin_Install_List_Table' ); + $status = array(); + + if ( ! $wp_list_table->ajax_user_can() ) { + $status['errorMessage'] = __( 'You do not have sufficient permissions to manage plugins on this site.' ); + wp_send_json_error( $status ); + } + + // Set the correct requester, so pagination works. + $_SERVER['REQUEST_URI'] = add_query_arg( array_diff_key( $_POST, array( + '_ajax_nonce' => null, + 'action' => null, + ) ), network_admin_url( 'plugin-install.php', 'relative' ) ); + + $wp_list_table->prepare_items(); + + ob_start(); + $wp_list_table->display(); + $status['items'] = ob_get_clean(); + + wp_send_json_success( $status ); +} diff --git a/src/wp-admin/includes/class-wp-filesystem-base.php b/src/wp-admin/includes/class-wp-filesystem-base.php index c944051a3f..12b52d8f27 100644 --- a/src/wp-admin/includes/class-wp-filesystem-base.php +++ b/src/wp-admin/includes/class-wp-filesystem-base.php @@ -41,6 +41,7 @@ class WP_Filesystem_Base { /** * @access public + * @var WP_Error */ public $errors = null; diff --git a/src/wp-admin/includes/class-wp-ms-themes-list-table.php b/src/wp-admin/includes/class-wp-ms-themes-list-table.php index e0c856d6b7..fe4b7cb8e2 100644 --- a/src/wp-admin/includes/class-wp-ms-themes-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-themes-list-table.php @@ -149,6 +149,10 @@ class WP_MS_Themes_List_Table extends WP_List_Table { $this->has_items = ! empty( $themes['all'] ); $total_this_page = $totals[ $status ]; + wp_localize_script( 'updates', '_wpUpdatesItemCounts', array( + 'totals' => $totals, + ) ); + if ( $orderby ) { $orderby = ucfirst( $orderby ); $order = strtoupper( $order ); diff --git a/src/wp-admin/includes/class-wp-plugin-install-list-table.php b/src/wp-admin/includes/class-wp-plugin-install-list-table.php index c16786242e..d917fbd8f0 100644 --- a/src/wp-admin/includes/class-wp-plugin-install-list-table.php +++ b/src/wp-admin/includes/class-wp-plugin-install-list-table.php @@ -461,18 +461,42 @@ class WP_Plugin_Install_List_Table extends WP_List_Table { /* translators: 1: Plugin name and version. */ $action_links[] = '' . __( 'Install Now' ) . ''; } - break; + case 'update_available': if ( $status['url'] ) { /* translators: 1: Plugin name and version */ $action_links[] = '' . __( 'Update Now' ) . ''; } - break; + case 'latest_installed': case 'newer_installed': - $action_links[] = '' . _x( 'Installed', 'plugin' ) . ''; + if ( is_plugin_active( $status['file'] ) ) { + $action_links[] = ''; + } elseif ( current_user_can( 'activate_plugins' ) ) { + $button_text = __( 'Activate' ); + $activate_url = add_query_arg( array( + '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ), + 'action' => 'activate', + 'plugin' => $status['file'], + ), network_admin_url( 'plugins.php' ) ); + + if ( is_network_admin() ) { + $button_text = __( 'Network Activate' ); + $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url ); + } + + $action_links[] = sprintf( + '%3$s', + esc_url( $activate_url ), + /* translators: %s: Plugin name */ + esc_attr( sprintf( __( 'Activate %s' ), $plugin['name'] ) ), + $button_text + ); + } else { + $action_links[] = ''; + } break; } } diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php index 961775d093..39d70e2087 100644 --- a/src/wp-admin/includes/class-wp-plugins-list-table.php +++ b/src/wp-admin/includes/class-wp-plugins-list-table.php @@ -246,6 +246,15 @@ class WP_Plugins_List_Table extends WP_List_Table { $total_this_page = $totals[ $status ]; + $js_plugins = array(); + foreach ( $plugins as $key => $list ) { + $js_plugins[ $key ] = array_keys( (array) $list ); + } + + wp_localize_script( 'updates', '_wpUpdatesItemCounts', array( + 'plugins' => $js_plugins, + ) ); + if ( ! $orderby ) { $orderby = 'Name'; } else { diff --git a/src/wp-admin/includes/class-wp-upgrader-skin.php b/src/wp-admin/includes/class-wp-upgrader-skin.php index e75ff16675..7e2411260b 100644 --- a/src/wp-admin/includes/class-wp-upgrader-skin.php +++ b/src/wp-admin/includes/class-wp-upgrader-skin.php @@ -18,6 +18,11 @@ class WP_Upgrader_Skin { public $upgrader; public $done_header = false; public $done_footer = false; + + /** + * + * @var string|false|WP_Error + */ public $result = false; public $options = array(); diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php index e7818c6ec1..6995983d6f 100644 --- a/src/wp-admin/includes/class-wp-upgrader.php +++ b/src/wp-admin/includes/class-wp-upgrader.php @@ -61,7 +61,7 @@ class WP_Upgrader { * * @since 2.8.0 * @access public - * @var WP_Upgrader_Skin $skin + * @var Automatic_Upgrader_Skin|WP_Upgrader_Skin $skin */ public $skin = null; diff --git a/src/wp-admin/includes/plugin-install.php b/src/wp-admin/includes/plugin-install.php index 231be2dc97..a37765d89c 100644 --- a/src/wp-admin/includes/plugin-install.php +++ b/src/wp-admin/includes/plugin-install.php @@ -540,41 +540,50 @@ function install_plugin_information() { echo "\n"; ?> -
+
    - version ) ) { ?> -
  • version; ?>
  • - author ) ) { ?> -
  • author, '_blank' ); ?>
  • - last_updated ) ) { ?> -
  • - last_updated ) ) ); ?> -
  • - requires ) ) { ?> -
  • requires ); ?>
  • - tested ) ) { ?> -
  • tested; ?>
  • - active_installs ) ) { ?> -
  • active_installs >= 1000000 ) { - _ex( '1+ Million', 'Active plugin installs' ); - } else { - echo number_format_i18n( $api->active_installs ) . '+'; - } - ?>
  • - slug ) && empty( $api->external ) ) { ?> -
  • - homepage ) ) { ?> -
  • - donate_link ) && empty( $api->contributors ) ) { ?> -
  • - + version ) ) { ?> +
  • version; ?>
  • + author ) ) { ?> +
  • author, '_blank' ); ?>
  • + last_updated ) ) { ?> +
  • + last_updated ) ) ); + ?> +
  • + requires ) ) { ?> +
  • + + requires ); + ?> +
  • + tested ) ) { ?> +
  • tested; ?>
  • + active_installs ) ) { ?> +
  • active_installs >= 1000000 ) { + _ex( '1+ Million', 'Active plugin installs' ); + } else { + echo number_format_i18n( $api->active_installs ) . '+'; + } + ?>
  • + slug ) && empty( $api->external ) ) { ?> +
  • + homepage ) ) { ?> +
  • + donate_link ) && empty( $api->contributors ) ) { ?> +
  • +
rating ) ) { ?> -

- $api->rating, 'type' => 'percent', 'number' => $api->num_ratings ) ); ?> - +

+ $api->rating, 'type' => 'percent', 'number' => $api->num_ratings ) ); ?> + ratings ) && array_sum( (array) $api->ratings ) > 0 ) { ?> @@ -591,11 +600,11 @@ function install_plugin_information() { ) ); ?>
- - - - + + + +
tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) ) { - echo '

' . __( 'Warning: This plugin has not been tested with your current version of WordPress.' ) . '

'; - } elseif ( ! empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) ) { - echo '

' . __( 'Warning: This plugin has not been marked as compatible with your version of WordPress.' ) . '

'; - } + if ( ! empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) ) { + echo '

' . __( 'Warning: This plugin has not been tested with your current version of WordPress.' ) . '

'; + } elseif ( ! empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) ) { + echo '

' . __( 'Warning: This plugin has not been marked as compatible with your version of WordPress.' ) . '

'; + } - foreach ( (array) $api->sections as $section_name => $content ) { - $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' ); - $content = links_add_target( $content, '_blank' ); + foreach ( (array) $api->sections as $section_name => $content ) { + $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' ); + $content = links_add_target( $content, '_blank' ); - $san_section = esc_attr( $section_name ); + $san_section = esc_attr( $section_name ); - $display = ( $section_name === $section ) ? 'block' : 'none'; + $display = ( $section_name === $section ) ? 'block' : 'none'; - echo "\t
\n"; - echo $content; - echo "\t
\n"; - } + echo "\t
\n"; + echo $content; + echo "\t
\n"; + } echo "
\n"; echo "
\n"; echo "
\n"; // #plugin-information-scrollable @@ -655,7 +664,7 @@ function install_plugin_information() { switch ( $status['status'] ) { case 'install': if ( $status['url'] ) { - echo '' . __( 'Install Now' ) . ''; + echo '' . __( 'Install Now' ) . ''; } break; case 'update_available': @@ -664,6 +673,7 @@ function install_plugin_information() { } break; case 'newer_installed': + /* translators: %s: Plugin version */ echo '' . sprintf( __( 'Newer Version (%s) Installed'), $status['version'] ) . ''; break; case 'latest_installed': diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php index 43432ff4f7..32e3ab7148 100644 --- a/src/wp-admin/includes/theme.php +++ b/src/wp-admin/includes/theme.php @@ -172,7 +172,7 @@ function get_theme_update_available( $theme ) { if ( !is_multisite() ) { if ( ! current_user_can('update_themes') ) { /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number */ - $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details.' ) . '

', + $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details.' ) . '

', $theme_name, esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ @@ -181,7 +181,7 @@ function get_theme_update_available( $theme ) { ); } elseif ( empty( $update['package'] ) ) { /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number */ - $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details. Automatic update is unavailable for this theme.' ) . '

', + $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details. Automatic update is unavailable for this theme.' ) . '

', $theme_name, esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ @@ -190,7 +190,7 @@ function get_theme_update_available( $theme ) { ); } else { /* translators: 1: theme name, 2: theme details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */ - $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details or update now.' ) . '

', + $html = sprintf( '

' . __( 'There is a new version of %1$s available. View version %4$s details or update now.' ) . '

', $theme_name, esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ @@ -198,7 +198,8 @@ function get_theme_update_available( $theme ) { $update['new_version'], $update_url, /* translators: %s: theme name */ - esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) ) + esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) ), + $stylesheet ); } } diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index 4f6b06d5a5..88452190d0 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -329,33 +329,43 @@ function wp_plugin_update_rows() { } /** + * Displays update information for a plugin. * - * @param string $file - * @param array $plugin_data + * @param string $file Plugin basename. + * @param array $plugin_data Plugin information. * @return false|void */ function wp_plugin_update_row( $file, $plugin_data ) { $current = get_site_transient( 'update_plugins' ); - if ( !isset( $current->response[ $file ] ) ) + if ( ! isset( $current->response[ $file ] ) ) { return false; + } - $r = $current->response[ $file ]; + $response = $current->response[ $file ]; - $plugins_allowedtags = array('a' => array('href' => array(),'title' => array()),'abbr' => array('title' => array()),'acronym' => array('title' => array()),'code' => array(),'em' => array(),'strong' => array()); - $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags ); + $plugins_allowedtags = array( + 'a' => array( 'href' => array(), 'title' => array() ), + 'abbr' => array( 'title' => array() ), + 'acronym' => array( 'title' => array() ), + 'code' => array(), + 'em' => array(), + 'strong' => array(), + ); - $details_url = self_admin_url('plugin-install.php?tab=plugin-information&plugin=' . $r->slug . '§ion=changelog&TB_iframe=true&width=600&height=800'); + $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags ); + $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '§ion=changelog&TB_iframe=true&width=600&height=800' ); - $wp_list_table = _get_list_table('WP_Plugins_List_Table'); + /** @var WP_Plugins_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); - if ( is_network_admin() || !is_multisite() ) { + if ( is_network_admin() || ! is_multisite() ) { if ( is_network_admin() ) { - $active_class = is_plugin_active_for_network( $file ) ? ' active': ''; + $active_class = is_plugin_active_for_network( $file ) ? ' active' : ''; } else { $active_class = is_plugin_active( $file ) ? ' active' : ''; } - echo '
'; + echo '

'; if ( ! current_user_can( 'update_plugins' ) ) { /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number */ @@ -363,17 +373,17 @@ function wp_plugin_update_row( $file, $plugin_data ) { $plugin_name, esc_url( $details_url ), /* translators: 1: plugin name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ), - $r->new_version + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ), + $response->new_version ); - } elseif ( empty( $r->package ) ) { + } elseif ( empty( $response->package ) ) { /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number */ printf( __( 'There is a new version of %1$s available. View version %4$s details. Automatic update is unavailable for this plugin.' ), $plugin_name, esc_url( $details_url ), /* translators: 1: plugin name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ), - $r->new_version + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ), + $response->new_version ); } else { /* translators: 1: plugin name, 2: details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */ @@ -381,13 +391,14 @@ function wp_plugin_update_row( $file, $plugin_data ) { $plugin_name, esc_url( $details_url ), /* translators: 1: plugin name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $r->new_version ) ), - $r->new_version, + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) ), + $response->new_version, wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $file, 'upgrade-plugin_' . $file ), /* translators: %s: plugin name */ esc_attr( sprintf( __( 'Update %s now' ), $plugin_name ) ) ); } + /** * Fires at the end of the update message container in each * row of the plugins list table. @@ -400,32 +411,32 @@ function wp_plugin_update_row( $file, $plugin_data ) { * @param array $plugin_data { * An array of plugin metadata. * - * @type string $name The human-readable name of the plugin. - * @type string $plugin_uri Plugin URI. - * @type string $version Plugin version. - * @type string $description Plugin description. - * @type string $author Plugin author. - * @type string $author_uri Plugin author URI. - * @type string $text_domain Plugin text domain. - * @type string $domain_path Relative path to the plugin's .mo file(s). - * @type bool $network Whether the plugin can only be activated network wide. - * @type string $title The human-readable title of the plugin. - * @type string $author_name Plugin author's name. - * @type bool $update Whether there's an available update. Default null. - * } - * @param array $r { - * An array of metadata about the available plugin update. - * - * @type int $id Plugin ID. - * @type string $slug Plugin slug. - * @type string $new_version New plugin version. - * @type string $url Plugin URL. - * @type string $package Plugin update package URL. - * } + * @type string $name The human-readable name of the plugin. + * @type string $plugin_uri Plugin URI. + * @type string $version Plugin version. + * @type string $description Plugin description. + * @type string $author Plugin author. + * @type string $author_uri Plugin author URI. + * @type string $text_domain Plugin text domain. + * @type string $domain_path Relative path to the plugin's .mo file(s). + * @type bool $network Whether the plugin can only be activated network wide. + * @type string $title The human-readable title of the plugin. + * @type string $author_name Plugin author's name. + * @type bool $update Whether there's an available update. Default null. + * } + * @param array $response { + * An array of metadata about the available plugin update. + * + * @type int $id Plugin ID. + * @type string $slug Plugin slug. + * @type string $new_version New plugin version. + * @type string $url Plugin URL. + * @type string $package Plugin update package URL. + * } */ - do_action( "in_plugin_update_message-{$file}", $plugin_data, $r ); + do_action( "in_plugin_update_message-{$file}", $plugin_data, $response ); - echo '

'; + echo '

'; } } @@ -466,58 +477,65 @@ function wp_theme_update_rows() { } /** + * Displays update information for a theme. * - * @param string $theme_key - * @param WP_Theme $theme + * @param string $theme_key Theme stylesheet. + * @param WP_Theme $theme Theme object. * @return false|void */ function wp_theme_update_row( $theme_key, $theme ) { $current = get_site_transient( 'update_themes' ); - if ( !isset( $current->response[ $theme_key ] ) ) + + if ( ! isset( $current->response[ $theme_key ] ) ) { return false; + } - $r = $current->response[ $theme_key ]; + $response = $current->response[ $theme_key ]; - $theme_name = $theme['Name']; + $details_url = add_query_arg( array( + 'TB_iframe' => 'true', + 'width' => 1024, + 'height' => 800, + ), $current->response[ $theme_key ]['url'] ); - $details_url = add_query_arg( array( 'TB_iframe' => 'true', 'width' => 1024, 'height' => 800 ), $current->response[ $theme_key ]['url'] ); + /** @var WP_MS_Themes_List_Table $wp_list_table */ + $wp_list_table = _get_list_table( 'WP_MS_Themes_List_Table' ); - $wp_list_table = _get_list_table('WP_MS_Themes_List_Table'); + $active = $theme->is_allowed( 'network' ) ? ' active' : ''; - $active = $theme->is_allowed( 'network' ) ? ' active': ''; - - echo '
'; - if ( ! current_user_can('update_themes') ) { + echo '

'; + if ( ! current_user_can( 'update_themes' ) ) { /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number */ printf( __( 'There is a new version of %1$s available. View version %4$s details.'), - $theme_name, + $theme['Name'], esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ), - $r['new_version'] + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ), + $response['new_version'] ); - } elseif ( empty( $r['package'] ) ) { + } elseif ( empty( $response['package'] ) ) { /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number */ printf( __( 'There is a new version of %1$s available. View version %4$s details. Automatic update is unavailable for this theme.' ), - $theme_name, + $theme['Name'], esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ), - $r['new_version'] + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ), + $response['new_version'] ); } else { /* translators: 1: theme name, 2: details URL, 3: accessibility text, 4: version number, 5: update URL, 6: accessibility text */ printf( __( 'There is a new version of %1$s available. View version %4$s details or update now.' ), - $theme_name, + $theme['Name'], esc_url( $details_url ), /* translators: 1: theme name, 2: version number */ - esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $r['new_version'] ) ), - $r['new_version'], + esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) ), + $response['new_version'], wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' ) . $theme_key, 'upgrade-theme_' . $theme_key ), /* translators: %s: theme name */ - esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) ) + esc_attr( sprintf( __( 'Update %s now' ), $theme['Name'] ) ) ); } + /** * Fires at the end of the update message container in each * row of the themes list table. @@ -527,8 +545,8 @@ function wp_theme_update_row( $theme_key, $theme ) { * * @since 3.1.0 * - * @param WP_Theme $theme The WP_Theme object. - * @param array $r { + * @param WP_Theme $theme The WP_Theme object. + * @param array $response { * An array of metadata about the available theme update. * * @type string $new_version New theme version. @@ -536,9 +554,9 @@ function wp_theme_update_row( $theme_key, $theme ) { * @type string $package Theme update package URL. * } */ - do_action( "in_theme_update_message-{$theme_key}", $theme, $r ); + do_action( "in_theme_update_message-{$theme_key}", $theme, $response ); - echo '

'; + echo '

'; } /** @@ -577,3 +595,136 @@ function maintenance_nag() { echo "
$msg
"; } + +/** + * Prints the JavaScript templates for update admin notices. + * + * Template takes one argument with four values: + * + * param {object} data { + * Arguments for admin notice. + * + * @type string id ID of the notice. + * @type string className Class names for the notice. + * @type string message The notice's message. + * @type string type The type of update the notice is for. Either 'plugin' or 'theme'. + * } + * + * @since 4.6.0 + */ +function wp_print_admin_notice_templates() { + ?> + + + + + + 0 ) { - $( '.subsubsub .upgrade .count' ).text( '(' + pluginCount + ')' ); - } else { - $( '.subsubsub .upgrade' ).remove(); - } + if ( $notice.length ) { + $notice.replaceWith( $adminNotice ); + } else { + $( '.wrap' ).find( '> h1' ).after( $adminNotice ); } + + $document.trigger( 'wp-updates-notice-added' ); }; /** - * Send an Ajax request to the server to update a plugin. + * Handles Ajax requests to WordPress. * - * @since 4.2.0 + * @since 4.6.0 * - * @param {string} plugin - * @param {string} slug + * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc). + * @param {object} data Data that needs to be passed to the ajax callback. + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. */ - wp.updates.updatePlugin = function( plugin, slug ) { - var $message, name, - $card = $( '.plugin-card-' + slug ); + wp.updates.ajax = function( action, data ) { + var options = {}; - if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { - $message = $( '[data-plugin="' + plugin + '"]' ).next().find( '.update-message' ); - } else if ( 'plugin-install' === pagenow ) { - $message = $card.find( '.update-now' ); - name = $message.data( 'name' ); - $message.attr( 'aria-label', wp.updates.l10n.updatingLabel.replace( '%s', name ) ); - // Remove previous error messages, if any. - $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); - } - - $message.addClass( 'updating-message' ); - if ( $message.html() !== wp.updates.l10n.updating ){ - $message.data( 'originaltext', $message.html() ); - } - - $message.text( wp.updates.l10n.updating ); - wp.a11y.speak( wp.updates.l10n.updatingMsg ); - - if ( wp.updates.updateLock ) { - wp.updates.updateQueue.push( { - type: 'update-plugin', - data: { - plugin: plugin, - slug: slug - } + if ( wp.updates.ajaxLocked ) { + wp.updates.queue.push( { + action: action, + data: data } ); - return; + + // Return a Deferred object so callbacks can always be registered. + return $.Deferred(); } - wp.updates.updateLock = true; + wp.updates.ajaxLocked = true; - var data = { + if ( data.success ) { + options.success = data.success; + delete data.success; + } + + if ( data.error ) { + options.error = data.error; + delete data.error; + } + + options.data = _.extend( data, { + action: action, _ajax_nonce: wp.updates.ajaxNonce, - plugin: plugin, - slug: slug, username: wp.updates.filesystemCredentials.ftp.username, password: wp.updates.filesystemCredentials.ftp.password, hostname: wp.updates.filesystemCredentials.ftp.hostname, connection_type: wp.updates.filesystemCredentials.ftp.connectionType, public_key: wp.updates.filesystemCredentials.ssh.publicKey, private_key: wp.updates.filesystemCredentials.ssh.privateKey - }; + } ); - wp.ajax.post( 'update-plugin', data ) - .done( wp.updates.updateSuccess ) - .fail( wp.updates.updateError ); + return wp.ajax.send( options ).always( wp.updates.ajaxAlways ); }; /** - * On a successful plugin update, update the UI with the result. + * Actions performed after every Ajax request. * - * @since 4.2.0 + * @since 4.6.0 * - * @param {object} response + * @param {object} response + * @param {array=} response.debug Optional. Debug information. + * @param {string=} response.errorCode Optional. Error code for an error that occurred. */ - wp.updates.updateSuccess = function( response ) { - var $updateMessage, name, $pluginRow, newText; - if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { - $pluginRow = $( '[data-plugin="' + response.plugin + '"]' ).first(); - $updateMessage = $pluginRow.next().find( '.update-message' ); - $pluginRow.addClass( 'updated' ).removeClass( 'update' ); - - // Update the version number in the row. - newText = $pluginRow.find('.plugin-version-author-uri').html().replace( response.oldVersion, response.newVersion ); - $pluginRow.find('.plugin-version-author-uri').html( newText ); - - // Add updated class to update message parent tr - $pluginRow.next().addClass( 'updated' ); - } else if ( 'plugin-install' === pagenow ) { - $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ); - $updateMessage.addClass( 'button-disabled' ); - name = $updateMessage.data( 'name' ); - $updateMessage.attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', name ) ); + wp.updates.ajaxAlways = function( response ) { + if ( ! response.errorCode && 'unable_to_connect_to_filesystem' !== response.errorCode ) { + wp.updates.ajaxLocked = false; + wp.updates.queueChecker(); } - $updateMessage.removeClass( 'updating-message' ).addClass( 'updated-message' ); - $updateMessage.text( wp.updates.l10n.updated ); - wp.a11y.speak( wp.updates.l10n.updatedMsg ); - - wp.updates.decrementCount( 'plugin' ); - - wp.updates.updateDoneSuccessfully = true; - - /* - * The lock can be released since the update was successful, - * and any other updates can commence. - */ - wp.updates.updateLock = false; - - $(document).trigger( 'wp-plugin-update-success', response ); - - wp.updates.queueChecker(); + if ( 'undefined' !== typeof response.debug ) { + _.map( response.debug, function( message ) { + window.console.log( $( '

' ).html( message ).text() ); + } ); + } }; - /** - * On a plugin update error, update the UI appropriately. + * Decrements the update counts throughout the various menus. * - * @since 4.2.0 + * This includes the toolbar, the "Updates" menu item and the menu items + * for plugins and themes. * - * @param {object} response + * @since 3.9.0 + * + * @param {string} type The type of item that was updated or deleted. + * Can be 'plugin', 'theme'. */ - wp.updates.updateError = function( response ) { - var $card = $( '.plugin-card-' + response.slug ), - $message, - $button, - name, - error_message; + wp.updates.decrementCount = function( type ) { + var $adminBarUpdates = $( '#wp-admin-bar-updates' ), + $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), + count = $adminBarUpdates.find( '.ab-label' ).text(), + $menuItem, $itemCount, itemCount; - wp.updates.updateDoneSuccessfully = false; + count = parseInt( count, 10 ) - 1; - if ( response.errorCode && response.errorCode == 'unable_to_connect_to_filesystem' && wp.updates.shouldRequestFilesystemCredentials ) { - wp.updates.credentialError( response, 'update-plugin' ); + if ( count < 0 || isNaN( count ) ) { return; } - error_message = wp.updates.l10n.updateFailed.replace( '%s', response.error ); + $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' ); + $adminBarUpdates.find( '.ab-label' ).text( count ); + + // Remove the update count from the toolbar if it's zero. + if ( ! count ) { + $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove(); + } + + // Update the "Updates" menu item. + $dashboardNavMenuUpdateCount.each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + count ); + } ); + + $dashboardNavMenuUpdateCount.removeAttr( 'title' ); + $dashboardNavMenuUpdateCount.find( '.update-count' ).text( count ); + + switch ( type ) { + case 'plugin': + $menuItem = $( '#menu-plugins' ); + $itemCount = $menuItem.find( '.plugin-count' ); + break; + + case 'theme': + $menuItem = $( '#menu-appearance' ); + $itemCount = $menuItem.find( '.theme-count' ); + break; + + default: + window.console.error( '"%s" is not white-listed to have its count decremented.', type ); + return; + } + + // Decrement the counter of the other menu items. + if ( $itemCount ) { + itemCount = $itemCount.eq( 0 ).text(); + itemCount = parseInt( itemCount, 10 ) - 1; + } + + if ( itemCount < 0 || isNaN( itemCount ) ) { + return; + } + + if ( itemCount > 0 ) { + $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); + + $itemCount.text( itemCount ); + $menuItem.find( '.update-plugins' ).each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + itemCount ); + } ); + } else { + $( '.subsubsub .upgrade' ).remove(); + $menuItem.find( '.update-plugins' ).remove(); + } + }; + + /** + * Sends an Ajax request to the server to update a plugin. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePlugin`. + * + * @param {object} args Arguments. + * @param {string} args.plugin Plugin basename. + * @param {string} args.slug Plugin slug. + * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess + * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.updatePlugin = function( args ) { + var $updateRow, $card, $message, message; + + args = _.extend( { + success: wp.updates.updatePluginSuccess, + error: wp.updates.updatePluginError + }, args ); if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { - $message = $( '[data-plugin="' + response.plugin + '"]' ).next().find( '.update-message' ); - $message.html( error_message ).removeClass( 'updating-message' ); - } else if ( 'plugin-install' === pagenow ) { - $button = $card.find( '.update-now' ); - name = $button.data( 'name' ); + $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' ); + $message = $updateRow.find( '.update-message' ).addClass( 'updating-message' ).find( 'p' ); + message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + args.slug ); + $message = $card.find( '.update-now' ).addClass( 'updating-message' ); + message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) ); - $card + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); + } + + if ( $message.html() !== wp.updates.l10n.updating ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .attr( 'aria-label', message ) + .text( wp.updates.l10n.updating ); + + $document.trigger( 'wp-plugin-updating' ); + + return wp.updates.ajax( 'update-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginSuccess`. + * + * @typedef {object} updatePluginSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string} response.pluginName Name of the plugin to be updated. + * @param {string} response.oldVersion Old version of the plugin. + * @param {string} response.newVersion New version of the plugin. + */ + wp.updates.updatePluginSuccess = function( response ) { + var $pluginRow, $updateMessage, newText; + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) + .removeClass( 'update' ) + .addClass( 'updated' ); + $updateMessage = $pluginRow.find( '.update-message' ) + .removeClass( 'updating-message notice-warning' ) + .addClass( 'updated-message notice-success' ).find( 'p' ); + + // Update the version number in the row. + newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); + $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ) + .removeClass( 'updating-message' ) + .addClass( 'button-disabled updated-message' ); + } + + $updateMessage + .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.updated ); + + wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' ); + + wp.updates.decrementCount( 'plugin' ); + + $document.trigger( 'wp-plugin-update-success', response ); + }; + + /** + * Updates the UI appropriately after a failed plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginError`. + * + * @typedef {object} updatePluginError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string=} response.pluginName Optional. Name of the plugin to be updated. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.updatePluginError = function( response ) { + var $card, $message, errorMessage; + + if ( ! wp.updates.isValidResponse( response, 'update' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) { + return; + } + + errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); + $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + response.slug ) .addClass( 'plugin-card-update-failed' ) - .append( '

' + error_message + '

' ); + .append( wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt is-dismissible', + message: errorMessage + } ) ); - $button - .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', name ) ) - .html( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); + $card.find( '.update-now' ) + .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' ); $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { + // Use same delay as the total duration of the notice fadeTo + slideUp animation. setTimeout( function() { $card .removeClass( 'plugin-card-update-failed' ) .find( '.column-name a' ).focus(); + + $card.find( '.update-now' ) + .attr( 'aria-label', false ) + .text( wp.updates.l10n.updateNow ); }, 200 ); - }); + } ); } - wp.a11y.speak( error_message, 'assertive' ); + wp.a11y.speak( errorMessage, 'assertive' ); - /* - * The lock can be released since this failure was - * after the credentials form. - */ - wp.updates.updateLock = false; - - $(document).trigger( 'wp-plugin-update-error', response ); - - wp.updates.queueChecker(); + $document.trigger( 'wp-plugin-update-error', response ); }; /** - * Show an error message in the request for credentials form. + * Sends an Ajax request to the server to install a plugin. * - * @param {string} message - * @since 4.2.0 + * @since 4.6.0 + * + * @param {object} args Arguments. + * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. + * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess + * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. */ - wp.updates.showErrorInCredentialsForm = function( message ) { - var $modal = $( '.notification-dialog' ); + wp.updates.installPlugin = function( args ) { + var $card = $( '.plugin-card-' + args.slug ), + $message = $card.find( '.install-now' ); - // Remove any existing error. - $modal.find( '.error' ).remove(); + args = _.extend( { + success: wp.updates.installPluginSuccess, + error: wp.updates.installPluginError + }, args ); - $modal.find( 'h3' ).after( '
' + message + '
' ); + if ( 'import' === pagenow ) { + $message = $( 'a[href*="' + args.slug + '"]' ); + } else { + $message.text( wp.updates.l10n.installing ); + } + + $message.addClass( 'updating-message' ); + + wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' ); + + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove(); + + return wp.updates.ajax( 'install-plugin', args ); }; /** - * Events that need to happen when there is a credential error + * Updates the UI appropriately after a successful plugin install. * - * @since 4.2.0 + * @since 4.6.0 + * + * @typedef {object} installPluginSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the installed plugin. + * @param {string} response.pluginName Name of the installed plugin. + * @param {string} response.activateUrl URL to activate the just installed plugin. */ - wp.updates.credentialError = function( response, type ) { - wp.updates.updateQueue.push( { - 'type': type, - 'data': { - // Not cool that we're depending on response for this data. - // This would feel more whole in a view all tied together. - plugin: response.plugin, - slug: response.slug - } - } ); - wp.updates.showErrorInCredentialsForm( response.error ); - wp.updates.requestFilesystemCredentials(); + wp.updates.installPluginSuccess = function( response ) { + var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' ); + + $message + .removeClass( 'updating-message' ) + .addClass( 'updated-message installed button-disabled' ) + .text( wp.updates.l10n.installed ); + + wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' ); + + $document.trigger( 'wp-plugin-install-success', response ); + + if ( response.activateUrl ) { + setTimeout( function() { + + // Transform the 'Install' button into an 'Activate' button. + $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' ) + .attr( 'href', response.activateUrl ) + .text( wp.updates.l10n.activatePlugin ); + }, 1000 ); + } }; /** - * If an update job has been placed in the queue, queueChecker pulls it out and runs it. + * Updates the UI appropriately after a failed plugin install. * - * @since 4.2.0 + * @since 4.6.0 + * + * @typedef {object} installPluginError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be installed. + * @param {string=} response.pluginName Optional. Name of the plugin to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. */ - wp.updates.queueChecker = function() { - if ( wp.updates.updateLock || wp.updates.updateQueue.length <= 0 ) { + wp.updates.installPluginError = function( response ) { + var $card = $( '.plugin-card-' + response.slug ), + $button = $card.find( '.install-now' ), + errorMessage; + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { return; } - var job = wp.updates.updateQueue.shift(); + if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { + return; + } - wp.updates.updatePlugin( job.data.plugin, job.data.slug ); + errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ); + + $card + .addClass( 'plugin-card-update-failed' ) + .append( '

' + errorMessage + '

' ); + + $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { + + // Use same delay as the total duration of the notice fadeTo + slideUp animation. + setTimeout( function() { + $card + .removeClass( 'plugin-card-update-failed' ) + .find( '.column-name a' ).focus(); + }, 200 ); + } ); + + $button + .removeClass( 'updating-message' ).addClass( 'button-disabled' ) + .attr( 'aria-label', wp.updates.l10n.installFailedLabel.replace( '%s', response.pluginName ) ) + .text( wp.updates.l10n.installFailedShort ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-plugin-install-error', response ); }; + /** + * Updates the UI appropriately after a successful importer install. + * + * @since 4.6.0 + * + * @typedef {object} installImporterSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the installed plugin. + * @param {string} response.pluginName Name of the installed plugin. + * @param {string} response.activateUrl URL to activate the just installed plugin. + */ + wp.updates.installImporterSuccess = function( response ) { + wp.updates.addAdminNotice( { + id: 'install-success', + className: 'notice-success is-dismissible', + message: wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' ) + } ); + + $( 'a[href*="' + response.slug + '"]' ) + .removeClass( 'thickbox open-plugin-details-modal updating-message' ) + .off( 'click' ) + .attr( 'href', response.activateUrl + '&from=import' ) + .attr( 'title', wp.updates.l10n.activateImporter ); + + wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' ); + + $document.trigger( 'wp-installer-install-success', response ); + }; /** - * Request the users filesystem credentials if we don't have them already. + * Updates the UI appropriately after a failed importer install. + * + * @since 4.6.0 + * + * @typedef {object} installImporterError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be installed. + * @param {string=} response.pluginName Optional. Name of the plugin to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installImporterError = function( response ) { + var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ); + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { + return; + } + + wp.updates.addAdminNotice( { + id: response.errorCode, + className: 'notice-error is-dismissible', + message: errorMessage + } ); + + $( 'a[href*="' + response.slug + '"]' ).removeClass( 'updating-message' ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-importer-install-error', response ); + }; + + /** + * Sends an Ajax request to the server to delete a plugin. + * + * @since 4.6.0 + * + * @param {object} args Arguments. + * @param {string} args.plugin Basename of the plugin to be deleted. + * @param {string} args.slug Slug of the plugin to be deleted. + * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess + * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.deletePlugin = function( args ) { + var $message = $( '[data-plugin="' + args.plugin + '"]' ).find( '.update-message p' ); + + args = _.extend( { + success: wp.updates.deletePluginSuccess, + error: wp.updates.deletePluginError + }, args ); + + if ( $message.html() !== wp.updates.l10n.updating ) { + $message.data( 'originaltext', $message.html() ); + } + + wp.a11y.speak( wp.updates.l10n.deleting, 'polite' ); + + return wp.updates.ajax( 'delete-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin deletion. + * + * @since 4.6.0 + * + * @typedef {object} deletePluginSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin that was deleted. + * @param {string} response.plugin Base name of the plugin that was deleted. + * @param {string} response.pluginName Name of the plugin that was deleted. + */ + wp.updates.deletePluginSuccess = function( response ) { + + // Removes the plugin and updates rows. + $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { + var $form = $( '#bulk-action-form' ), + $views = $( '.subsubsub' ), + $pluginRow = $( this ), + columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length, + pluginDeletedRow = wp.template( 'item-deleted-row' ), + /** @type {object} plugins Base names of plugins in their different states. */ + plugins = settings.plugins; + + // Add a success message after deleting a plugin. + if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) { + $pluginRow.after( + pluginDeletedRow( { + slug: response.slug, + plugin: response.plugin, + colspan: columnCount, + name: response.pluginName + } ) + ); + } + + $pluginRow.remove(); + + // Remove plugin from update count. + if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) { + plugins.upgrade = _.without( plugins.upgrade, response.plugin ); + wp.updates.decrementCount( 'plugin' ); + } + + // Remove from views. + if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) { + plugins.inactive = _.without( plugins.inactive, response.plugin ); + if ( plugins.inactive.length ) { + $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' ); + } else { + $views.find( '.inactive' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) { + plugins.active = _.without( plugins.active, response.plugin ); + if ( plugins.active.length ) { + $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' ); + } else { + $views.find( '.active' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) { + plugins.recently_activated = _.without( plugins.recently_activated, response.plugin ); + if ( plugins.recently_activated.length ) { + $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' ); + } else { + $views.find( '.recently_activated' ).remove(); + } + } + + plugins.all = _.without( plugins.all, response.plugin ); + + if ( plugins.all.length ) { + $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' ); + } else { + $form.find( '.tablenav' ).css( { visibility: 'hidden' } ); + $views.find( '.all' ).remove(); + + if ( ! $form.find( 'tr.no-items' ).length ) { + $form.find( '#the-list' ).append( '' + wp.updates.l10n.noPlugins + '' ); + } + } + } ); + + wp.a11y.speak( wp.updates.l10n.deleted, 'polite' ); + + $document.trigger( 'wp-plugin-delete-success', response ); + }; + + /** + * Updates the UI appropriately after a failed plugin deletion. + * + * @since 4.6.0 + * + * @typedef {object} deletePluginError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be deleted. + * @param {string} response.plugin Base name of the plugin to be deleted + * @param {string=} response.pluginName Optional. Name of the plugin to be deleted. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.deletePluginError = function( response ) { + var $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ), + pluginUpdateRow = wp.template( 'item-update-row' ), + $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ), + noticeContent = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: response.errorMessage + } ); + + if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) { + return; + } + + // Add a plugin update row if it doesn't exist yet. + if ( ! $pluginUpdateRow.length ) { + $plugin.addClass( 'update' ).after( + pluginUpdateRow( { + slug: response.slug, + plugin: response.plugin, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + content: noticeContent + } ) + ); + } else { + + // Remove previous error messages, if any. + $pluginUpdateRow.find( '.notice-error' ).remove(); + + $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent ); + } + + $document.trigger( 'wp-plugin-delete-error', response ); + }; + + /** + * Sends an Ajax request to the server to update a theme. + * + * @since 4.6.0 + * + * @param {object} args Arguments. + * @param {string} args.slug Theme stylesheet. + * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess + * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.updateTheme = function( args ) { + var $notice; + + args = _.extend( { + success: wp.updates.updateThemeSuccess, + error: wp.updates.updateThemeError + }, args ); + + if ( 'themes-network' === pagenow ) { + $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).addClass( 'updating-message' ).find( 'p' ); + + } else { + $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); + + $notice.find( 'h3' ).remove(); + + $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) ); + $notice = $notice.addClass( 'updating-message' ).find( 'p' ); + } + + if ( $notice.html() !== wp.updates.l10n.updating ) { + $notice.data( 'originaltext', $notice.html() ); + } + + wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' ); + $notice.text( wp.updates.l10n.updating ); + + return wp.updates.ajax( 'update-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme update. + * + * @since 4.6.0 + * + * @typedef {object} updateThemeSuccess + * @param {object} response + * @param {string} response.slug Slug of the theme to be updated. + * @param {object} response.theme Updated theme. + * @param {string} response.oldVersion Old version of the theme. + * @param {string} response.newVersion New version of the theme. + */ + wp.updates.updateThemeSuccess = function( response ) { + var isModalOpen = $( 'body.modal-open' ).length, + $theme = $( '[data-slug="' + response.slug + '"]' ), + updatedMessage = { + className: 'updated-message notice-success notice-alt', + message: wp.updates.l10n.updated + }, + $notice, newText; + + if ( 'themes-network' === pagenow ) { + $notice = $theme.find( '.update-message' ); + + // Update the version number in the row. + newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); + $theme.find( '.theme-version-author-uri' ).html( newText ); + } else { + $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) ); + + // Focus on Customize button after updating. + if ( isModalOpen ) { + $( '.load-customize:visible' ).focus(); + } else { + $theme.find( '.load-customize' ).focus(); + } + } + + wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) ); + wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' ); + + wp.updates.decrementCount( 'theme' ); + + $document.trigger( 'wp-theme-update-success', response ); + + // Show updated message after modal re-rendered. + if ( isModalOpen ) { + $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) ); + } + }; + + /** + * Updates the UI appropriately after a failed theme update. + * + * @since 4.6.0 + * + * @typedef {object} updateThemeError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the theme to be updated. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.updateThemeError = function( response ) { + var $theme = $( '[data-slug="' + response.slug + '"]' ), + errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ), + $notice; + + if ( ! wp.updates.isValidResponse( response, 'update' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { + return; + } + + if ( 'themes-network' === pagenow ) { + $notice = $theme.find( '.update-message ' ); + } else { + $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) ); + + $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus(); + } + + wp.updates.addAdminNotice( { + selector: $notice, + className: 'update-message notice-error notice-alt is-dismissible', + message: errorMessage + } ); + + wp.a11y.speak( errorMessage, 'polite' ); + + $document.trigger( 'wp-theme-update-error', response ); + }; + + /** + * Sends an Ajax request to the server to install a theme. + * + * @since 4.6.0 + * + * @param {object} args + * @param {string} args.slug Theme stylesheet. + * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess + * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.installTheme = function( args ) { + var $message = $( '.theme-install[data-slug="' + args.slug + '"]' ); + + args = _.extend( { + success: wp.updates.installThemeSuccess, + error: wp.updates.installThemeError + }, args ); + + $message.addClass( 'updating-message' ); + $message.parents( '.theme' ).addClass( 'focus' ); + if ( $message.html() !== wp.updates.l10n.installing ) { + $message.data( 'originaltext', $message.html() ); + } + + $message.text( wp.updates.l10n.installing ); + wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' ); + + // Remove previous error messages, if any. + $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove(); + + return wp.updates.ajax( 'install-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme install. + * + * @since 4.6.0 + * + * @typedef {object} installThemeSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the theme to be installed. + * @param {string} response.customizeUrl URL to the Customizer for the just installed theme. + * @param {string} response.activateUrl URL to activate the just installed theme. + */ + wp.updates.installThemeSuccess = function( response ) { + var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ), + $message; + + $document.trigger( 'wp-install-theme-success', response ); + + $message = $card.find( '.button-primary' ) + .removeClass( 'updating-message' ) + .addClass( 'updated-message disabled' ) + .text( wp.updates.l10n.installed ); + + wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' ); + + setTimeout( function() { + + if ( response.activateUrl ) { + + // Transform the 'Install' button into an 'Activate' button. + $message + .attr( 'href', response.activateUrl ) + .removeClass( 'theme-install updated-message disabled' ) + .addClass( 'activate' ) + .text( wp.updates.l10n.activateTheme ); + } + + if ( response.customizeUrl ) { + + // Transform the 'Preview' button into a 'Live Preview' button. + $message.siblings( '.preview' ).replaceWith( function () { + return $( '' ) + .attr( 'href', response.customizeUrl ) + .addClass( 'button button-secondary load-customize' ) + .text( wp.updates.l10n.livePreview ); + } ); + } + }, 1000 ); + }; + + /** + * Updates the UI appropriately after a failed theme install. + * + * @since 4.6.0 + * + * @typedef {object} installThemeError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the theme to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installThemeError = function( response ) { + var $card, $button, + errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ), + $message = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: errorMessage + } ); + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) { + return; + } + + if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { + $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); + $card = $( '.install-theme-info' ).prepend( $message ); + } else { + $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); + $button = $card.find( '.theme-install' ); + } + + $button + .removeClass( 'updating-message' ) + .attr( 'aria-label', wp.updates.l10n.installFailedLabel.replace( '%s', $card.find( '.theme-name' ).text() ) ) + .text( wp.updates.l10n.installFailedShort ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-theme-install-error', response ); + }; + + /** + * Sends an Ajax request to the server to install a theme. + * + * @since 4.6.0 + * + * @param {object} args + * @param {string} args.slug Theme stylesheet. + * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess + * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.deleteTheme = function( args ) { + var $button = $( '.theme-actions .delete-theme' ); + + args = _.extend( { + success: wp.updates.deleteThemeSuccess, + error: wp.updates.deleteThemeError + }, args ); + + if ( $button.html() !== wp.updates.l10n.deleting ) { + $button.data( 'originaltext', $button.html() ); + } + + $button.text( wp.updates.l10n.deleting ); + wp.a11y.speak( wp.updates.l10n.deleting, 'polite' ); + + // Remove previous error messages, if any. + $( '.theme-info .update-message' ).remove(); + + return wp.updates.ajax( 'delete-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme deletion. + * + * @since 4.6.0 + * + * @typedef {object} deleteThemeSuccess + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the theme that was deleted. + */ + wp.updates.deleteThemeSuccess = function( response ) { + var $themeRows = $( '[data-slug="' + response.slug + '"]' ); + + if ( 'themes-network' === pagenow ) { + + // Removes the theme and updates rows. + $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { + var $views = $( '.subsubsub' ), + $themeRow = $( this ), + totals = settings.totals, + deletedRow = wp.template( 'item-deleted-row' ); + + if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) { + $themeRow.after( + deletedRow( { + slug: response.slug, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + name: $themeRow.find( '.theme-title strong' ).text() + } ) + ); + } + + $themeRow.remove(); + + // Remove theme from update count. + if ( $themeRow.hasClass( 'update' ) ) { + totals.upgrade--; + wp.updates.decrementCount( 'theme' ); + } + + // Remove from views. + if ( $themeRow.hasClass( 'inactive' ) ) { + totals.disabled--; + if ( totals.disabled ) { + $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' ); + } else { + $views.find( '.disabled' ).remove(); + } + } + + // There is always at least one theme available. + $views.find( '.all .count' ).text( '(' + --totals.all + ')' ); + } ); + } + + wp.a11y.speak( wp.updates.l10n.deleted, 'polite' ); + + $document.trigger( 'wp-delete-theme-success', response ); + }; + + /** + * Updates the UI appropriately after a failed theme deletion. + * + * @since 4.6.0 + * + * @typedef {object} deleteThemeError + * @param {object} response Response from the server. + * @param {string} response.slug Slug of the theme to be deleted. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.deleteThemeError = function( response ) { + var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ), + $button = $( '.theme-actions .delete-theme' ), + updateRow = wp.template( 'item-update-row' ), + $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ), + errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ), + $message = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: errorMessage + } ); + + if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) { + return; + } + + if ( 'themes-network' === pagenow ) { + if ( ! $updateRow.length ) { + $themeRow.addClass( 'update' ).after( + updateRow( { + slug: response.slug, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + content: $message + } ) + ); + } else { + // Remove previous error messages, if any. + $updateRow.find( '.notice-error' ).remove(); + $updateRow.find( '.plugin-update' ).append( $message ); + } + } else { + $( '.theme-info .theme-description' ).before( $message ); + } + + $button.html( $button.data( 'originaltext' ) ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-theme-delete-error', response ); + }; + + /** + * Adds the appropriate callback based on the type of action and the current page. + * + * @since 4.6.0 + * @private + * + * @param {object} data AJAX payload. + * @param {string} action The type of request to perform. + * @return {object} The AJAX payload with the appropriate callbacks. + */ + wp.updates._addCallbacks = function( data, action ) { + if ( 'import' === pagenow && 'install-plugin' === action ) { + data.success = wp.updates.installImporterSuccess; + data.error = wp.updates.installImporterError; + } + + return data; + }; + + /** + * Pulls available jobs from the queue and runs them. * * @since 4.2.0 + * @since 4.6.0 Can handle multiple job types. + */ + wp.updates.queueChecker = function() { + var job; + + if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) { + return; + } + + job = wp.updates.queue.shift(); + + // Handle a queue job. + switch ( job.action ) { + case 'install-plugin': + wp.updates.installPlugin( job.data ); + break; + + case 'update-plugin': + wp.updates.updatePlugin( job.data ); + break; + + case 'delete-plugin': + wp.updates.deletePlugin( job.data ); + break; + + case 'install-theme': + wp.updates.installTheme( job.data ); + break; + + case 'update-theme': + wp.updates.updateTheme( job.data ); + break; + + case 'delete-theme': + wp.updates.deleteTheme( job.data ); + break; + + default: + window.console.error( 'Failed to execute queued update job.', job ); + break; + } + }; + + /** + * Requests the users filesystem credentials if they aren't already known. + * + * @since 4.2.0 + * + * @param {Event=} event Optional. Event interface. */ wp.updates.requestFilesystemCredentials = function( event ) { - if ( wp.updates.updateDoneSuccessfully === false ) { + if ( false === wp.updates.filesystemCredentials.available ) { /* - * For the plugin install screen, return the focus to the install button - * after exiting the credentials request modal. + * After exiting the credentials request modal, + * return the focus to the element triggering the request. */ - if ( 'plugin-install' === pagenow && event ) { + if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) { wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target ); } - wp.updates.updateLock = true; - + wp.updates.ajaxLocked = true; wp.updates.requestForCredentialsModalOpen(); } }; + /** + * Requests the users filesystem credentials if needed and there is no lock. + * + * @since 4.6.0 + * + * @param {Event=} event Optional. Event interface. + */ + wp.updates.maybeRequestFilesystemCredentials = function( event ) { + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + } + }; + /** * Keydown handler for the request for credentials modal. * - * Close the modal when the escape key is pressed. - * Constrain keyboard navigation to inside the modal. + * Closes the modal when the escape key is pressed and + * constrains keyboard navigation to inside the modal. * * @since 4.2.0 + * + * @param {Event} event Event interface. */ wp.updates.keydown = function( event ) { if ( 27 === event.keyCode ) { wp.updates.requestForCredentialsModalCancel(); } else if ( 9 === event.keyCode ) { - // #upgrade button must always be the last focusable element in the dialog. - if ( event.target.id === 'upgrade' && ! event.shiftKey ) { + + // #upgrade button must always be the last focus-able element in the dialog. + if ( 'upgrade' === event.target.id && ! event.shiftKey ) { $( '#hostname' ).focus(); + event.preventDefault(); - } else if ( event.target.id === 'hostname' && event.shiftKey ) { + } else if ( 'hostname' === event.target.id && event.shiftKey ) { $( '#upgrade' ).focus(); + event.preventDefault(); } } }; /** - * Open the request for credentials modal. + * Opens the request for credentials modal. * * @since 4.2.0 */ wp.updates.requestForCredentialsModalOpen = function() { var $modal = $( '#request-filesystem-credentials-dialog' ); + $( 'body' ).addClass( 'modal-open' ); $modal.show(); - $modal.find( 'input:enabled:first' ).focus(); - $modal.keydown( wp.updates.keydown ); + $modal.on( 'keydown', wp.updates.keydown ); }; /** - * Close the request for credentials modal. + * Closes the request for credentials modal. * * @since 4.2.0 */ wp.updates.requestForCredentialsModalClose = function() { $( '#request-filesystem-credentials-dialog' ).hide(); $( 'body' ).removeClass( 'modal-open' ); - wp.updates.$elToReturnFocusToFromCredentialsModal.focus(); + + if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) { + wp.updates.$elToReturnFocusToFromCredentialsModal.focus(); + } }; /** - * The steps that need to happen when the modal is canceled out + * Takes care of the steps that need to happen when the modal is canceled out. * * @since 4.2.0 + * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions. */ wp.updates.requestForCredentialsModalCancel = function() { - // no updateLock and no updateQueue means we already have cleared things up - var data, $message; - if( wp.updates.updateLock === false && wp.updates.updateQueue.length === 0 ){ + // Not ajaxLocked and no queue means we already have cleared things up. + if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) { return; } - data = wp.updates.updateQueue[0].data; + _.each( wp.updates.queue, function( job ) { + $document.trigger( 'credential-modal-cancel', job ); + } ); - // remove the lock, and clear the queue - wp.updates.updateLock = false; - wp.updates.updateQueue = []; + // Remove the lock, and clear the queue. + wp.updates.ajaxLocked = false; + wp.updates.queue = []; wp.updates.requestForCredentialsModalClose(); - if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { - $message = $( '[data-plugin="' + data.plugin + '"]' ).next().find( '.update-message' ); - } else if ( 'plugin-install' === pagenow ) { - $message = $( '.plugin-card-' + data.slug ).find( '.update-now' ); + }; + + /** + * Displays an error message in the request for credentials form. + * + * @since 4.2.0 + * + * @param {string} message Error message. + */ + wp.updates.showErrorInCredentialsForm = function( message ) { + var $modal = $( '#request-filesystem-credentials-form' ); + + // Remove any existing error. + $modal.find( '.notice' ).remove(); + $modal.find( '#request-filesystem-credentials-title' ).after( '

' + message + '

' ); + }; + + /** + * Handles credential errors and runs events that need to happen in that case. + * + * @since 4.2.0 + * + * @param {object} response Ajax response. + * @param {string} action The type of request to perform. + */ + wp.updates.credentialError = function( response, action ) { + + // Restore callbacks. + response = wp.updates._addCallbacks( response, action ); + + wp.updates.queue.push( { + action: action, + + /* + * Not cool that we're depending on response for this data. + * This would feel more whole in a view all tied together. + */ + data: response + } ); + + wp.updates.filesystemCredentials.available = false; + wp.updates.showErrorInCredentialsForm( response.errorMessage ); + wp.updates.requestFilesystemCredentials(); + }; + + /** + * Handles credentials errors if it could not connect to the filesystem. + * + * @since 4.6.0 + * + * @typedef {object} maybeHandleCredentialError + * @param {object} response Response from the server. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + * @param {string} action The type of request to perform. + * @returns {boolean} Whether there is an error that needs to be handled or not. + */ + wp.updates.maybeHandleCredentialError = function( response, action ) { + if ( response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) { + wp.updates.credentialError( response, action ); + return true; } - $message.removeClass( 'updating-message' ); - $message.html( $message.data( 'originaltext' ) ); - wp.a11y.speak( wp.updates.l10n.updateCancel ); + return false; }; + /** - * Potentially add an AYS to a user attempting to leave the page + * Validates an AJAX response to ensure it's a proper object. + * + * If the response deems to be invalid, an admin notice is being displayed. + * + * @param {(object|string)} response Response from the server. + * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected. + * @param {string=} response.statusText Optional. Status message corresponding to the status code. + * @param {string=} response.responseText Optional. Request response as text. + * @param {string} action Type of action the response is referring to. Can be 'delete', + * 'update' or 'install'. + */ + wp.updates.isValidResponse = function( response, action ) { + var error = wp.updates.l10n.unknownError, + errorMessage; + + // Make sure the response is a valid data object and not a Promise object. + if ( _.isObject( response ) && ! _.isFunction( response.always ) ) { + return true; + } + + if ( _.isString( response ) ) { + error = response; + } else if ( _.isString( response.responseText ) && '' !== response.responseText ) { + error = response.responseText; + } else if ( _.isString( response.statusText ) ) { + error = response.statusText; + } + + switch ( action ) { + case 'update': + errorMessage = wp.updates.l10n.updateFailed; + break; + + case 'install': + errorMessage = wp.updates.l10n.installFailed; + break; + + case 'delete': + errorMessage = wp.updates.l10n.deleteFailed; + break; + } + + errorMessage = errorMessage.replace( '%s', error ); + + // Add admin notice. + wp.updates.addAdminNotice( { + id: 'unknown_error', + className: 'notice-error is-dismissible', + message: errorMessage + } ); + + // Remove the lock, and clear the queue. + wp.updates.ajaxLocked = false; + wp.updates.queue = []; + + // Change buttons of all running updates. + $( '.button.updating-message' ) + .removeClass( 'updating-message' ) + .attr( 'aria-label', wp.updates.l10n.updateFailedShort ) + .prop( 'disabled', true ) + .text( wp.updates.l10n.updateFailedShort ); + + $( '.updating-message:not(.button):not(.thickbox)' ) + .removeClass( 'updating-message notice-warning' ) + .addClass( 'notice-error' ) + .find( 'p' ).text( errorMessage ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + return false; + }; + + /** + * Potentially adds an AYS to a user attempting to leave the page. * * If an update is on-going and a user attempts to leave the page, - * open an "Are you sure?" alert. + * opens an "Are you sure?" alert. * * @since 4.2.0 */ - wp.updates.beforeunload = function() { - if ( wp.updates.updateLock ) { + if ( wp.updates.ajaxLocked ) { return wp.updates.l10n.beforeunload; } }; - - $( document ).ready( function() { - // Set initial focus on the first empty form field. - $( '#request-filesystem-credentials-form input[value=""]:first' ).focus(); + $( function() { + var $pluginFilter = $( '#plugin-filter' ), + $bulkActionForm = $( '#bulk-action-form' ), + $filesystemModal = $( '#request-filesystem-credentials-dialog' ); /* - * Check whether a user needs to submit filesystem credentials based on whether - * the form was output on the page server-side. + * Whether a user needs to submit filesystem credentials. + * + * This is based on whether the form was output on the page server-side. * * @see {wp_print_request_filesystem_credentials_modal() in PHP} */ - wp.updates.shouldRequestFilesystemCredentials = ( $( '#request-filesystem-credentials-dialog' ).length <= 0 ) ? false : true; + wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0; + + /** + * File system credentials form submit noop-er / handler. + * + * @since 4.2.0 + */ + $filesystemModal.on( 'submit', 'form', function( event ) { + event.preventDefault(); - // File system credentials form submit noop-er / handler. - $( '#request-filesystem-credentials-dialog form' ).on( 'submit', function() { // Persist the credentials input by the user for the duration of the page load. - wp.updates.filesystemCredentials.ftp.hostname = $('#hostname').val(); - wp.updates.filesystemCredentials.ftp.username = $('#username').val(); - wp.updates.filesystemCredentials.ftp.password = $('#password').val(); - wp.updates.filesystemCredentials.ftp.connectionType = $('input[name="connection_type"]:checked').val(); - wp.updates.filesystemCredentials.ssh.publicKey = $('#public_key').val(); - wp.updates.filesystemCredentials.ssh.privateKey = $('#private_key').val(); - - wp.updates.requestForCredentialsModalClose(); + wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val(); + wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val(); + wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val(); + wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val(); + wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val(); + wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val(); + wp.updates.filesystemCredentials.available = true; // Unlock and invoke the queue. - wp.updates.updateLock = false; + wp.updates.ajaxLocked = false; wp.updates.queueChecker(); - return false; - }); - - // Close the request credentials modal when - $( '#request-filesystem-credentials-dialog [data-js-action="close"], .notification-dialog-background' ).on( 'click', function() { - wp.updates.requestForCredentialsModalCancel(); - }); - - // Hide SSH fields when not selected. - $( '#request-filesystem-credentials-form input[name="connection_type"]' ).on( 'change', function() { - $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) ); - }); - - // Click handler for plugin updates in List Table view. - $( '.plugin-update-tr' ).on( 'click', '.update-link', function( e ) { - e.preventDefault(); - if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.updateLock ) { - wp.updates.requestFilesystemCredentials( e ); - } - var updateRow = $( e.target ).parents( '.plugin-update-tr' ); - // Return the user to the input box of the plugin's table row after closing the modal. - wp.updates.$elToReturnFocusToFromCredentialsModal = updateRow.prev().find( '.check-column input' ); - wp.updates.updatePlugin( updateRow.data( 'plugin' ), updateRow.data( 'slug' ) ); + wp.updates.requestForCredentialsModalClose(); } ); - $( '.plugin-card' ).on( 'click', '.update-now', function( e ) { - e.preventDefault(); - var $button = $( e.target ); + /** + * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal. + * + * @since 4.2.0 + */ + $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel ); + + /** + * Hide SSH fields when not selected. + * + * @since 4.2.0 + */ + $filesystemModal.on( 'change', 'input[name="connection_type"]', function() { + $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) ); + } ).change(); + + /** + * Handles events after the credential modal was closed. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + * @param {string} job The install/update.delete request. + */ + $document.on( 'credential-modal-cancel', function( event, job ) { + var $updatingMessage = $( '.updating-message' ), + $message, originalText; + + if ( 'import' === pagenow ) { + $updatingMessage.removeClass( 'updating-message' ); + } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $message = $( '.update-now.updating-message' ); + } else { + $message = $updatingMessage; + } + + if ( $message ) { + originalText = $message.data( 'originaltext' ); + + if ( 'undefined' === typeof originalText ) { + originalText = $( '

' ).html( $message.find( 'p' ).data( 'originaltext' ) ); + } + + $message + .removeClass( 'updating-message' ) + .html( originalText ); + } + + wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); + } ); + + /** + * Click handler for plugin updates in List Table view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { + var $message = $( event.target ), + $pluginRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the plugin's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); + wp.updates.updatePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin updates in plugin install view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.update-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); - // Do nothing while updating and when the button is disabled. if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { return; } - if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.updateLock ) { - wp.updates.requestFilesystemCredentials( e ); - } + wp.updates.maybeRequestFilesystemCredentials( event ); - wp.updates.updatePlugin( $button.data( 'plugin' ), $button.data( 'slug' ) ); + wp.updates.updatePlugin( { + plugin: $button.data( 'plugin' ), + slug: $button.data( 'slug' ) + } ); } ); - $( '#plugin_update_from_iframe' ).on( 'click' , function( e ) { - var target, job; + /** + * Click handler for plugin installs in plugin install view. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.install-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { + return; + } + + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + + $document.on( 'credential-modal-cancel', function() { + var $message = $( '.install-now.updating-message' ); + + $message + .removeClass( 'updating-message' ) + .text( wp.updates.l10n.installNow ); + + wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' ); + } ); + } + + wp.updates.installPlugin( { + slug: $button.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { + var $pluginRow = $( event.target ).parents( 'tr' ); + + event.preventDefault(); + + if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deletePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + + } ); + + /** + * Click handler for theme updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { + var $message = $( event.target ), + $themeRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the theme's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); + wp.updates.updateTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for theme deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { + var $themeRow = $( event.target ).parents( 'tr' ); + + event.preventDefault(); + + if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deleteTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Bulk action handler for plugins and themes. + * + * Handles both deletions and updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[type="submit"]', function( event ) { + var bulkAction = $( event.target ).siblings( 'select' ).val(), + itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), + success = 0, + error = 0, + errorMessages = [], + type, action; + + // Determine which type of item we're dealing with. + switch ( pagenow ) { + case 'plugins': + case 'plugins-network': + type = 'plugin'; + break; + + case 'themes-network': + type = 'theme'; + break; + + default: + window.console.error( 'The page "%s" is not white-listed for bulk action handling.', pagenow ); + return; + } + + // Bail if there were no items selected. + if ( ! itemsSelected.length ) { + event.preventDefault(); + $( 'html, body' ).animate( { scrollTop: 0 } ); + + return wp.updates.addAdminNotice( { + id: 'no-items-selected', + className: 'notice-error is-dismissible', + message: wp.updates.l10n.noItemsSelected + } ); + } + + // Determine the type of request we're dealing with. + switch ( bulkAction ) { + case 'update-selected': + action = bulkAction.replace( 'selected', type ); + break; + + case 'delete-selected': + if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) { + event.preventDefault(); + return; + } + + action = bulkAction.replace( 'selected', type ); + break; + + default: + window.console.error( 'Failed to identify bulk action: %s', bulkAction ); + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + event.preventDefault(); + + // Un-check the bulk checkboxes. + $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); + + // Find all the checkboxes which have been checked. + itemsSelected.each( function( index, element ) { + var $checkbox = $( element ), + $itemRow = $checkbox.parents( 'tr' ); + + // Un-check the box. + $checkbox.prop( 'checked', false ); + + // Only add update-able items to the update queue. + if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { + return; + } + + // Add it to the queue. + wp.updates.queue.push( { + action: action, + data: { + plugin: $itemRow.data( 'plugin' ), + slug: $itemRow.data( 'slug' ) + } + } ); + } ); + + // Display bulk notification for updates of any kind. + $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { + var $bulkActionNotice, itemName; + + if ( 'wp-' + response.update + '-update-success' === event.type ) { + success++; + } else { + itemName = response.pluginName ? response.pluginName : $( '[data-slug="' + response.slug + '"]' ).find( '.theme-title strong' ).text(); + + error++; + errorMessages.push( itemName + ': ' + response.errorMessage ); + } + + wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); + + wp.updates.addAdminNotice( { + id: 'bulk-action-notice', + successes: success, + errors: error, + errorMessages: errorMessages, + type: response.update + } ); + + $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { + $bulkActionNotice.find( 'ul' ).toggleClass( 'hidden' ); + } ); + + if ( error > 0 && ! wp.updates.queue.length ) { + $( 'html, body' ).animate( { scrollTop: 0 } ); + } + } ); + + // Reset admin notice template after #bulk-action-notice was added. + $document.on( 'wp-updates-notice-added', function() { + wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); + } ); + + // Check the queue, now that the event handlers have been added. + wp.updates.queueChecker(); + } ); + + /** + * Handles changes to the plugin search box on the new-plugin page, + * searching the repository dynamically. + * + * @since 4.6.0 + */ + $( 'input.wp-filter-search, .wp-filter input[name="s"]' ).on( 'keyup search', _.debounce( function() { + var $form = $( '#plugin-filter' ).empty(), + data = _.extend( { + _ajax_nonce: wp.updates.ajaxNonce, + s: $( '

' ).html( $( this ).val() ).text(), + tab: 'search', + type: $( '#typeselector' ).val() + }, { type: 'term' } ); + + if ( wp.updates.searchTerm === data.s ) { + return; + } else { + wp.updates.searchTerm = data.s; + } + + history.pushState( null, '', location.href.split( '?' )[0] + '?' + $.param( _.omit( data, '_ajax_nonce' ) ) ); + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + $( 'body' ).addClass( 'loading-content' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { + $( 'body' ).removeClass( 'loading-content' ); + $form.append( response.items ); + delete wp.updates.searchRequest; + } ); + }, 500 ) ); + + /** + * Handles changes to the plugin search box on the Installed Plugins screen, + * searching the plugin list dynamically. + * + * @since 4.6.0 + */ + $( '#plugin-search-input' ).on( 'keyup search', _.debounce( function() { + var data = { + _ajax_nonce: wp.updates.ajaxNonce, + s: $( '

' ).html( $( this ).val() ).text() + }; + + if ( wp.updates.searchTerm === data.s ) { + return; + } else { + wp.updates.searchTerm = data.s; + } + + history.pushState( null, '', location.href.split( '?' )[0] + '?s=' + data.s ); + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + + $bulkActionForm.empty(); + $( 'body' ).addClass( 'loading-content' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { + + // Can we just ditch this whole subtitle business? + var $subTitle = $( '' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', data.s ) ), + $oldSubTitle = $( '.wrap .subtitle' ); + + if ( ! data.s.length ) { + $oldSubTitle.remove(); + } else if ( $oldSubTitle.length ) { + $oldSubTitle.replaceWith( $subTitle ); + } else { + $( '.wrap h1' ).append( $subTitle ); + } + + $( 'body' ).removeClass( 'loading-content' ); + $bulkActionForm.append( response.items ); + delete wp.updates.searchRequest; + } ); + }, 500 ) ); + + /** + * Trigger a search event when the search form gets submitted. + * + * @since 4.6.0 + */ + $document.on( 'submit', '.search-plugins', function( event ) { + event.preventDefault(); + + $( 'input.wp-filter-search' ).trigger( 'search' ); + } ); + + /** + * Trigger a search event when the search type gets changed. + * + * @since 4.6.0 + */ + $( '#typeselector' ).on( 'change', function() { + $( 'input[name="s"]' ).trigger( 'search' ); + } ); + + /** + * Click handler for updating a plugin from the details modal on `plugin-install.php`. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + update; - target = window.parent == window ? null : window.parent, $.support.postMessage = !! window.postMessage; - if ( $.support.postMessage === false || target === null || window.parent.location.pathname.indexOf( 'update-core.php' ) !== -1 ) + if ( false === $.support.postMessage || null === target ) { return; + } - e.preventDefault(); + event.preventDefault(); - job = { - action: 'updatePlugin', - type: 'update-plugin', - data: { + update = { + action: 'update-plugin', + data: { plugin: $( this ).data( 'plugin' ), + slug: $( this ).data( 'slug' ) + } + }; + + target.postMessage( JSON.stringify( update ), window.location.origin ); + } ); + + /** + * Click handler for installing a plugin from the details modal on `plugin-install.php`. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_install_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + install; + + $.support.postMessage = !! window.postMessage; + + if ( false === $.support.postMessage || null === target ) { + return; + } + + event.preventDefault(); + + install = { + action: 'install-plugin', + data: { slug: $( this ).data( 'slug' ) } }; - target.postMessage( JSON.stringify( job ), window.location.origin ); - }); + target.postMessage( JSON.stringify( install ), window.location.origin ); + } ); + /** + * Handles postMessage events. + * + * @since 4.2.0 + * @since 4.6.0 Switched `update-plugin` action to use the queue. + * + * @param {Event} event Event interface. + */ + $( window ).on( 'message', function( event ) { + var originalEvent = event.originalEvent, + expectedOrigin = document.location.protocol + '//' + document.location.hostname, + message; + + if ( originalEvent.origin !== expectedOrigin ) { + return; + } + + message = $.parseJSON( originalEvent.data ); + + if ( 'undefined' === typeof message.action ) { + return; + } + + switch ( message.action ) { + + // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. + case 'decrementUpdateCount': + /** @property {string} message.upgradeType */ + wp.updates.decrementCount( message.upgradeType ); + break; + + case 'install-plugin': + case 'update-plugin': + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + window.tb_remove(); + /* jscs:enable */ + + message.data = wp.updates._addCallbacks( message.data, message.action ); + + wp.updates.queue.push( message ); + wp.updates.queueChecker(); + break; + } + } ); + + /** + * Adds a callback to display a warning before leaving the page. + * + * @since 4.2.0 + */ + $( window ).on( 'beforeunload', wp.updates.beforeunload ); } ); - - $( window ).on( 'message', function( e ) { - var event = e.originalEvent, - message, - loc = document.location, - expectedOrigin = loc.protocol + '//' + loc.hostname; - - if ( event.origin !== expectedOrigin ) { - return; - } - - message = $.parseJSON( event.data ); - - if ( typeof message.action === 'undefined' ) { - return; - } - - switch (message.action){ - case 'decrementUpdateCount' : - wp.updates.decrementCount( message.upgradeType ); - break; - case 'updatePlugin' : - tb_remove(); - - wp.updates.updateQueue.push( message ); - wp.updates.queueChecker(); - break; - } - - } ); - - $( window ).on( 'beforeunload', wp.updates.beforeunload ); - -})( jQuery, window.wp, window.pagenow, window.ajaxurl ); +})( jQuery, window.wp, _.extend( window._wpUpdatesSettings, window._wpUpdatesItemCounts || {} ) ); diff --git a/src/wp-admin/network/themes.php b/src/wp-admin/network/themes.php index 1b9830ccce..9a13bca3a7 100644 --- a/src/wp-admin/network/themes.php +++ b/src/wp-admin/network/themes.php @@ -228,6 +228,7 @@ get_current_screen()->set_screen_reader_content( array( $title = __('Themes'); $parent_file = 'themes.php'; +wp_enqueue_script( 'updates' ); wp_enqueue_script( 'theme-preview' ); require_once(ABSPATH . 'wp-admin/admin-header.php'); @@ -287,7 +288,7 @@ if ( 'broken' == $status ) echo '

' . __( 'The following themes are installed but incomplete.' ) . '

'; ?> -
+ @@ -297,4 +298,8 @@ if ( 'broken' == $status )
+ + views(); ?> - + search_box( __( 'Search Installed Plugins' ), 'plugin' ); ?> @@ -519,9 +519,12 @@ do_action( 'pre_current_active_plugins', $plugins['all'] ); display(); ?> + <# } #> -
+
+ +

{{ data.name }}

- - + <# if ( data.installed ) { #> + <# if ( data.activate_url ) { #> + + <# } #> + <# if ( data.customize_url ) { #> + + <# } else { #> + + <# } #> + <# } else { #> + + + <# } #>
<# if ( data.installed ) { #> -
+

<# } #>
+ + +
+

Update now' ); ?>

+
+ +
@@ -276,10 +284,6 @@ foreach ( $themes as $theme ) : - - -
- @@ -368,13 +372,23 @@ $can_install = current_user_can( 'install_themes' ); <# } else { #>
<# } #> + + <# if ( data.hasUpdate ) { #> +

Update now' ); ?>

+ <# } #> + -
+
+ +
<# if ( data.active ) { #>

Active: %s' ), '{{{ data.name }}}' ); ?>

@@ -383,21 +397,15 @@ $can_install = current_user_can( 'install_themes' ); <# } #>
- - <# if ( data.active ) { #> - <# if ( data.actions.customize ) { #> - + <# if ( data.active ) { #> + <# if ( data.actions.customize ) { #> + + <# } #> + <# } else { #> + + <# } #> - <# } else { #> - - - <# } #> -
- - <# if ( data.hasUpdate ) { #> -
- <# } #> -\n" "Language-Team: LANGUAGE \n" diff --git a/src/wp-includes/js/wp-util.js b/src/wp-includes/js/wp-util.js index 24991d0b02..11a830f120 100644 --- a/src/wp-includes/js/wp-util.js +++ b/src/wp-includes/js/wp-util.js @@ -48,8 +48,9 @@ window.wp = window.wp || {}; * * Sends a POST request to WordPress. * - * @param {string} action The slug of the action to fire in WordPress. - * @param {object} data The data to populate $_POST with. + * @param {(string|object)} action The slug of the action to fire in WordPress or options passed + * to jQuery.ajax. + * @param {object=} data Optional. The data to populate $_POST with. * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ @@ -64,8 +65,9 @@ window.wp = window.wp || {}; * * Sends a POST request to WordPress. * - * @param {string} action The slug of the action to fire in WordPress. - * @param {object} options The options passed to jQuery.ajax. + * @param {(string|object)} action The slug of the action to fire in WordPress or options passed + * to jQuery.ajax. + * @param {object=} options Optional. The options passed to jQuery.ajax. * @return {$.promise} A jQuery promise that represents the request, * decorated with an abort() method. */ diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 1b2096149a..e111842945 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -595,25 +595,62 @@ function wp_default_scripts( &$scripts ) { did_action( 'init' ) && $scripts->localize( 'updates', '_wpUpdatesSettings', array( 'ajax_nonce' => wp_create_nonce( 'updates' ), 'l10n' => array( - 'updating' => __( 'Updating...' ), // no ellipsis - 'updated' => __( 'Updated!' ), - 'updateFailedShort' => __( 'Update Failed!' ), + /* translators: %s: Search string */ + 'searchResults' => __( 'Search results for “%s”' ), + 'noPlugins' => __( 'You do not appear to have any plugins available at this time.' ), + 'noItemsSelected' => __( 'Please select at least one item to perform this action on.' ), + 'updating' => __( 'Updating...' ), // No ellipsis. + 'updated' => __( 'Updated!' ), + 'update' => __( 'Update' ), + 'updateNow' => __( 'Update Now' ), + 'updateFailedShort' => __( 'Update Failed!' ), /* translators: Error string for a failed update */ - 'updateFailed' => __( 'Update Failed: %s' ), + 'updateFailed' => __( 'Update Failed: %s' ), /* translators: Plugin name and version */ - 'updatingLabel' => __( 'Updating %s...' ), // no ellipsis + 'updatingLabel' => __( 'Updating %s...' ), // No ellipsis. /* translators: Plugin name and version */ - 'updatedLabel' => __( '%s updated!' ), + 'updatedLabel' => __( '%s updated!' ), /* translators: Plugin name and version */ - 'updateFailedLabel' => __( '%s update failed' ), + 'updateFailedLabel' => __( '%s update failed' ), /* translators: JavaScript accessible string */ - 'updatingMsg' => __( 'Updating... please wait.' ), // no ellipsis + 'updatingMsg' => __( 'Updating... please wait.' ), // No ellipsis. /* translators: JavaScript accessible string */ - 'updatedMsg' => __( 'Update completed successfully.' ), + 'updatedMsg' => __( 'Update completed successfully.' ), /* translators: JavaScript accessible string */ - 'updateCancel' => __( 'Update canceled.' ), - 'beforeunload' => __( 'Plugin updates may not complete if you navigate away from this page.' ), - ) + 'updateCancel' => __( 'Update canceled.' ), + 'beforeunload' => __( 'Updates may not complete if you navigate away from this page.' ), + 'installNow' => __( 'Install Now' ), + 'installing' => __( 'Installing...' ), + 'installed' => __( 'Installed!' ), + 'installFailedShort' => __( 'Install Failed!' ), + /* translators: Error string for a failed installation */ + 'installFailed' => __( 'Installation failed: %s' ), + /* translators: Plugin/Theme name and version */ + 'installingLabel' => __( 'Installing %s...' ), // no ellipsis + /* translators: Plugin/Theme name and version */ + 'installedLabel' => __( '%s installed!' ), + /* translators: Plugin/Theme name and version */ + 'installFailedLabel' => __( '%s installation failed' ), + 'installingMsg' => __( 'Installing... please wait.' ), + 'installedMsg' => __( 'Installation completed successfully.' ), + /* translators: Activation URL */ + 'importerInstalledMsg' => __( 'Importer installed successfully. Activate plugin & run importer' ), + /* translators: %s: Theme name */ + 'aysDelete' => __( 'Are you sure you want to delete %s?' ), + /* translators: %s: Plugin name */ + 'aysDeleteUninstall' => __( 'Are you sure you want to delete %s and its data?' ), + 'aysBulkDelete' => __( 'Are you sure you want to delete the selected plugins and their data?' ), + 'aysBulkDeleteThemes' => __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ), + 'deleting' => __( 'Deleting...' ), + /* translators: %s: Error string for a failed deletion */ + 'deleteFailed' => __( 'Deletion failed: %s' ), + 'deleted' => __( 'Deleted!' ), + 'livePreview' => __( 'Live Preview' ), + 'activatePlugin' => is_network_admin() ? __( 'Network Activate' ) : __( 'Activate' ), + 'activateTheme' => is_network_admin() ? __( 'Network Enable' ) : __( 'Activate' ), + 'activateImporter' => __( 'Activate importer' ), + 'unknownError' => __( 'An unknown error occured' ), + ), ) ); $scripts->add( 'farbtastic', '/wp-admin/js/farbtastic.js', array('jquery'), '1.2' ); diff --git a/tests/qunit/fixtures/updates.js b/tests/qunit/fixtures/updates.js new file mode 100644 index 0000000000..657ef73308 --- /dev/null +++ b/tests/qunit/fixtures/updates.js @@ -0,0 +1,48 @@ +window._wpUpdatesSettings = { + 'ajax_nonce': '719b10f05d', + 'l10n': { + 'searchResults': 'Search results for “%s”', + 'noPlugins': 'You do not appear to have any plugins available at this time.', + 'noItemsSelected': 'Please select at least one item to perform this action on.', + 'updating': 'Updating...', + 'updated': 'Updated!', + 'update': 'Update', + 'updateNow': 'Update Now', + 'updateFailedShort': 'Update Failed!', + 'updateFailed': 'Update Failed: %s', + 'updatingLabel': 'Updating %s...', + 'updatedLabel': '%s updated!', + 'updateFailedLabel': '%s update failed', + 'updatingMsg': 'Updating... please wait.', + 'updatedMsg': 'Update completed successfully.', + 'updateCancel': 'Update canceled.', + 'beforeunload': 'Updates may not complete if you navigate away from this page.', + 'installNow': 'Install Now', + 'installing': 'Installing...', + 'installed': 'Installed!', + 'installFailedShort': 'Install Failed!', + 'installFailed': 'Installation failed: %s', + 'installingLabel': 'Installing %s...', // No ellipsis + 'installedLabel': '%s installed!', + 'installFailedLabel': '%s installation failed', + 'installingMsg': 'Installing... please wait.', + 'installedMsg': 'Installation completed successfully.', + 'importerInstalledMsg': 'Importer installed successfully. Activate plugin & run importer', + 'aysDelete': 'Are you sure you want to delete %s?', + 'aysDeleteUninstall': 'Are you sure you want to delete %s and its data?', + 'aysBulkDelete': 'Are you sure you want to delete the selected plugins and their data?', + 'aysBulkDeleteThemes': 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?', + 'deleting': 'Deleting...', + 'deleteFailed': 'Deletion failed: %s', + 'deleted': 'Deleted!', + 'livePreview': 'Live Preview', + 'activatePlugin': 'Activate', + 'activateTheme': 'Activate', + 'activateImporter': 'Activate importer', + 'unknownError': 'An unknown error occured' + } +}; +window._wpUpdatesItemCounts = { + plugins: {}, + totals: {} +}; diff --git a/tests/qunit/index.html b/tests/qunit/index.html index ad8a4ebaa7..ff8ae004e4 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -10,6 +10,13 @@ + @@ -482,5 +489,42 @@ + + + + diff --git a/tests/qunit/wp-admin/js/updates.js b/tests/qunit/wp-admin/js/updates.js new file mode 100644 index 0000000000..a8c32219c3 --- /dev/null +++ b/tests/qunit/wp-admin/js/updates.js @@ -0,0 +1,179 @@ +/*global QUnit, wp, sinon */ +jQuery( function( $ ) { + + QUnit.module( 'wp.updates' ); + + QUnit.test( 'Initially, the update lock should be false', function( assert ) { + assert.strictEqual( wp.updates.ajaxLocked, false ); + }); + + QUnit.test( 'The nonce should be set correctly', function( assert ) { + assert.equal( wp.updates.ajaxNonce, window._wpUpdatesSettings.ajax_nonce ); + }); + + QUnit.test( 'decrementCount correctly decreases the update number', function( assert ) { + var menuItemCount = $( '#menu-plugins' ).find( '.plugin-count' ).eq( 0 ).text(); + var screenReaderItemCount = $( '#wp-admin-bar-updates' ).find( '.screen-reader-text' ).text(); + var adminItemCount = $( '#wp-admin-bar-updates' ).find( '.ab-label' ).text(); + assert.equal( menuItemCount, 2, 'Intial value is correct' ); + assert.equal( screenReaderItemCount, '2 Plugin Updates', 'Intial value is correct' ); + assert.equal( adminItemCount, 2, 'Intial value is correct' ); + + wp.updates.decrementCount( 'plugin' ); + + // Re-read these values + menuItemCount = $( '#menu-plugins' ).find( '.plugin-count' ).eq( 0 ).text(); + screenReaderItemCount = $( '#wp-admin-bar-updates' ).find( '.screen-reader-text' ).text(); + adminItemCount = $( '#wp-admin-bar-updates' ).find( '.ab-label' ).text(); + assert.equal( menuItemCount, 1 ); + + // @todo: Update screen reader count. + // Should the screenReader count change? Is that announced to the user? + // assert.equal( screenReaderItemCount, '1 Plugin Update' ); + assert.equal( adminItemCount, 1 ); + }); + + QUnit.test( '`beforeunload` should only fire when locked', function( assert ) { + wp.updates.ajaxLocked = false; + assert.notOk( wp.updates.beforeunload(), '`beforeunload` should not fire.' ); + wp.updates.ajaxLocked = true; + assert.equal( wp.updates.beforeunload(), window._wpUpdatesSettings.l10n.beforeunload, '`beforeunload` should equal the localized `beforeunload` string.' ); + wp.updates.ajaxLocked = false; + }); + + // FTP creds... exist? + // Admin notice? + + QUnit.module( 'wp.updates.plugins', { + beforeEach: function() { + this.oldPagenow = window.pagenow; + window.pagenow = 'plugins'; + sinon.spy( jQuery, 'ajax' ); + }, + afterEach: function() { + window.pagenow = this.oldPagenow; + wp.updates.ajaxLocked = false; + wp.updates.queue = []; + jQuery.ajax.restore(); + } + } ); + + QUnit.test( 'Update lock is set when plugins are updating', function( assert ) { + wp.updates.updatePlugin( { + plugin: 'test/test.php', + slug: 'test' + } ); + assert.strictEqual( wp.updates.ajaxLocked, true ); + }); + + QUnit.test( 'Plugins are queued when the lock is set', function( assert ) { + var value = [ + { + action: 'update-plugin', + data: { + plugin: 'test/test.php', + slug: 'test', + success: null, + error: null + } + } + ]; + + wp.updates.ajaxLocked = true; + wp.updates.updatePlugin( { + plugin: 'test/test.php', + slug: 'test', + success: null, + error: null + } ); + + assert.deepEqual( wp.updates.queue, value ); + }); + + QUnit.test( 'If plugins are installing (lock is set), the beforeUnload function should fire', function( assert ) { + wp.updates.updatePlugin( { + plugin: 'test/test.php', + slug: 'test' + } ); + assert.equal( wp.updates.beforeunload(), window._wpUpdatesSettings.l10n.beforeunload ); + } ); + + QUnit.test( 'Starting a plugin update should call the update API', function( assert ) { + wp.updates.updatePlugin( { + plugin: 'test/test.php', + slug: 'test' + } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'update-plugin' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'test' ); + } ); + QUnit.test( 'Installing a plugin should call the API', function( assert ) { + wp.updates.installPlugin( { slug: 'jetpack' } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'install-plugin' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'jetpack' ); + } ); + QUnit.test( 'Deleting a plugin should call the API', function( assert ) { + wp.updates.deletePlugin( { slug: 'jetpack', plugin: 'jetpack/jetpack.php' } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'delete-plugin' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'jetpack' ); + } ); + + // QUnit.test( 'A successful update changes the message?', function( assert ) {} ); + // QUnit.test( 'A failed update changes the message?', function( assert ) {} ); + + QUnit.module( 'wp.updates.themes', { + beforeEach: function() { + this.oldPagenow = window.pagenow; + window.pagenow = 'themes'; + sinon.spy( jQuery, 'ajax' ); + }, + afterEach: function() { + window.pagenow = this.oldPagenow; + wp.updates.ajaxLocked = false; + wp.updates.queue = []; + jQuery.ajax.restore(); + } + } ); + + QUnit.test( 'Update lock is set when themes are updating', function( assert ) { + wp.updates.updateTheme( 'twentyeleven' ); + assert.strictEqual( wp.updates.ajaxLocked, true ); + }); + + QUnit.test( 'If themes are installing (lock is set), the beforeUnload function should fire', function( assert ) { + wp.updates.updateTheme( { slug: 'twentyeleven' } ); + assert.equal( wp.updates.beforeunload(), window._wpUpdatesSettings.l10n.beforeunload ); + } ); + + QUnit.test( 'Starting a theme update should call the update API', function( assert ) { + wp.updates.updateTheme( { slug: 'twentyeleven' } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'update-theme' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'twentyeleven' ); + } ); + + QUnit.test( 'Installing a theme should call the API', function( assert ) { + wp.updates.installTheme( { slug: 'twentyeleven' } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'install-theme' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'twentyeleven' ); + } ); + + QUnit.test( 'Deleting a theme should call the API', function( assert ) { + wp.updates.deleteTheme( { slug: 'twentyeleven' } ); + assert.ok( jQuery.ajax.calledOnce ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].url, '/wp-admin/admin-ajax.php' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'delete-theme' ); + assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'twentyeleven' ); + } ); + + // QUnit.test( 'A successful update changes the message?', function( assert ) {} ); + // QUnit.test( 'A failed update changes the message?', function( assert ) {} ); +});