| [ 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 = $( 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 plugins.all = _.without( plugins.all, response.plugin ); 1451 1452 if ( plugins.all.length ) { 1453 $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' ); 1454 } else { 1455 $form.find( '.tablenav' ).css( { visibility: 'hidden' } ); 1456 $views.find( '.all' ).remove(); 1457 1458 if ( ! $form.find( 'tr.no-items' ).length ) { 1459 $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' ); 1460 } 1461 } 1462 1463 if ( $itemsCount.length && $currentView.length ) { 1464 remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; 1465 $itemsCount.text( 1466 sprintf( 1467 /* translators: %s: The remaining number of plugins. */ 1468 _nx( '%s item', '%s items', remainingCount, 'plugin/plugins' ), 1469 remainingCount 1470 ) 1471 ); 1472 } 1473 } ); 1474 1475 wp.a11y.speak( _x( 'Deleted!', 'plugin' ) ); 1476 1477 $document.trigger( 'wp-plugin-delete-success', response ); 1478 }; 1479 1480 /** 1481 * Updates the UI appropriately after a failed plugin deletion. 1482 * 1483 * @since 4.6.0 1484 * 1485 * @param {Object} response Response from the server. 1486 * @param {string} response.slug Slug of the plugin to be deleted. 1487 * @param {string} response.plugin Base name of the plugin to be deleted 1488 * @param {string} response.pluginName Optional. Name of the plugin to be deleted. 1489 * @param {string} response.errorCode Error code for the error that occurred. 1490 * @param {string} response.errorMessage The error that occurred. 1491 */ 1492 wp.updates.deletePluginError = function( response ) { 1493 var $plugin, $pluginUpdateRow, 1494 pluginUpdateRow = wp.template( 'item-update-row' ), 1495 noticeContent = wp.updates.adminNotice( { 1496 className: 'update-message notice-error notice-alt', 1497 message: response.errorMessage 1498 } ); 1499 1500 if ( response.plugin ) { 1501 $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ); 1502 $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ); 1503 } else { 1504 $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' ); 1505 $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' ); 1506 } 1507 1508 if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { 1509 return; 1510 } 1511 1512 if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) { 1513 return; 1514 } 1515 1516 // Add a plugin update row if it doesn't exist yet. 1517 if ( ! $pluginUpdateRow.length ) { 1518 $plugin.addClass( 'update' ).after( 1519 pluginUpdateRow( { 1520 slug: response.slug, 1521 plugin: response.plugin || response.slug, 1522 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 1523 content: noticeContent 1524 } ) 1525 ); 1526 } else { 1527 1528 // Remove previous error messages, if any. 1529 $pluginUpdateRow.find( '.notice-error' ).remove(); 1530 1531 $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent ); 1532 } 1533 1534 $document.trigger( 'wp-plugin-delete-error', response ); 1535 }; 1536 1537 /** 1538 * Sends an Ajax request to the server to update a theme. 1539 * 1540 * @since 4.6.0 1541 * 1542 * @param {Object} args Arguments. 1543 * @param {string} args.slug Theme stylesheet. 1544 * @param {updateThemeSuccess} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess 1545 * @param {updateThemeError} args.error Optional. Error callback. Default: wp.updates.updateThemeError 1546 * @return {$.promise} A jQuery promise that represents the request, 1547 * decorated with an abort() method. 1548 */ 1549 wp.updates.updateTheme = function( args ) { 1550 var $notice; 1551 1552 args = _.extend( { 1553 success: wp.updates.updateThemeSuccess, 1554 error: wp.updates.updateThemeError 1555 }, args ); 1556 1557 if ( 'themes-network' === pagenow ) { 1558 $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); 1559 1560 } else if ( 'customize' === pagenow ) { 1561 1562 // Update the theme details UI. 1563 $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' ); 1564 1565 $notice.find( 'h3' ).remove(); 1566 1567 // Add the top-level UI, and update both. 1568 $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) ); 1569 $notice = $notice.addClass( 'updating-message' ).find( 'p' ); 1570 1571 } else { 1572 $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); 1573 1574 $notice.find( 'h3' ).remove(); 1575 1576 $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) ); 1577 $notice = $notice.addClass( 'updating-message' ).find( 'p' ); 1578 } 1579 1580 if ( $notice.html() !== __( 'Updating...' ) ) { 1581 $notice.data( 'originaltext', $notice.html() ); 1582 } 1583 1584 wp.a11y.speak( __( 'Updating... please wait.' ) ); 1585 $notice.text( __( 'Updating...' ) ); 1586 1587 $document.trigger( 'wp-theme-updating', args ); 1588 1589 return wp.updates.ajax( 'update-theme', args ); 1590 }; 1591 1592 /** 1593 * Updates the UI appropriately after a successful theme update. 1594 * 1595 * @since 4.6.0 1596 * @since 5.5.0 Auto-update "time to next update" text cleared. 1597 * 1598 * @param {Object} response The response object from the Ajax request. 1599 * @param {string} response.slug Slug of the theme to be updated. 1600 * @param {Object} response.theme Updated theme. 1601 * @param {string} response.oldVersion Old version of the theme. 1602 * @param {string} response.newVersion New version of the theme. 1603 */ 1604 wp.updates.updateThemeSuccess = function( response ) { 1605 var isModalOpen = $( 'body.modal-open' ).length, 1606 $theme = $( '[data-slug="' + response.slug + '"]' ), 1607 updatedMessage = { 1608 className: 'updated-message notice-success notice-alt', 1609 message: _x( 'Updated!', 'theme' ) 1610 }, 1611 $notice, newText; 1612 1613 if ( 'customize' === pagenow ) { 1614 $theme = $( '.updating-message' ).siblings( '.theme-name' ); 1615 1616 if ( $theme.length ) { 1617 1618 // Update the version number in the row. 1619 newText = $theme.html().replace( response.oldVersion, response.newVersion ); 1620 $theme.html( newText ); 1621 } 1622 1623 $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) ); 1624 } else if ( 'themes-network' === pagenow ) { 1625 $notice = $theme.find( '.update-message' ); 1626 1627 // Update the version number in the row. 1628 newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); 1629 $theme.find( '.theme-version-author-uri' ).html( newText ); 1630 1631 // Clear the "time to next auto-update" text. 1632 $theme.find( '.auto-update-time' ).empty(); 1633 } else { 1634 $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) ); 1635 1636 // Focus on Customize button after updating. 1637 if ( isModalOpen ) { 1638 $( '.load-customize:visible' ).trigger( 'focus' ); 1639 $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty(); 1640 } else { 1641 $theme.find( '.load-customize' ).trigger( 'focus' ); 1642 } 1643 } 1644 1645 wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) ); 1646 wp.a11y.speak( __( 'Update completed successfully.' ) ); 1647 1648 wp.updates.decrementCount( 'theme' ); 1649 1650 $document.trigger( 'wp-theme-update-success', response ); 1651 1652 // Show updated message after modal re-rendered. 1653 if ( isModalOpen && 'customize' !== pagenow ) { 1654 $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) ); 1655 } 1656 }; 1657 1658 /** 1659 * Updates the UI appropriately after a failed theme update. 1660 * 1661 * @since 4.6.0 1662 * 1663 * @param {Object} response Response from the server. 1664 * @param {string} response.slug Slug of the theme to be updated. 1665 * @param {string} response.errorCode Error code for the error that occurred. 1666 * @param {string} response.errorMessage The error that occurred. 1667 */ 1668 wp.updates.updateThemeError = function( response ) { 1669 var $theme = $( '[data-slug="' + response.slug + '"]' ), 1670 errorMessage = sprintf( 1671 /* translators: %s: Error string for a failed update. */ 1672 __( 'Update failed: %s' ), 1673 response.errorMessage 1674 ), 1675 $notice; 1676 1677 if ( ! wp.updates.isValidResponse( response, 'update' ) ) { 1678 return; 1679 } 1680 1681 if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { 1682 return; 1683 } 1684 1685 if ( 'customize' === pagenow ) { 1686 $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ); 1687 } 1688 1689 if ( 'themes-network' === pagenow ) { 1690 $notice = $theme.find( '.update-message ' ); 1691 } else { 1692 $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) ); 1693 1694 $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus'); 1695 } 1696 1697 wp.updates.addAdminNotice( { 1698 selector: $notice, 1699 className: 'update-message notice-error notice-alt is-dismissible', 1700 message: errorMessage 1701 } ); 1702 1703 wp.a11y.speak( errorMessage ); 1704 1705 $document.trigger( 'wp-theme-update-error', response ); 1706 }; 1707 1708 /** 1709 * Sends an Ajax request to the server to install a theme. 1710 * 1711 * @since 4.6.0 1712 * 1713 * @param {Object} args The arguments for the theme installation request. 1714 * @param {string} args.slug Theme stylesheet. 1715 * @param {installThemeSuccess} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess 1716 * @param {installThemeError} args.error Optional. Error callback. Default: wp.updates.installThemeError 1717 * @return {$.promise} A jQuery promise that represents the request, 1718 * decorated with an abort() method. 1719 */ 1720 wp.updates.installTheme = function( args ) { 1721 var $message = $( '.theme-install[data-slug="' + args.slug + '"]' ); 1722 1723 args = _.extend( { 1724 success: wp.updates.installThemeSuccess, 1725 error: wp.updates.installThemeError 1726 }, args ); 1727 1728 $message.addClass( 'updating-message' ); 1729 $message.parents( '.theme' ).addClass( 'focus' ); 1730 if ( $message.html() !== __( 'Installing...' ) ) { 1731 $message.data( 'originaltext', $message.html() ); 1732 } 1733 1734 $message 1735 .attr( 1736 'aria-label', 1737 sprintf( 1738 /* translators: %s: Theme name and version. */ 1739 _x( 'Installing %s...', 'theme' ), 1740 $message.data( 'name' ) 1741 ) 1742 ) 1743 .text( __( 'Installing...' ) ); 1744 1745 wp.a11y.speak( __( 'Installing... please wait.' ) ); 1746 1747 // Remove previous error messages, if any. 1748 $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove(); 1749 1750 $document.trigger( 'wp-theme-installing', args ); 1751 1752 return wp.updates.ajax( 'install-theme', args ); 1753 }; 1754 1755 /** 1756 * Updates the UI appropriately after a successful theme install. 1757 * 1758 * @since 4.6.0 1759 * 1760 * @param {Object} response Response from the server. 1761 * @param {string} response.slug Slug of the theme to be installed. 1762 * @param {string} response.customizeUrl URL to the Customizer for the just installed theme. 1763 * @param {string} response.activateUrl URL to activate the just installed theme. 1764 */ 1765 wp.updates.installThemeSuccess = function( response ) { 1766 var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ), 1767 $message; 1768 1769 $document.trigger( 'wp-theme-install-success', response ); 1770 1771 $message = $card.find( '.button-primary' ) 1772 .removeClass( 'updating-message' ) 1773 .addClass( 'updated-message disabled' ) 1774 .attr( 1775 'aria-label', 1776 sprintf( 1777 /* translators: %s: Theme name and version. */ 1778 _x( '%s installed!', 'theme' ), 1779 response.themeName 1780 ) 1781 ) 1782 .text( _x( 'Installed!', 'theme' ) ); 1783 1784 wp.a11y.speak( __( 'Installation completed successfully.' ) ); 1785 1786 setTimeout( function() { 1787 1788 if ( response.activateUrl ) { 1789 1790 // Transform the 'Install' button into an 'Activate' button. 1791 $message 1792 .attr( 'href', response.activateUrl ) 1793 .removeClass( 'theme-install updated-message disabled' ) 1794 .addClass( 'activate' ); 1795 1796 if ( 'themes-network' === pagenow ) { 1797 $message 1798 .attr( 1799 'aria-label', 1800 sprintf( 1801 /* translators: %s: Theme name. */ 1802 _x( 'Network Activate %s', 'theme' ), 1803 response.themeName 1804 ) 1805 ) 1806 .text( __( 'Network Enable' ) ); 1807 } else { 1808 $message 1809 .attr( 1810 'aria-label', 1811 sprintf( 1812 /* translators: %s: Theme name. */ 1813 _x( 'Activate %s', 'theme' ), 1814 response.themeName 1815 ) 1816 ) 1817 .text( _x( 'Activate', 'theme' ) ); 1818 } 1819 } 1820 1821 if ( response.customizeUrl ) { 1822 1823 // Transform the 'Preview' button into a 'Live Preview' button. 1824 $message.siblings( '.preview' ).replaceWith( function () { 1825 return $( '<a>' ) 1826 .attr( 'href', response.customizeUrl ) 1827 .addClass( 'button load-customize' ) 1828 .text( __( 'Live Preview' ) ); 1829 } ); 1830 } 1831 }, 1000 ); 1832 }; 1833 1834 /** 1835 * Updates the UI appropriately after a failed theme install. 1836 * 1837 * @since 4.6.0 1838 * 1839 * @param {Object} response Response from the server. 1840 * @param {string} response.slug Slug of the theme to be installed. 1841 * @param {string} response.errorCode Error code for the error that occurred. 1842 * @param {string} response.errorMessage The error that occurred. 1843 */ 1844 wp.updates.installThemeError = function( response ) { 1845 var $card, $button, 1846 errorMessage = sprintf( 1847 /* translators: %s: Error string for a failed installation. */ 1848 __( 'Installation failed: %s' ), 1849 response.errorMessage 1850 ), 1851 $message = wp.updates.adminNotice( { 1852 className: 'update-message notice-error notice-alt', 1853 message: errorMessage 1854 } ); 1855 1856 if ( ! wp.updates.isValidResponse( response, 'install' ) ) { 1857 return; 1858 } 1859 1860 if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) { 1861 return; 1862 } 1863 1864 if ( 'customize' === pagenow ) { 1865 if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) { 1866 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1867 $card = $( '.theme-overlay .theme-info' ).prepend( $message ); 1868 } else { 1869 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1870 $card = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message ); 1871 } 1872 wp.customize.notifications.remove( 'theme_installing' ); 1873 } else { 1874 if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { 1875 $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); 1876 $card = $( '.install-theme-info' ).prepend( $message ); 1877 } else { 1878 $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); 1879 $button = $card.find( '.theme-install' ); 1880 } 1881 } 1882 1883 $button 1884 .removeClass( 'updating-message' ) 1885 .attr( 1886 'aria-label', 1887 sprintf( 1888 /* translators: %s: Theme name and version. */ 1889 _x( '%s installation failed', 'theme' ), 1890 $button.data( 'name' ) 1891 ) 1892 ) 1893 .text( __( 'Installation failed.' ) ); 1894 1895 wp.a11y.speak( errorMessage, 'assertive' ); 1896 1897 $document.trigger( 'wp-theme-install-error', response ); 1898 }; 1899 1900 /** 1901 * Sends an Ajax request to the server to delete a theme. 1902 * 1903 * @since 4.6.0 1904 * 1905 * @param {Object} args The arguments for the theme deletion request. 1906 * @param {string} args.slug Theme stylesheet. 1907 * @param {deleteThemeSuccess} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess 1908 * @param {deleteThemeError} args.error Optional. Error callback. Default: wp.updates.deleteThemeError 1909 * @return {$.promise} A jQuery promise that represents the request, 1910 * decorated with an abort() method. 1911 */ 1912 wp.updates.deleteTheme = function( args ) { 1913 var $button; 1914 1915 if ( 'themes' === pagenow ) { 1916 $button = $( '.theme-actions .delete-theme' ); 1917 } else if ( 'themes-network' === pagenow ) { 1918 $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' ); 1919 } 1920 1921 args = _.extend( { 1922 success: wp.updates.deleteThemeSuccess, 1923 error: wp.updates.deleteThemeError 1924 }, args ); 1925 1926 if ( $button && $button.html() !== __( 'Deleting...' ) ) { 1927 $button 1928 .data( 'originaltext', $button.html() ) 1929 .text( __( 'Deleting...' ) ); 1930 } 1931 1932 wp.a11y.speak( __( 'Deleting...' ) ); 1933 1934 // Remove previous error messages, if any. 1935 $( '.theme-info .update-message' ).remove(); 1936 1937 $document.trigger( 'wp-theme-deleting', args ); 1938 1939 return wp.updates.ajax( 'delete-theme', args ); 1940 }; 1941 1942 /** 1943 * Updates the UI appropriately after a successful theme deletion. 1944 * 1945 * @since 4.6.0 1946 * 1947 * @param {Object} response Response from the server. 1948 * @param {string} response.slug Slug of the theme that was deleted. 1949 */ 1950 wp.updates.deleteThemeSuccess = function( response ) { 1951 var $themeRows = $( '[data-slug="' + response.slug + '"]' ); 1952 1953 if ( 'themes-network' === pagenow ) { 1954 1955 // Removes the theme and updates rows. 1956 $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { 1957 var $views = $( '.subsubsub' ), 1958 $themeRow = $( this ), 1959 themes = settings.themes, 1960 deletedRow = wp.template( 'item-deleted-row' ); 1961 1962 if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) { 1963 $themeRow.after( 1964 deletedRow( { 1965 slug: response.slug, 1966 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 1967 name: $themeRow.find( '.theme-title strong' ).text() 1968 } ) 1969 ); 1970 } 1971 1972 $themeRow.remove(); 1973 1974 // Remove theme from update count. 1975 if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) { 1976 themes.upgrade = _.without( themes.upgrade, response.slug ); 1977 wp.updates.decrementCount( 'theme' ); 1978 } 1979 1980 // Remove from views. 1981 if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) { 1982 themes.disabled = _.without( themes.disabled, response.slug ); 1983 if ( themes.disabled.length ) { 1984 $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' ); 1985 } else { 1986 $views.find( '.disabled' ).remove(); 1987 } 1988 } 1989 1990 if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) { 1991 themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug ); 1992 if ( themes['auto-update-enabled'].length ) { 1993 $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' ); 1994 } else { 1995 $views.find( '.auto-update-enabled' ).remove(); 1996 } 1997 } 1998 1999 if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) { 2000 themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug ); 2001 if ( themes['auto-update-disabled'].length ) { 2002 $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' ); 2003 } else { 2004 $views.find( '.auto-update-disabled' ).remove(); 2005 } 2006 } 2007 2008 themes.all = _.without( themes.all, response.slug ); 2009 2010 // There is always at least one theme available. 2011 $views.find( '.all .count' ).text( '(' + themes.all.length + ')' ); 2012 } ); 2013 } 2014 2015 // DecrementCount from update count. 2016 if ( 'themes' === pagenow ) { 2017 var theme = _.find( _wpThemeSettings.themes, { id: response.slug } ); 2018 if ( theme.hasUpdate ) { 2019 wp.updates.decrementCount( 'theme' ); 2020 } 2021 } 2022 2023 wp.a11y.speak( _x( 'Deleted!', 'theme' ) ); 2024 2025 $document.trigger( 'wp-theme-delete-success', response ); 2026 }; 2027 2028 /** 2029 * Updates the UI appropriately after a failed theme deletion. 2030 * 2031 * @since 4.6.0 2032 * 2033 * @param {Object} response Response from the server. 2034 * @param {string} response.slug Slug of the theme to be deleted. 2035 * @param {string} response.errorCode Error code for the error that occurred. 2036 * @param {string} response.errorMessage The error that occurred. 2037 */ 2038 wp.updates.deleteThemeError = function( response ) { 2039 var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ), 2040 $button = $( '.theme-actions .delete-theme' ), 2041 updateRow = wp.template( 'item-update-row' ), 2042 $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ), 2043 errorMessage = sprintf( 2044 /* translators: %s: Error string for a failed deletion. */ 2045 __( 'Deletion failed: %s' ), 2046 response.errorMessage 2047 ), 2048 $message = wp.updates.adminNotice( { 2049 className: 'update-message notice-error notice-alt', 2050 message: errorMessage 2051 } ); 2052 2053 if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) { 2054 return; 2055 } 2056 2057 if ( 'themes-network' === pagenow ) { 2058 if ( ! $updateRow.length ) { 2059 $themeRow.addClass( 'update' ).after( 2060 updateRow( { 2061 slug: response.slug, 2062 colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, 2063 content: $message 2064 } ) 2065 ); 2066 } else { 2067 // Remove previous error messages, if any. 2068 $updateRow.find( '.notice-error' ).remove(); 2069 $updateRow.find( '.plugin-update' ).append( $message ); 2070 } 2071 } else { 2072 $( '.theme-info .theme-description' ).before( $message ); 2073 } 2074 2075 $button.html( $button.data( 'originaltext' ) ); 2076 2077 wp.a11y.speak( errorMessage, 'assertive' ); 2078 2079 $document.trigger( 'wp-theme-delete-error', response ); 2080 }; 2081 2082 /** 2083 * Adds the appropriate callback based on the type of action and the current page. 2084 * 2085 * @since 4.6.0 2086 * @private 2087 * 2088 * @param {Object} data Ajax payload. 2089 * @param {string} action The type of request to perform. 2090 * @return {Object} The Ajax payload with the appropriate callbacks. 2091 */ 2092 wp.updates._addCallbacks = function( data, action ) { 2093 if ( 'import' === pagenow && 'install-plugin' === action ) { 2094 data.success = wp.updates.installImporterSuccess; 2095 data.error = wp.updates.installImporterError; 2096 } 2097 2098 return data; 2099 }; 2100 2101 /** 2102 * Pulls available jobs from the queue and runs them. 2103 * 2104 * @since 4.2.0 2105 * @since 4.6.0 Can handle multiple job types. 2106 */ 2107 wp.updates.queueChecker = function() { 2108 var job; 2109 2110 if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) { 2111 return; 2112 } 2113 2114 job = wp.updates.queue.shift(); 2115 2116 // Handle a queue job. 2117 switch ( job.action ) { 2118 case 'install-plugin': 2119 wp.updates.installPlugin( job.data ); 2120 break; 2121 2122 case 'update-plugin': 2123 wp.updates.updatePlugin( job.data ); 2124 break; 2125 2126 case 'delete-plugin': 2127 wp.updates.deletePlugin( job.data ); 2128 break; 2129 2130 case 'install-theme': 2131 wp.updates.installTheme( job.data ); 2132 break; 2133 2134 case 'update-theme': 2135 wp.updates.updateTheme( job.data ); 2136 break; 2137 2138 case 'delete-theme': 2139 wp.updates.deleteTheme( job.data ); 2140 break; 2141 2142 default: 2143 break; 2144 } 2145 }; 2146 2147 /** 2148 * Requests the users filesystem credentials if they aren't already known. 2149 * 2150 * @since 4.2.0 2151 * 2152 * @param {Event} event Optional. Event interface. 2153 */ 2154 wp.updates.requestFilesystemCredentials = function( event ) { 2155 if ( false === wp.updates.filesystemCredentials.available ) { 2156 /* 2157 * After exiting the credentials request modal, 2158 * return the focus to the element triggering the request. 2159 */ 2160 if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) { 2161 wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target ); 2162 } 2163 2164 wp.updates.ajaxLocked = true; 2165 wp.updates.requestForCredentialsModalOpen(); 2166 } 2167 }; 2168 2169 /** 2170 * Requests the users filesystem credentials if needed and there is no lock. 2171 * 2172 * @since 4.6.0 2173 * 2174 * @param {Event} event Optional. Event interface. 2175 */ 2176 wp.updates.maybeRequestFilesystemCredentials = function( event ) { 2177 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2178 wp.updates.requestFilesystemCredentials( event ); 2179 } 2180 }; 2181 2182 /** 2183 * Keydown handler for the request for credentials modal. 2184 * 2185 * Closes the modal when the escape key is pressed and 2186 * constrains keyboard navigation to inside the modal. 2187 * 2188 * @since 4.2.0 2189 * 2190 * @param {Event} event Event interface. 2191 */ 2192 wp.updates.keydown = function( event ) { 2193 if ( 27 === event.keyCode ) { 2194 wp.updates.requestForCredentialsModalCancel(); 2195 } else if ( 9 === event.keyCode ) { 2196 2197 // #upgrade button must always be the last focus-able element in the dialog. 2198 if ( 'upgrade' === event.target.id && ! event.shiftKey ) { 2199 $( '#hostname' ).trigger( 'focus' ); 2200 2201 event.preventDefault(); 2202 } else if ( 'hostname' === event.target.id && event.shiftKey ) { 2203 $( '#upgrade' ).trigger( 'focus' ); 2204 2205 event.preventDefault(); 2206 } 2207 } 2208 }; 2209 2210 /** 2211 * Opens the request for credentials modal. 2212 * 2213 * @since 4.2.0 2214 */ 2215 wp.updates.requestForCredentialsModalOpen = function() { 2216 var $modal = $( '#request-filesystem-credentials-dialog' ); 2217 2218 $( 'body' ).addClass( 'modal-open' ); 2219 $modal.show(); 2220 $modal.find( 'input:enabled:first' ).trigger( 'focus' ); 2221 $modal.on( 'keydown', wp.updates.keydown ); 2222 }; 2223 2224 /** 2225 * Closes the request for credentials modal. 2226 * 2227 * @since 4.2.0 2228 */ 2229 wp.updates.requestForCredentialsModalClose = function() { 2230 $( '#request-filesystem-credentials-dialog' ).hide(); 2231 $( 'body' ).removeClass( 'modal-open' ); 2232 2233 if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) { 2234 wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' ); 2235 } 2236 }; 2237 2238 /** 2239 * Takes care of the steps that need to happen when the modal is canceled out. 2240 * 2241 * @since 4.2.0 2242 * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions. 2243 */ 2244 wp.updates.requestForCredentialsModalCancel = function() { 2245 2246 // Not ajaxLocked and no queue means we already have cleared things up. 2247 if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) { 2248 return; 2249 } 2250 2251 _.each( wp.updates.queue, function( job ) { 2252 $document.trigger( 'credential-modal-cancel', job ); 2253 } ); 2254 2255 // Remove the lock, and clear the queue. 2256 wp.updates.ajaxLocked = false; 2257 wp.updates.queue = []; 2258 2259 wp.updates.requestForCredentialsModalClose(); 2260 }; 2261 2262 /** 2263 * Displays an error message in the request for credentials form. 2264 * 2265 * @since 4.2.0 2266 * 2267 * @param {string} message Error message. 2268 */ 2269 wp.updates.showErrorInCredentialsForm = function( message ) { 2270 var $filesystemForm = $( '#request-filesystem-credentials-form' ); 2271 2272 // Remove any existing error. 2273 $filesystemForm.find( '.notice' ).remove(); 2274 $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error" role="alert"><p>' + message + '</p></div>' ); 2275 }; 2276 2277 /** 2278 * Handles credential errors and runs events that need to happen in that case. 2279 * 2280 * @since 4.2.0 2281 * 2282 * @param {Object} response Ajax response. 2283 * @param {string} action The type of request to perform. 2284 */ 2285 wp.updates.credentialError = function( response, action ) { 2286 2287 // Restore callbacks. 2288 response = wp.updates._addCallbacks( response, action ); 2289 2290 wp.updates.queue.unshift( { 2291 action: action, 2292 2293 /* 2294 * Not cool that we're depending on response for this data. 2295 * This would feel more whole in a view all tied together. 2296 */ 2297 data: response 2298 } ); 2299 2300 wp.updates.filesystemCredentials.available = false; 2301 wp.updates.showErrorInCredentialsForm( response.errorMessage ); 2302 wp.updates.requestFilesystemCredentials(); 2303 }; 2304 2305 /** 2306 * Handles credentials errors if it could not connect to the filesystem. 2307 * 2308 * @since 4.6.0 2309 * 2310 * @param {Object} response Response from the server. 2311 * @param {string} response.errorCode Error code for the error that occurred. 2312 * @param {string} response.errorMessage The error that occurred. 2313 * @param {string} action The type of request to perform. 2314 * @return {boolean} Whether there is an error that needs to be handled or not. 2315 */ 2316 wp.updates.maybeHandleCredentialError = function( response, action ) { 2317 if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) { 2318 wp.updates.credentialError( response, action ); 2319 return true; 2320 } 2321 2322 return false; 2323 }; 2324 2325 /** 2326 * Validates an Ajax response to ensure it's a proper object. 2327 * 2328 * If the response deems to be invalid, an admin notice is being displayed. 2329 * 2330 * @param {(Object|string)} response Response from the server. 2331 * @param {Function} [response.always] Optional. Callback for when the Deferred is resolved or rejected. 2332 * @param {string} [response.statusText] Optional. Status message corresponding to the status code. 2333 * @param {string} [response.responseText] Optional. Request response as text. 2334 * @param {string} action Type of action the response is referring to. Can be 'delete', 2335 * 'update' or 'install'. 2336 * @return {boolean} Whether the response is valid or not. 2337 */ 2338 wp.updates.isValidResponse = function( response, action ) { 2339 var error = __( 'An error occurred during the update process. Please try again.' ), 2340 errorMessage; 2341 2342 // Make sure the response is a valid data object and not a Promise object. 2343 if ( _.isObject( response ) && ! _.isFunction( response.always ) ) { 2344 return true; 2345 } 2346 2347 if ( _.isString( response ) && '-1' === response ) { 2348 error = __( 'An error has occurred. Please reload the page and try again.' ); 2349 } else if ( _.isString( response ) ) { 2350 error = response; 2351 } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) { 2352 error = __( 'Connection lost or the server is busy. Please try again later.' ); 2353 } else if ( _.isString( response.responseText ) && '' !== response.responseText ) { 2354 error = response.responseText; 2355 } else if ( _.isString( response.statusText ) ) { 2356 error = response.statusText; 2357 } 2358 2359 switch ( action ) { 2360 case 'update': 2361 /* translators: %s: Error string for a failed update. */ 2362 errorMessage = __( 'Update failed: %s' ); 2363 break; 2364 2365 case 'install': 2366 /* translators: %s: Error string for a failed installation. */ 2367 errorMessage = __( 'Installation failed: %s' ); 2368 break; 2369 2370 case 'check-dependencies': 2371 /* translators: %s: Error string for a failed dependencies check. */ 2372 errorMessage = __( 'Dependencies check failed: %s' ); 2373 break; 2374 2375 case 'activate': 2376 /* translators: %s: Error string for a failed activation. */ 2377 errorMessage = __( 'Activation failed: %s' ); 2378 break; 2379 2380 case 'delete': 2381 /* translators: %s: Error string for a failed deletion. */ 2382 errorMessage = __( 'Deletion failed: %s' ); 2383 break; 2384 } 2385 2386 // Messages are escaped, remove HTML tags to make them more readable. 2387 error = error.replace( /<[\/a-z][^<>]*>/gi, '' ); 2388 errorMessage = errorMessage.replace( '%s', error ); 2389 2390 // Add admin notice. 2391 wp.updates.addAdminNotice( { 2392 id: 'unknown_error', 2393 className: 'notice-error is-dismissible', 2394 message: _.escape( errorMessage ) 2395 } ); 2396 2397 // Remove the lock, and clear the queue. 2398 wp.updates.ajaxLocked = false; 2399 wp.updates.queue = []; 2400 2401 // Change buttons of all running updates. 2402 $( '.button.updating-message' ) 2403 .removeClass( 'updating-message' ) 2404 .removeAttr( 'aria-label' ) 2405 .prop( 'disabled', true ) 2406 .text( __( 'Update failed.' ) ); 2407 2408 $( '.updating-message:not(.button):not(.thickbox)' ) 2409 .removeClass( 'updating-message notice-warning' ) 2410 .addClass( 'notice-error' ) 2411 .find( 'p' ) 2412 .removeAttr( 'aria-label' ) 2413 .text( errorMessage ); 2414 2415 wp.a11y.speak( errorMessage, 'assertive' ); 2416 2417 return false; 2418 }; 2419 2420 /** 2421 * Potentially adds an AYS to a user attempting to leave the page. 2422 * 2423 * If an update is on-going and a user attempts to leave the page, 2424 * opens an "Are you sure?" alert. 2425 * 2426 * @since 4.2.0 2427 * @return {string|void} Message to display in the "Are you sure?" alert, or nothing. 2428 */ 2429 wp.updates.beforeunload = function() { 2430 if ( wp.updates.ajaxLocked ) { 2431 return __( 'Updates may not complete if you navigate away from this page.' ); 2432 } 2433 }; 2434 2435 $( function() { 2436 var $pluginFilter = $( '#plugin-filter, #plugin-information-footer' ), 2437 $bulkActionForm = $( '#bulk-action-form' ), 2438 $filesystemForm = $( '#request-filesystem-credentials-form' ), 2439 $filesystemModal = $( '#request-filesystem-credentials-dialog' ), 2440 $pluginSearch = $( '.plugins-php .wp-filter-search' ), 2441 $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' ); 2442 2443 settings = _.extend( settings, window._wpUpdatesItemCounts || {} ); 2444 2445 if ( settings.totals ) { 2446 wp.updates.refreshCount(); 2447 } 2448 2449 /* 2450 * Whether a user needs to submit filesystem credentials. 2451 * 2452 * This is based on whether the form was output on the page server-side. 2453 * 2454 * @see {wp_print_request_filesystem_credentials_modal() in PHP} 2455 */ 2456 wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0; 2457 2458 /** 2459 * File system credentials form submit noop-er / handler. 2460 * 2461 * @since 4.2.0 2462 */ 2463 $filesystemModal.on( 'submit', 'form', function( event ) { 2464 event.preventDefault(); 2465 2466 // Persist the credentials input by the user for the duration of the page load. 2467 wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val(); 2468 wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val(); 2469 wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val(); 2470 wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val(); 2471 wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val(); 2472 wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val(); 2473 wp.updates.filesystemCredentials.fsNonce = $( '#_fs_nonce' ).val(); 2474 wp.updates.filesystemCredentials.available = true; 2475 2476 // Unlock and invoke the queue. 2477 wp.updates.ajaxLocked = false; 2478 wp.updates.queueChecker(); 2479 2480 wp.updates.requestForCredentialsModalClose(); 2481 } ); 2482 2483 /** 2484 * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal. 2485 * 2486 * @since 4.2.0 2487 */ 2488 $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel ); 2489 2490 /** 2491 * Hide SSH fields when not selected. 2492 * 2493 * @since 4.2.0 2494 */ 2495 $filesystemForm.on( 'change', 'input[name="connection_type"]', function() { 2496 $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) ); 2497 } ).trigger( 'change' ); 2498 2499 /** 2500 * Handles events after the credential modal was closed. 2501 * 2502 * @since 4.6.0 2503 * 2504 * @param {Event} event Event interface. 2505 * @param {string} job The install/update.delete request. 2506 */ 2507 $document.on( 'credential-modal-cancel', function( event, job ) { 2508 var $updatingMessage = $( '.updating-message' ), 2509 $message, originalText; 2510 2511 if ( 'import' === pagenow ) { 2512 $updatingMessage.removeClass( 'updating-message' ); 2513 } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { 2514 if ( 'update-plugin' === job.action ) { 2515 $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' ); 2516 } else if ( 'delete-plugin' === job.action ) { 2517 $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' ); 2518 } 2519 } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { 2520 if ( 'update-theme' === job.action ) { 2521 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' ); 2522 } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) { 2523 $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' ); 2524 } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) { 2525 $message = $( '.theme-actions .delete-theme' ); 2526 } 2527 } else { 2528 $message = $updatingMessage; 2529 } 2530 2531 if ( $message && $message.hasClass( 'updating-message' ) ) { 2532 originalText = $message.data( 'originaltext' ); 2533 2534 if ( 'undefined' === typeof originalText ) { 2535 originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) ); 2536 } 2537 2538 $message 2539 .removeClass( 'updating-message' ) 2540 .html( originalText ); 2541 2542 if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { 2543 if ( 'update-plugin' === job.action ) { 2544 $message.attr( 2545 'aria-label', 2546 sprintf( 2547 /* translators: %s: Plugin name and version. */ 2548 _x( 'Update %s now', 'plugin' ), 2549 $message.data( 'name' ) 2550 ) 2551 ); 2552 } else if ( 'install-plugin' === job.action ) { 2553 $message.attr( 2554 'aria-label', 2555 sprintf( 2556 /* translators: %s: Plugin name. */ 2557 _x( 'Install %s now', 'plugin' ), 2558 $message.data( 'name' ) 2559 ) 2560 ); 2561 } 2562 } 2563 } 2564 2565 wp.a11y.speak( __( 'Update canceled.' ) ); 2566 } ); 2567 2568 /** 2569 * Click handler for plugin updates in List Table view. 2570 * 2571 * @since 4.2.0 2572 * 2573 * @param {Event} event Event interface. 2574 */ 2575 $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { 2576 var $message = $( event.target ), 2577 $pluginRow = $message.parents( 'tr' ); 2578 2579 event.preventDefault(); 2580 2581 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { 2582 return; 2583 } 2584 2585 wp.updates.maybeRequestFilesystemCredentials( event ); 2586 2587 // Return the user to the input box of the plugin's table row after closing the modal. 2588 wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); 2589 wp.updates.updatePlugin( { 2590 plugin: $pluginRow.data( 'plugin' ), 2591 slug: $pluginRow.data( 'slug' ) 2592 } ); 2593 } ); 2594 2595 /** 2596 * Click handler for plugin updates in plugin install view. 2597 * 2598 * @since 4.2.0 2599 * 2600 * @param {Event} event Event interface. 2601 */ 2602 $pluginFilter.on( 'click', '.update-now', function( event ) { 2603 var $button = $( event.target ); 2604 event.preventDefault(); 2605 2606 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { 2607 return; 2608 } 2609 2610 wp.updates.maybeRequestFilesystemCredentials( event ); 2611 2612 wp.updates.updatePlugin( { 2613 plugin: $button.data( 'plugin' ), 2614 slug: $button.data( 'slug' ) 2615 } ); 2616 } ); 2617 2618 /** 2619 * Click handler for plugin installs in plugin install view. 2620 * 2621 * @since 4.6.0 2622 * 2623 * @param {Event} event Event interface. 2624 */ 2625 $pluginFilter.on( 'click', '.install-now', function( event ) { 2626 var $button = $( event.target ); 2627 event.preventDefault(); 2628 2629 if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { 2630 return; 2631 } 2632 2633 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2634 wp.updates.requestFilesystemCredentials( event ); 2635 2636 $document.on( 'credential-modal-cancel', function() { 2637 var $message = $( '.install-now.updating-message' ); 2638 2639 $message 2640 .removeClass( 'updating-message' ) 2641 .text( _x( 'Install Now', 'plugin' ) ); 2642 2643 wp.a11y.speak( __( 'Update canceled.' ) ); 2644 } ); 2645 } 2646 2647 wp.updates.installPlugin( { 2648 slug: $button.data( 'slug' ) 2649 } ); 2650 } ); 2651 2652 /** 2653 * Click handler for plugin activations in plugin activation modal view. 2654 * 2655 * @since 6.5.0 2656 * @since 6.5.4 Redirect the parent window to the activation URL. 2657 * 2658 * @param {Event} event Event interface. 2659 */ 2660 $document.on( 'click', '#plugin-information-footer .activate-now', function( event ) { 2661 event.preventDefault(); 2662 window.parent.location.href = $( event.target ).attr( 'href' ); 2663 }); 2664 2665 /** 2666 * Click handler for importer plugins installs in the Import screen. 2667 * 2668 * @since 4.6.0 2669 * 2670 * @param {Event} event Event interface. 2671 */ 2672 $document.on( 'click', '.importer-item .install-now', function( event ) { 2673 var $button = $( event.target ), 2674 pluginName = $( this ).data( 'name' ); 2675 2676 event.preventDefault(); 2677 2678 if ( $button.hasClass( 'updating-message' ) ) { 2679 return; 2680 } 2681 2682 if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { 2683 wp.updates.requestFilesystemCredentials( event ); 2684 2685 $document.on( 'credential-modal-cancel', function() { 2686 2687 $button 2688 .removeClass( 'updating-message' ) 2689 .attr( 2690 'aria-label', 2691 sprintf( 2692 /* translators: %s: Plugin name. */ 2693 _x( 'Install %s now', 'plugin' ), 2694 pluginName 2695 ) 2696 ) 2697 .text( _x( 'Install Now', 'plugin' ) ); 2698 2699 wp.a11y.speak( __( 'Update canceled.' ) ); 2700 } ); 2701 } 2702 2703 wp.updates.installPlugin( { 2704 slug: $button.data( 'slug' ), 2705 pagenow: pagenow, 2706 success: wp.updates.installImporterSuccess, 2707 error: wp.updates.installImporterError 2708 } ); 2709 } ); 2710 2711 /** 2712 * Click handler for plugin deletions. 2713 * 2714 * @since 4.6.0 2715 * 2716 * @param {Event} event Event interface. 2717 */ 2718 $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { 2719 var $pluginRow = $( event.target ).parents( 'tr' ), 2720 confirmMessage; 2721 2722 if ( $pluginRow.hasClass( 'is-uninstallable' ) ) { 2723 confirmMessage = sprintf( 2724 /* translators: %s: Plugin name. */ 2725 __( 'Are you sure you want to delete %s and its data?' ), 2726 $pluginRow.find( '.plugin-title strong' ).text() 2727 ); 2728 } else { 2729 confirmMessage = sprintf( 2730 /* translators: %s: Plugin name. */ 2731 __( 'Are you sure you want to delete %s?' ), 2732 $pluginRow.find( '.plugin-title strong' ).text() 2733 ); 2734 } 2735 2736 event.preventDefault(); 2737 2738 if ( ! window.confirm( confirmMessage ) ) { 2739 return; 2740 } 2741 2742 wp.updates.maybeRequestFilesystemCredentials( event ); 2743 2744 wp.updates.deletePlugin( { 2745 plugin: $pluginRow.data( 'plugin' ), 2746 slug: $pluginRow.data( 'slug' ) 2747 } ); 2748 2749 } ); 2750 2751 /** 2752 * Click handler for theme updates. 2753 * 2754 * @since 4.6.0 2755 * 2756 * @param {Event} event Event interface. 2757 */ 2758 $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { 2759 var $message = $( event.target ), 2760 $themeRow = $message.parents( 'tr' ); 2761 2762 event.preventDefault(); 2763 2764 if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { 2765 return; 2766 } 2767 2768 wp.updates.maybeRequestFilesystemCredentials( event ); 2769 2770 // Return the user to the input box of the theme's table row after closing the modal. 2771 wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); 2772 wp.updates.updateTheme( { 2773 slug: $themeRow.data( 'slug' ) 2774 } ); 2775 } ); 2776 2777 /** 2778 * Click handler for theme deletions. 2779 * 2780 * @since 4.6.0 2781 * 2782 * @param {Event} event Event interface. 2783 */ 2784 $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { 2785 var $themeRow = $( event.target ).parents( 'tr' ), 2786 confirmMessage = sprintf( 2787 /* translators: %s: Theme name. */ 2788 __( 'Are you sure you want to delete %s?' ), 2789 $themeRow.find( '.theme-title strong' ).text() 2790 ); 2791 2792 event.preventDefault(); 2793 2794 if ( ! window.confirm( confirmMessage ) ) { 2795 return; 2796 } 2797 2798 wp.updates.maybeRequestFilesystemCredentials( event ); 2799 2800 wp.updates.deleteTheme( { 2801 slug: $themeRow.data( 'slug' ) 2802 } ); 2803 } ); 2804 2805 /** 2806 * Bulk action handler for plugins and themes. 2807 * 2808 * Handles both deletions and updates. 2809 * 2810 * @since 4.6.0 2811 * 2812 * @param {Event} event Event interface. 2813 */ 2814 $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) { 2815 var bulkAction = $( event.target ).siblings( 'select' ).val(), 2816 itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), 2817 success = 0, 2818 error = 0, 2819 errorMessages = [], 2820 type, action; 2821 2822 // Determine which type of item we're dealing with. 2823 switch ( pagenow ) { 2824 case 'plugins': 2825 case 'plugins-network': 2826 type = 'plugin'; 2827 break; 2828 2829 case 'themes-network': 2830 type = 'theme'; 2831 break; 2832 2833 default: 2834 return; 2835 } 2836 2837 // Bail if there were no items selected. 2838 if ( ! itemsSelected.length ) { 2839 bulkAction = false; 2840 } 2841 2842 // Determine the type of request we're dealing with. 2843 switch ( bulkAction ) { 2844 case 'update-selected': 2845 action = bulkAction.replace( 'selected', type ); 2846 break; 2847 2848 case 'delete-selected': 2849 var confirmMessage = 'plugin' === type ? 2850 __( 'Are you sure you want to delete the selected plugins and their data?' ) : 2851 __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ); 2852 2853 if ( ! window.confirm( confirmMessage ) ) { 2854 event.preventDefault(); 2855 return; 2856 } 2857 2858 action = bulkAction.replace( 'selected', type ); 2859 break; 2860 2861 default: 2862 return; 2863 } 2864 2865 wp.updates.maybeRequestFilesystemCredentials( event ); 2866 2867 event.preventDefault(); 2868 2869 // Un-check the bulk checkboxes. 2870 $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); 2871 2872 $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected ); 2873 2874 // Find all the checkboxes which have been checked. 2875 itemsSelected.each( function( index, element ) { 2876 var $checkbox = $( element ), 2877 $itemRow = $checkbox.parents( 'tr' ); 2878 2879 // Only add update-able items to the update queue. 2880 if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { 2881 2882 // Un-check the box. 2883 $checkbox.prop( 'checked', false ); 2884 return; 2885 } 2886 2887 // Don't add items to the update queue again, even if the user clicks the update button several times. 2888 if ( 'update-selected' === bulkAction && $itemRow.hasClass( 'is-enqueued' ) ) { 2889 return; 2890 } 2891 2892 $itemRow.addClass( 'is-enqueued' ); 2893 2894 // Add it to the queue. 2895 wp.updates.queue.push( { 2896 action: action, 2897 data: { 2898 plugin: $itemRow.data( 'plugin' ), 2899 slug: $itemRow.data( 'slug' ) 2900 } 2901 } ); 2902 } ); 2903 2904 // Display bulk notification for updates of any kind. 2905 $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { 2906 var $itemRow = $( '[data-slug="' + response.slug + '"]' ), 2907 $bulkActionNotice, itemName; 2908 2909 if ( 'wp-' + response.update + '-update-success' === event.type ) { 2910 success++; 2911 } else { 2912 itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text(); 2913 2914 error++; 2915 errorMessages.push( itemName + ': ' + response.errorMessage ); 2916 } 2917 2918 $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false ); 2919 2920 wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); 2921 2922 var successMessage = null; 2923 2924 if ( success ) { 2925 if ( 'plugin' === response.update ) { 2926 successMessage = sprintf( 2927 /* translators: %s: Number of plugins. */ 2928 _n( '%s plugin successfully updated.', '%s plugins successfully updated.', success ), 2929 success 2930 ); 2931 } else { 2932 successMessage = sprintf( 2933 /* translators: %s: Number of themes. */ 2934 _n( '%s theme successfully updated.', '%s themes successfully updated.', success ), 2935 success 2936 ); 2937 } 2938 } 2939 2940 var errorMessage = null; 2941 2942 if ( error ) { 2943 errorMessage = sprintf( 2944 /* translators: %s: Number of failed updates. */ 2945 _n( '%s update failed.', '%s updates failed.', error ), 2946 error 2947 ); 2948 } 2949 2950 wp.updates.addAdminNotice( { 2951 id: 'bulk-action-notice', 2952 className: 'bulk-action-notice', 2953 successMessage: successMessage, 2954 errorMessage: errorMessage, 2955 errorMessages: errorMessages, 2956 type: response.update 2957 } ); 2958 2959 $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { 2960 // $( this ) is the clicked button, no need to get it again. 2961 $( this ) 2962 .toggleClass( 'bulk-action-errors-collapsed' ) 2963 .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) ); 2964 // Show the errors list. 2965 $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' ); 2966 } ); 2967 2968 if ( error > 0 && ! wp.updates.queue.length ) { 2969 $( 'html, body' ).animate( { scrollTop: 0 } ); 2970 } 2971 } ); 2972 2973 // Reset admin notice template after #bulk-action-notice was added. 2974 $document.on( 'wp-updates-notice-added', function() { 2975 wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); 2976 } ); 2977 2978 // Check the queue, now that the event handlers have been added. 2979 wp.updates.queueChecker(); 2980 } ); 2981 2982 if ( $pluginInstallSearch.length ) { 2983 $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' ); 2984 } 2985 2986 // Track the previous search string length. 2987 var previousSearchStringLength = 0; 2988 wp.updates.shouldSearch = function( searchStringLength ) { 2989 var shouldSearch = searchStringLength >= wp.updates.searchMinCharacters || 2990 previousSearchStringLength > wp.updates.searchMinCharacters; 2991 previousSearchStringLength = searchStringLength; 2992 return shouldSearch; 2993 }; 2994 2995 /** 2996 * Handles changes to the plugin search box on the new-plugin page, 2997 * searching the repository dynamically. 2998 * 2999 * @since 4.6.0 3000 */ 3001 $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) { 3002 var $searchTab = $( '.plugin-install-search' ), data, searchLocation, 3003 searchStringLength = $pluginInstallSearch.val().length; 3004 3005 data = { 3006 _ajax_nonce: wp.updates.ajaxNonce, 3007 s: encodeURIComponent( event.target.value ), 3008 tab: 'search', 3009 type: $( '#typeselector' ).val(), 3010 pagenow: pagenow 3011 }; 3012 searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) ); 3013 3014 // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in. 3015 if ( wp.updates.shouldSearch( searchStringLength ) ) { 3016 $pluginInstallSearch.attr( 'autocomplete', 'off' ); 3017 } else { 3018 $pluginInstallSearch.attr( 'autocomplete', 'on' ); 3019 return; 3020 } 3021 3022 // Clear on escape. 3023 if ( 'keyup' === event.type && 27 === event.which ) { 3024 event.target.value = ''; 3025 } 3026 3027 if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) { 3028 return; 3029 } else { 3030 $pluginFilter.empty(); 3031 wp.updates.searchTerm = data.s; 3032 } 3033 3034 if ( window.history && window.history.replaceState ) { 3035 window.history.replaceState( null, '', searchLocation ); 3036 } 3037 3038 if ( ! $searchTab.length ) { 3039 $searchTab = $( '<li class="plugin-install-search" />' ) 3040 .append( $( '<a />', { 3041 'class': 'current', 3042 'href': searchLocation, 3043 'text': __( 'Search Results' ) 3044 } ) ); 3045 3046 $( '.wp-filter .filter-links .current' ) 3047 .removeClass( 'current' ) 3048 .parents( '.filter-links' ) 3049 .prepend( $searchTab ); 3050 3051 $pluginFilter.prev( 'p' ).remove(); 3052 $( '.plugins-popular-tags-wrapper' ).remove(); 3053 } 3054 3055 if ( 'undefined' !== typeof wp.updates.searchRequest ) { 3056 wp.updates.searchRequest.abort(); 3057 } 3058 $( 'body' ).addClass( 'loading-content' ); 3059 3060 wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { 3061 $( 'body' ).removeClass( 'loading-content' ); 3062 $pluginFilter.append( response.items ); 3063 delete wp.updates.searchRequest; 3064 3065 if ( 0 === response.count ) { 3066 wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) ); 3067 } else { 3068 wp.a11y.speak( 3069 sprintf( 3070 /* translators: %s: Number of plugins. */ 3071 __( 'Number of plugins found: %d' ), 3072 response.count 3073 ) 3074 ); 3075 } 3076 } ); 3077 }, 1000 ) ); 3078 3079 if ( $pluginSearch.length ) { 3080 $pluginSearch.attr( 'aria-describedby', 'live-search-desc' ); 3081 3082 } 3083 3084 /** 3085 * Handles changes to the plugin search box on the Installed Plugins screen, 3086 * searching the plugin list dynamically. 3087 * 3088 * @since 4.6.0 3089 */ 3090 $pluginSearch.on( 'keyup input', _.debounce( function( event ) { 3091 var data = { 3092 _ajax_nonce: wp.updates.ajaxNonce, 3093 s: encodeURIComponent( event.target.value ), 3094 pagenow: pagenow, 3095 plugin_status: 'all' 3096 }, 3097 queryArgs, 3098 searchStringLength = $pluginSearch.val().length; 3099 3100 // Set the autocomplete attribute, turning off autocomplete 1 character before ajax search kicks in. 3101 if ( wp.updates.shouldSearch( searchStringLength ) ) { 3102 $pluginSearch.attr( 'autocomplete', 'off' ); 3103 } else { 3104 $pluginSearch.attr( 'autocomplete', 'on' ); 3105 return; 3106 } 3107 3108 // Clear on escape. 3109 if ( 'keyup' === event.type && 27 === event.which ) { 3110 event.target.value = ''; 3111 } 3112 3113 if ( wp.updates.searchTerm === data.s ) { 3114 return; 3115 } else { 3116 wp.updates.searchTerm = data.s; 3117 } 3118 3119 queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) { 3120 if ( item ) return item.split( '=' ); 3121 } ) ) ); 3122 3123 data.plugin_status = queryArgs.plugin_status || 'all'; 3124 3125 if ( window.history && window.history.replaceState ) { 3126 window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status ); 3127 } 3128 3129 if ( 'undefined' !== typeof wp.updates.searchRequest ) { 3130 wp.updates.searchRequest.abort(); 3131 } 3132 3133 $bulkActionForm.empty(); 3134 $( 'body' ).addClass( 'loading-content' ); 3135 $( '.subsubsub .current' ).removeClass( 'current' ); 3136 3137 wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { 3138 3139 // Can we just ditch this whole subtitle business? 3140 var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( 3141 sprintf( 3142 /* translators: %s: Search query. */ 3143 __( 'Search results for: %s' ), 3144 '<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>' 3145 ) ), 3146 $oldSubTitle = $( '.wrap .subtitle' ); 3147 3148 if ( ! data.s.length ) { 3149 $oldSubTitle.remove(); 3150 $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' ); 3151 } else if ( $oldSubTitle.length ) { 3152 $oldSubTitle.replaceWith( $subTitle ); 3153 } else { 3154 $( '.wp-header-end' ).before( $subTitle ); 3155 } 3156 3157 $( 'body' ).removeClass( 'loading-content' ); 3158 $bulkActionForm.append( response.items ); 3159 delete wp.updates.searchRequest; 3160 3161 if ( 0 === response.count ) { 3162 wp.a11y.speak( __( 'No plugins found. Try a different search.' ) ); 3163 } else { 3164 wp.a11y.speak( 3165 sprintf( 3166 /* translators: %s: Number of plugins. */ 3167 __( 'Number of plugins found: %d' ), 3168 response.count 3169 ) 3170 ); 3171 } 3172 } ); 3173 }, 500 ) ); 3174 3175 /** 3176 * Trigger a search event when the search form gets submitted. 3177 * 3178 * @since 4.6.0 3179 */ 3180 $document.on( 'submit', '.search-plugins', function( event ) { 3181 event.preventDefault(); 3182 3183 $( 'input.wp-filter-search' ).trigger( 'input' ); 3184 } ); 3185 3186 /** 3187 * Trigger a search event when the "Try Again" button is clicked. 3188 * 3189 * @since 4.9.0 3190 */ 3191 $document.on( 'click', '.try-again', function( event ) { 3192 event.preventDefault(); 3193 $pluginInstallSearch.trigger( 'input' ); 3194 } ); 3195 3196 /** 3197 * Trigger a search event when the search type gets changed. 3198 * 3199 * @since 4.6.0 3200 */ 3201 $( '#typeselector' ).on( 'change', function() { 3202 var $search = $( 'input[name="s"]' ); 3203 3204 if ( $search.val().length ) { 3205 $search.trigger( 'input', 'typechange' ); 3206 } 3207 } ); 3208 3209 /** 3210 * Click handler for updating a plugin from the details modal on `plugin-install.php`. 3211 * 3212 * @since 4.2.0 3213 * 3214 * @param {Event} event Event interface. 3215 */ 3216 $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { 3217 var target = window.parent === window ? null : window.parent, 3218 update; 3219 3220 $.support.postMessage = !! window.postMessage; 3221 3222 if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) { 3223 return; 3224 } 3225 3226 event.preventDefault(); 3227 3228 update = { 3229 action: 'update-plugin', 3230 data: { 3231 plugin: $( this ).data( 'plugin' ), 3232 slug: $( this ).data( 'slug' ) 3233 } 3234 }; 3235 3236 target.postMessage( JSON.stringify( update ), window.location.origin ); 3237 } ); 3238 3239 /** 3240 * Handles postMessage events. 3241 * 3242 * @since 4.2.0 3243 * @since 4.6.0 Switched `update-plugin` action to use the queue. 3244 * 3245 * @param {Event} event Event interface. 3246 */ 3247 $( window ).on( 'message', function( event ) { 3248 var originalEvent = event.originalEvent, 3249 expectedOrigin = document.location.protocol + '//' + document.location.host, 3250 message; 3251 3252 if ( originalEvent.origin !== expectedOrigin ) { 3253 return; 3254 } 3255 3256 try { 3257 message = JSON.parse( originalEvent.data ); 3258 } catch ( e ) { 3259 return; 3260 } 3261 3262 if ( ! message ) { 3263 return; 3264 } 3265 3266 if ( 3267 'undefined' !== typeof message.status && 3268 'undefined' !== typeof message.slug && 3269 'undefined' !== typeof message.text && 3270 'undefined' !== typeof message.ariaLabel 3271 ) { 3272 var $card = $( '.plugin-card-' + message.slug ), 3273 $message = $card.find( '[data-slug="' + message.slug + '"]' ); 3274 3275 if ( 'undefined' !== typeof message.removeClasses ) { 3276 $message.removeClass( message.removeClasses ); 3277 } 3278 3279 if ( 'undefined' !== typeof message.addClasses ) { 3280 $message.addClass( message.addClasses ); 3281 } 3282 3283 if ( '' === message.ariaLabel ) { 3284 $message.removeAttr( 'aria-label' ); 3285 } else { 3286 $message.attr( 'aria-label', message.ariaLabel ); 3287 } 3288 3289 if ( 'dependencies-check-success' === message.status ) { 3290 $message 3291 .attr( 'data-name', message.pluginName ) 3292 .attr( 'data-slug', message.slug ) 3293 .attr( 'data-plugin', message.plugin ) 3294 .attr( 'href', message.href ); 3295 } 3296 3297 $message.text( message.text ); 3298 } 3299 3300 if ( 'undefined' === typeof message.action ) { 3301 return; 3302 } 3303 3304 switch ( message.action ) { 3305 3306 // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. 3307 case 'decrementUpdateCount': 3308 /** 3309 * @property {string} message The message associated with the update. 3310 * @property {string} message.upgradeType The type of upgrade being performed. 3311 */ 3312 wp.updates.decrementCount( message.upgradeType ); 3313 break; 3314 3315 case 'install-plugin': 3316 case 'update-plugin': 3317 if ( 'undefined' === typeof message.data || 'undefined' === typeof message.data.slug ) { 3318 return; 3319 } 3320 3321 message.data = wp.updates._addCallbacks( message.data, message.action ); 3322 3323 wp.updates.queue.push( message ); 3324 wp.updates.queueChecker(); 3325 break; 3326 } 3327 } ); 3328 3329 /** 3330 * Adds a callback to display a warning before leaving the page. 3331 * 3332 * @since 4.2.0 3333 */ 3334 $( window ).on( 'beforeunload', wp.updates.beforeunload ); 3335 3336 /** 3337 * Prevents the page form scrolling when activating auto-updates with the Spacebar key. 3338 * 3339 * @since 5.5.0 3340 */ 3341 $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { 3342 if ( 32 === event.which ) { 3343 event.preventDefault(); 3344 } 3345 } ); 3346 3347 /** 3348 * Click and keyup handler for enabling and disabling plugin and theme auto-updates. 3349 * 3350 * These controls can be either links or buttons. When JavaScript is enabled, 3351 * we want them to behave like buttons. An ARIA role `button` is added via 3352 * the JavaScript that targets elements with the CSS class `aria-button-if-js`. 3353 * 3354 * @since 5.5.0 3355 */ 3356 $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { 3357 var data, asset, type, $parent, 3358 $toggler = $( this ), 3359 action = $toggler.attr( 'data-wp-action' ), 3360 $label = $toggler.find( '.label' ); 3361 3362 if ( 'keyup' === event.type && 32 !== event.which ) { 3363 return; 3364 } 3365 3366 if ( 'themes' !== pagenow ) { 3367 $parent = $toggler.closest( '.column-auto-updates' ); 3368 } else { 3369 $parent = $toggler.closest( '.theme-autoupdate' ); 3370 } 3371 3372 event.preventDefault(); 3373 3374 // Prevent multiple simultaneous requests. 3375 if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) { 3376 return; 3377 } 3378 3379 $toggler.attr( 'data-doing-ajax', 'yes' ); 3380 3381 switch ( pagenow ) { 3382 case 'plugins': 3383 case 'plugins-network': 3384 type = 'plugin'; 3385 asset = $toggler.closest( 'tr' ).attr( 'data-plugin' ); 3386 break; 3387 case 'themes-network': 3388 type = 'theme'; 3389 asset = $toggler.closest( 'tr' ).attr( 'data-slug' ); 3390 break; 3391 case 'themes': 3392 type = 'theme'; 3393 asset = $toggler.attr( 'data-slug' ); 3394 break; 3395 } 3396 3397 // Clear any previous errors. 3398 $parent.find( '.notice.notice-error' ).addClass( 'hidden' ); 3399 3400 // Show loading status. 3401 if ( 'enable' === action ) { 3402 $label.text( __( 'Enabling...' ) ); 3403 } else { 3404 $label.text( __( 'Disabling...' ) ); 3405 } 3406 3407 $toggler.find( '.dashicons-update' ).removeClass( 'hidden' ); 3408 3409 data = { 3410 action: 'toggle-auto-updates', 3411 _ajax_nonce: settings.ajax_nonce, 3412 state: action, 3413 type: type, 3414 asset: asset 3415 }; 3416 3417 $.post( window.ajaxurl, data ) 3418 .done( function( response ) { 3419 var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage, 3420 href = $toggler.attr( 'href' ); 3421 3422 if ( ! response.success ) { 3423 // if WP returns 0 for response (which can happen in a few cases), 3424 // output the general error message since we won't have response.data.error. 3425 if ( response.data && response.data.error ) { 3426 errorMessage = response.data.error; 3427 } else { 3428 errorMessage = __( 'The request could not be completed.' ); 3429 } 3430 3431 $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage ); 3432 wp.a11y.speak( errorMessage, 'assertive' ); 3433 return; 3434 } 3435 3436 // Update the counts in the enabled/disabled views if on a screen 3437 // with a list table. 3438 if ( 'themes' !== pagenow ) { 3439 $enabled = $( '.auto-update-enabled span' ); 3440 $disabled = $( '.auto-update-disabled span' ); 3441 enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; 3442 disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; 3443 3444 switch ( action ) { 3445 case 'enable': 3446 ++enabledNumber; 3447 --disabledNumber; 3448 break; 3449 case 'disable': 3450 --enabledNumber; 3451 ++disabledNumber; 3452 break; 3453 } 3454 3455 enabledNumber = Math.max( 0, enabledNumber ); 3456 disabledNumber = Math.max( 0, disabledNumber ); 3457 3458 $enabled.text( '(' + enabledNumber + ')' ); 3459 $disabled.text( '(' + disabledNumber + ')' ); 3460 } 3461 3462 if ( 'enable' === action ) { 3463 // The toggler control can be either a link or a button. 3464 if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { 3465 href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' ); 3466 $toggler.attr( 'href', href ); 3467 } 3468 $toggler.attr( 'data-wp-action', 'disable' ); 3469 3470 $label.text( __( 'Disable auto-updates' ) ); 3471 $parent.find( '.auto-update-time' ).removeClass( 'hidden' ); 3472 wp.a11y.speak( __( 'Auto-updates enabled' ) ); 3473 } else { 3474 // The toggler control can be either a link or a button. 3475 if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { 3476 href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' ); 3477 $toggler.attr( 'href', href ); 3478 } 3479 $toggler.attr( 'data-wp-action', 'enable' ); 3480 3481 $label.text( __( 'Enable auto-updates' ) ); 3482 $parent.find( '.auto-update-time' ).addClass( 'hidden' ); 3483 wp.a11y.speak( __( 'Auto-updates disabled' ) ); 3484 } 3485 3486 $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } ); 3487 } ) 3488 .fail( function() { 3489 $parent.find( '.notice.notice-error' ) 3490 .removeClass( 'hidden' ) 3491 .find( 'p' ) 3492 .text( __( 'The request could not be completed.' ) ); 3493 3494 wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' ); 3495 } ) 3496 .always( function() { 3497 $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' ); 3498 } ); 3499 } 3500 ); 3501 } ); 3502 })( jQuery, window.wp, window._wpUpdatesSettings );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Oct 4 08:20:33 2026 | Cross-referenced by PHPXref |