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