You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3020 lines
93 KiB

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