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 ) ); ?>
-
num_ratings ), number_format_i18n( $api->num_ratings ) ); ?>
+
+ $api->rating, 'type' => 'percent', 'number' => $api->num_ratings ) ); ?>
+
num_ratings ), number_format_i18n( $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( '
' );
+ .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( '
' );
+
+ $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( '' );
+ };
+
+ /**
+ * 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.' ) . '
';
?>
-
+
+
views(); ?>
-
@@ -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 ) { #>
-
+
<# } #>
+
+
+
+
+
@@ -276,10 +284,6 @@ foreach ( $themes as $theme ) :
-
-
-
-
@@ -368,13 +372,23 @@ $can_install = current_user_can( 'install_themes' );
<# } else { #>
<# } #>
+
+ <# if ( data.hasUpdate ) { #>
+
+ <# } #>
+
-
+
+
+
<# 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 @@
+
+
+
+