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