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 ) {} ); +});