| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/updates.js 3 */ 4 5 /* global pagenow, _wpThemeSettings */ 6 7 /** 8 * Provides functions for ajaxified updates, deletions and installs inside the WordPress admin. 9 * 10 * @version 4.2.0 11 * 12 * @param {JQueryStatic} $ The jQuery object. 13 * @param {Object} wp The WordPress global object. 14 * @param {Object} settings WP Updates settings. 15 * @param {string} settings.ajax_nonce Ajax nonce. 16 * @param {Object} settings.plugins Base names of plugins in their different states. 17 * @param {Array} settings.plugins.all Base names of all plugins. 18 * @param {Array} settings.plugins.active Base names of active plugins. 19 * @param {Array} settings.plugins.inactive Base names of inactive plugins. 20 * @param {Array} settings.plugins.upgrade Base names of plugins with updates available. 21 * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins. 22 * @param {Array} settings.plugins['auto-update-enabled'] Base names of plugins set to auto-update. 23 * @param {Array} settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update. 24 * @param {Object} settings.themes Slugs of themes in their different states. 25 * @param {Array} settings.themes.all Slugs of all themes. 26 * @param {Array} settings.themes.upgrade Slugs of themes with updates available. 27 * @param {Array} settings.themes.disabled Slugs of disabled themes. 28 * @param {Array} settings.themes['auto-update-enabled'] Slugs of themes set to auto-update. 29 * @param {Array} settings.themes['auto-update-disabled'] Slugs of themes set to not auto-update. 30 * @param {Object} settings.totals Combined information for available update counts. 31 * @param {number} settings.totals.count Holds the amount of available updates. 32 */ 33 (function( $, wp, settings ) { 34 var $document = $( document ), 35 __ = wp.i18n.__, 36 _x = wp.i18n._x, 37 _n = wp.i18n._n, 38 _nx = wp.i18n._nx, 39 sprintf = wp.i18n.sprintf; 40 41 wp = wp || {}; 42 43 /** 44 * The WP Updates object. 45 * 46 * @since 4.2.0 47 * 48 * @namespace wp.updates 49 */ 50 wp.updates = {}; 51 52 /** 53 * Removed in 5.5.0, needed for back-compatibility. 54 * 55 * @since 4.2.0 56 * @deprecated 5.5.0 57 * 58 * @type {Object} 59 */ 60 wp.updates.l10n = { 61 searchResults: '', 62 searchResultsLabel: '', 63 noPlugins: '', 64 noItemsSelected: '', 65 updating: '', 66 pluginUpdated: '', 67 themeUpdated: '', 68 update: '', 69 updateNow: '', 70 pluginUpdateNowLabel: '', 71 updateFailedShort: '', 72 updateFailed: '', 73 pluginUpdatingLabel: '', 74 pluginUpdatedLabel: '', 75 pluginUpdateFailedLabel: '', 76 updatingMsg: '', 77 updatedMsg: '', 78 updateCancel: '', 79 beforeunload: '', 80 installNow: '', 81 pluginInstallNowLabel: '', 82 installing: '', 83 pluginInstalled: '', 84 themeInstalled: '', 85 installFailedShort: '', 86 installFailed: '', 87 pluginInstallingLabel: '', 88 themeInstallingLabel: '', 89 pluginInstalledLabel: '', 90 themeInstalledLabel: '', 91 pluginInstallFailedLabel: '', 92 themeInstallFailedLabel: '', 93 installingMsg: '', 94 installedMsg: '', 95 importerInstalledMsg: '', 96 aysDelete: '', 97 aysDeleteUninstall: '', 98 aysBulkDelete: '', 99 aysBulkDeleteThemes: '', 100 deleting: '', 101 deleteFailed: '', 102 pluginDeleted: '', 103 themeDeleted: '', 104 livePreview: '', 105 activatePlugin: '', 106 activateTheme: '', 107 activatePluginLabel: '', 108 activateThemeLabel: '', 109 activateImporter: '', 110 activateImporterLabel: '', 111 unknownError: '', 112 connectionError: '', 113 nonceError: '', 114 pluginsFound: '', 115 noPluginsFound: '', 116 autoUpdatesEnable: '', 117 autoUpdatesEnabling: '', 118 autoUpdatesEnabled: '', 119 autoUpdatesDisable: '', 120 autoUpdatesDisabling: '', 121 autoUpdatesDisabled: '', 122 autoUpdatesError: '' 123 }; 124 125 wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' ); 126 127 /** 128 * User nonce for ajax calls. 129 * 130 * @since 4.2.0 131 * 132 * @type {string} 133 */ 134 wp.updates.ajaxNonce = settings.ajax_nonce; 135 136 /** 137 * Current search term. 138 * 139 * @since 4.6.0 140 * 141 * @type {string} 142 */ 143 wp.updates.searchTerm = ''; 144 145 /** 146 * Minimum number of characters before an ajax search is fired. 147 * 148 * @since 6.7.0 149 * 150 * @type {number} 151 */ 152 wp.updates.searchMinCharacters = 2; 153 154 /** 155 * Whether filesystem credentials need to be requested from the user. 156 * 157 * @since 4.2.0 158 * 159 * @type {boolean} 160 */ 161 wp.updates.shouldRequestFilesystemCredentials = false; 162 163 /** 164 * Filesystem credentials to be packaged along with the request. 165 * 166 * @since 4.2.0 167 * @since 4.6.0 Added `available` property to indicate whether credentials have been provided. 168 * 169 * @type {Object} 170 * @property {Object} filesystemCredentials Holds all filesystem credentials. 171 * @property {Object} filesystemCredentials.ftp Holds FTP credentials. 172 * @property {string} filesystemCredentials.ftp.host FTP host. Default empty string. 173 * @property {string} filesystemCredentials.ftp.username FTP user name. Default empty string. 174 * @property {string} filesystemCredentials.ftp.password FTP password. Default empty string. 175 * @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'. 176 * Default empty string. 177 * @property {Object} filesystemCredentials.ssh Holds SSH credentials. 178 * @property {string} filesystemCredentials.ssh.publicKey The public key. Default empty string. 179 * @property {string} filesystemCredentials.ssh.privateKey The private key. Default empty string. 180 * @property {string} filesystemCredentials.fsNonce Filesystem credentials form nonce. 181 * @property {boolean} filesystemCredentials.available Whether filesystem credentials have been provided. 182 * Default 'false'. 183 */ 184 wp.updates.filesystemCredentials = { 185 ftp: { 186 host: '', 187 username: '', 188 password: '', 189 connectionType: '' 190 }, 191 ssh: { 192 publicKey: '', 193 privateKey: '' 194 }, 195 fsNonce: '', 196 available: false 197 }; 198 199 /** 200 * Whether we're waiting for an Ajax request to complete. 201 * 202 * @since 4.2.0 203 * @since 4.6.0 More accurately named `ajaxLocked`. 204 * 205 * @type {boolean} 206 */ 207 wp.updates.ajaxLocked = false; 208 209 /** 210 * Admin notice template. 211 * 212 * @since 4.6.0 213 * 214 * @type {Function} 215 */ 216 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); 217 218 /** 219 * Update queue. 220 * 221 * If the user tries to update a plugin while an update is 222 * already happening, it can be placed in this queue to perform later. 223 * 224 * @since 4.2.0 225 * @since 4.6.0 More accurately named `queue`. 226 * 227 * @type {Array.object} 228 */ 229 wp.updates.queue = []; 230 231 /** 232 * Holds a jQuery reference to return focus to when exiting the request credentials modal. 233 * 234 * @since 4.2.0 235 * 236 * @type {jQuery} 237 */ 238 wp.updates.$elToReturnFocusToFromCredentialsModal = undefined; 239 240 /** 241 * Adds or updates an admin notice. 242 * 243 * @since 4.6.0 244 * 245 * @param {Object} data The data for the admin notice. 246 * @param {*} [data.selector] Optional. Selector of an element to be replaced with the admin notice. 247 * @param {string} [data.id] Optional. Unique id that will be used as the notice's id attribute. 248 * @param {string} [data.className] Optional. Class names that will be used in the admin notice. 249 * @param {string} [data.message] Optional. The message displayed in the notice. 250 * @param {number} [data.successes] Optional. The amount of successful operations. 251 * @param {number} [data.errors] Optional. The amount of failed operations. 252 * @param {Array} [data.errorMessages] Optional. Error messages of failed operations. 253 * 254 */ 255 wp.updates.addAdminNotice = function( data ) { 256 var $notice = $( document ).find( data.selector ), 257 $headerEnd = $( '.wp-header-end' ), 258 $adminNotice; 259 260 delete data.selector; 261 $adminNotice = wp.updates.adminNotice( data ); 262 263 // Check if this admin notice already exists. 264 if ( ! $notice.length ) { 265 $notice = $( '#' + data.id ); 266 } 267 268 if ( $notice.length ) { 269 $notice.replaceWith( $adminNotice ); 270 } else if ( $headerEnd.length ) { 271 $headerEnd.after( $adminNotice ); 272 } else { 273 if ( 'customize' === pagenow ) { 274 $( '.customize-themes-notifications' ).append( $adminNotice ); 275 } else { 276 $( '.wrap' ).find( '> h1' ).after( $adminNotice ); 277 } 278 } 279 280 $document.trigger( 'wp-updates-notice-added' ); 281 }; 282 283 /** 284 * Handles Ajax requests to WordPress. 285 * 286 * @since 4.6.0 287 * 288 * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc). 289 * @param {Object} data Data that needs to be passed to the ajax callback. 290 * @return {$.promise} A jQuery promise that represents the request, 291 * decorated with an abort() method. 292 */ 293 wp.updates.ajax = function( action, data ) { 294 var options = {}; 295 296 if ( wp.updates.ajaxLocked ) { 297 wp.updates.queue.push( { 298 action: action, 299 data: data 300 } ); 301 302 // Return a Deferred object so callbacks can always be registered. 303 return $.Deferred(); 304 } 305 306 wp.updates.ajaxLocked = true; 307 308 if ( data.success ) { 309 options.success = data.success; 310 delete data.success; 311 } 312 313 if ( data.error ) { 314 options.error = data.error; 315 delete data.error; 316 } 317 318 options.data = _.extend( data, { 319 action: action, 320 _ajax_nonce: wp.updates.ajaxNonce, 321 _fs_nonce: wp.updates.filesystemCredentials.fsNonce, 322 username: wp.updates.filesystemCredentials.ftp.username, 323 password: wp.updates.filesystemCredentials.ftp.password, 324 hostname: wp.updates.filesystemCredentials.ftp.hostname, 325 connection_type: wp.updates.filesystemCredentials.ftp.connectionType, 326 public_key: wp.updates.filesystemCredentials.ssh.publicKey, 327 private_key: wp.updates.filesystemCredentials.ssh.privateKey 328 } ); 329 330 return wp.ajax.send( options ).always( wp.updates.ajaxAlways ); 331 }; 332 333 /** 334 * Actions performed after every Ajax request. 335 * 336 * @since 4.6.0 337 * 338 * @param {Object} response The response object from the Ajax request. 339 * @param {Array} [response.debug] Optional. Debug information. 340 * @param {string} [response.errorCode] Optional. Error code for an error that occurred. 341 */ 342 wp.updates.ajaxAlways = function( response ) { 343 if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) { 344 wp.updates.ajaxLocked = false; 345 wp.updates.queueChecker(); 346 } 347 348 if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) { 349 _.map( response.debug, function( message ) { 350 // Remove all HTML tags and write a message to the console. 351 window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) ); 352 } ); 353 } 354 }; 355 356 /** 357 * Refreshes update counts everywhere on the screen. 358 * 359 * @since 4.7.0 360 */ 361 wp.updates.refreshCount = function() { 362 var $adminBarUpdates = $( '#wp-admin-bar-updates' ), 363 $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), 364 $pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ), 365 $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ), 366 itemCount; 367 368 $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total ); 369 $adminBarUpdates.find( '.updates-available-text' ).text( 370 sprintf( 371 /* translators: %s: Total number of updates available. */ 372 _n( '%s update available', '%s updates available', settings.totals.counts.total ), 373 settings.totals.counts.total 374 ) 375 ); 376 377 // Remove the update count from the toolbar if it's zero. 378 if ( 0 === settings.totals.counts.total ) { 379 $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove(); 380 } 381 382 // Update the "Updates" menu item. 383 $dashboardNavMenuUpdateCount.each( function( index, element ) { 384 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total ); 385 } ); 386 if ( settings.totals.counts.total > 0 ) { 387 $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total ); 388 } else { 389 $dashboardNavMenuUpdateCount.remove(); 390 } 391 392 // Update the "Plugins" menu item. 393 $pluginsNavMenuUpdateCount.each( function( index, element ) { 394 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins ); 395 } ); 396 if ( settings.totals.counts.total > 0 ) { 397 $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins ); 398 } else { 399 $pluginsNavMenuUpdateCount.remove(); 400 } 401 402 // Update the "Appearance" menu item. 403 $appearanceNavMenuUpdateCount.each( function( index, element ) { 404 element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes ); 405 } ); 406 if ( settings.totals.counts.total > 0 ) { 407 $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes ); 408 } else { 409 $appearanceNavMenuUpdateCount.remove(); 410 } 411 412 // Update list table filter navigation. 413 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 414 itemCount = settings.totals.counts.plugins; 415 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { 416 itemCount = settings.totals.counts.themes; 417 } 418 419 if ( itemCount > 0 ) { 420 $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); 421 } else { 422 $( '.subsubsub .upgrade' ).remove(); 423 $( '.subsubsub li:last' ).html( function() { return $( this ).children(); } ); 424 } 425 }; 426 427 /** 428 * Sends a message from a modal to the main screen to update buttons in plugin cards. 429 * 430 * @since 6.5.0 431 * 432 * @param {Object} data An object of data to use for the button. 433 * @param {string} data.slug The plugin's slug. 434 * @param {string} data.text The text to use for the button. 435 * @param {string} data.ariaLabel The value for the button's aria-label attribute. An empty string removes the attribute. 436 * @param {string} [data.status] Optional. An identifier for the status. 437 * @param {string} [data.removeClasses] Optional. A space-separated list of classes to remove from the button. 438 * @param {string} [data.addClasses] Optional. A space-separated list of classes to add to the button. 439 * @param {string} [data.href] Optional. The button's URL. 440 * @param {string} [data.pluginName] Optional. The plugin's name. 441 * @param {string} [data.plugin] Optional. The plugin file, relative to the plugins directory. 442 */ 443 wp.updates.setCardButtonStatus = function( data ) { 444 var target = window.parent === window ? null : window.parent; 445 446 $.support.postMessage = !! window.postMessage; 447 if ( false !== $.support.postMessage && null !== target && -1 === window.parent.location.pathname.indexOf( 'index.php' ) ) { 448 target.postMessage( JSON.stringify( data ), window.location.origin ); 449 } 450 }; 451 452 /** 453 * Decrements the update counts throughout the various menus. 454 * 455 * This includes the toolbar, the "Updates" menu item and the menu items 456 * for plugins and themes. 457 * 458 * @since 3.9.0 459 * 460 * @param {string} type The type of item that was updated or deleted. 461 * Can be 'plugin', 'theme'. 462 */ 463 wp.updates.decrementCount = function( type ) { 464 settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 ); 465 466 if ( 'plugin' === type ) { 467 settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 ); 468 } else if ( 'theme' === type ) { 469 settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 ); 470 } 471 472 wp.updates.refreshCount( type ); 473 }; 474 475 /** 476 * Sends an Ajax request to the server to update a plugin. 477 * 478 * @since 4.2.0 479 * @since 4.6.0 More accurately named `updatePlugin`. 480 * 481 * @param {Object} args Arguments. 482 * @param {string} args.plugin Plugin basename. 483 * @param {string} args.slug Plugin slug. 484 * @param {updatePluginSuccess} [args.success] Optional. Success callback. Default: wp.updates.updatePluginSuccess 485 * @param {updatePluginError} [args.error] Optional. Error callback. Default: wp.updates.updatePluginError 486 * @return {$.promise} A jQuery promise that represents the request, 487 * decorated with an abort() method. 488 */ 489 wp.updates.updatePlugin = function( args ) { 490 var $updateRow, $card, $message, message, 491 $adminBarUpdates = $( '#wp-admin-bar-updates' ), 492 buttonText = __( 'Updating...' ), 493 isPluginInstall = 'plugin-install' === pagenow || 'plugin-install-network' === pagenow; 494 495 args = _.extend( { 496 success: wp.updates.updatePluginSuccess, 497 error: wp.updates.updatePluginError 498 }, args ); 499 500 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 501 $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' ); 502 $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); 503 message = sprintf( 504 /* translators: %s: Plugin name and version. */ 505 _x( 'Updating %s...', 'plugin' ), 506 $updateRow.find( '.plugin-title strong' ).text() 507 ); 508 } else if ( isPluginInstall ) { 509 $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ); 510 $message = $card.find( '.update-now' ).addClass( 'updating-message' ); 511 message = sprintf( 512 /* translators: %s: Plugin name and version. */ 513 _x( 'Updating %s...', 'plugin' ), 514 $message.data( 'name' ) 515 ); 516 517 // Remove previous error messages, if any. 518 $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); 519 } 520 521 $adminBarUpdates.addClass( 'spin' ); 522 523 if ( $message.html() !== __( 'Updating...' ) ) { 524 $message.data( 'originaltext', $message.html() ); 525 } 526 527 $message 528 .attr( 'aria-label', message ) 529 .text( buttonText ); 530 531 $document.trigger( 'wp-plugin-updating', args ); 532 533 if ( isPluginInstall && 'plugin-information-footer' === $card.attr( 'id' ) ) { 534 wp.updates.setCardButtonStatus( 535 { 536 status: 'updating-plugin', 537 slug: args.slug, 538 addClasses: 'updating-message', 539 text: buttonText, 540 ariaLabel: message 541 } 542 ); 543 } 544 545 return wp.updates.ajax( 'update-plugin', args ); 546 }; 547 548 /** 549 * Updates the UI appropriately after a successful plugin update. 550 * 551 * @since 4.2.0 552 * @since 4.6.0 More accurately named `updatePluginSuccess`. 553 * @since 5.5.0 Auto-update "time to next update" text cleared. 554 * 555 * @param {Object} response Response from the server. 556 * @param {string} response.slug Slug of the plugin to be updated. 557 * @param {string} response.plugin Basename of the plugin to be updated. 558 * @param {string} response.pluginName Name of the plugin to be updated. 559 * @param {string} response.oldVersion Old version of the plugin. 560 * @param {string} response.newVersion New version of the plugin. 561 */ 562 wp.updates.updatePluginSuccess = function( response ) { 563 var $pluginRow, $updateMessage, newText, 564 $adminBarUpdates = $( '#wp-admin-bar-updates' ), 565 buttonText = _x( 'Updated!', 'plugin' ), 566 ariaLabel = sprintf( 567 /* translators: %s: Plugin name and version. */ 568 _x( '%s updated!', 'plugin' ), 569 response.pluginName 570 ); 571 572 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 573 $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) 574 .removeClass( 'update is-enqueued' ) 575 .addClass( 'updated' ); 576 $updateMessage = $pluginRow.find( '.update-message' ) 577 .removeClass( 'updating-message notice-warning' ) 578 .addClass( 'updated-message notice-success' ).find( 'p' ); 579 580 // Update the version number in the row. 581 newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); 582 $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); 583 584 // Clear the "time to next auto-update" text. 585 $pluginRow.find( '.auto-update-time' ).empty(); 586 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { 587 $updateMessage = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.update-now' ) 588 .removeClass( 'updating-message' ) 589 .addClass( 'button-disabled updated-message' ); 590 } 591 592 $adminBarUpdates.removeClass( 'spin' ); 593 594 $updateMessage 595 .attr( 'aria-label', ariaLabel ) 596 .text( buttonText ); 597 598 wp.a11y.speak( __( 'Update completed successfully.' ) ); 599 600 if ( 'plugin_install_from_iframe' !== $updateMessage.attr( 'id' ) ) { 601 wp.updates.decrementCount( 'plugin' ); 602 } else { 603 wp.updates.setCardButtonStatus( 604 { 605 status: 'updated-plugin', 606 slug: response.slug, 607 removeClasses: 'updating-message', 608 addClasses: 'button-disabled updated-message', 609 text: buttonText, 610 ariaLabel: ariaLabel 611 } 612 ); 613 } 614 615 $document.trigger( 'wp-plugin-update-success', response ); 616 }; 617 618 /** 619 * Updates the UI appropriately after a failed plugin update. 620 * 621 * @since 4.2.0 622 * @since 4.6.0 More accurately named `updatePluginError`. 623 * 624 * @param {Object} response Response from the server. 625 * @param {string} response.slug Slug of the plugin to be updated. 626 * @param {string} response.plugin Basename of the plugin to be updated. 627 * @param {string} response.pluginName Optional. Name of the plugin to be updated. 628 * @param {string} response.errorCode Error code for the error that occurred. 629 * @param {string} response.errorMessage The error that occurred. 630 */ 631 wp.updates.updatePluginError = function( response ) { 632 var $pluginRow, $card, $message, errorMessage, buttonText, ariaLabel, 633 $adminBarUpdates = $( '#wp-admin-bar-updates' ); 634 635 if ( ! wp.updates.isValidResponse( response, 'update' ) ) { 636 return; 637 } 638 639 if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) { 640 return; 641 } 642 643 errorMessage = sprintf( 644 /* translators: %s: Error string for a failed update. */ 645 __( 'Update failed: %s' ), 646 response.errorMessage 647 ); 648 649 if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 650 $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ).removeClass( 'is-enqueued' ); 651 652 if ( response.plugin ) { 653 $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); 654 } else { 655 $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' ); 656 } 657 $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); 658 659 if ( response.pluginName ) { 660 $message.find( 'p' ) 661 .attr( 662 'aria-label', 663 sprintf( 664 /* translators: %s: Plugin name and version. */ 665 _x( '%s update failed.', 'plugin' ), 666 response.pluginName 667 ) 668 ); 669 } else { 670 $message.find( 'p' ).removeAttr( 'aria-label' ); 671 } 672 } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { 673 buttonText = __( 'Update failed.' ); 674 675 $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ) 676 .append( wp.updates.adminNotice( { 677 className: 'update-message notice-error notice-alt is-dismissible', 678 message: errorMessage 679 } ) ); 680 681 if ( $card.hasClass( 'plugin-card-' + response.slug ) ) { 682 $card.addClass( 'plugin-card-update-failed' ); 683 } 684 685 $card.find( '.update-now' ) 686 .text( buttonText ) 687 .removeClass( 'updating-message' ); 688 689 if ( response.pluginName ) { 690 ariaLabel = sprintf( 691 /* translators: %s: Plugin name and version. */ 692 _x( '%s update failed.', 'plugin' ), 693 response.pluginName 694 ); 695 696 $card.find( '.update-now' ).attr( 'aria-label', ariaLabel ); 697 } else { 698 ariaLabel = ''; 699 $card.find( '.update-now' ).removeAttr( 'aria-label' ); 700 } 701 702 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { 703 704 // Use same delay as the total duration of the notice fadeTo + slideUp animation. 705 setTimeout( function() { 706 $card 707 .removeClass( 'plugin-card-update-failed' ) 708 .find( '.column-name a' ).trigger( 'focus' ); 709 710 $card.find( '.update-now' ) 711 .attr( 'aria-label', false ) 712 .text( __( 'Update Now' ) ); 713 }, 200 ); 714 } ); 715 } 716 717 $adminBarUpdates.removeClass( 'spin' ); 718 719 wp.a11y.speak( errorMessage, 'assertive' ); 720 721 if ( 'plugin-information-footer' === $card.attr('id' ) ) { 722 wp.updates.setCardButtonStatus( 723 { 724 status: 'plugin-update-failed', 725 slug: response.slug, 726 removeClasses: 'updating-message', 727 text: buttonText, 728 ariaLabel: ariaLabel 729 } 730 ); 731 } 732 733 $document.trigger( 'wp-plugin-update-error', response ); 734 }; 735 736 /** 737 * Sends an Ajax request to the server to install a plugin. 738 * 739 * @since 4.6.0 740 * 741 * @param {Object} args Arguments. 742 * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. 743 * @param {installPluginSuccess} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess 744 * @param {installPluginError} args.error Optional. Error callback. Default: wp.updates.installPluginError 745 * @return {$.promise} A jQuery promise that represents the request, 746 * decorated with an abort() method. 747 */ 748 wp.updates.installPlugin = function( args ) { 749 var $card = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ), 750 $message = $card.find( '.install-now' ), 751 buttonText = __( 'Installing...' ), 752 ariaLabel; 753 754 args = _.extend( { 755 success: wp.updates.installPluginSuccess, 756 error: wp.updates.installPluginError 757 }, args ); 758 759 if ( 'import' === pagenow ) { 760 $message = $( '[data-slug="' + args.slug + '"]' ); 761 } 762 763 if ( $message.html() !== __( 'Installing...' ) ) { 764 $message.data( 'originaltext', $message.html() ); 765 } 766 767 ariaLabel = sprintf( 768 /* translators: %s: Plugin name and version. */ 769 _x( 'Installing %s...', 'plugin' ), 770 $message.data( 'name' ) 771 ); 772 773 $message 774 .addClass( 'updating-message' ) 775 .attr( 'aria-label', ariaLabel ) 776 .text( buttonText ); 777 778 wp.a11y.speak( __( 'Installing... please wait.' ) ); 779 780 // Remove previous error messages, if any. 781 $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove(); 782 783 $document.trigger( 'wp-plugin-installing', args ); 784 785 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 786 wp.updates.setCardButtonStatus( 787 { 788 status: 'installing-plugin', 789 slug: args.slug, 790 addClasses: 'updating-message', 791 text: buttonText, 792 ariaLabel: ariaLabel 793 } 794 ); 795 } 796 797 return wp.updates.ajax( 'install-plugin', args ); 798 }; 799 800 /** 801 * Updates the UI appropriately after a successful plugin install. 802 * 803 * @since 4.6.0 804 * 805 * @param {Object} response Response from the server. 806 * @param {string} response.slug Slug of the installed plugin. 807 * @param {string} response.pluginName Name of the installed plugin. 808 * @param {string} response.activateUrl URL to activate the just installed plugin. 809 */ 810 wp.updates.installPluginSuccess = function( response ) { 811 var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ), 812 buttonText = _x( 'Installed!', 'plugin' ), 813 ariaLabel = sprintf( 814 /* translators: %s: Plugin name and version. */ 815 _x( '%s installed!', 'plugin' ), 816 response.pluginName 817 ); 818 819 $message 820 .removeClass( 'updating-message' ) 821 .addClass( 'updated-message installed button-disabled' ) 822 .attr( 'aria-label', ariaLabel ) 823 .text( buttonText ); 824 825 wp.a11y.speak( __( 'Installation completed successfully.' ) ); 826 827 $document.trigger( 'wp-plugin-install-success', response ); 828 829 if ( response.activateUrl ) { 830 setTimeout( function() { 831 wp.updates.checkPluginDependencies( { 832 slug: response.slug 833 } ); 834 }, 1000 ); 835 } 836 837 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 838 wp.updates.setCardButtonStatus( 839 { 840 status: 'installed-plugin', 841 slug: response.slug, 842 removeClasses: 'updating-message', 843 addClasses: 'updated-message installed button-disabled', 844 text: buttonText, 845 ariaLabel: ariaLabel 846 } 847 ); 848 } 849 }; 850 851 /** 852 * Updates the UI appropriately after a failed plugin install. 853 * 854 * @since 4.6.0 855 * 856 * @param {Object} response Response from the server. 857 * @param {string} response.slug Slug of the plugin to be installed. 858 * @param {string} response.pluginName Optional. Name of the plugin to be installed. 859 * @param {string} response.errorCode Error code for the error that occurred. 860 * @param {string} response.errorMessage The error that occurred. 861 */ 862 wp.updates.installPluginError = function( response ) { 863 var $card = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ), 864 $button = $card.find( '.install-now' ), 865 buttonText = __( 'Installation failed.' ), 866 ariaLabel = sprintf( 867 /* translators: %s: Plugin name and version. */ 868 _x( '%s installation failed', 'plugin' ), 869 $button.data( 'name' ) 870 ), 871 errorMessage; 872 873 if ( ! wp.updates.isValidResponse( response, 'install' ) ) { 874 return; 875 } 876 877 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { 878 return; 879 } 880 881 errorMessage = sprintf( 882 /* translators: %s: Error string for a failed installation. */ 883 __( 'Installation failed: %s' ), 884 response.errorMessage 885 ); 886 887 $card 888 .addClass( 'plugin-card-update-failed' ) 889 .append( '<div class="notice notice-error notice-alt is-dismissible" role="alert"><p>' + errorMessage + '</p></div>' ); 890 891 $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { 892 893 // Use same delay as the total duration of the notice fadeTo + slideUp animation. 894 setTimeout( function() { 895 $card 896 .removeClass( 'plugin-card-update-failed' ) 897 .find( '.column-name a' ).trigger( 'focus' ); 898 }, 200 ); 899 } ); 900 901 $button 902 .removeClass( 'updating-message' ).addClass( 'button-disabled' ) 903 .attr( 'aria-label', ariaLabel ) 904 .text( buttonText ); 905 906 wp.a11y.speak( errorMessage, 'assertive' ); 907 908 wp.updates.setCardButtonStatus( 909 { 910 status: 'plugin-install-failed', 911 slug: response.slug, 912 removeClasses: 'updating-message', 913 addClasses: 'button-disabled', 914 text: buttonText, 915 ariaLabel: ariaLabel 916 } 917 ); 918 919 $document.trigger( 'wp-plugin-install-error', response ); 920 }; 921 922 /** 923 * Sends an Ajax request to the server to check a plugin's dependencies. 924 * 925 * @since 6.5.0 926 * 927 * @param {Object} args Arguments. 928 * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. 929 * @param {checkPluginDependenciesSuccess} args.success Optional. Success callback. Default: wp.updates.checkPluginDependenciesSuccess 930 * @param {checkPluginDependenciesError} args.error Optional. Error callback. Default: wp.updates.checkPluginDependenciesError 931 * @return {$.promise} A jQuery promise that represents the request, 932 * decorated with an abort() method. 933 */ 934 wp.updates.checkPluginDependencies = function( args ) { 935 args = _.extend( { 936 success: wp.updates.checkPluginDependenciesSuccess, 937 error: wp.updates.checkPluginDependenciesError 938 }, args ); 939 940 wp.a11y.speak( __( 'Checking plugin dependencies... please wait.' ) ); 941 $document.trigger( 'wp-checking-plugin-dependencies', args ); 942 943 return wp.updates.ajax( 'check_plugin_dependencies', args ); 944 }; 945 946 /** 947 * Updates the UI appropriately after a successful plugin dependencies check. 948 * 949 * @since 6.5.0 950 * 951 * @param {Object} response Response from the server. 952 * @param {string} response.slug Slug of the checked plugin. 953 * @param {string} response.pluginName Name of the checked plugin. 954 * @param {string} response.plugin The plugin file, relative to the plugins directory. 955 * @param {string} response.activateUrl URL to activate the just checked plugin. 956 */ 957 wp.updates.checkPluginDependenciesSuccess = function( response ) { 958 var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ), 959 buttonText, ariaLabel; 960 961 // Transform the 'Install' button into an 'Activate' button. 962 $message 963 .removeClass( 'install-now installed button-disabled updated-message' ) 964 .addClass( 'activate-now button-primary' ) 965 .attr( 'href', response.activateUrl ); 966 967 wp.a11y.speak( __( 'Plugin dependencies check completed successfully.' ) ); 968 $document.trigger( 'wp-check-plugin-dependencies-success', response ); 969 970 if ( 'plugins-network' === pagenow || 'plugin-install-network' === pagenow ) { 971 buttonText = _x( 'Network Activate', 'plugin' ); 972 ariaLabel = sprintf( 973 /* translators: %s: Plugin name. */ 974 _x( 'Network Activate %s', 'plugin' ), 975 response.pluginName 976 ); 977 978 $message 979 .attr( 'aria-label', ariaLabel ) 980 .text( buttonText ); 981 } else { 982 buttonText = _x( 'Activate', 'plugin' ); 983 ariaLabel = sprintf( 984 /* translators: %s: Plugin name. */ 985 _x( 'Activate %s', 'plugin' ), 986 response.pluginName 987 ); 988 989 $message 990 .attr( 'aria-label', ariaLabel ) 991 .attr( 'data-name', response.pluginName ) 992 .attr( 'data-slug', response.slug ) 993 .attr( 'data-plugin', response.plugin ) 994 .text( buttonText ); 995 } 996 997 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 998 wp.updates.setCardButtonStatus( 999 { 1000 status: 'dependencies-check-success', 1001 slug: response.slug, 1002 removeClasses: 'install-now installed button-disabled updated-message', 1003 addClasses: 'activate-now button-primary', 1004 text: buttonText, 1005 ariaLabel: ariaLabel, 1006 pluginName: response.pluginName, 1007 plugin: response.plugin, 1008 href: response.activateUrl 1009 } 1010 ); 1011 } 1012 }; 1013 1014 /** 1015 * Updates the UI appropriately after a failed plugin dependencies check. 1016 * 1017 * @since 6.5.0 1018 * 1019 * @param {Object} response Response from the server. 1020 * @param {string} response.slug Slug of the plugin to be checked. 1021 * @param {string} response.pluginName Optional. Name of the plugin to be checked. 1022 * @param {string} response.errorCode Error code for the error that occurred. 1023 * @param {string} response.errorMessage The error that occurred. 1024 */ 1025 wp.updates.checkPluginDependenciesError = function( response ) { 1026 var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.install-now' ), 1027 buttonText = _x( 'Activate', 'plugin' ), 1028 ariaLabel = sprintf( 1029 /* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */ 1030 _x( 'Cannot activate %1$s. %2$s', 'plugin' ), 1031 response.pluginName, 1032 response.errorMessage 1033 ), 1034 errorMessage; 1035 1036 if ( ! wp.updates.isValidResponse( response, 'check-dependencies' ) ) { 1037 return; 1038 } 1039 1040 errorMessage = sprintf( 1041 /* translators: %s: Error string for a failed activation. */ 1042 __( 'Activation failed: %s' ), 1043 response.errorMessage 1044 ); 1045 1046 wp.a11y.speak( errorMessage, 'assertive' ); 1047 $document.trigger( 'wp-check-plugin-dependencies-error', response ); 1048 1049 $message 1050 .removeClass( 'install-now installed updated-message' ) 1051 .addClass( 'activate-now button-primary' ) 1052 .attr( 'aria-label', ariaLabel ) 1053 .text( buttonText ); 1054 1055 if ( 'plugin-information-footer' === $message.parent().attr('id' ) ) { 1056 wp.updates.setCardButtonStatus( 1057 { 1058 status: 'dependencies-check-failed', 1059 slug: response.slug, 1060 removeClasses: 'install-now installed updated-message', 1061 addClasses: 'activate-now button-primary', 1062 text: buttonText, 1063 ariaLabel: ariaLabel 1064 } 1065 ); 1066 } 1067 }; 1068 1069 /** 1070 * Sends an Ajax request to the server to activate a plugin. 1071 * 1072 * @since 6.5.0 1073 * 1074 * @param {Object} args Arguments. 1075 * @param {string} args.name The name of the plugin. 1076 * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. 1077 * @param {string} args.plugin The plugin file, relative to the plugins directory. 1078 * @param {activatePluginSuccess} args.success Optional. Success callback. Default: wp.updates.activatePluginSuccess 1079 * @param {activatePluginError} args.error Optional. Error callback. Default: wp.updates.activatePluginError 1080 * @return {$.promise} A jQuery promise that represents the request, 1081 * decorated with an abort() method. 1082 */ 1083 wp.updates.activatePlugin = function( args ) { 1084 var $message = $( '.plugin-card-' + args.slug + ', #plugin-information-footer' ).find( '.activate-now, .activating-message' ); 1085 1086 args = _.extend( { 1087 success: wp.updates.activatePluginSuccess, 1088 error: wp.updates.activatePluginError 1089 }, args ); 1090 1091 wp.a11y.speak( __( 'Activating... please wait.' ) ); 1092 $document.trigger( 'wp-activating-plugin', args ); 1093 1094 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 1095 wp.updates.setCardButtonStatus( 1096 { 1097 status: 'activating-plugin', 1098 slug: args.slug, 1099 removeClasses: 'installed updated-message button-primary', 1100 addClasses: 'activating-message', 1101 text: __( 'Activating...' ), 1102 ariaLabel: sprintf( 1103 /* translators: %s: Plugin name. */ 1104 _x( 'Activating %s', 'plugin' ), 1105 args.name 1106 ) 1107 } 1108 ); 1109 } 1110 1111 return wp.updates.ajax( 'activate-plugin', args ); 1112 }; 1113 1114 /** 1115 * Updates the UI appropriately after a successful plugin activation. 1116 * 1117 * @since 6.5.0 1118 * 1119 * @param {Object} response Response from the server. 1120 * @param {string} response.slug Slug of the activated plugin. 1121 * @param {string} response.pluginName Name of the activated plugin. 1122 * @param {string} response.plugin The plugin file, relative to the plugins directory. 1123 */ 1124 wp.updates.activatePluginSuccess = function( response ) { 1125 var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ), 1126 buttonText = _x( 'Activated!', 'plugin' ), 1127 ariaLabel = sprintf( 1128 /* translators: %s: The plugin name. */ 1129 __( '%s activated successfully.' ), 1130 response.pluginName 1131 ); 1132 1133 wp.a11y.speak( __( 'Activation completed successfully.' ) ); 1134 $document.trigger( 'wp-plugin-activate-success', response ); 1135 1136 $message 1137 .removeClass( 'activating-message' ) 1138 .addClass( 'activated-message button-disabled' ) 1139 .attr( 'aria-label', ariaLabel ) 1140 .text( buttonText ); 1141 1142 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 1143 wp.updates.setCardButtonStatus( 1144 { 1145 status: 'activated-plugin', 1146 slug: response.slug, 1147 removeClasses: 'activating-message', 1148 addClasses: 'activated-message button-disabled', 1149 text: buttonText, 1150 ariaLabel: ariaLabel 1151 } 1152 ); 1153 } 1154 1155 setTimeout( function() { 1156 $message.removeClass( 'activated-message' ) 1157 .text( _x( 'Active', 'plugin' ) ); 1158 1159 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 1160 wp.updates.setCardButtonStatus( 1161 { 1162 status: 'plugin-active', 1163 slug: response.slug, 1164 removeClasses: 'activated-message', 1165 text: _x( 'Active', 'plugin' ), 1166 ariaLabel: sprintf( 1167 /* translators: %s: The plugin name. */ 1168 __( '%s is active.' ), 1169 response.pluginName 1170 ) 1171 } 1172 ); 1173 } 1174 }, 1000 ); 1175 }; 1176 1177 /** 1178 * Updates the UI appropriately after a failed plugin activation. 1179 * 1180 * @since 6.5.0 1181 * 1182 * @param {Object} response Response from the server. 1183 * @param {string} response.slug Slug of the plugin to be activated. 1184 * @param {string} response.pluginName Optional. Name of the plugin to be activated. 1185 * @param {string} response.errorCode Error code for the error that occurred. 1186 * @param {string} response.errorMessage The error that occurred. 1187 */ 1188 wp.updates.activatePluginError = function( response ) { 1189 var $message = $( '.plugin-card-' + response.slug + ', #plugin-information-footer' ).find( '.activating-message' ), 1190 buttonText = __( 'Activation failed.' ), 1191 ariaLabel = sprintf( 1192 /* translators: %s: Plugin name. */ 1193 _x( '%s activation failed', 'plugin' ), 1194 response.pluginName 1195 ), 1196 errorMessage; 1197 1198 if ( ! wp.updates.isValidResponse( response, 'activate' ) ) { 1199 return; 1200 } 1201 1202 errorMessage = sprintf( 1203 /* translators: %s: Error string for a failed activation. */ 1204 __( 'Activation failed: %s' ), 1205 response.errorMessage 1206 ); 1207 1208 wp.a11y.speak( errorMessage, 'assertive' ); 1209 $document.trigger( 'wp-plugin-activate-error', response ); 1210 1211 $message 1212 .removeClass( 'install-now installed activating-message' ) 1213 .addClass( 'button-disabled' ) 1214 .attr( 'aria-label', ariaLabel ) 1215 .text( buttonText ); 1216 1217 if ( 'plugin-information-footer' === $message.parent().attr( 'id' ) ) { 1218 wp.updates.setCardButtonStatus( 1219 { 1220 status: 'plugin-activation-failed', 1221 slug: response.slug, 1222 removeClasses: 'install-now installed activating-message', 1223 addClasses: 'button-disabled', 1224 text: buttonText, 1225 ariaLabel: ariaLabel 1226 } 1227 ); 1228 } 1229 }; 1230 1231 /** 1232 * Updates the UI appropriately after a successful importer install. 1233 * 1234 * @since 4.6.0 1235 * 1236 * @param {Object} response Response from the server. 1237 * @param {string} response.slug Slug of the installed plugin. 1238 * @param {string} response.pluginName Name of the installed plugin. 1239 * @param {string} response.activateUrl URL to activate the just installed plugin. 1240 */ 1241 wp.updates.installImporterSuccess = function( response ) { 1242 wp.updates.addAdminNotice( { 1243 id: 'install-success', 1244 className: 'notice-success is-dismissible', 1245 message: sprintf( 1246 /* translators: %s: Activation URL. */ 1247 __( 'Importer installed successfully. <a href="%s">Run importer</a>' ), 1248 response.activateUrl + '&from=import' 1249 ) 1250 } ); 1251 1252 $( '[data-slug="' + response.slug + '"]' ) 1253 .removeClass( 'install-now updating-message' ) 1254 .addClass( 'activate-now' ) 1255 .attr({ 1256 'href': response.activateUrl + '&from=import', 1257 'aria-label':sprintf( 1258 /* translators: %s: Importer name. */ 1259 __( 'Run %s' ), 1260 response.pluginName 1261 ) 1262 }) 1263 .text( __( 'Run Importer' ) ); 1264 1265 wp.a11y.speak( __( 'Installation completed successfully.' ) ); 1266 1267 $document.trigger( 'wp-importer-install-success', response ); 1268 }; 1269 1270 /** 1271 * Updates the UI appropriately after a failed importer install. 1272 * 1273 * @since 4.6.0 1274 * 1275 * @param {Object} response Response from the server. 1276 * @param {string} response.slug Slug of the plugin to be installed. 1277 * @param {string} response.pluginName Optional. Name of the plugin to be installed. 1278 * @param {string} response.errorCode Error code for the error that occurred. 1279 * @param {string} response.errorMessage The error that occurred. 1280 */ 1281 wp.updates.installImporterError = function( response ) { 1282 var errorMessage = sprintf( 1283 /* translators: %s: Error string for a failed installation. */ 1284 __( 'Installation failed: %s' ), 1285 response.errorMessage 1286 ), 1287 $installLink = $( '[data-slug="' + response.slug + '"]' ), 1288 pluginName = $installLink.data( 'name' ); 1289 1290 if ( ! wp.updates.isValidResponse( response, 'install' ) ) { 1291 return; 1292 } 1293 1294 if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { 1295 return; 1296 } 1297 1298 wp.updates.addAdminNotice( { 1299 id: response.errorCode, 1300 className: 'notice-error is-dismissible', 1301 message: errorMessage 1302 } ); 1303 1304 $installLink 1305 .removeClass( 'updating-message' ) 1306 .attr( 1307 'aria-label', 1308 sprintf( 1309 /* translators: %s: Plugin name. */ 1310 _x( 'Install %s now', 'plugin' ), 1311 pluginName 1312 ) 1313 ) 1314 .text( _x( 'Install Now', 'plugin' ) ); 1315 1316 wp.a11y.speak( errorMessage, 'assertive' ); 1317 1318 $document.trigger( 'wp-importer-install-error', response ); 1319 }; 1320 1321 /** 1322 * Sends an Ajax request to the server to delete a plugin. 1323 * 1324 * @since 4.6.0 1325 * 1326 * @param {Object} args Arguments. 1327 * @param {string} args.plugin Basename of the plugin to be deleted. 1328 * @param {string} args.slug Slug of the plugin to be deleted. 1329 * @param {deletePluginSuccess} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess 1330 * @param {deletePluginError} args.error Optional. Error callback. Default: wp.updates.deletePluginError 1331 * @return {$.promise} A jQuery promise that represents the request, 1332 * decorated with an abort() method. 1333 */ 1334 wp.updates.deletePlugin = function( args ) { 1335 var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' ); 1336 1337 args = _.extend( { 1338 success: wp.updates.deletePluginSuccess, 1339 error: wp.updates.deletePluginError 1340 }, args ); 1341 1342 if ( $link.html() !== __( 'Deleting...' ) ) { 1343 $link 1344 .data( 'originaltext', $link.html() ) 1345 .text( __( 'Deleting...' ) ); 1346 } 1347 1348 wp.a11y.speak( __( 'Deleting...' ) ); 1349 1350 $document.trigger( 'wp-plugin-deleting', args ); 1351 1352 return wp.updates.ajax( 'delete-plugin', args ); 1353 }; 1354 1355 /** 1356 * Updates the UI appropriately after a successful plugin deletion. 1357 * 1358 * @since 4.6.0 1359 * 1360 * @param {Object} response Response from the server. 1361 * @param {string} response.slug Slug of the plugin that was deleted. 1362 * @param {string} response.plugin Base name of the plugin that was deleted. 1363 * @param {string} response.pluginName Name of the plugin that was deleted. 1364 */ 1365 wp.updates.deletePluginSuccess = function( response ) { 1366 1367 // Removes the plugin and updates rows. 1368 $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { 1369 var $form = $( '#bulk-action-form' ), 1370 $views = $( '.subsubsub' ), 1371 $pluginRow = $( this ), 1372 $currentView = $views.find( '[aria-current="page"]' ), 1373 $itemsCount = $( '.displaying-num' ), 1374 columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length, 1375 pluginDeletedRow = wp.template( 'item-deleted-row' ), 1376 /** 1377 * Plugins Base names of plugins in their different states. 1378 * 1379 * @type {Object} 1380 */ 1381 plugins = settings.plugins, 1382 remainingCount; 1383 1384 // Add a success message after deleting a plugin. 1385 if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) { 1386 $pluginRow.after( 1387 pluginDeletedRow( { 1388 slug: response.slug, 1389 plugin: response.plugin, 1390 colspan: columnCount, 1391 name: response.pluginName 1392 } ) 1393 ); 1394 } 1395 1396 $pluginRow.remove(); 1397 1398 // Remove plugin from update count. 1399 if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) { 1400 plugins.upgrade = _.without( plugins.upgrade, response.plugin ); 1401 wp.updates.decrementCount( 'plugin' ); 1402 } 1403 1404 // Remove from views. 1405 if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) { 1406 plugins.inactive = _.without( plugins.inactive, response.plugin ); 1407 if ( plugins.inactive.length ) { 1408 $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' ); 1409 } else { 1410 $views.find( '.inactive' ).remove(); 1411 } 1412 } 1413 1414 if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) { 1415 plugins.active = _.without( plugins.active, response.plugin ); 1416 if ( plugins.active.length ) { 1417 $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' ); 1418 } else { 1419 $views.find( '.active' ).remove(); 1420 } 1421 } 1422 1423 if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) { 1424 plugins.recently_activated = _.without( plugins.recently_activated, response.plugin ); 1425 if ( plugins.recently_activated.length ) { 1426 $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' ); 1427 } else { 1428 $views.find( '.recently_activated' ).remove(); 1429 } 1430 } 1431 1432 if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) { 1433 plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin ); 1434 if ( plugins['auto-update-enabled'].length ) { 1435 $views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' ); 1436 } else { 1437 $views.find( '.auto-update-enabled' ).remove(); 1438 } 1439 } 1440 1441 if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) { 1442 plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin ); 1443 if ( plugins['auto-update-disabled'].length ) { 1444 $views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' ); 1445 } else { 1446 $views.find( '.auto-update-disabled' ).remove(); 1447 } 1448 } 1449 1450 // Remove from the other groups, such as those added through the `plugins_list` filter. 1451 _.each( _.keys( plugins ), function( group ) { 1452 var $view; 1453 1454 if ( 'all' === group || ! _.isArray( plugins[ group ] ) || -1 === _.indexOf( plugins[ group ], response.plugin ) ) { 1455 return; 1456 } 1457 1458 plugins[ group ] = _.without( plugins[ group ], response.plugin ); 1459 $view = $views.children( 'li.' + $.escapeSelector( group ) ); 1460 1461 if ( plugins[ group ].length ) { 1462 $view.find( '.count' ).text( '(' + plugins[ group ].length + ')' ); 1463 } else { 1464 $view.remove(); 1465 } 1466 } ); 1467 1468 plugins.all = _.without( plugins.all, response.plugin ); 1469 1470 if ( plugins.all.length ) { 1471 $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' ); 1472 } else { 1473 $form.find( '.tablenav' ).css( { visibility: 'hidden' } ); 1474 $views.find( '.all' ).remove(); 1475 1476 if ( ! $form.find( 'tr.no-items' ).length ) { 1477 $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' ); 1478 } 1479 } 1480 1481 // Remove the trailing separator left on the last view when the view after it was removed. 1482 $views.find( 'li:last' ).html( function() { return $( this ).children(); } ); 1483 1484 if ( $itemsCount.length && $currentView.length ) { 1485 remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; 1486 $itemsCount.text( 1487 sprintf( 1488 /* translators: %s: The remaining number of plugins. */ 1489 _nx( '%s item', '%s items', remainingCount, 'plugin/plugins' ), 1490 remainingCount 1491 ) 1492 ); 1493 } 1494 } ); 1495 1496 wp.a11y.speak( _x( 'Deleted!', 'plugin' ) ); 1497 1498 $document.trigger( 'wp-plugin-delete-success', response ); 1499 }; 1500 1501 /** 1502 * Updates the UI appropriately after a failed plugin deletion. 1503 * 1504 * @since 4.6.0 1505 * 1506 * @param {Object} response Response from the server. 1507 * @param {string} response.slug Slug of the plugin to be deleted. 1508 * @param {string} response.plugin Base name of the plugin to be deleted 1509 * @param {string} response.pluginName Optional. Name of the plugin to be deleted. 1510 * @param {string} response.errorCode Error code for the error that occurred. 1511 * @param {string} response.errorMessage The error that occurred. 1512 */ 1513 wp.updates.deletePluginError = function( response ) { 1514 var $plugin, $pluginUpdateRow, 1515 pluginUpdateRow = wp.template( 'item-update-row' ), 1516 noticeContent = wp.updates.adminNotice( { 1517 className: 'update-message notice-error notice-alt', 1518 message: response.errorMessage 1519 } ); 1520 1521 if ( response.plugin ) { 1522 $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ); 1523 $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ); 1524 } else { 1525 $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' ); 1526 $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' ); 1527 } 1528 1529 if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { 1530 return; 1531 } 1532 1533 if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) { 1534 return; 1535 } 1536 1537 // Add a plugin update row if it doesn't exist yet. 1538 if ( ! $pluginUpdateRow.length ) { 1539 $plugin.addClass( 'update' ).after( 1540 pluginUpdateRow( { 1541 slug: response.slug, 1542 plugin: response.plugin || response.slug, 1543 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 1544 content: noticeContent 1545 } ) 1546 ); 1547 } else { 1548 1549 // Remove previous error messages, if any. 1550 $pluginUpdateRow.find( '.notice-error' ).remove(); 1551 1552 $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent ); 1553 } 1554 1555 $document.trigger( 'wp-plugin-delete-error', response ); 1556 }; 1557 1558 /** 1559 * Sends an Ajax request to the server to update a theme. 1560 * 1561 * @since 4.6.0 1562 * 1563 * @param {Object} args Arguments. 1564 * @param {string} args.slug Theme stylesheet. 1565 * @param {updateThemeSuccess} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess 1566 * @param {updateThemeError} args.error Optional. Error callback. Default: wp.updates.updateThemeError 1567 * @return {$.promise} A jQuery promise that represents the request, 1568 * decorated with an abort() method. 1569 */ 1570 wp.updates.updateTheme = function( args ) { 1571 var $notice; 1572 1573 args = _.extend( { 1574 success: wp.updates.updateThemeSuccess, 1575 error: wp.updates.updateThemeError 1576 }, args ); 1577 1578 if ( 'themes-network' === pagenow ) { 1579 $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); 1580 1581 } else if ( 'customize' === pagenow ) { 1582 1583 // Update the theme details UI. 1584 $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' ); 1585 1586 $notice.find( 'h3' ).remove(); 1587 1588 // Add the top-level UI, and update both. 1589 $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) ); 1590 $notice = $notice.addClass( 'updating-message' ).find( 'p' ); 1591 1592 } else { 1593 $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); 1594 1595 $notice.find( 'h3' ).remove(); 1596 1597 $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) ); 1598 $notice = $notice.addClass( 'updating-message' ).find( 'p' ); 1599 } 1600 1601 if ( $notice.html() !== __( 'Updating...' ) ) { 1602 $notice.data( 'originaltext', $notice.html() ); 1603 } 1604 1605 wp.a11y.speak( __( 'Updating... please wait.' ) ); 1606 $notice.text( __( 'Updating...' ) ); 1607 1608 $document.trigger( 'wp-theme-updating', args ); 1609 1610 return wp.updates.ajax( 'update-theme', args ); 1611 }; 1612 1613 /** 1614 * Updates the UI appropriately after a successful theme update. 1615 * 1616 * @since 4.6.0 1617 * @since 5.5.0 Auto-update "time to next update" text cleared. 1618 * 1619 * @param {Object} response The response object from the Ajax request. 1620 * @param {string} response.slug Slug of the theme to be updated. 1621 * @param {Object} response.theme Updated theme. 1622 * @param {string} response.oldVersion Old version of the theme. 1623 * @param {string} response.newVersion New version of the theme. 1624 */ 1625 wp.updates.updateThemeSuccess = function( response ) { 1626 var isModalOpen = $( 'body.modal-open' ).length, 1627 $theme = $( '[data-slug="' + response.slug + '"]' ), 1628 updatedMessage = { 1629 className: 'updated-message notice-success notice-alt', 1630 message: _x( 'Updated!', 'theme' ) 1631 }, 1632 $notice, newText; 1633 1634 if ( 'customize' === pagenow ) { 1635 $theme = $( '.updating-message' ).siblings( '.theme-name' ); 1636 1637 if ( $theme.length ) { 1638 1639 // Update the version number in the row. 1640 newText = $theme.html().replace( response.oldVersion, response.newVersion ); 1641 $theme.html( newText ); 1642 } 1643 1644 $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) ); 1645 } else if ( 'themes-network' === pagenow ) { 1646 $notice = $theme.find( '.update-message' ); 1647 1648 // Update the version number in the row. 1649 newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); 1650 $theme.find( '.theme-version-author-uri' ).html( newText ); 1651 1652 // Clear the "time to next auto-update" text. 1653 $theme.find( '.auto-update-time' ).empty(); 1654 } else { 1655 $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) ); 1656 1657 // Focus on Customize button after updating. 1658 if ( isModalOpen ) { 1659 $( '.load-customize:visible' ).trigger( 'focus' ); 1660 $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty(); 1661 } else { 1662 $theme.find( '.load-customize' ).trigger( 'focus' ); 1663 } 1664 } 1665 1666 wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) ); 1667 wp.a11y.speak( __( 'Update completed successfully.' ) ); 1668 1669 wp.updates.decrementCount( 'theme' ); 1670 1671 $document.trigger( 'wp-theme-update-success', response ); 1672 1673 // Show updated message after modal re-rendered. 1674 if ( isModalOpen && 'customize' !== pagenow ) { 1675 $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) ); 1676 } 1677 }; 1678 1679 /** 1680 * Updates the UI appropriately after a failed theme update. 1681 * 1682 * @since 4.6.0 1683 * 1684 * @param {Object} response Response from the server. 1685 * @param {string} response.slug Slug of the theme to be updated. 1686 * @param {string} response.errorCode Error code for the error that occurred. 1687 * @param {string} response.errorMessage The error that occurred. 1688 */ 1689 wp.updates.updateThemeError = function( response ) { 1690 var $theme = $( '[data-slug="' + response.slug + '"]' ), 1691 errorMessage = sprintf( 1692 /* translators: %s: Error string for a failed update. */ 1693 __( 'Update failed: %s' ), 1694 response.errorMessage 1695 ), 1696 $notice; 1697 1698 if ( ! wp.updates.isValidResponse( response, 'update' ) ) { 1699 return; 1700 } 1701 1702 if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { 1703 return; 1704 } 1705 1706 if ( 'customize' === pagenow ) { 1707 $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ); 1708 } 1709 1710 if ( 'themes-network' === pagenow ) { 1711 $notice = $theme.find( '.update-message ' ); 1712 } else { 1713 $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) ); 1714 1715 $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus'); 1716 } 1717 1718 wp.updates.addAdminNotice( { 1719 selector: $notice, 1720 className: 'update-message notice-error notice-alt is-dismissible', 1721 message: errorMessage 1722 } ); 1723 1724 wp.a11y.speak( errorMessage ); 1725 1726 $document.trigger( 'wp-theme-update-error', response ); 1727 }; 1728 1729 /** 1730 * Sends an Ajax request to the server to install a theme. 1731 * 1732 * @since 4.6.0 1733 * 1734 * @param {Object} args The arguments for the theme installation request. 1735 * @param {string} args.slug Theme stylesheet. 1736 * @param {installThemeSuccess} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess 1737 * @param {installThemeError} args.error Optional. Error callback. Default: wp.updates.installThemeError 1738 * @return {$.promise} A jQuery promise that represents the request, 1739 * decorated with an abort() method. 1740 */ 1741 wp.updates.installTheme = function( args ) { 1742 var $message = $( '.theme-install[data-slug="' + args.slug + '"]' ); 1743 1744 args = _.extend( { 1745 success: wp.updates.installThemeSuccess, 1746 error: wp.updates.installThemeError 1747 }, args ); 1748 1749 $message.addClass( 'updating-message' ); 1750 $message.parents( '.theme' ).addClass( 'focus' ); 1751 if ( $message.html() !== __( 'Installing...' ) ) { 1752 $message.data( 'originaltext', $message.html() ); 1753 } 1754 1755 $message 1756 .attr( 1757 'aria-label', 1758 sprintf( 1759 /* translators: %s: Theme name and version. */ 1760 _x( 'Installing %s...', 'theme' ), 1761 $message.data( 'name' ) 1762 ) 1763 ) 1764 .text( __( 'Installing...' ) ); 1765 1766 wp.a11y.speak( __( 'Installing... please wait.' ) ); 1767 1768 // Remove previous error messages, if any. 1769 $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove(); 1770 1771 $document.trigger( 'wp-theme-installing', args ); 1772 1773 return wp.updates.ajax( 'install-theme', args ); 1774 }; 1775 1776 /** 1777 * Updates the UI appropriately after a successful theme install. 1778 * 1779 * @since 4.6.0 1780 * 1781 * @param {Object} response Response from the server. 1782 * @param {string} response.slug Slug of the theme to be installed. 1783 * @param {string} response.customizeUrl URL to the Customizer for the just installed theme. 1784 * @param {string} response.activateUrl URL to activate the just installed theme. 1785 */ 1786 wp.updates.installThemeSuccess = function( response ) { 1787 var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ), 1788 $message; 1789 1790 $document.trigger( 'wp-theme-install-success', response ); 1791 1792 $message = $card.find( '.button-primary' ) 1793 .removeClass( 'updating-message' ) 1794 .addClass( 'updated-message disabled' ) 1795 .attr( 1796 'aria-label', 1797 sprintf( 1798 /* translators: %s: Theme name and version. */ 1799 _x( '%s installed!', 'theme' ), 1800 response.themeName 1801 ) 1802 ) 1803 .text( _x( 'Installed!', 'theme' ) ); 1804 1805 wp.a11y.speak( __( 'Installation completed successfully.' ) ); 1806 1807 setTimeout( function() { 1808 1809 if ( response.activateUrl ) { 1810 1811 // Transform the 'Install' button into an 'Activate' button. 1812 $message 1813 .attr( 'href', response.activateUrl ) 1814 .removeClass( 'theme-install updated-message disabled' ) 1815 .addClass( 'activate' ); 1816 1817 if ( 'themes-network' === pagenow ) { 1818 $message 1819 .attr( 1820 'aria-label', 1821 sprintf( 1822 /* translators: %s: Theme name. */ 1823 _x( 'Network Activate %s', 'theme' ), 1824 response.themeName 1825 ) 1826 ) 1827 .text( __( 'Network Enable' ) ); 1828 } else { 1829 $message 1830 .attr( 1831 'aria-label', 1832 sprintf( 1833 /* translators: %s: Theme name. */ 1834 _x( 'Activate %s', 'theme' ), 1835 response.themeName 1836 ) 1837 ) 1838 .text( _x( 'Activate', 'theme' ) ); 1839 } 1840 } 1841 1842 if ( response.customizeUrl ) { 1843 1844 // Transform the 'Preview' button into a 'Live Preview' button. 1845 $message.siblings( '.preview' ).replaceWith( function () { 1846 return $( '<a>' ) 1847 .attr( 'href', response.customizeUrl ) 1848 .addClass( 'button load-customize' ) 1849 .text( __( 'Live Preview' ) ); 1850 } ); 1851 } 1852 }, 1000 ); 1853 }; 1854 1855 /** 1856 * Updates the UI appropriately after a failed theme install. 1857 * 1858 * @since 4.6.0 1859 * 1860 * @param {Object} response Response from the server. 1861 * @param {string} response.slug Slug of the theme to be installed. 1862 * @param {string} response.errorCode Error code for the error that occurred. 1863 * @param {string} response.errorMessage The error that occurred. 1864 */ 1865 wp.updates.installThemeError = function( response ) { 1866 var $card, $button, 1867 errorMessage = sprintf( 1868 /* translators: %s: Error string for a failed installation. */ 1869 __( 'Installation failed: %s' ), 1870 response.errorMessage 1871 ), 1872 $message = wp.updates.adminNotice( { 1873 className: 'update-message notice-error notice-alt', 1874 message: errorMessage 1875 } ); 1876 1877 if ( ! wp.updates.isValidResponse( response, 'install' ) ) { 1878 return; 1879 } 1880 1881 if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) { 1882 return; 1883 } 1884 1885 if ( 'customize' === pagenow ) { 1886 if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) { 1887 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1888 $card = $( '.theme-overlay .theme-info' ).prepend( $message ); 1889 } else { 1890 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1891 $card = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message ); 1892 } 1893 wp.customize.notifications.remove( 'theme_installing' ); 1894 } else { 1895 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { 1896 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1897 $card = $( '.install-theme-info' ).prepend( $message ); 1898 } else { 1899 $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); 1900 $button = $card.find( '.theme-install' ); 1901 } 1902 } 1903 1904 $button 1905 .removeClass( 'updating-message' ) 1906 .attr( 1907 'aria-label', 1908 sprintf( 1909 /* translators: %s: Theme name and version. */ 1910 _x( '%s installation failed', 'theme' ), 1911 $button.data( 'name' ) 1912 ) 1913 ) 1914 .text( __( 'Installation failed.' ) ); 1915 1916 wp.a11y.speak( errorMessage, 'assertive' ); 1917 1918 $document.trigger( 'wp-theme-install-error', response ); 1919 }; 1920 1921 /** 1922 * Sends an Ajax request to the server to delete a theme. 1923 * 1924 * @since 4.6.0 1925 * 1926 * @param {Object} args The arguments for the theme deletion request. 1927 * @param {string} args.slug Theme stylesheet. 1928 * @param {deleteThemeSuccess} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess 1929 * @param {deleteThemeError} args.error Optional. Error callback. Default: wp.updates.deleteThemeError 1930 * @return {$.promise} A jQuery promise that represents the request, 1931 * decorated with an abort() method. 1932 */ 1933 wp.updates.deleteTheme = function( args ) { 1934 var $button; 1935 1936 if ( 'themes' === pagenow ) { 1937 $button = $( '.theme-actions .delete-theme' ); 1938 } else if ( 'themes-network' === pagenow ) { 1939 $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' ); 1940 } 1941 1942 args = _.extend( { 1943 success: wp.updates.deleteThemeSuccess, 1944 error: wp.updates.deleteThemeError 1945 }, args ); 1946 1947 if ( $button && $button.html() !== __( 'Deleting...' ) ) { 1948 $button 1949 .data( 'originaltext', $button.html() ) 1950 .text( __( 'Deleting...' ) ); 1951 } 1952 1953 wp.a11y.speak( __( 'Deleting...' ) ); 1954 1955 // Remove previous error messages, if any. 1956 $( '.theme-info .update-message' ).remove(); 1957 1958 $document.trigger( 'wp-theme-deleting', args ); 1959 1960 return wp.updates.ajax( 'delete-theme', args ); 1961 }; 1962 1963 /** 1964 * Updates the UI appropriately after a successful theme deletion. 1965 * 1966 * @since 4.6.0 1967 * 1968 * @param {Object} response Response from the server. 1969 * @param {string} response.slug Slug of the theme that was deleted. 1970 */ 1971 wp.updates.deleteThemeSuccess = function( response ) { 1972 var $themeRows = $( '[data-slug="' + response.slug + '"]' ); 1973 1974 if ( 'themes-network' === pagenow ) { 1975 1976 // Removes the theme and updates rows. 1977 $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { 1978 var $views = $( '.subsubsub' ), 1979 $themeRow = $( this ), 1980 themes = settings.themes, 1981 deletedRow = wp.template( 'item-deleted-row' ); 1982 1983 if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) { 1984 $themeRow.after( 1985 deletedRow( { 1986 slug: response.slug, 1987 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 1988 name: $themeRow.find( '.theme-title strong' ).text() 1989 } ) 1990 ); 1991 } 1992 1993 $themeRow.remove(); 1994 1995 // Remove theme from update count. 1996 if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) { 1997 themes.upgrade = _.without( themes.upgrade, response.slug ); 1998 wp.updates.decrementCount( 'theme' ); 1999 } 2000 2001 // Remove from views. 2002 if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) { 2003 themes.disabled = _.without( themes.disabled, response.slug ); 2004 if ( themes.disabled.length ) { 2005 $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' ); 2006 } else { 2007 $views.find( '.disabled' ).remove(); 2008 } 2009 } 2010 2011 if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) { 2012 themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug ); 2013 if ( themes['auto-update-enabled'].length ) { 2014 $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' ); 2015 } else { 2016 $views.find( '.auto-update-enabled' ).remove(); 2017 } 2018 } 2019 2020 if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) { 2021 themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug ); 2022 if ( themes['auto-update-disabled'].length ) { 2023 $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' ); 2024 } else { 2025 $views.find( '.auto-update-disabled' ).remove(); 2026 } 2027 } 2028 2029 themes.all = _.without( themes.all, response.slug ); 2030 2031 // There is always at least one theme available. 2032 $views.find( '.all .count' ).text( '(' + themes.all.length + ')' ); 2033 } ); 2034 } 2035 2036 // DecrementCount from update count. 2037 if ( 'themes' === pagenow ) { 2038 var theme = _.find( _wpThemeSettings.themes, { id: response.slug } ); 2039 if ( theme.hasUpdate ) { 2040 wp.updates.decrementCount( 'theme' ); 2041 } 2042 } 2043 2044 wp.a11y.speak( _x( 'Deleted!', 'theme' ) ); 2045 2046 $document.trigger( 'wp-theme-delete-success', response ); 2047 }; 2048 2049 /** 2050 * Updates the UI appropriately after a failed theme deletion. 2051 * 2052 * @since 4.6.0 2053 * 2054 * @param {Object} response Response from the server. 2055 * @param {string} response.slug Slug of the theme to be deleted. 2056 * @param {string} response.errorCode Error code for the error that occurred. 2057 * @param {string} response.errorMessage The error that occurred. 2058 */ 2059 wp.updates.deleteThemeError = function( response ) { 2060 var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ), 2061 $button = $( '.theme-actions .delete-theme' ), 2062 updateRow = wp.template( 'item-update-row' ), 2063 $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ), 2064 errorMessage = sprintf( 2065 /* translators: %s: Error string for a failed deletion. */ 2066 __( 'Deletion failed: %s' ), 2067 response.errorMessage 2068 ), 2069 $message = wp.updates.adminNotice( { 2070 className: 'update-message notice-error notice-alt', 2071 message: errorMessage 2072 } ); 2073 2074 if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) { 2075 return; 2076 } 2077 2078 if ( 'themes-network' === pagenow ) { 2079 if ( ! $updateRow.length ) { 2080 $themeRow.addClass( 'update' ).after( 2081 updateRow( { 2082 slug: response.slug, 2083 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 2084 content: $message 2085 } ) 2086 ); 2087 } else { 2088 // Remove previous error messages, if any. 2089 $updateRow.find( '.notice-error' ).remove(); 2090 $updateRow.find( '.plugin-update' ).append( $message ); 2091 } 2092 } else { 2093 $( '.theme-info .theme-description' ).before( $message ); 2094 } 2095 2096 $button.html( $button.data( 'originaltext' ) ); 2097 2098 wp.a11y.speak( errorMessage, 'assertive' ); 2099 2100 $document.trigger( 'wp-theme-delete-error', response ); 2101 }; 2102 2103 /** 2104 * Adds the appropriate callback based on the type of action and the current page. 2105 * 2106 * @since 4.6.0 2107 * @private 2108 * 2109 * @param {Object} data Ajax payload. 2110 * @param {string} action The type of request to perform. 2111 * @return {Object} The Ajax payload with the appropriate callbacks. 2112 */ 2113 wp.updates._addCallbacks = function( data, action ) { 2114 if ( 'import' === pagenow && 'install-plugin' === action ) { 2115 data.success = wp.updates.installImporterSuccess; 2116 data.error = wp.updates.installImporterError; 2117 } 2118 2119 return data; 2120 }; 2121 2122 /** 2123 * Pulls available jobs from the queue and runs them. 2124 * 2125 * @since 4.2.0 2126 * @since 4.6.0 Can handle multiple job types. 2127 */ 2128 wp.updates.queueChecker = function() { 2129 var job; 2130 2131 if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) { 2132 return; 2133 } 2134 2135 job = wp.updates.queue.shift(); 2136 2137 // Handle a queue job. 2138 switch ( job.action ) { 2139 case 'install-plugin': 2140 wp.updates.installPlugin( job.data ); 2141 break; 2142 2143 case 'update-plugin': 2144 wp.updates.updatePlugin( job.data ); 2145 break; 2146 2147 case 'delete-plugin': 2148 wp.updates.deletePlugin( job.data ); 2149 break; 2150 2151 case 'install-theme': 2152 wp.updates.installTheme( job.data ); 2153 break; 2154 2155 case 'update-theme': 2156 wp.updates.updateTheme( job.data ); 2157 break; 2158 2159 case 'delete-theme': 2160 wp.updates.deleteTheme( job.data ); 2161 break; 2162 2163 default: 2164 break; 2165 } 2166 }; 2167 2168 /** 2169 * Requests the users filesystem credentials if they aren't already known. 2170 * 2171 * @since 4.2.0 2172 * 2173 * @param {Event} event Optional. Event interface. 2174 */ 2175 wp.updates.requestFilesystemCredentials = function( event ) { 2176 if ( false === wp.updates.filesystemCredentials.available ) { 2177 /* 2178 * After exiting the credentials request modal, 2179 * return the focus to the element triggering the request. 2180 */ 2181 if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) { 2182 wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target ); 2183 } 2184 2185 wp.updates.ajaxLocked = true; 2186 wp.updates.requestForCredentialsModalOpen(); 2187 } 2188 }; 2189 2190 /** 2191 * Requests the users filesystem credentials if needed and there is no lock. 2192 * 2193 * @since 4.6.0 2194 * 2195 * @param {Event} event Optional. Event interface. 2196 */ 2197 wp.updates.maybeRequestFilesystemCredentials = function( event ) { 2198 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2199 wp.updates.requestFilesystemCredentials( event ); 2200 } 2201 }; 2202 2203 /** 2204 * Keydown handler for the request for credentials modal. 2205 * 2206 * Closes the modal when the escape key is pressed and 2207 * constrains keyboard navigation to inside the modal. 2208 * 2209 * @since 4.2.0 2210 * 2211 * @param {Event} event Event interface. 2212 */ 2213 wp.updates.keydown = function( event ) { 2214 if ( 27 === event.keyCode ) { 2215 wp.updates.requestForCredentialsModalCancel(); 2216 } else if ( 9 === event.keyCode ) { 2217 2218 // #upgrade button must always be the last focus-able element in the dialog. 2219 if ( 'upgrade' === event.target.id && ! event.shiftKey ) { 2220 $( '#hostname' ).trigger( 'focus' ); 2221 2222 event.preventDefault(); 2223 } else if ( 'hostname' === event.target.id && event.shiftKey ) { 2224 $( '#upgrade' ).trigger( 'focus' ); 2225 2226 event.preventDefault(); 2227 } 2228 } 2229 }; 2230 2231 /** 2232 * Opens the request for credentials modal. 2233 * 2234 * @since 4.2.0 2235 */ 2236 wp.updates.requestForCredentialsModalOpen = function() { 2237 var $modal = $( '#request-filesystem-credentials-dialog' ); 2238 2239 $( 'body' ).addClass( 'modal-open' ); 2240 $modal.show(); 2241 $modal.find( 'input:enabled:first' ).trigger( 'focus' ); 2242 $modal.on( 'keydown', wp.updates.keydown ); 2243 }; 2244 2245 /** 2246 * Closes the request for credentials modal. 2247 * 2248 * @since 4.2.0 2249 */ 2250 wp.updates.requestForCredentialsModalClose = function() { 2251 $( '#request-filesystem-credentials-dialog' ).hide(); 2252 $( 'body' ).removeClass( 'modal-open' ); 2253 2254 if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) { 2255 wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' ); 2256 } 2257 }; 2258 2259 /** 2260 * Takes care of the steps that need to happen when the modal is canceled out. 2261 * 2262 * @since 4.2.0 2263 * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions. 2264 */ 2265 wp.updates.requestForCredentialsModalCancel = function() { 2266 2267 // Not ajaxLocked and no queue means we already have cleared things up. 2268 if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) { 2269 return; 2270 } 2271 2272 _.each( wp.updates.queue, function( job ) { 2273 $document.trigger( 'credential-modal-cancel', job ); 2274 } ); 2275 2276 // Remove the lock, and clear the queue. 2277 wp.updates.ajaxLocked = false; 2278 wp.updates.queue = []; 2279 2280 wp.updates.requestForCredentialsModalClose(); 2281 }; 2282 2283 /** 2284 * Displays an error message in the request for credentials form. 2285 * 2286 * @since 4.2.0 2287 * 2288 * @param {string} message Error message. 2289 */ 2290 wp.updates.showErrorInCredentialsForm = function( message ) { 2291 var $filesystemForm = $( '#request-filesystem-credentials-form' ); 2292 2293 // Remove any existing error. 2294 $filesystemForm.find( '.notice' ).remove(); 2295 $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error" role="alert"><p>' + message + '</p></div>' ); 2296 }; 2297 2298 /** 2299 * Handles credential errors and runs events that need to happen in that case. 2300 * 2301 * @since 4.2.0 2302 * 2303 * @param {Object} response Ajax response. 2304 * @param {string} action The type of request to perform. 2305 */ 2306 wp.updates.credentialError = function( response, action ) { 2307 2308 // Restore callbacks. 2309 response = wp.updates._addCallbacks( response, action ); 2310 2311 wp.updates.queue.unshift( { 2312 action: action, 2313 2314 /* 2315 * Not cool that we're depending on response for this data. 2316 * This would feel more whole in a view all tied together. 2317 */ 2318 data: response 2319 } ); 2320 2321 wp.updates.filesystemCredentials.available = false; 2322 wp.updates.showErrorInCredentialsForm( response.errorMessage ); 2323 wp.updates.requestFilesystemCredentials(); 2324 }; 2325 2326 /** 2327 * Handles credentials errors if it could not connect to the filesystem. 2328 * 2329 * @since 4.6.0 2330 * 2331 * @param {Object} response Response from the server. 2332 * @param {string} response.errorCode Error code for the error that occurred. 2333 * @param {string} response.errorMessage The error that occurred. 2334 * @param {string} action The type of request to perform. 2335 * @return {boolean} Whether there is an error that needs to be handled or not. 2336 */ 2337 wp.updates.maybeHandleCredentialError = function( response, action ) { 2338 if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) { 2339 wp.updates.credentialError( response, action ); 2340 return true; 2341 } 2342 2343 return false; 2344 }; 2345 2346 /** 2347 * Validates an Ajax response to ensure it's a proper object. 2348 * 2349 * If the response deems to be invalid, an admin notice is being displayed. 2350 * 2351 * @param {(Object|string)} response Response from the server. 2352 * @param {Function} [response.always] Optional. Callback for when the Deferred is resolved or rejected. 2353 * @param {string} [response.statusText] Optional. Status message corresponding to the status code. 2354 * @param {string} [response.responseText] Optional. Request response as text. 2355 * @param {string} action Type of action the response is referring to. Can be 'delete', 2356 * 'update' or 'install'. 2357 * @return {boolean} Whether the response is valid or not. 2358 */ 2359 wp.updates.isValidResponse = function( response, action ) { 2360 var error = __( 'An error occurred during the update process. Please try again.' ), 2361 errorMessage; 2362 2363 // Make sure the response is a valid data object and not a Promise object. 2364 if ( _.isObject( response ) && ! _.isFunction( response.always ) ) { 2365 return true; 2366 } 2367 2368 if ( _.isString( response ) && '-1' === response ) { 2369 error = __( 'An error has occurred. Please reload the page and try again.' ); 2370 } else if ( _.isString( response ) ) { 2371 error = response; 2372 } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) { 2373 error = __( 'Connection lost or the server is busy. Please try again later.' ); 2374 } else if ( _.isString( response.responseText ) && '' !== response.responseText ) { 2375 error = response.responseText; 2376 } else if ( _.isString( response.statusText ) ) { 2377 error = response.statusText; 2378 } 2379 2380 switch ( action ) { 2381 case 'update': 2382 /* translators: %s: Error string for a failed update. */ 2383 errorMessage = __( 'Update failed: %s' ); 2384 break; 2385 2386 case 'install': 2387 /* translators: %s: Error string for a failed installation. */ 2388 errorMessage = __( 'Installation failed: %s' ); 2389 break; 2390 2391 case 'check-dependencies': 2392 /* translators: %s: Error string for a failed dependencies check. */ 2393 errorMessage = __( 'Dependencies check failed: %s' ); 2394 break; 2395 2396 case 'activate': 2397 /* translators: %s: Error string for a failed activation. */ 2398 errorMessage = __( 'Activation failed: %s' ); 2399 break; 2400 2401 case 'delete': 2402 /* translators: %s: Error string for a failed deletion. */ 2403 errorMessage = __( 'Deletion failed: %s' ); 2404 break; 2405 } 2406 2407 // Messages are escaped, remove HTML tags to make them more readable. 2408 error = error.replace( /<[\/a-z][^<>]*>/gi, '' ); 2409 errorMessage = errorMessage.replace( '%s', error ); 2410 2411 // Add admin notice. 2412 wp.updates.addAdminNotice( { 2413 id: 'unknown_error', 2414 className: 'notice-error is-dismissible', 2415 message: _.escape( errorMessage ) 2416 } ); 2417 2418 // Remove the lock, and clear the queue. 2419 wp.updates.ajaxLocked = false; 2420 wp.updates.queue = []; 2421 2422 // Change buttons of all running updates. 2423 $( '.button.updating-message' ) 2424 .removeClass( 'updating-message' ) 2425 .removeAttr( 'aria-label' ) 2426 .prop( 'disabled', true ) 2427 .text( __( 'Update failed.' ) ); 2428 2429 $( '.updating-message:not(.button):not(.thickbox)' ) 2430 .removeClass( 'updating-message notice-warning' ) 2431 .addClass( 'notice-error' ) 2432 .find( 'p' ) 2433 .removeAttr( 'aria-label' ) 2434 .text( errorMessage ); 2435 2436 wp.a11y.speak( errorMessage, 'assertive' ); 2437 2438 return false; 2439 }; 2440 2441 /** 2442 * Potentially adds an AYS to a user attempting to leave the page. 2443 * 2444 * If an update is on-going and a user attempts to leave the page, 2445 * opens an "Are you sure?" alert. 2446 * 2447 * @since 4.2.0 2448 * @return {string|void} Message to display in the "Are you sure?" alert, or nothing. 2449 */ 2450 wp.updates.beforeunload = function() { 2451 if ( wp.updates.ajaxLocked ) { 2452 return __( 'Updates may not complete if you navigate away from this page.' ); 2453 } 2454 }; 2455 2456 $( function() { 2457 var $pluginFilter = $( '#plugin-filter, #plugin-information-footer' ), 2458 $bulkActionForm = $( '#bulk-action-form' ), 2459 $filesystemForm = $( '#request-filesystem-credentials-form' ), 2460 $filesystemModal = $( '#request-filesystem-credentials-dialog' ), 2461 $pluginSearch = $( '.plugins-php .wp-filter-search' ), 2462 $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' ); 2463 2464 settings = _.extend( settings, window._wpUpdatesItemCounts || {} ); 2465 2466 if ( settings.totals ) { 2467 wp.updates.refreshCount(); 2468 } 2469 2470 /* 2471 * Whether a user needs to submit filesystem credentials. 2472 * 2473 * This is based on whether the form was output on the page server-side. 2474 * 2475 * @see {wp_print_request_filesystem_credentials_modal() in PHP} 2476 */ 2477 wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0; 2478 2479 /** 2480 * File system credentials form submit noop-er / handler. 2481 * 2482 * @since 4.2.0 2483 */ 2484 $filesystemModal.on( 'submit', 'form', function( event ) { 2485 event.preventDefault(); 2486 2487 // Persist the credentials input by the user for the duration of the page load. 2488 wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val(); 2489 wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val(); 2490 wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val(); 2491 wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val(); 2492 wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val(); 2493 wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val(); 2494 wp.updates.filesystemCredentials.fsNonce = $( '#_fs_nonce' ).val(); 2495 wp.updates.filesystemCredentials.available = true; 2496 2497 // Unlock and invoke the queue. 2498 wp.updates.ajaxLocked = false; 2499 wp.updates.queueChecker(); 2500 2501 wp.updates.requestForCredentialsModalClose(); 2502 } ); 2503 2504 /** 2505 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal. 2506 * 2507 * @since 4.2.0 2508 */ 2509 $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel ); 2510 2511 /** 2512 * Hide SSH fields when not selected. 2513 * 2514 * @since 4.2.0 2515 */ 2516 $filesystemForm.on( 'change', 'input[name="connection_type"]', function() { 2517 $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) ); 2518 } ).trigger( 'change' ); 2519 2520 /** 2521 * Handles events after the credential modal was closed. 2522 * 2523 * @since 4.6.0 2524 * 2525 * @param {Event} event Event interface. 2526 * @param {string} job The install/update.delete request. 2527 */ 2528 $document.on( 'credential-modal-cancel', function( event, job ) { 2529 var $updatingMessage = $( '.updating-message' ), 2530 $message, originalText; 2531 2532 if ( 'import' === pagenow ) { 2533 $updatingMessage.removeClass( 'updating-message' ); 2534 } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 2535 if ( 'update-plugin' === job.action ) { 2536 $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' ); 2537 } else if ( 'delete-plugin' === job.action ) { 2538 $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' ); 2539 } 2540 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { 2541 if ( 'update-theme' === job.action ) { 2542 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' ); 2543 } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) { 2544 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' ); 2545 } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) { 2546 $message = $( '.theme-actions .delete-theme' ); 2547 } 2548 } else { 2549 $message = $updatingMessage; 2550 } 2551 2552 if ( $message && $message.hasClass( 'updating-message' ) ) { 2553 originalText = $message.data( 'originaltext' ); 2554 2555 if ( 'undefined' === typeof originalText ) { 2556 originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) ); 2557 } 2558 2559 $message 2560 .removeClass( 'updating-message' ) 2561 .html( originalText ); 2562 2563 if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { 2564 if ( 'update-plugin' === job.action ) { 2565 $message.attr( 2566 'aria-label', 2567 sprintf( 2568 /* translators: %s: Plugin name and version. */ 2569 _x( 'Update %s now', 'plugin' ), 2570 $message.data( 'name' ) 2571 ) 2572 ); 2573 } else if ( 'install-plugin' === job.action ) { 2574 $message.attr( 2575 'aria-label', 2576 sprintf( 2577 /* translators: %s: Plugin name. */ 2578 _x( 'Install %s now', 'plugin' ), 2579 $message.data( 'name' ) 2580 ) 2581 ); 2582 } 2583 } 2584 } 2585 2586 wp.a11y.speak( __( 'Update canceled.' ) ); 2587 } ); 2588 2589 /** 2590 * Click handler for plugin updates in List Table view. 2591 * 2592 * @since 4.2.0 2593 * 2594 * @param {Event} event Event interface. 2595 */ 2596 $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { 2597 var $message = $( event.target ), 2598 $pluginRow = $message.parents( 'tr' ); 2599 2600 event.preventDefault(); 2601 2602 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { 2603 return; 2604 } 2605 2606 wp.updates.maybeRequestFilesystemCredentials( event ); 2607 2608 // Return the user to the input box of the plugin's table row after closing the modal. 2609 wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); 2610 wp.updates.updatePlugin( { 2611 plugin: $pluginRow.data( 'plugin' ), 2612 slug: $pluginRow.data( 'slug' ) 2613 } ); 2614 } ); 2615 2616 /** 2617 * Click handler for plugin updates in plugin install view. 2618 * 2619 * @since 4.2.0 2620 * 2621 * @param {Event} event Event interface. 2622 */ 2623 $pluginFilter.on( 'click', '.update-now', function( event ) { 2624 var $button = $( event.target ); 2625 event.preventDefault(); 2626 2627 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { 2628 return; 2629 } 2630 2631 wp.updates.maybeRequestFilesystemCredentials( event ); 2632 2633 wp.updates.updatePlugin( { 2634 plugin: $button.data( 'plugin' ), 2635 slug: $button.data( 'slug' ) 2636 } ); 2637 } ); 2638 2639 /** 2640 * Click handler for plugin installs in plugin install view. 2641 * 2642 * @since 4.6.0 2643 * 2644 * @param {Event} event Event interface. 2645 */ 2646 $pluginFilter.on( 'click', '.install-now', function( event ) { 2647 var $button = $( event.target ); 2648 event.preventDefault(); 2649 2650 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { 2651 return; 2652 } 2653 2654 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2655 wp.updates.requestFilesystemCredentials( event ); 2656 2657 $document.on( 'credential-modal-cancel', function() { 2658 var $message = $( '.install-now.updating-message' ); 2659 2660 $message 2661 .removeClass( 'updating-message' ) 2662 .text( _x( 'Install Now', 'plugin' ) ); 2663 2664 wp.a11y.speak( __( 'Update canceled.' ) ); 2665 } ); 2666 } 2667 2668 wp.updates.installPlugin( { 2669 slug: $button.data( 'slug' ) 2670 } ); 2671 } ); 2672 2673 /** 2674 * Click handler for plugin activations in plugin activation modal view. 2675 * 2676 * @since 6.5.0 2677 * @since 6.5.4 Redirect the parent window to the activation URL. 2678 * 2679 * @param {Event} event Event interface. 2680 */ 2681 $document.on( 'click', '#plugin-information-footer .activate-now', function( event ) { 2682 event.preventDefault(); 2683 window.parent.location.href = $( event.target ).attr( 'href' ); 2684 }); 2685 2686 /** 2687 * Click handler for importer plugins installs in the Import screen. 2688 * 2689 * @since 4.6.0 2690 * 2691 * @param {Event} event Event interface. 2692 */ 2693 $document.on( 'click', '.importer-item .install-now', function( event ) { 2694 var $button = $( event.target ), 2695 pluginName = $( this ).data( 'name' ); 2696 2697 event.preventDefault(); 2698 2699 if ( $button.hasClass( 'updating-message' ) ) { 2700 return; 2701 } 2702 2703 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2704 wp.updates.requestFilesystemCredentials( event ); 2705 2706 $document.on( 'credential-modal-cancel', function() { 2707 2708 $button 2709 .removeClass( 'updating-message' ) 2710 .attr( 2711 'aria-label', 2712 sprintf( 2713 /* translators: %s: Plugin name. */ 2714 _x( 'Install %s now', 'plugin' ), 2715 pluginName 2716 ) 2717 ) 2718 .text( _x( 'Install Now', 'plugin' ) ); 2719 2720 wp.a11y.speak( __( 'Update canceled.' ) ); 2721 } ); 2722 } 2723 2724 wp.updates.installPlugin( { 2725 slug: $button.data( 'slug' ), 2726 pagenow: pagenow, 2727 success: wp.updates.installImporterSuccess, 2728 error: wp.updates.installImporterError 2729 } ); 2730 } ); 2731 2732 /** 2733 * Click handler for plugin deletions. 2734 * 2735 * @since 4.6.0 2736 * 2737 * @param {Event} event Event interface. 2738 */ 2739 $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { 2740 var $pluginRow = $( event.target ).parents( 'tr' ), 2741 confirmMessage; 2742 2743 if ( $pluginRow.hasClass( 'is-uninstallable' ) ) { 2744 confirmMessage = sprintf( 2745 /* translators: %s: Plugin name. */ 2746 __( 'Are you sure you want to delete %s and its data?' ), 2747 $pluginRow.find( '.plugin-title strong' ).text() 2748 ); 2749 } else { 2750 confirmMessage = sprintf( 2751 /* translators: %s: Plugin name. */ 2752 __( 'Are you sure you want to delete %s?' ), 2753 $pluginRow.find( '.plugin-title strong' ).text() 2754 ); 2755 } 2756 2757 event.preventDefault(); 2758 2759 if ( ! window.confirm( confirmMessage ) ) { 2760 return; 2761 } 2762 2763 wp.updates.maybeRequestFilesystemCredentials( event ); 2764 2765 wp.updates.deletePlugin( { 2766 plugin: $pluginRow.data( 'plugin' ), 2767 slug: $pluginRow.data( 'slug' ) 2768 } ); 2769 2770 } ); 2771 2772 /** 2773 * Click handler for theme updates. 2774 * 2775 * @since 4.6.0 2776 * 2777 * @param {Event} event Event interface. 2778 */ 2779 $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { 2780 var $message = $( event.target ), 2781 $themeRow = $message.parents( 'tr' ); 2782 2783 event.preventDefault(); 2784 2785 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { 2786 return; 2787 } 2788 2789 wp.updates.maybeRequestFilesystemCredentials( event ); 2790 2791 // Return the user to the input box of the theme's table row after closing the modal. 2792 wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); 2793 wp.updates.updateTheme( { 2794 slug: $themeRow.data( 'slug' ) 2795 } ); 2796 } ); 2797 2798 /** 2799 * Click handler for theme deletions. 2800 * 2801 * @since 4.6.0 2802 * 2803 * @param {Event} event Event interface. 2804 */ 2805 $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { 2806 var $themeRow = $( event.target ).parents( 'tr' ), 2807 confirmMessage = sprintf( 2808 /* translators: %s: Theme name. */ 2809 __( 'Are you sure you want to delete %s?' ), 2810 $themeRow.find( '.theme-title strong' ).text() 2811 ); 2812 2813 event.preventDefault(); 2814 2815 if ( ! window.confirm( confirmMessage ) ) { 2816 return; 2817 } 2818 2819 wp.updates.maybeRequestFilesystemCredentials( event ); 2820 2821 wp.updates.deleteTheme( { 2822 slug: $themeRow.data( 'slug' ) 2823 } ); 2824 } ); 2825 2826 /** 2827 * Bulk action handler for plugins and themes. 2828 * 2829 * Handles both deletions and updates. 2830 * 2831 * @since 4.6.0 2832 * 2833 * @param {Event} event Event interface. 2834 */ 2835 $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) { 2836 var bulkAction = $( event.target ).siblings( 'select' ).val(), 2837 itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), 2838 success = 0, 2839 error = 0, 2840 errorMessages = [], 2841 type, action; 2842 2843 // Determine which type of item we're dealing with. 2844 switch ( pagenow ) { 2845 case 'plugins': 2846 case 'plugins-network': 2847 type = 'plugin'; 2848 break; 2849 2850 case 'themes-network': 2851 type = 'theme'; 2852 break; 2853 2854 default: 2855 return; 2856 } 2857 2858 // Bail if there were no items selected. 2859 if ( ! itemsSelected.length ) { 2860 bulkAction = false; 2861 } 2862 2863 // Determine the type of request we're dealing with. 2864 switch ( bulkAction ) { 2865 case 'update-selected': 2866 action = bulkAction.replace( 'selected', type ); 2867 break; 2868 2869 case 'delete-selected': 2870 var confirmMessage = 'plugin' === type ? 2871 __( 'Are you sure you want to delete the selected plugins and their data?' ) : 2872 __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ); 2873 2874 if ( ! window.confirm( confirmMessage ) ) { 2875 event.preventDefault(); 2876 return; 2877 } 2878 2879 action = bulkAction.replace( 'selected', type ); 2880 break; 2881 2882 default: 2883 return; 2884 } 2885 2886 wp.updates.maybeRequestFilesystemCredentials( event ); 2887 2888 event.preventDefault(); 2889 2890 // Un-check the bulk checkboxes. 2891 $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); 2892 2893 $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected ); 2894 2895 // Find all the checkboxes which have been checked. 2896 itemsSelected.each( function( index, element ) { 2897 var $checkbox = $( element ), 2898 $itemRow = $checkbox.parents( 'tr' ); 2899 2900 // Only add update-able items to the update queue. 2901 if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { 2902 2903 // Un-check the box. 2904 $checkbox.prop( 'checked', false ); 2905 return; 2906 } 2907 2908 // Don't add items to the update queue again, even if the user clicks the update button several times. 2909 if ( 'update-selected' === bulkAction && $itemRow.hasClass( 'is-enqueued' ) ) { 2910 return; 2911 } 2912 2913 $itemRow.addClass( 'is-enqueued' ); 2914 2915 // Add it to the queue. 2916 wp.updates.queue.push( { 2917 action: action, 2918 data: { 2919 plugin: $itemRow.data( 'plugin' ), 2920 slug: $itemRow.data( 'slug' ) 2921 } 2922 } ); 2923 } ); 2924 2925 // Display bulk notification for updates of any kind. 2926 $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { 2927 var $itemRow = $( '[data-slug="' + response.slug + '"]' ), 2928 $bulkActionNotice, itemName; 2929 2930 if ( 'wp-' + response.update + '-update-success' === event.type ) { 2931 success++; 2932 } else { 2933 itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text(); 2934 2935 error++; 2936 errorMessages.push( itemName + ': ' + response.errorMessage ); 2937 } 2938 2939 $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false ); 2940 2941 wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); 2942 2943 var successMessage = null; 2944 2945 if ( success ) { 2946 if ( 'plugin' === response.update ) { 2947 successMessage = sprintf( 2948 /* translators: %s: Number of plugins. */ 2949 _n( '%s plugin successfully updated.', '%s plugins successfully updated.', success ), 2950 success 2951 ); 2952 } else { 2953 successMessage = sprintf( 2954 /* translators: %s: Number of themes. */ 2955 _n( '%s theme successfully updated.', '%s themes successfully updated.', success ), 2956 success 2957 ); 2958 } 2959 } 2960 2961 var errorMessage = null; 2962 2963 if ( error ) { 2964 errorMessage = sprintf( 2965 /* translators: %s: Number of failed updates. */ 2966 _n( '%s update failed.', '%s updates failed.', error ), 2967 error 2968 ); 2969 } 2970 2971 wp.updates.addAdminNotice( { 2972 id: 'bulk-action-notice', 2973 className: 'bulk-action-notice', 2974 successMessage: successMessage, 2975 errorMessage: errorMessage, 2976 errorMessages: errorMessages, 2977 type: response.update 2978 } ); 2979 2980 $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { 2981 // $( this ) is the clicked button, no need to get it again. 2982 $( this ) 2983 .toggleClass( 'bulk-action-errors-collapsed' ) 2984 .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) ); 2985 // Show the errors list. 2986 $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' ); 2987 } ); 2988 2989 if ( error > 0 && ! wp.updates.queue.length ) { 2990 $( 'html, body' ).animate( { scrollTop: 0 } ); 2991 } 2992 } ); 2993 2994 // Reset admin notice template after #bulk-action-notice was added. 2995 $document.on( 'wp-updates-notice-added', function() { 2996 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); 2997 } ); 2998 2999 // Check the queue, now that the event handlers have been added. 3000 wp.updates.queueChecker(); 3001 } ); 3002 3003 if ( $pluginInstallSearch.length ) { 3004 $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' ); 3005 } 3006 3007 // Track the previous search string length. 3008 var previousSearchStringLength = 0; 3009 wp.updates.shouldSearch = function( searchStringLength ) { 3010 var shouldSearch = searchStringLength >= wp.updates.searchMinCharacters || 3011 previousSearchStringLength > wp.updates.searchMinCharacters; 3012 previousSearchStringLength = searchStringLength; 3013 return shouldSearch; 3014 }; 3015 3016 /** 3017 * Handles changes to the plugin search box on the new-plugin page, 3018 * searching the repository dynamically. 3019 * 3020 * @since 4.6.0 3021 */ 3022 $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) { 3023 var $searchTab = $( '.plugin-install-search' ), data, searchLocation, 3024 searchStringLength = $pluginInstallSearch.val().length; 3025 3026 data = { 3027 _ajax_nonce: wp.updates.ajaxNonce, 3028 s: encodeURIComponent( event.target.value ), 3029 tab: 'search', 3030 type: $( '#typeselector' ).val(), 3031 pagenow: pagenow 3032 }; 3033 searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) ); 3034 3035 // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in. 3036 if ( wp.updates.shouldSearch( searchStringLength ) ) { 3037 $pluginInstallSearch.attr( 'autocomplete', 'off' ); 3038 } else { 3039 $pluginInstallSearch.attr( 'autocomplete', 'on' ); 3040 return; 3041 } 3042 3043 // Clear on escape. 3044 if ( 'keyup' === event.type && 27 === event.which ) { 3045 event.target.value = ''; 3046 } 3047 3048 if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) { 3049 return; 3050 } else { 3051 $pluginFilter.empty(); 3052 wp.updates.searchTerm = data.s; 3053 } 3054 3055 if ( window.history && window.history.replaceState ) { 3056 window.history.replaceState( null, '', searchLocation ); 3057 } 3058 3059 if ( ! $searchTab.length ) { 3060 $searchTab = $( '<li class="plugin-install-search" />' ) 3061 .append( $( '<a />', { 3062 'class': 'current', 3063 'href': searchLocation, 3064 'text': __( 'Search Results' ) 3065 } ) ); 3066 3067 $( '.wp-filter .filter-links .current' ) 3068 .removeClass( 'current' ) 3069 .parents( '.filter-links' ) 3070 .prepend( $searchTab ); 3071 3072 $pluginFilter.prev( 'p' ).remove(); 3073 $( '.plugins-popular-tags-wrapper' ).remove(); 3074 } 3075 3076 if ( 'undefined' !== typeof wp.updates.searchRequest ) { 3077 wp.updates.searchRequest.abort(); 3078 } 3079 $( 'body' ).addClass( 'loading-content' ); 3080 3081 wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { 3082 $( 'body' ).removeClass( 'loading-content' ); 3083 $pluginFilter.append( response.items ); 3084 delete wp.updates.searchRequest; 3085 3086 if ( 0 === response.count ) { 3087 wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) ); 3088 } else { 3089 wp.a11y.speak( 3090 sprintf( 3091 /* translators: %s: Number of plugins. */ 3092 __( 'Number of plugins found: %d' ), 3093 response.count 3094 ) 3095 ); 3096 } 3097 } ); 3098 }, 1000 ) ); 3099 3100 if ( $pluginSearch.length ) { 3101 $pluginSearch.attr( 'aria-describedby', 'live-search-desc' ); 3102 3103 } 3104 3105 /** 3106 * Handles changes to the plugin search box on the Installed Plugins screen, 3107 * searching the plugin list dynamically. 3108 * 3109 * @since 4.6.0 3110 */ 3111 $pluginSearch.on( 'keyup input', _.debounce( function( event ) { 3112 var data = { 3113 _ajax_nonce: wp.updates.ajaxNonce, 3114 s: encodeURIComponent( event.target.value ), 3115 pagenow: pagenow, 3116 plugin_status: 'all' 3117 }, 3118 queryArgs, 3119 searchStringLength = $pluginSearch.val().length; 3120 3121 // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in. 3122 if ( wp.updates.shouldSearch( searchStringLength ) ) { 3123 $pluginSearch.attr( 'autocomplete', 'off' ); 3124 } else { 3125 $pluginSearch.attr( 'autocomplete', 'on' ); 3126 return; 3127 } 3128 3129 // Clear on escape. 3130 if ( 'keyup' === event.type && 27 === event.which ) { 3131 event.target.value = ''; 3132 } 3133 3134 if ( wp.updates.searchTerm === data.s ) { 3135 return; 3136 } else { 3137 wp.updates.searchTerm = data.s; 3138 } 3139 3140 queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) { 3141 if ( item ) return item.split( '=' ); 3142 } ) ) ); 3143 3144 data.plugin_status = queryArgs.plugin_status || 'all'; 3145 3146 if ( window.history && window.history.replaceState ) { 3147 window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status ); 3148 } 3149 3150 if ( 'undefined' !== typeof wp.updates.searchRequest ) { 3151 wp.updates.searchRequest.abort(); 3152 } 3153 3154 $bulkActionForm.empty(); 3155 $( 'body' ).addClass( 'loading-content' ); 3156 $( '.subsubsub .current' ).removeClass( 'current' ); 3157 3158 wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { 3159 3160 // Can we just ditch this whole subtitle business? 3161 var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( 3162 sprintf( 3163 /* translators: %s: Search query. */ 3164 __( 'Search results for: %s' ), 3165 '<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>' 3166 ) ), 3167 $oldSubTitle = $( '.wrap .subtitle' ); 3168 3169 if ( ! data.s.length ) { 3170 $oldSubTitle.remove(); 3171 $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' ); 3172 } else if ( $oldSubTitle.length ) { 3173 $oldSubTitle.replaceWith( $subTitle ); 3174 } else { 3175 $( '.wp-header-end' ).before( $subTitle ); 3176 } 3177 3178 $( 'body' ).removeClass( 'loading-content' ); 3179 $bulkActionForm.append( response.items ); 3180 delete wp.updates.searchRequest; 3181 3182 if ( 0 === response.count ) { 3183 wp.a11y.speak( __( 'No plugins found. Try a different search.' ) ); 3184 } else { 3185 wp.a11y.speak( 3186 sprintf( 3187 /* translators: %s: Number of plugins. */ 3188 __( 'Number of plugins found: %d' ), 3189 response.count 3190 ) 3191 ); 3192 } 3193 } ); 3194 }, 500 ) ); 3195 3196 /** 3197 * Trigger a search event when the search form gets submitted. 3198 * 3199 * @since 4.6.0 3200 */ 3201 $document.on( 'submit', '.search-plugins', function( event ) { 3202 event.preventDefault(); 3203 3204 $( 'input.wp-filter-search' ).trigger( 'input' ); 3205 } ); 3206 3207 /** 3208 * Trigger a search event when the "Try Again" button is clicked. 3209 * 3210 * @since 4.9.0 3211 */ 3212 $document.on( 'click', '.try-again', function( event ) { 3213 event.preventDefault(); 3214 $pluginInstallSearch.trigger( 'input' ); 3215 } ); 3216 3217 /** 3218 * Trigger a search event when the search type gets changed. 3219 * 3220 * @since 4.6.0 3221 */ 3222 $( '#typeselector' ).on( 'change', function() { 3223 var $search = $( 'input[name="s"]' ); 3224 3225 if ( $search.val().length ) { 3226 $search.trigger( 'input', 'typechange' ); 3227 } 3228 } ); 3229 3230 /** 3231 * Click handler for updating a plugin from the details modal on `plugin-install.php`. 3232 * 3233 * @since 4.2.0 3234 * 3235 * @param {Event} event Event interface. 3236 */ 3237 $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { 3238 var target = window.parent === window ? null : window.parent, 3239 update; 3240 3241 $.support.postMessage = !! window.postMessage; 3242 3243 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) { 3244 return; 3245 } 3246 3247 event.preventDefault(); 3248 3249 update = { 3250 action: 'update-plugin', 3251 data: { 3252 plugin: $( this ).data( 'plugin' ), 3253 slug: $( this ).data( 'slug' ) 3254 } 3255 }; 3256 3257 target.postMessage( JSON.stringify( update ), window.location.origin ); 3258 } ); 3259 3260 /** 3261 * Handles postMessage events. 3262 * 3263 * @since 4.2.0 3264 * @since 4.6.0 Switched `update-plugin` action to use the queue. 3265 * 3266 * @param {Event} event Event interface. 3267 */ 3268 $( window ).on( 'message', function( event ) { 3269 var originalEvent = event.originalEvent, 3270 expectedOrigin = document.location.protocol + '//' + document.location.host, 3271 message; 3272 3273 if ( originalEvent.origin !== expectedOrigin ) { 3274 return; 3275 } 3276 3277 try { 3278 message = JSON.parse( originalEvent.data ); 3279 } catch ( e ) { 3280 return; 3281 } 3282 3283 if ( ! message ) { 3284 return; 3285 } 3286 3287 if ( 3288 'undefined' !== typeof message.status && 3289 'undefined' !== typeof message.slug && 3290 'undefined' !== typeof message.text && 3291 'undefined' !== typeof message.ariaLabel 3292 ) { 3293 var $card = $( '.plugin-card-' + message.slug ), 3294 $message = $card.find( '[data-slug="' + message.slug + '"]' ); 3295 3296 if ( 'undefined' !== typeof message.removeClasses ) { 3297 $message.removeClass( message.removeClasses ); 3298 } 3299 3300 if ( 'undefined' !== typeof message.addClasses ) { 3301 $message.addClass( message.addClasses ); 3302 } 3303 3304 if ( '' === message.ariaLabel ) { 3305 $message.removeAttr( 'aria-label' ); 3306 } else { 3307 $message.attr( 'aria-label', message.ariaLabel ); 3308 } 3309 3310 if ( 'dependencies-check-success' === message.status ) { 3311 $message 3312 .attr( 'data-name', message.pluginName ) 3313 .attr( 'data-slug', message.slug ) 3314 .attr( 'data-plugin', message.plugin ) 3315 .attr( 'href', message.href ); 3316 } 3317 3318 $message.text( message.text ); 3319 } 3320 3321 if ( 'undefined' === typeof message.action ) { 3322 return; 3323 } 3324 3325 switch ( message.action ) { 3326 3327 // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. 3328 case 'decrementUpdateCount': 3329 /** 3330 * @property {string} message The message associated with the update. 3331 * @property {string} message.upgradeType The type of upgrade being performed. 3332 */ 3333 wp.updates.decrementCount( message.upgradeType ); 3334 break; 3335 3336 case 'install-plugin': 3337 case 'update-plugin': 3338 if ( 'undefined' === typeof message.data || 'undefined' === typeof message.data.slug ) { 3339 return; 3340 } 3341 3342 message.data = wp.updates._addCallbacks( message.data, message.action ); 3343 3344 wp.updates.queue.push( message ); 3345 wp.updates.queueChecker(); 3346 break; 3347 } 3348 } ); 3349 3350 /** 3351 * Adds a callback to display a warning before leaving the page. 3352 * 3353 * @since 4.2.0 3354 */ 3355 $( window ).on( 'beforeunload', wp.updates.beforeunload ); 3356 3357 /** 3358 * Prevents the page form scrolling when activating auto-updates with the Spacebar key. 3359 * 3360 * @since 5.5.0 3361 */ 3362 $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { 3363 if ( 32 === event.which ) { 3364 event.preventDefault(); 3365 } 3366 } ); 3367 3368 /** 3369 * Click and keyup handler for enabling and disabling plugin and theme auto-updates. 3370 * 3371 * These controls can be either links or buttons. When JavaScript is enabled, 3372 * we want them to behave like buttons. An ARIA role `button` is added via 3373 * the JavaScript that targets elements with the CSS class `aria-button-if-js`. 3374 * 3375 * @since 5.5.0 3376 */ 3377 $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { 3378 var data, asset, type, $parent, 3379 $toggler = $( this ), 3380 action = $toggler.attr( 'data-wp-action' ), 3381 $label = $toggler.find( '.label' ); 3382 3383 if ( 'keyup' === event.type && 32 !== event.which ) { 3384 return; 3385 } 3386 3387 if ( 'themes' !== pagenow ) { 3388 $parent = $toggler.closest( '.column-auto-updates' ); 3389 } else { 3390 $parent = $toggler.closest( '.theme-autoupdate' ); 3391 } 3392 3393 event.preventDefault(); 3394 3395 // Prevent multiple simultaneous requests. 3396 if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) { 3397 return; 3398 } 3399 3400 $toggler.attr( 'data-doing-ajax', 'yes' ); 3401 3402 switch ( pagenow ) { 3403 case 'plugins': 3404 case 'plugins-network': 3405 type = 'plugin'; 3406 asset = $toggler.closest( 'tr' ).attr( 'data-plugin' ); 3407 break; 3408 case 'themes-network': 3409 type = 'theme'; 3410 asset = $toggler.closest( 'tr' ).attr( 'data-slug' ); 3411 break; 3412 case 'themes': 3413 type = 'theme'; 3414 asset = $toggler.attr( 'data-slug' ); 3415 break; 3416 } 3417 3418 // Clear any previous errors. 3419 $parent.find( '.notice.notice-error' ).addClass( 'hidden' ); 3420 3421 // Show loading status. 3422 if ( 'enable' === action ) { 3423 $label.text( __( 'Enabling...' ) ); 3424 } else { 3425 $label.text( __( 'Disabling...' ) ); 3426 } 3427 3428 $toggler.find( '.dashicons-update' ).removeClass( 'hidden' ); 3429 3430 data = { 3431 action: 'toggle-auto-updates', 3432 _ajax_nonce: settings.ajax_nonce, 3433 state: action, 3434 type: type, 3435 asset: asset 3436 }; 3437 3438 $.post( window.ajaxurl, data ) 3439 .done( function( response ) { 3440 var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage, 3441 href = $toggler.attr( 'href' ); 3442 3443 if ( ! response.success ) { 3444 // if WP returns 0 for response (which can happen in a few cases), 3445 // output the general error message since we won't have response.data.error. 3446 if ( response.data && response.data.error ) { 3447 errorMessage = response.data.error; 3448 } else { 3449 errorMessage = __( 'The request could not be completed.' ); 3450 } 3451 3452 $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage ); 3453 wp.a11y.speak( errorMessage, 'assertive' ); 3454 return; 3455 } 3456 3457 // Update the counts in the enabled/disabled views if on a screen 3458 // with a list table. 3459 if ( 'themes' !== pagenow ) { 3460 $enabled = $( '.auto-update-enabled span' ); 3461 $disabled = $( '.auto-update-disabled span' ); 3462 enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; 3463 disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; 3464 3465 switch ( action ) { 3466 case 'enable': 3467 ++enabledNumber; 3468 --disabledNumber; 3469 break; 3470 case 'disable': 3471 --enabledNumber; 3472 ++disabledNumber; 3473 break; 3474 } 3475 3476 enabledNumber = Math.max( 0, enabledNumber ); 3477 disabledNumber = Math.max( 0, disabledNumber ); 3478 3479 $enabled.text( '(' + enabledNumber + ')' ); 3480 $disabled.text( '(' + disabledNumber + ')' ); 3481 } 3482 3483 if ( 'enable' === action ) { 3484 // The toggler control can be either a link or a button. 3485 if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { 3486 href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' ); 3487 $toggler.attr( 'href', href ); 3488 } 3489 $toggler.attr( 'data-wp-action', 'disable' ); 3490 3491 $label.text( __( 'Disable auto-updates' ) ); 3492 $parent.find( '.auto-update-time' ).removeClass( 'hidden' ); 3493 wp.a11y.speak( __( 'Auto-updates enabled' ) ); 3494 } else { 3495 // The toggler control can be either a link or a button. 3496 if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { 3497 href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' ); 3498 $toggler.attr( 'href', href ); 3499 } 3500 $toggler.attr( 'data-wp-action', 'enable' ); 3501 3502 $label.text( __( 'Enable auto-updates' ) ); 3503 $parent.find( '.auto-update-time' ).addClass( 'hidden' ); 3504 wp.a11y.speak( __( 'Auto-updates disabled' ) ); 3505 } 3506 3507 $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } ); 3508 } ) 3509 .fail( function() { 3510 $parent.find( '.notice.notice-error' ) 3511 .removeClass( 'hidden' ) 3512 .find( 'p' ) 3513 .text( __( 'The request could not be completed.' ) ); 3514 3515 wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' ); 3516 } ) 3517 .always( function() { 3518 $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' ); 3519 } ); 3520 } 3521 ); 3522 } ); 3523 })( jQuery, window.wp, window._wpUpdatesSettings );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Nov 12 08:20:02 2026 | Cross-referenced by PHPXref |