25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2418 lines
77 KiB

  1. /**
  2. * Functions for ajaxified updates, deletions and installs inside the WordPress admin.
  3. *
  4. * @version 4.2.0
  5. *
  6. * @package WordPress
  7. * @subpackage Administration
  8. */
  9. /* global pagenow */
  10. /**
  11. * @param {jQuery} $ jQuery object.
  12. * @param {object} wp WP object.
  13. * @param {object} settings WP Updates settings.
  14. * @param {string} settings.ajax_nonce AJAX nonce.
  15. * @param {object} settings.l10n Translation strings.
  16. * @param {object=} settings.plugins Base names of plugins in their different states.
  17. * @param {Array} settings.plugins.all Base names of all plugins.
  18. * @param {Array} settings.plugins.active Base names of active plugins.
  19. * @param {Array} settings.plugins.inactive Base names of inactive plugins.
  20. * @param {Array} settings.plugins.upgrade Base names of plugins with updates available.
  21. * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins.
  22. * @param {object=} settings.themes Plugin/theme status information or null.
  23. * @param {number} settings.themes.all Amount of all themes.
  24. * @param {number} settings.themes.upgrade Amount of themes with updates available.
  25. * @param {number} settings.themes.disabled Amount of disabled themes.
  26. * @param {object=} settings.totals Combined information for available update counts.
  27. * @param {number} settings.totals.count Holds the amount of available updates.
  28. */
  29. (function( $, wp, settings ) {
  30. var $document = $( document );
  31. wp = wp || {};
  32. /**
  33. * The WP Updates object.
  34. *
  35. * @since 4.2.0
  36. *
  37. * @type {object}
  38. */
  39. wp.updates = {};
  40. /**
  41. * User nonce for ajax calls.
  42. *
  43. * @since 4.2.0
  44. *
  45. * @type {string}
  46. */
  47. wp.updates.ajaxNonce = settings.ajax_nonce;
  48. /**
  49. * Localized strings.
  50. *
  51. * @since 4.2.0
  52. *
  53. * @type {object}
  54. */
  55. wp.updates.l10n = settings.l10n;
  56. /**
  57. * Current search term.
  58. *
  59. * @since 4.6.0
  60. *
  61. * @type {string}
  62. */
  63. wp.updates.searchTerm = '';
  64. /**
  65. * Whether filesystem credentials need to be requested from the user.
  66. *
  67. * @since 4.2.0
  68. *
  69. * @type {bool}
  70. */
  71. wp.updates.shouldRequestFilesystemCredentials = false;
  72. /**
  73. * Filesystem credentials to be packaged along with the request.
  74. *
  75. * @since 4.2.0
  76. * @since 4.6.0 Added `available` property to indicate whether credentials have been provided.
  77. *
  78. * @type {object} filesystemCredentials Holds filesystem credentials.
  79. * @type {object} filesystemCredentials.ftp Holds FTP credentials.
  80. * @type {string} filesystemCredentials.ftp.host FTP host. Default empty string.
  81. * @type {string} filesystemCredentials.ftp.username FTP user name. Default empty string.
  82. * @type {string} filesystemCredentials.ftp.password FTP password. Default empty string.
  83. * @type {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'.
  84. * Default empty string.
  85. * @type {object} filesystemCredentials.ssh Holds SSH credentials.
  86. * @type {string} filesystemCredentials.ssh.publicKey The public key. Default empty string.
  87. * @type {string} filesystemCredentials.ssh.privateKey The private key. Default empty string.
  88. * @type {bool} filesystemCredentials.available Whether filesystem credentials have been provided.
  89. * Default 'false'.
  90. */
  91. wp.updates.filesystemCredentials = {
  92. ftp: {
  93. host: '',
  94. username: '',
  95. password: '',
  96. connectionType: ''
  97. },
  98. ssh: {
  99. publicKey: '',
  100. privateKey: ''
  101. },
  102. available: false
  103. };
  104. /**
  105. * Whether we're waiting for an Ajax request to complete.
  106. *
  107. * @since 4.2.0
  108. * @since 4.6.0 More accurately named `ajaxLocked`.
  109. *
  110. * @type {bool}
  111. */
  112. wp.updates.ajaxLocked = false;
  113. /**
  114. * Admin notice template.
  115. *
  116. * @since 4.6.0
  117. *
  118. * @type {function} A function that lazily-compiles the template requested.
  119. */
  120. wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
  121. /**
  122. * Update queue.
  123. *
  124. * If the user tries to update a plugin while an update is
  125. * already happening, it can be placed in this queue to perform later.
  126. *
  127. * @since 4.2.0
  128. * @since 4.6.0 More accurately named `queue`.
  129. *
  130. * @type {Array.object}
  131. */
  132. wp.updates.queue = [];
  133. /**
  134. * Holds a jQuery reference to return focus to when exiting the request credentials modal.
  135. *
  136. * @since 4.2.0
  137. *
  138. * @type {jQuery}
  139. */
  140. wp.updates.$elToReturnFocusToFromCredentialsModal = undefined;
  141. /**
  142. * Adds or updates an admin notice.
  143. *
  144. * @since 4.6.0
  145. *
  146. * @param {object} data
  147. * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice.
  148. * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute.
  149. * @param {string=} data.className Optional. Class names that will be used in the admin notice.
  150. * @param {string=} data.message Optional. The message displayed in the notice.
  151. * @param {number=} data.successes Optional. The amount of successful operations.
  152. * @param {number=} data.errors Optional. The amount of failed operations.
  153. * @param {Array=} data.errorMessages Optional. Error messages of failed operations.
  154. *
  155. */
  156. wp.updates.addAdminNotice = function( data ) {
  157. var $notice = $( data.selector ), $adminNotice;
  158. delete data.selector;
  159. $adminNotice = wp.updates.adminNotice( data );
  160. // Check if this admin notice already exists.
  161. if ( ! $notice.length ) {
  162. $notice = $( '#' + data.id );
  163. }
  164. if ( $notice.length ) {
  165. $notice.replaceWith( $adminNotice );
  166. } else {
  167. $( '.wrap' ).find( '> h1' ).after( $adminNotice );
  168. }
  169. $document.trigger( 'wp-updates-notice-added' );
  170. };
  171. /**
  172. * Handles Ajax requests to WordPress.
  173. *
  174. * @since 4.6.0
  175. *
  176. * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc).
  177. * @param {object} data Data that needs to be passed to the ajax callback.
  178. * @return {$.promise} A jQuery promise that represents the request,
  179. * decorated with an abort() method.
  180. */
  181. wp.updates.ajax = function( action, data ) {
  182. var options = {};
  183. if ( wp.updates.ajaxLocked ) {
  184. wp.updates.queue.push( {
  185. action: action,
  186. data: data
  187. } );
  188. // Return a Deferred object so callbacks can always be registered.
  189. return $.Deferred();
  190. }
  191. wp.updates.ajaxLocked = true;
  192. if ( data.success ) {
  193. options.success = data.success;
  194. delete data.success;
  195. }
  196. if ( data.error ) {
  197. options.error = data.error;
  198. delete data.error;
  199. }
  200. options.data = _.extend( data, {
  201. action: action,
  202. _ajax_nonce: wp.updates.ajaxNonce,
  203. username: wp.updates.filesystemCredentials.ftp.username,
  204. password: wp.updates.filesystemCredentials.ftp.password,
  205. hostname: wp.updates.filesystemCredentials.ftp.hostname,
  206. connection_type: wp.updates.filesystemCredentials.ftp.connectionType,
  207. public_key: wp.updates.filesystemCredentials.ssh.publicKey,
  208. private_key: wp.updates.filesystemCredentials.ssh.privateKey
  209. } );
  210. return wp.ajax.send( options ).always( wp.updates.ajaxAlways );
  211. };
  212. /**
  213. * Actions performed after every Ajax request.
  214. *
  215. * @since 4.6.0
  216. *
  217. * @param {object} response
  218. * @param {array=} response.debug Optional. Debug information.
  219. * @param {string=} response.errorCode Optional. Error code for an error that occurred.
  220. */
  221. wp.updates.ajaxAlways = function( response ) {
  222. if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) {
  223. wp.updates.ajaxLocked = false;
  224. wp.updates.queueChecker();
  225. }
  226. if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) {
  227. _.map( response.debug, function( message ) {
  228. window.console.log( $( '<p />' ).html( message ).text() );
  229. } );
  230. }
  231. };
  232. /**
  233. * Refreshes update counts everywhere on the screen.
  234. *
  235. * @since 4.7.0
  236. */
  237. wp.updates.refreshCount = function() {
  238. var $adminBarUpdates = $( '#wp-admin-bar-updates' ),
  239. $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ),
  240. $pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ),
  241. $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ),
  242. itemCount;
  243. $adminBarUpdates.find( '.ab-item' ).removeAttr( 'title' );
  244. $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total );
  245. // Remove the update count from the toolbar if it's zero.
  246. if ( 0 === settings.totals.counts.total ) {
  247. $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove();
  248. }
  249. // Update the "Updates" menu item.
  250. $dashboardNavMenuUpdateCount.each( function( index, element ) {
  251. element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total );
  252. } );
  253. if ( settings.totals.counts.total > 0 ) {
  254. $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total );
  255. } else {
  256. $dashboardNavMenuUpdateCount.remove();
  257. }
  258. // Update the "Plugins" menu item.
  259. $pluginsNavMenuUpdateCount.each( function( index, element ) {
  260. element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins );
  261. } );
  262. if ( settings.totals.counts.total > 0 ) {
  263. $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins );
  264. } else {
  265. $pluginsNavMenuUpdateCount.remove();
  266. }
  267. // Update the "Appearance" menu item.
  268. $appearanceNavMenuUpdateCount.each( function( index, element ) {
  269. element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes );
  270. } );
  271. if ( settings.totals.counts.total > 0 ) {
  272. $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes );
  273. } else {
  274. $appearanceNavMenuUpdateCount.remove();
  275. }
  276. // Update list table filter navigation.
  277. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
  278. itemCount = settings.totals.counts.plugins;
  279. } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
  280. itemCount = settings.totals.counts.themes;
  281. }
  282. if ( itemCount > 0 ) {
  283. $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' );
  284. } else {
  285. $( '.subsubsub .upgrade' ).remove();
  286. }
  287. };
  288. /**
  289. * Decrements the update counts throughout the various menus.
  290. *
  291. * This includes the toolbar, the "Updates" menu item and the menu items
  292. * for plugins and themes.
  293. *
  294. * @since 3.9.0
  295. *
  296. * @param {string} type The type of item that was updated or deleted.
  297. * Can be 'plugin', 'theme'.
  298. */
  299. wp.updates.decrementCount = function( type ) {
  300. settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 );
  301. if ( 'plugin' === type ) {
  302. settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 );
  303. } else if ( 'theme' === type ) {
  304. settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 );
  305. }
  306. wp.updates.refreshCount( type );
  307. };
  308. /**
  309. * Sends an Ajax request to the server to update a plugin.
  310. *
  311. * @since 4.2.0
  312. * @since 4.6.0 More accurately named `updatePlugin`.
  313. *
  314. * @param {object} args Arguments.
  315. * @param {string} args.plugin Plugin basename.
  316. * @param {string} args.slug Plugin slug.
  317. * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess
  318. * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError
  319. * @return {$.promise} A jQuery promise that represents the request,
  320. * decorated with an abort() method.
  321. */
  322. wp.updates.updatePlugin = function( args ) {
  323. var $updateRow, $card, $message, message;
  324. args = _.extend( {
  325. success: wp.updates.updatePluginSuccess,
  326. error: wp.updates.updatePluginError
  327. }, args );
  328. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
  329. $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' );
  330. $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
  331. message = wp.updates.l10n.updatingLabel.replace( '%s', $updateRow.find( '.plugin-title strong' ).text() );
  332. } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
  333. $card = $( '.plugin-card-' + args.slug );
  334. $message = $card.find( '.update-now' ).addClass( 'updating-message' );
  335. message = wp.updates.l10n.updatingLabel.replace( '%s', $message.data( 'name' ) );
  336. // Remove previous error messages, if any.
  337. $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove();
  338. }
  339. if ( $message.html() !== wp.updates.l10n.updating ) {
  340. $message.data( 'originaltext', $message.html() );
  341. }
  342. $message
  343. .attr( 'aria-label', message )
  344. .text( wp.updates.l10n.updating );
  345. $document.trigger( 'wp-plugin-updating', args );
  346. return wp.updates.ajax( 'update-plugin', args );
  347. };
  348. /**
  349. * Updates the UI appropriately after a successful plugin update.
  350. *
  351. * @since 4.2.0
  352. * @since 4.6.0 More accurately named `updatePluginSuccess`.
  353. *
  354. * @typedef {object} updatePluginSuccess
  355. * @param {object} response Response from the server.
  356. * @param {string} response.slug Slug of the plugin to be updated.
  357. * @param {string} response.plugin Basename of the plugin to be updated.
  358. * @param {string} response.pluginName Name of the plugin to be updated.
  359. * @param {string} response.oldVersion Old version of the plugin.
  360. * @param {string} response.newVersion New version of the plugin.
  361. */
  362. wp.updates.updatePluginSuccess = function( response ) {
  363. var $pluginRow, $updateMessage, newText;
  364. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
  365. $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' )
  366. .removeClass( 'update' )
  367. .addClass( 'updated' );
  368. $updateMessage = $pluginRow.find( '.update-message' )
  369. .removeClass( 'updating-message notice-warning' )
  370. .addClass( 'updated-message notice-success' ).find( 'p' );
  371. // Update the version number in the row.
  372. newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
  373. $pluginRow.find( '.plugin-version-author-uri' ).html( newText );
  374. } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
  375. $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' )
  376. .removeClass( 'updating-message' )
  377. .addClass( 'button-disabled updated-message' );
  378. }
  379. $updateMessage
  380. .attr( 'aria-label', wp.updates.l10n.updatedLabel.replace( '%s', response.pluginName ) )
  381. .text( wp.updates.l10n.updated );
  382. wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
  383. wp.updates.decrementCount( 'plugin' );
  384. $document.trigger( 'wp-plugin-update-success', response );
  385. };
  386. /**
  387. * Updates the UI appropriately after a failed plugin update.
  388. *
  389. * @since 4.2.0
  390. * @since 4.6.0 More accurately named `updatePluginError`.
  391. *
  392. * @typedef {object} updatePluginError
  393. * @param {object} response Response from the server.
  394. * @param {string} response.slug Slug of the plugin to be updated.
  395. * @param {string} response.plugin Basename of the plugin to be updated.
  396. * @param {string=} response.pluginName Optional. Name of the plugin to be updated.
  397. * @param {string} response.errorCode Error code for the error that occurred.
  398. * @param {string} response.errorMessage The error that occurred.
  399. */
  400. wp.updates.updatePluginError = function( response ) {
  401. var $card, $message, errorMessage;
  402. if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
  403. return;
  404. }
  405. if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) {
  406. return;
  407. }
  408. errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage );
  409. if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
  410. if ( response.plugin ) {
  411. $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' );
  412. } else {
  413. $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' );
  414. }
  415. $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage );
  416. if ( response.pluginName ) {
  417. $message.find( 'p' )
  418. .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
  419. } else {
  420. $message.find( 'p' ).removeAttr( 'aria-label' );
  421. }
  422. } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
  423. $card = $( '.plugin-card-' + response.slug )
  424. .addClass( 'plugin-card-update-failed' )
  425. .append( wp.updates.adminNotice( {
  426. className: 'update-message notice-error notice-alt is-dismissible',
  427. message: errorMessage
  428. } ) );
  429. $card.find( '.update-now' )
  430. .text( wp.updates.l10n.updateFailedShort ).removeClass( 'updating-message' );
  431. if ( response.pluginName ) {
  432. $card.find( '.update-now' )
  433. .attr( 'aria-label', wp.updates.l10n.updateFailedLabel.replace( '%s', response.pluginName ) );
  434. } else {
  435. $card.find( '.update-now' ).removeAttr( 'aria-label' );
  436. }
  437. $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
  438. // Use same delay as the total duration of the notice fadeTo + slideUp animation.
  439. setTimeout( function() {
  440. $card
  441. .removeClass( 'plugin-card-update-failed' )
  442. .find( '.column-name a' ).focus();
  443. $card.find( '.update-now' )
  444. .attr( 'aria-label', false )
  445. .text( wp.updates.l10n.updateNow );
  446. }, 200 );
  447. } );
  448. }
  449. wp.a11y.speak( errorMessage, 'assertive' );
  450. $document.trigger( 'wp-plugin-update-error', response );
  451. };
  452. /**
  453. * Sends an Ajax request to the server to install a plugin.
  454. *
  455. * @since 4.6.0
  456. *
  457. * @param {object} args Arguments.
  458. * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository.
  459. * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess
  460. * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError
  461. * @return {$.promise} A jQuery promise that represents the request,
  462. * decorated with an abort() method.
  463. */
  464. wp.updates.installPlugin = function( args ) {
  465. var $card = $( '.plugin-card-' + args.slug ),
  466. $message = $card.find( '.install-now' );
  467. args = _.extend( {
  468. success: wp.updates.installPluginSuccess,
  469. error: wp.updates.installPluginError
  470. }, args );
  471. if ( 'import' === pagenow ) {
  472. $message = $( '[data-slug="' + args.slug + '"]' );
  473. }
  474. if ( $message.html() !== wp.updates.l10n.installing ) {
  475. $message.data( 'originaltext', $message.html() );
  476. }
  477. $message
  478. .addClass( 'updating-message' )
  479. .attr( 'aria-label', wp.updates.l10n.pluginInstallingLabel.replace( '%s', $message.data( 'name' ) ) )
  480. .text( wp.updates.l10n.installing );
  481. wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
  482. // Remove previous error messages, if any.
  483. $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove();
  484. $document.trigger( 'wp-plugin-installing', args );
  485. return wp.updates.ajax( 'install-plugin', args );
  486. };
  487. /**
  488. * Updates the UI appropriately after a successful plugin install.
  489. *
  490. * @since 4.6.0
  491. *
  492. * @typedef {object} installPluginSuccess
  493. * @param {object} response Response from the server.
  494. * @param {string} response.slug Slug of the installed plugin.
  495. * @param {string} response.pluginName Name of the installed plugin.
  496. * @param {string} response.activateUrl URL to activate the just installed plugin.
  497. */
  498. wp.updates.installPluginSuccess = function( response ) {
  499. var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' );
  500. $message
  501. .removeClass( 'updating-message' )
  502. .addClass( 'updated-message installed button-disabled' )
  503. .attr( 'aria-label', wp.updates.l10n.pluginInstalledLabel.replace( '%s', response.pluginName ) )
  504. .text( wp.updates.l10n.installed );
  505. wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
  506. $document.trigger( 'wp-plugin-install-success', response );
  507. if ( response.activateUrl ) {
  508. setTimeout( function() {
  509. // Transform the 'Install' button into an 'Activate' button.
  510. $message.removeClass( 'install-now installed button-disabled updated-message' ).addClass( 'activate-now button-primary' )
  511. .attr( 'href', response.activateUrl )
  512. .attr( 'aria-label', wp.updates.l10n.activatePluginLabel.replace( '%s', response.pluginName ) )
  513. .text( wp.updates.l10n.activatePlugin );
  514. }, 1000 );
  515. }
  516. };
  517. /**
  518. * Updates the UI appropriately after a failed plugin install.
  519. *
  520. * @since 4.6.0
  521. *
  522. * @typedef {object} installPluginError
  523. * @param {object} response Response from the server.
  524. * @param {string} response.slug Slug of the plugin to be installed.
  525. * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
  526. * @param {string} response.errorCode Error code for the error that occurred.
  527. * @param {string} response.errorMessage The error that occurred.
  528. */
  529. wp.updates.installPluginError = function( response ) {
  530. var $card = $( '.plugin-card-' + response.slug ),
  531. $button = $card.find( '.install-now' ),
  532. errorMessage;
  533. if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
  534. return;
  535. }
  536. if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
  537. return;
  538. }
  539. errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage );
  540. $card
  541. .addClass( 'plugin-card-update-failed' )
  542. .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' );
  543. $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() {
  544. // Use same delay as the total duration of the notice fadeTo + slideUp animation.
  545. setTimeout( function() {
  546. $card
  547. .removeClass( 'plugin-card-update-failed' )
  548. .find( '.column-name a' ).focus();
  549. }, 200 );
  550. } );
  551. $button
  552. .removeClass( 'updating-message' ).addClass( 'button-disabled' )
  553. .attr( 'aria-label', wp.updates.l10n.pluginInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
  554. .text( wp.updates.l10n.installFailedShort );
  555. wp.a11y.speak( errorMessage, 'assertive' );
  556. $document.trigger( 'wp-plugin-install-error', response );
  557. };
  558. /**
  559. * Updates the UI appropriately after a successful importer install.
  560. *
  561. * @since 4.6.0
  562. *
  563. * @typedef {object} installImporterSuccess
  564. * @param {object} response Response from the server.
  565. * @param {string} response.slug Slug of the installed plugin.
  566. * @param {string} response.pluginName Name of the installed plugin.
  567. * @param {string} response.activateUrl URL to activate the just installed plugin.
  568. */
  569. wp.updates.installImporterSuccess = function( response ) {
  570. wp.updates.addAdminNotice( {
  571. id: 'install-success',
  572. className: 'notice-success is-dismissible',
  573. message: wp.updates.l10n.importerInstalledMsg.replace( '%s', response.activateUrl + '&from=import' )
  574. } );
  575. $( '[data-slug="' + response.slug + '"]' )
  576. .removeClass( 'install-now updating-message' )
  577. .addClass( 'activate-now' )
  578. .attr({
  579. 'href': response.activateUrl + '&from=import',
  580. 'aria-label': wp.updates.l10n.activateImporterLabel.replace( '%s', response.pluginName )
  581. })
  582. .text( wp.updates.l10n.activateImporter );
  583. wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
  584. $document.trigger( 'wp-importer-install-success', response );
  585. };
  586. /**
  587. * Updates the UI appropriately after a failed importer install.
  588. *
  589. * @since 4.6.0
  590. *
  591. * @typedef {object} installImporterError
  592. * @param {object} response Response from the server.
  593. * @param {string} response.slug Slug of the plugin to be installed.
  594. * @param {string=} response.pluginName Optional. Name of the plugin to be installed.
  595. * @param {string} response.errorCode Error code for the error that occurred.
  596. * @param {string} response.errorMessage The error that occurred.
  597. */
  598. wp.updates.installImporterError = function( response ) {
  599. var errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
  600. $installLink = $( '[data-slug="' + response.slug + '"]' ),
  601. pluginName = $installLink.data( 'name' );
  602. if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
  603. return;
  604. }
  605. if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) {
  606. return;
  607. }
  608. wp.updates.addAdminNotice( {
  609. id: response.errorCode,
  610. className: 'notice-error is-dismissible',
  611. message: errorMessage
  612. } );
  613. $installLink
  614. .removeClass( 'updating-message' )
  615. .text( wp.updates.l10n.installNow )
  616. .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
  617. wp.a11y.speak( errorMessage, 'assertive' );
  618. $document.trigger( 'wp-importer-install-error', response );
  619. };
  620. /**
  621. * Sends an Ajax request to the server to delete a plugin.
  622. *
  623. * @since 4.6.0
  624. *
  625. * @param {object} args Arguments.
  626. * @param {string} args.plugin Basename of the plugin to be deleted.
  627. * @param {string} args.slug Slug of the plugin to be deleted.
  628. * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess
  629. * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError
  630. * @return {$.promise} A jQuery promise that represents the request,
  631. * decorated with an abort() method.
  632. */
  633. wp.updates.deletePlugin = function( args ) {
  634. var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' );
  635. args = _.extend( {
  636. success: wp.updates.deletePluginSuccess,
  637. error: wp.updates.deletePluginError
  638. }, args );
  639. if ( $link.html() !== wp.updates.l10n.deleting ) {
  640. $link
  641. .data( 'originaltext', $link.html() )
  642. .text( wp.updates.l10n.deleting );
  643. }
  644. wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
  645. $document.trigger( 'wp-plugin-deleting', args );
  646. return wp.updates.ajax( 'delete-plugin', args );
  647. };
  648. /**
  649. * Updates the UI appropriately after a successful plugin deletion.
  650. *
  651. * @since 4.6.0
  652. *
  653. * @typedef {object} deletePluginSuccess
  654. * @param {object} response Response from the server.
  655. * @param {string} response.slug Slug of the plugin that was deleted.
  656. * @param {string} response.plugin Base name of the plugin that was deleted.
  657. * @param {string} response.pluginName Name of the plugin that was deleted.
  658. */
  659. wp.updates.deletePluginSuccess = function( response ) {
  660. // Removes the plugin and updates rows.
  661. $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
  662. var $form = $( '#bulk-action-form' ),
  663. $views = $( '.subsubsub' ),
  664. $pluginRow = $( this ),
  665. columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length,
  666. pluginDeletedRow = wp.template( 'item-deleted-row' ),
  667. /** @type {object} plugins Base names of plugins in their different states. */
  668. plugins = settings.plugins;
  669. // Add a success message after deleting a plugin.
  670. if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) {
  671. $pluginRow.after(
  672. pluginDeletedRow( {
  673. slug: response.slug,
  674. plugin: response.plugin,
  675. colspan: columnCount,
  676. name: response.pluginName
  677. } )
  678. );
  679. }
  680. $pluginRow.remove();
  681. // Remove plugin from update count.
  682. if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) {
  683. plugins.upgrade = _.without( plugins.upgrade, response.plugin );
  684. wp.updates.decrementCount( 'plugin' );
  685. }
  686. // Remove from views.
  687. if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) {
  688. plugins.inactive = _.without( plugins.inactive, response.plugin );
  689. if ( plugins.inactive.length ) {
  690. $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' );
  691. } else {
  692. $views.find( '.inactive' ).remove();
  693. }
  694. }
  695. if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) {
  696. plugins.active = _.without( plugins.active, response.plugin );
  697. if ( plugins.active.length ) {
  698. $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' );
  699. } else {
  700. $views.find( '.active' ).remove();
  701. }
  702. }
  703. if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) {
  704. plugins.recently_activated = _.without( plugins.recently_activated, response.plugin );
  705. if ( plugins.recently_activated.length ) {
  706. $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' );
  707. } else {
  708. $views.find( '.recently_activated' ).remove();
  709. }
  710. }
  711. plugins.all = _.without( plugins.all, response.plugin );
  712. if ( plugins.all.length ) {
  713. $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' );
  714. } else {
  715. $form.find( '.tablenav' ).css( { visibility: 'hidden' } );
  716. $views.find( '.all' ).remove();
  717. if ( ! $form.find( 'tr.no-items' ).length ) {
  718. $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + wp.updates.l10n.noPlugins + '</td></tr>' );
  719. }
  720. }
  721. } );
  722. wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
  723. $document.trigger( 'wp-plugin-delete-success', response );
  724. };
  725. /**
  726. * Updates the UI appropriately after a failed plugin deletion.
  727. *
  728. * @since 4.6.0
  729. *
  730. * @typedef {object} deletePluginError
  731. * @param {object} response Response from the server.
  732. * @param {string} response.slug Slug of the plugin to be deleted.
  733. * @param {string} response.plugin Base name of the plugin to be deleted
  734. * @param {string=} response.pluginName Optional. Name of the plugin to be deleted.
  735. * @param {string} response.errorCode Error code for the error that occurred.
  736. * @param {string} response.errorMessage The error that occurred.
  737. */
  738. wp.updates.deletePluginError = function( response ) {
  739. var $plugin, $pluginUpdateRow,
  740. pluginUpdateRow = wp.template( 'item-update-row' ),
  741. noticeContent = wp.updates.adminNotice( {
  742. className: 'update-message notice-error notice-alt',
  743. message: response.errorMessage
  744. } );
  745. if ( response.plugin ) {
  746. $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' );
  747. $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' );
  748. } else {
  749. $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' );
  750. $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' );
  751. }
  752. if ( ! wp.updates.isValidResponse( response, 'delete' ) ) {
  753. return;
  754. }
  755. if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) {
  756. return;
  757. }
  758. // Add a plugin update row if it doesn't exist yet.
  759. if ( ! $pluginUpdateRow.length ) {
  760. $plugin.addClass( 'update' ).after(
  761. pluginUpdateRow( {
  762. slug: response.slug,
  763. plugin: response.plugin || response.slug,
  764. colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
  765. content: noticeContent
  766. } )
  767. );
  768. } else {
  769. // Remove previous error messages, if any.
  770. $pluginUpdateRow.find( '.notice-error' ).remove();
  771. $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent );
  772. }
  773. $document.trigger( 'wp-plugin-delete-error', response );
  774. };
  775. /**
  776. * Sends an Ajax request to the server to update a theme.
  777. *
  778. * @since 4.6.0
  779. *
  780. * @param {object} args Arguments.
  781. * @param {string} args.slug Theme stylesheet.
  782. * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess
  783. * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError
  784. * @return {$.promise} A jQuery promise that represents the request,
  785. * decorated with an abort() method.
  786. */
  787. wp.updates.updateTheme = function( args ) {
  788. var $notice;
  789. args = _.extend( {
  790. success: wp.updates.updateThemeSuccess,
  791. error: wp.updates.updateThemeError
  792. }, args );
  793. if ( 'themes-network' === pagenow ) {
  794. $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' );
  795. } else {
  796. $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' );
  797. $notice.find( 'h3' ).remove();
  798. $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) );
  799. $notice = $notice.addClass( 'updating-message' ).find( 'p' );
  800. }
  801. if ( $notice.html() !== wp.updates.l10n.updating ) {
  802. $notice.data( 'originaltext', $notice.html() );
  803. }
  804. wp.a11y.speak( wp.updates.l10n.updatingMsg, 'polite' );
  805. $notice.text( wp.updates.l10n.updating );
  806. $document.trigger( 'wp-theme-updating', args );
  807. return wp.updates.ajax( 'update-theme', args );
  808. };
  809. /**
  810. * Updates the UI appropriately after a successful theme update.
  811. *
  812. * @since 4.6.0
  813. *
  814. * @typedef {object} updateThemeSuccess
  815. * @param {object} response
  816. * @param {string} response.slug Slug of the theme to be updated.
  817. * @param {object} response.theme Updated theme.
  818. * @param {string} response.oldVersion Old version of the theme.
  819. * @param {string} response.newVersion New version of the theme.
  820. */
  821. wp.updates.updateThemeSuccess = function( response ) {
  822. var isModalOpen = $( 'body.modal-open' ).length,
  823. $theme = $( '[data-slug="' + response.slug + '"]' ),
  824. updatedMessage = {
  825. className: 'updated-message notice-success notice-alt',
  826. message: wp.updates.l10n.updated
  827. },
  828. $notice, newText;
  829. if ( 'themes-network' === pagenow ) {
  830. $notice = $theme.find( '.update-message' );
  831. // Update the version number in the row.
  832. newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion );
  833. $theme.find( '.theme-version-author-uri' ).html( newText );
  834. } else {
  835. $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) );
  836. // Focus on Customize button after updating.
  837. if ( isModalOpen ) {
  838. $( '.load-customize:visible' ).focus();
  839. } else {
  840. $theme.find( '.load-customize' ).focus();
  841. }
  842. }
  843. wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) );
  844. wp.a11y.speak( wp.updates.l10n.updatedMsg, 'polite' );
  845. wp.updates.decrementCount( 'theme' );
  846. $document.trigger( 'wp-theme-update-success', response );
  847. // Show updated message after modal re-rendered.
  848. if ( isModalOpen ) {
  849. $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) );
  850. }
  851. };
  852. /**
  853. * Updates the UI appropriately after a failed theme update.
  854. *
  855. * @since 4.6.0
  856. *
  857. * @typedef {object} updateThemeError
  858. * @param {object} response Response from the server.
  859. * @param {string} response.slug Slug of the theme to be updated.
  860. * @param {string} response.errorCode Error code for the error that occurred.
  861. * @param {string} response.errorMessage The error that occurred.
  862. */
  863. wp.updates.updateThemeError = function( response ) {
  864. var $theme = $( '[data-slug="' + response.slug + '"]' ),
  865. errorMessage = wp.updates.l10n.updateFailed.replace( '%s', response.errorMessage ),
  866. $notice;
  867. if ( ! wp.updates.isValidResponse( response, 'update' ) ) {
  868. return;
  869. }
  870. if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) {
  871. return;
  872. }
  873. if ( 'themes-network' === pagenow ) {
  874. $notice = $theme.find( '.update-message ' );
  875. } else {
  876. $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) );
  877. $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).focus() : $theme.find( '.load-customize' ).focus();
  878. }
  879. wp.updates.addAdminNotice( {
  880. selector: $notice,
  881. className: 'update-message notice-error notice-alt is-dismissible',
  882. message: errorMessage
  883. } );
  884. wp.a11y.speak( errorMessage, 'polite' );
  885. $document.trigger( 'wp-theme-update-error', response );
  886. };
  887. /**
  888. * Sends an Ajax request to the server to install a theme.
  889. *
  890. * @since 4.6.0
  891. *
  892. * @param {object} args
  893. * @param {string} args.slug Theme stylesheet.
  894. * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess
  895. * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError
  896. * @return {$.promise} A jQuery promise that represents the request,
  897. * decorated with an abort() method.
  898. */
  899. wp.updates.installTheme = function( args ) {
  900. var $message = $( '.theme-install[data-slug="' + args.slug + '"]' );
  901. args = _.extend( {
  902. success: wp.updates.installThemeSuccess,
  903. error: wp.updates.installThemeError
  904. }, args );
  905. $message.addClass( 'updating-message' );
  906. $message.parents( '.theme' ).addClass( 'focus' );
  907. if ( $message.html() !== wp.updates.l10n.installing ) {
  908. $message.data( 'originaltext', $message.html() );
  909. }
  910. $message
  911. .text( wp.updates.l10n.installing )
  912. .attr( 'aria-label', wp.updates.l10n.themeInstallingLabel.replace( '%s', $message.data( 'name' ) ) );
  913. wp.a11y.speak( wp.updates.l10n.installingMsg, 'polite' );
  914. // Remove previous error messages, if any.
  915. $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove();
  916. $document.trigger( 'wp-theme-installing', args );
  917. return wp.updates.ajax( 'install-theme', args );
  918. };
  919. /**
  920. * Updates the UI appropriately after a successful theme install.
  921. *
  922. * @since 4.6.0
  923. *
  924. * @typedef {object} installThemeSuccess
  925. * @param {object} response Response from the server.
  926. * @param {string} response.slug Slug of the theme to be installed.
  927. * @param {string} response.customizeUrl URL to the Customizer for the just installed theme.
  928. * @param {string} response.activateUrl URL to activate the just installed theme.
  929. */
  930. wp.updates.installThemeSuccess = function( response ) {
  931. var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ),
  932. $message;
  933. $document.trigger( 'wp-theme-install-success', response );
  934. $message = $card.find( '.button-primary' )
  935. .removeClass( 'updating-message' )
  936. .addClass( 'updated-message disabled' )
  937. .attr( 'aria-label', wp.updates.l10n.themeInstalledLabel.replace( '%s', response.themeName ) )
  938. .text( wp.updates.l10n.installed );
  939. wp.a11y.speak( wp.updates.l10n.installedMsg, 'polite' );
  940. setTimeout( function() {
  941. if ( response.activateUrl ) {
  942. // Transform the 'Install' button into an 'Activate' button.
  943. $message
  944. .attr( 'href', response.activateUrl )
  945. .removeClass( 'theme-install updated-message disabled' )
  946. .addClass( 'activate' )
  947. .attr( 'aria-label', wp.updates.l10n.activateThemeLabel.replace( '%s', response.themeName ) )
  948. .text( wp.updates.l10n.activateTheme );
  949. }
  950. if ( response.customizeUrl ) {
  951. // Transform the 'Preview' button into a 'Live Preview' button.
  952. $message.siblings( '.preview' ).replaceWith( function () {
  953. return $( '<a>' )
  954. .attr( 'href', response.customizeUrl )
  955. .addClass( 'button load-customize' )
  956. .text( wp.updates.l10n.livePreview );
  957. } );
  958. }
  959. }, 1000 );
  960. };
  961. /**
  962. * Updates the UI appropriately after a failed theme install.
  963. *
  964. * @since 4.6.0
  965. *
  966. * @typedef {object} installThemeError
  967. * @param {object} response Response from the server.
  968. * @param {string} response.slug Slug of the theme to be installed.
  969. * @param {string} response.errorCode Error code for the error that occurred.
  970. * @param {string} response.errorMessage The error that occurred.
  971. */
  972. wp.updates.installThemeError = function( response ) {
  973. var $card, $button,
  974. errorMessage = wp.updates.l10n.installFailed.replace( '%s', response.errorMessage ),
  975. $message = wp.updates.adminNotice( {
  976. className: 'update-message notice-error notice-alt',
  977. message: errorMessage
  978. } );
  979. if ( ! wp.updates.isValidResponse( response, 'install' ) ) {
  980. return;
  981. }
  982. if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) {
  983. return;
  984. }
  985. if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) {
  986. $button = $( '.theme-install[data-slug="' + response.slug + '"]' );
  987. $card = $( '.install-theme-info' ).prepend( $message );
  988. } else {
  989. $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message );
  990. $button = $card.find( '.theme-install' );
  991. }
  992. $button
  993. .removeClass( 'updating-message' )
  994. .attr( 'aria-label', wp.updates.l10n.themeInstallFailedLabel.replace( '%s', $button.data( 'name' ) ) )
  995. .text( wp.updates.l10n.installFailedShort );
  996. wp.a11y.speak( errorMessage, 'assertive' );
  997. $document.trigger( 'wp-theme-install-error', response );
  998. };
  999. /**
  1000. * Sends an Ajax request to the server to install a theme.
  1001. *
  1002. * @since 4.6.0
  1003. *
  1004. * @param {object} args
  1005. * @param {string} args.slug Theme stylesheet.
  1006. * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess
  1007. * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError
  1008. * @return {$.promise} A jQuery promise that represents the request,
  1009. * decorated with an abort() method.
  1010. */
  1011. wp.updates.deleteTheme = function( args ) {
  1012. var $button;
  1013. if ( 'themes' === pagenow ) {
  1014. $button = $( '.theme-actions .delete-theme' );
  1015. } else if ( 'themes-network' === pagenow ) {
  1016. $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' );
  1017. }
  1018. args = _.extend( {
  1019. success: wp.updates.deleteThemeSuccess,
  1020. error: wp.updates.deleteThemeError
  1021. }, args );
  1022. if ( $button && $button.html() !== wp.updates.l10n.deleting ) {
  1023. $button
  1024. .data( 'originaltext', $button.html() )
  1025. .text( wp.updates.l10n.deleting );
  1026. }
  1027. wp.a11y.speak( wp.updates.l10n.deleting, 'polite' );
  1028. // Remove previous error messages, if any.
  1029. $( '.theme-info .update-message' ).remove();
  1030. $document.trigger( 'wp-theme-deleting', args );
  1031. return wp.updates.ajax( 'delete-theme', args );
  1032. };
  1033. /**
  1034. * Updates the UI appropriately after a successful theme deletion.
  1035. *
  1036. * @since 4.6.0
  1037. *
  1038. * @typedef {object} deleteThemeSuccess
  1039. * @param {object} response Response from the server.
  1040. * @param {string} response.slug Slug of the theme that was deleted.
  1041. */
  1042. wp.updates.deleteThemeSuccess = function( response ) {
  1043. var $themeRows = $( '[data-slug="' + response.slug + '"]' );
  1044. if ( 'themes-network' === pagenow ) {
  1045. // Removes the theme and updates rows.
  1046. $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() {
  1047. var $views = $( '.subsubsub' ),
  1048. $themeRow = $( this ),
  1049. totals = settings.themes,
  1050. deletedRow = wp.template( 'item-deleted-row' );
  1051. if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) {
  1052. $themeRow.after(
  1053. deletedRow( {
  1054. slug: response.slug,
  1055. colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
  1056. name: $themeRow.find( '.theme-title strong' ).text()
  1057. } )
  1058. );
  1059. }
  1060. $themeRow.remove();
  1061. // Remove theme from update count.
  1062. if ( $themeRow.hasClass( 'update' ) ) {
  1063. totals.upgrade--;
  1064. wp.updates.decrementCount( 'theme' );
  1065. }
  1066. // Remove from views.
  1067. if ( $themeRow.hasClass( 'inactive' ) ) {
  1068. totals.disabled--;
  1069. if ( totals.disabled ) {
  1070. $views.find( '.disabled .count' ).text( '(' + totals.disabled + ')' );
  1071. } else {
  1072. $views.find( '.disabled' ).remove();
  1073. }
  1074. }
  1075. // There is always at least one theme available.
  1076. $views.find( '.all .count' ).text( '(' + --totals.all + ')' );
  1077. } );
  1078. }
  1079. wp.a11y.speak( wp.updates.l10n.deleted, 'polite' );
  1080. $document.trigger( 'wp-theme-delete-success', response );
  1081. };
  1082. /**
  1083. * Updates the UI appropriately after a failed theme deletion.
  1084. *
  1085. * @since 4.6.0
  1086. *
  1087. * @typedef {object} deleteThemeError
  1088. * @param {object} response Response from the server.
  1089. * @param {string} response.slug Slug of the theme to be deleted.
  1090. * @param {string} response.errorCode Error code for the error that occurred.
  1091. * @param {string} response.errorMessage The error that occurred.
  1092. */
  1093. wp.updates.deleteThemeError = function( response ) {
  1094. var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ),
  1095. $button = $( '.theme-actions .delete-theme' ),
  1096. updateRow = wp.template( 'item-update-row' ),
  1097. $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ),
  1098. errorMessage = wp.updates.l10n.deleteFailed.replace( '%s', response.errorMessage ),
  1099. $message = wp.updates.adminNotice( {
  1100. className: 'update-message notice-error notice-alt',
  1101. message: errorMessage
  1102. } );
  1103. if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) {
  1104. return;
  1105. }
  1106. if ( 'themes-network' === pagenow ) {
  1107. if ( ! $updateRow.length ) {
  1108. $themeRow.addClass( 'update' ).after(
  1109. updateRow( {
  1110. slug: response.slug,
  1111. colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length,
  1112. content: $message
  1113. } )
  1114. );
  1115. } else {
  1116. // Remove previous error messages, if any.
  1117. $updateRow.find( '.notice-error' ).remove();
  1118. $updateRow.find( '.plugin-update' ).append( $message );
  1119. }
  1120. } else {
  1121. $( '.theme-info .theme-description' ).before( $message );
  1122. }
  1123. $button.html( $button.data( 'originaltext' ) );
  1124. wp.a11y.speak( errorMessage, 'assertive' );
  1125. $document.trigger( 'wp-theme-delete-error', response );
  1126. };
  1127. /**
  1128. * Adds the appropriate callback based on the type of action and the current page.
  1129. *
  1130. * @since 4.6.0
  1131. * @private
  1132. *
  1133. * @param {object} data AJAX payload.
  1134. * @param {string} action The type of request to perform.
  1135. * @return {object} The AJAX payload with the appropriate callbacks.
  1136. */
  1137. wp.updates._addCallbacks = function( data, action ) {
  1138. if ( 'import' === pagenow && 'install-plugin' === action ) {
  1139. data.success = wp.updates.installImporterSuccess;
  1140. data.error = wp.updates.installImporterError;
  1141. }
  1142. return data;
  1143. };
  1144. /**
  1145. * Pulls available jobs from the queue and runs them.
  1146. *
  1147. * @since 4.2.0
  1148. * @since 4.6.0 Can handle multiple job types.
  1149. */
  1150. wp.updates.queueChecker = function() {
  1151. var job;
  1152. if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) {
  1153. return;
  1154. }
  1155. job = wp.updates.queue.shift();
  1156. // Handle a queue job.
  1157. switch ( job.action ) {
  1158. case 'install-plugin':
  1159. wp.updates.installPlugin( job.data );
  1160. break;
  1161. case 'update-plugin':
  1162. wp.updates.updatePlugin( job.data );
  1163. break;
  1164. case 'delete-plugin':
  1165. wp.updates.deletePlugin( job.data );
  1166. break;
  1167. case 'install-theme':
  1168. wp.updates.installTheme( job.data );
  1169. break;
  1170. case 'update-theme':
  1171. wp.updates.updateTheme( job.data );
  1172. break;
  1173. case 'delete-theme':
  1174. wp.updates.deleteTheme( job.data );
  1175. break;
  1176. default:
  1177. break;
  1178. }
  1179. };
  1180. /**
  1181. * Requests the users filesystem credentials if they aren't already known.
  1182. *
  1183. * @since 4.2.0
  1184. *
  1185. * @param {Event=} event Optional. Event interface.
  1186. */
  1187. wp.updates.requestFilesystemCredentials = function( event ) {
  1188. if ( false === wp.updates.filesystemCredentials.available ) {
  1189. /*
  1190. * After exiting the credentials request modal,
  1191. * return the focus to the element triggering the request.
  1192. */
  1193. if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) {
  1194. wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target );
  1195. }
  1196. wp.updates.ajaxLocked = true;
  1197. wp.updates.requestForCredentialsModalOpen();
  1198. }
  1199. };
  1200. /**
  1201. * Requests the users filesystem credentials if needed and there is no lock.
  1202. *
  1203. * @since 4.6.0
  1204. *
  1205. * @param {Event=} event Optional. Event interface.
  1206. */
  1207. wp.updates.maybeRequestFilesystemCredentials = function( event ) {
  1208. if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
  1209. wp.updates.requestFilesystemCredentials( event );
  1210. }
  1211. };
  1212. /**
  1213. * Keydown handler for the request for credentials modal.
  1214. *
  1215. * Closes the modal when the escape key is pressed and
  1216. * constrains keyboard navigation to inside the modal.
  1217. *
  1218. * @since 4.2.0
  1219. *
  1220. * @param {Event} event Event interface.
  1221. */
  1222. wp.updates.keydown = function( event ) {
  1223. if ( 27 === event.keyCode ) {
  1224. wp.updates.requestForCredentialsModalCancel();
  1225. } else if ( 9 === event.keyCode ) {
  1226. // #upgrade button must always be the last focus-able element in the dialog.
  1227. if ( 'upgrade' === event.target.id && ! event.shiftKey ) {
  1228. $( '#hostname' ).focus();
  1229. event.preventDefault();
  1230. } else if ( 'hostname' === event.target.id && event.shiftKey ) {
  1231. $( '#upgrade' ).focus();
  1232. event.preventDefault();
  1233. }
  1234. }
  1235. };
  1236. /**
  1237. * Opens the request for credentials modal.
  1238. *
  1239. * @since 4.2.0
  1240. */
  1241. wp.updates.requestForCredentialsModalOpen = function() {
  1242. var $modal = $( '#request-filesystem-credentials-dialog' );
  1243. $( 'body' ).addClass( 'modal-open' );
  1244. $modal.show();
  1245. $modal.find( 'input:enabled:first' ).focus();
  1246. $modal.on( 'keydown', wp.updates.keydown );
  1247. };
  1248. /**
  1249. * Closes the request for credentials modal.
  1250. *
  1251. * @since 4.2.0
  1252. */
  1253. wp.updates.requestForCredentialsModalClose = function() {
  1254. $( '#request-filesystem-credentials-dialog' ).hide();
  1255. $( 'body' ).removeClass( 'modal-open' );
  1256. if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) {
  1257. wp.updates.$elToReturnFocusToFromCredentialsModal.focus();
  1258. }
  1259. };
  1260. /**
  1261. * Takes care of the steps that need to happen when the modal is canceled out.
  1262. *
  1263. * @since 4.2.0
  1264. * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions.
  1265. */
  1266. wp.updates.requestForCredentialsModalCancel = function() {
  1267. // Not ajaxLocked and no queue means we already have cleared things up.
  1268. if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) {
  1269. return;
  1270. }
  1271. _.each( wp.updates.queue, function( job ) {
  1272. $document.trigger( 'credential-modal-cancel', job );
  1273. } );
  1274. // Remove the lock, and clear the queue.
  1275. wp.updates.ajaxLocked = false;
  1276. wp.updates.queue = [];
  1277. wp.updates.requestForCredentialsModalClose();
  1278. };
  1279. /**
  1280. * Displays an error message in the request for credentials form.
  1281. *
  1282. * @since 4.2.0
  1283. *
  1284. * @param {string} message Error message.
  1285. */
  1286. wp.updates.showErrorInCredentialsForm = function( message ) {
  1287. var $filesystemForm = $( '#request-filesystem-credentials-form' );
  1288. // Remove any existing error.
  1289. $filesystemForm.find( '.notice' ).remove();
  1290. $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' );
  1291. };
  1292. /**
  1293. * Handles credential errors and runs events that need to happen in that case.
  1294. *
  1295. * @since 4.2.0
  1296. *
  1297. * @param {object} response Ajax response.
  1298. * @param {string} action The type of request to perform.
  1299. */
  1300. wp.updates.credentialError = function( response, action ) {
  1301. // Restore callbacks.
  1302. response = wp.updates._addCallbacks( response, action );
  1303. wp.updates.queue.unshift( {
  1304. action: action,
  1305. /*
  1306. * Not cool that we're depending on response for this data.
  1307. * This would feel more whole in a view all tied together.
  1308. */
  1309. data: response
  1310. } );
  1311. wp.updates.filesystemCredentials.available = false;
  1312. wp.updates.showErrorInCredentialsForm( response.errorMessage );
  1313. wp.updates.requestFilesystemCredentials();
  1314. };
  1315. /**
  1316. * Handles credentials errors if it could not connect to the filesystem.
  1317. *
  1318. * @since 4.6.0
  1319. *
  1320. * @typedef {object} maybeHandleCredentialError
  1321. * @param {object} response Response from the server.
  1322. * @param {string} response.errorCode Error code for the error that occurred.
  1323. * @param {string} response.errorMessage The error that occurred.
  1324. * @param {string} action The type of request to perform.
  1325. * @returns {boolean} Whether there is an error that needs to be handled or not.
  1326. */
  1327. wp.updates.maybeHandleCredentialError = function( response, action ) {
  1328. if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) {
  1329. wp.updates.credentialError( response, action );
  1330. return true;
  1331. }
  1332. return false;
  1333. };
  1334. /**
  1335. * Validates an AJAX response to ensure it's a proper object.
  1336. *
  1337. * If the response deems to be invalid, an admin notice is being displayed.
  1338. *
  1339. * @param {(object|string)} response Response from the server.
  1340. * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected.
  1341. * @param {string=} response.statusText Optional. Status message corresponding to the status code.
  1342. * @param {string=} response.responseText Optional. Request response as text.
  1343. * @param {string} action Type of action the response is referring to. Can be 'delete',
  1344. * 'update' or 'install'.
  1345. */
  1346. wp.updates.isValidResponse = function( response, action ) {
  1347. var error = wp.updates.l10n.unknownError,
  1348. errorMessage;
  1349. // Make sure the response is a valid data object and not a Promise object.
  1350. if ( _.isObject( response ) && ! _.isFunction( response.always ) ) {
  1351. return true;
  1352. }
  1353. if ( _.isString( response ) && '-1' === response ) {
  1354. error = wp.updates.l10n.nonceError;
  1355. } else if ( _.isString( response ) ) {
  1356. error = response;
  1357. } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) {
  1358. error = wp.updates.l10n.connectionError;
  1359. } else if ( _.isString( response.responseText ) && '' !== response.responseText ) {
  1360. error = response.responseText;
  1361. } else if ( _.isString( response.statusText ) ) {
  1362. error = response.statusText;
  1363. }
  1364. switch ( action ) {
  1365. case 'update':
  1366. errorMessage = wp.updates.l10n.updateFailed;
  1367. break;
  1368. case 'install':
  1369. errorMessage = wp.updates.l10n.installFailed;
  1370. break;
  1371. case 'delete':
  1372. errorMessage = wp.updates.l10n.deleteFailed;
  1373. break;
  1374. }
  1375. // Messages are escaped, remove HTML tags to make them more readable.
  1376. error = error.replace( /<[\/a-z][^<>]*>/gi, '' );
  1377. errorMessage = errorMessage.replace( '%s', error );
  1378. // Add admin notice.
  1379. wp.updates.addAdminNotice( {
  1380. id: 'unknown_error',
  1381. className: 'notice-error is-dismissible',
  1382. message: _.escape( errorMessage )
  1383. } );
  1384. // Remove the lock, and clear the queue.
  1385. wp.updates.ajaxLocked = false;
  1386. wp.updates.queue = [];
  1387. // Change buttons of all running updates.
  1388. $( '.button.updating-message' )
  1389. .removeClass( 'updating-message' )
  1390. .removeAttr( 'aria-label' )
  1391. .prop( 'disabled', true )
  1392. .text( wp.updates.l10n.updateFailedShort );
  1393. $( '.updating-message:not(.button):not(.thickbox)' )
  1394. .removeClass( 'updating-message notice-warning' )
  1395. .addClass( 'notice-error' )
  1396. .find( 'p' )
  1397. .removeAttr( 'aria-label' )
  1398. .text( errorMessage );
  1399. wp.a11y.speak( errorMessage, 'assertive' );
  1400. return false;
  1401. };
  1402. /**
  1403. * Potentially adds an AYS to a user attempting to leave the page.
  1404. *
  1405. * If an update is on-going and a user attempts to leave the page,
  1406. * opens an "Are you sure?" alert.
  1407. *
  1408. * @since 4.2.0
  1409. */
  1410. wp.updates.beforeunload = function() {
  1411. if ( wp.updates.ajaxLocked ) {
  1412. return wp.updates.l10n.beforeunload;
  1413. }
  1414. };
  1415. $( function() {
  1416. var $pluginFilter = $( '#plugin-filter' ),
  1417. $bulkActionForm = $( '#bulk-action-form' ),
  1418. $filesystemForm = $( '#request-filesystem-credentials-form' ),
  1419. $filesystemModal = $( '#request-filesystem-credentials-dialog' ),
  1420. $pluginSearch = $( '.plugins-php .wp-filter-search' ),
  1421. $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' );
  1422. settings = _.extend( settings, window._wpUpdatesItemCounts || {} );
  1423. if ( settings.totals ) {
  1424. wp.updates.refreshCount();
  1425. }
  1426. /*
  1427. * Whether a user needs to submit filesystem credentials.
  1428. *
  1429. * This is based on whether the form was output on the page server-side.
  1430. *
  1431. * @see {wp_print_request_filesystem_credentials_modal() in PHP}
  1432. */
  1433. wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0;
  1434. /**
  1435. * File system credentials form submit noop-er / handler.
  1436. *
  1437. * @since 4.2.0
  1438. */
  1439. $filesystemModal.on( 'submit', 'form', function( event ) {
  1440. event.preventDefault();
  1441. // Persist the credentials input by the user for the duration of the page load.
  1442. wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val();
  1443. wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val();
  1444. wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val();
  1445. wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val();
  1446. wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val();
  1447. wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val();
  1448. wp.updates.filesystemCredentials.available = true;
  1449. // Unlock and invoke the queue.
  1450. wp.updates.ajaxLocked = false;
  1451. wp.updates.queueChecker();
  1452. wp.updates.requestForCredentialsModalClose();
  1453. } );
  1454. /**
  1455. * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal.
  1456. *
  1457. * @since 4.2.0
  1458. */
  1459. $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel );
  1460. /**
  1461. * Hide SSH fields when not selected.
  1462. *
  1463. * @since 4.2.0
  1464. */
  1465. $filesystemForm.on( 'change', 'input[name="connection_type"]', function() {
  1466. $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) );
  1467. } ).change();
  1468. /**
  1469. * Handles events after the credential modal was closed.
  1470. *
  1471. * @since 4.6.0
  1472. *
  1473. * @param {Event} event Event interface.
  1474. * @param {string} job The install/update.delete request.
  1475. */
  1476. $document.on( 'credential-modal-cancel', function( event, job ) {
  1477. var $updatingMessage = $( '.updating-message' ),
  1478. $message, originalText;
  1479. if ( 'import' === pagenow ) {
  1480. $updatingMessage.removeClass( 'updating-message' );
  1481. } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) {
  1482. if ( 'update-plugin' === job.action ) {
  1483. $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' );
  1484. } else if ( 'delete-plugin' === job.action ) {
  1485. $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' );
  1486. }
  1487. } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) {
  1488. if ( 'update-theme' === job.action ) {
  1489. $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' );
  1490. } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) {
  1491. $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' );
  1492. } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) {
  1493. $message = $( '.theme-actions .delete-theme' );
  1494. }
  1495. } else {
  1496. $message = $updatingMessage;
  1497. }
  1498. if ( $message && $message.hasClass( 'updating-message' ) ) {
  1499. originalText = $message.data( 'originaltext' );
  1500. if ( 'undefined' === typeof originalText ) {
  1501. originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) );
  1502. }
  1503. $message
  1504. .removeClass( 'updating-message' )
  1505. .html( originalText );
  1506. if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) {
  1507. if ( 'update-plugin' === job.action ) {
  1508. $message.attr( 'aria-label', wp.updates.l10n.updateNowLabel.replace( '%s', $message.data( 'name' ) ) );
  1509. } else if ( 'install-plugin' === job.action ) {
  1510. $message.attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', $message.data( 'name' ) ) );
  1511. }
  1512. }
  1513. }
  1514. wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
  1515. } );
  1516. /**
  1517. * Click handler for plugin updates in List Table view.
  1518. *
  1519. * @since 4.2.0
  1520. *
  1521. * @param {Event} event Event interface.
  1522. */
  1523. $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) {
  1524. var $message = $( event.target ),
  1525. $pluginRow = $message.parents( 'tr' );
  1526. event.preventDefault();
  1527. if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
  1528. return;
  1529. }
  1530. wp.updates.maybeRequestFilesystemCredentials( event );
  1531. // Return the user to the input box of the plugin's table row after closing the modal.
  1532. wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' );
  1533. wp.updates.updatePlugin( {
  1534. plugin: $pluginRow.data( 'plugin' ),
  1535. slug: $pluginRow.data( 'slug' )
  1536. } );
  1537. } );
  1538. /**
  1539. * Click handler for plugin updates in plugin install view.
  1540. *
  1541. * @since 4.2.0
  1542. *
  1543. * @param {Event} event Event interface.
  1544. */
  1545. $pluginFilter.on( 'click', '.update-now', function( event ) {
  1546. var $button = $( event.target );
  1547. event.preventDefault();
  1548. if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
  1549. return;
  1550. }
  1551. wp.updates.maybeRequestFilesystemCredentials( event );
  1552. wp.updates.updatePlugin( {
  1553. plugin: $button.data( 'plugin' ),
  1554. slug: $button.data( 'slug' )
  1555. } );
  1556. } );
  1557. /**
  1558. * Click handler for plugin installs in plugin install view.
  1559. *
  1560. * @since 4.6.0
  1561. *
  1562. * @param {Event} event Event interface.
  1563. */
  1564. $pluginFilter.on( 'click', '.install-now', function( event ) {
  1565. var $button = $( event.target );
  1566. event.preventDefault();
  1567. if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) {
  1568. return;
  1569. }
  1570. if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
  1571. wp.updates.requestFilesystemCredentials( event );
  1572. $document.on( 'credential-modal-cancel', function() {
  1573. var $message = $( '.install-now.updating-message' );
  1574. $message
  1575. .removeClass( 'updating-message' )
  1576. .text( wp.updates.l10n.installNow );
  1577. wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
  1578. } );
  1579. }
  1580. wp.updates.installPlugin( {
  1581. slug: $button.data( 'slug' )
  1582. } );
  1583. } );
  1584. /**
  1585. * Click handler for importer plugins installs in the Import screen.
  1586. *
  1587. * @since 4.6.0
  1588. *
  1589. * @param {Event} event Event interface.
  1590. */
  1591. $document.on( 'click', '.importer-item .install-now', function( event ) {
  1592. var $button = $( event.target ),
  1593. pluginName = $( this ).data( 'name' );
  1594. event.preventDefault();
  1595. if ( $button.hasClass( 'updating-message' ) ) {
  1596. return;
  1597. }
  1598. if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) {
  1599. wp.updates.requestFilesystemCredentials( event );
  1600. $document.on( 'credential-modal-cancel', function() {
  1601. $button
  1602. .removeClass( 'updating-message' )
  1603. .text( wp.updates.l10n.installNow )
  1604. .attr( 'aria-label', wp.updates.l10n.installNowLabel.replace( '%s', pluginName ) );
  1605. wp.a11y.speak( wp.updates.l10n.updateCancel, 'polite' );
  1606. } );
  1607. }
  1608. wp.updates.installPlugin( {
  1609. slug: $button.data( 'slug' ),
  1610. pagenow: pagenow,
  1611. success: wp.updates.installImporterSuccess,
  1612. error: wp.updates.installImporterError
  1613. } );
  1614. } );
  1615. /**
  1616. * Click handler for plugin deletions.
  1617. *
  1618. * @since 4.6.0
  1619. *
  1620. * @param {Event} event Event interface.
  1621. */
  1622. $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) {
  1623. var $pluginRow = $( event.target ).parents( 'tr' );
  1624. event.preventDefault();
  1625. if ( ! window.confirm( wp.updates.l10n.aysDeleteUninstall.replace( '%s', $pluginRow.find( '.plugin-title strong' ).text() ) ) ) {
  1626. return;
  1627. }
  1628. wp.updates.maybeRequestFilesystemCredentials( event );
  1629. wp.updates.deletePlugin( {
  1630. plugin: $pluginRow.data( 'plugin' ),
  1631. slug: $pluginRow.data( 'slug' )
  1632. } );
  1633. } );
  1634. /**
  1635. * Click handler for theme updates.
  1636. *
  1637. * @since 4.6.0
  1638. *
  1639. * @param {Event} event Event interface.
  1640. */
  1641. $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) {
  1642. var $message = $( event.target ),
  1643. $themeRow = $message.parents( 'tr' );
  1644. event.preventDefault();
  1645. if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) {
  1646. return;
  1647. }
  1648. wp.updates.maybeRequestFilesystemCredentials( event );
  1649. // Return the user to the input box of the theme's table row after closing the modal.
  1650. wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' );
  1651. wp.updates.updateTheme( {
  1652. slug: $themeRow.data( 'slug' )
  1653. } );
  1654. } );
  1655. /**
  1656. * Click handler for theme deletions.
  1657. *
  1658. * @since 4.6.0
  1659. *
  1660. * @param {Event} event Event interface.
  1661. */
  1662. $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) {
  1663. var $themeRow = $( event.target ).parents( 'tr' );
  1664. event.preventDefault();
  1665. if ( ! window.confirm( wp.updates.l10n.aysDelete.replace( '%s', $themeRow.find( '.theme-title strong' ).text() ) ) ) {
  1666. return;
  1667. }
  1668. wp.updates.maybeRequestFilesystemCredentials( event );
  1669. wp.updates.deleteTheme( {
  1670. slug: $themeRow.data( 'slug' )
  1671. } );
  1672. } );
  1673. /**
  1674. * Bulk action handler for plugins and themes.
  1675. *
  1676. * Handles both deletions and updates.
  1677. *
  1678. * @since 4.6.0
  1679. *
  1680. * @param {Event} event Event interface.
  1681. */
  1682. $bulkActionForm.on( 'click', '[type="submit"]', function( event ) {
  1683. var bulkAction = $( event.target ).siblings( 'select' ).val(),
  1684. itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ),
  1685. success = 0,
  1686. error = 0,
  1687. errorMessages = [],
  1688. type, action;
  1689. // Determine which type of item we're dealing with.
  1690. switch ( pagenow ) {
  1691. case 'plugins':
  1692. case 'plugins-network':
  1693. type = 'plugin';
  1694. break;
  1695. case 'themes-network':
  1696. type = 'theme';
  1697. break;
  1698. default:
  1699. return;
  1700. }
  1701. // Bail if there were no items selected.
  1702. if ( ! itemsSelected.length ) {
  1703. event.preventDefault();
  1704. $( 'html, body' ).animate( { scrollTop: 0 } );
  1705. return wp.updates.addAdminNotice( {
  1706. id: 'no-items-selected',
  1707. className: 'notice-error is-dismissible',
  1708. message: wp.updates.l10n.noItemsSelected
  1709. } );
  1710. }
  1711. // Determine the type of request we're dealing with.
  1712. switch ( bulkAction ) {
  1713. case 'update-selected':
  1714. action = bulkAction.replace( 'selected', type );
  1715. break;
  1716. case 'delete-selected':
  1717. if ( ! window.confirm( 'plugin' === type ? wp.updates.l10n.aysBulkDelete : wp.updates.l10n.aysBulkDeleteThemes ) ) {
  1718. event.preventDefault();
  1719. return;
  1720. }
  1721. action = bulkAction.replace( 'selected', type );
  1722. break;
  1723. default:
  1724. return;
  1725. }
  1726. wp.updates.maybeRequestFilesystemCredentials( event );
  1727. event.preventDefault();
  1728. // Un-check the bulk checkboxes.
  1729. $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false );
  1730. $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected );
  1731. // Find all the checkboxes which have been checked.
  1732. itemsSelected.each( function( index, element ) {
  1733. var $checkbox = $( element ),
  1734. $itemRow = $checkbox.parents( 'tr' );
  1735. // Only add update-able items to the update queue.
  1736. if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) {
  1737. // Un-check the box.
  1738. $checkbox.prop( 'checked', false );
  1739. return;
  1740. }
  1741. // Add it to the queue.
  1742. wp.updates.queue.push( {
  1743. action: action,
  1744. data: {
  1745. plugin: $itemRow.data( 'plugin' ),
  1746. slug: $itemRow.data( 'slug' )
  1747. }
  1748. } );
  1749. } );
  1750. // Display bulk notification for updates of any kind.
  1751. $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) {
  1752. var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
  1753. $bulkActionNotice, itemName;
  1754. if ( 'wp-' + response.update + '-update-success' === event.type ) {
  1755. success++;
  1756. } else {
  1757. itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text();
  1758. error++;
  1759. errorMessages.push( itemName + ': ' + response.errorMessage );
  1760. }
  1761. $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false );
  1762. wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' );
  1763. wp.updates.addAdminNotice( {
  1764. id: 'bulk-action-notice',
  1765. className: 'bulk-action-notice',
  1766. successes: success,
  1767. errors: error,
  1768. errorMessages: errorMessages,
  1769. type: response.update
  1770. } );
  1771. $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
  1772. // $( this ) is the clicked button, no need to get it again.
  1773. $( this )
  1774. .toggleClass( 'bulk-action-errors-collapsed' )
  1775. .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
  1776. // Show the errors list.
  1777. $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
  1778. } );
  1779. if ( error > 0 && ! wp.updates.queue.length ) {
  1780. $( 'html, body' ).animate( { scrollTop: 0 } );
  1781. }
  1782. } );
  1783. // Reset admin notice template after #bulk-action-notice was added.
  1784. $document.on( 'wp-updates-notice-added', function() {
  1785. wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
  1786. } );
  1787. // Check the queue, now that the event handlers have been added.
  1788. wp.updates.queueChecker();
  1789. } );
  1790. if ( $pluginInstallSearch.length ) {
  1791. $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' );
  1792. }
  1793. /**
  1794. * Handles changes to the plugin search box on the new-plugin page,
  1795. * searching the repository dynamically.
  1796. *
  1797. * @since 4.6.0
  1798. */
  1799. $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) {
  1800. var $searchTab = $( '.plugin-install-search' ), data, searchLocation;
  1801. data = {
  1802. _ajax_nonce: wp.updates.ajaxNonce,
  1803. s: event.target.value,
  1804. tab: 'search',
  1805. type: $( '#typeselector' ).val(),
  1806. pagenow: pagenow
  1807. };
  1808. searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) );
  1809. // Clear on escape.
  1810. if ( 'keyup' === event.type && 27 === event.which ) {
  1811. event.target.value = '';
  1812. }
  1813. if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) {
  1814. return;
  1815. } else {
  1816. $pluginFilter.empty();
  1817. wp.updates.searchTerm = data.s;
  1818. }
  1819. if ( window.history && window.history.replaceState ) {
  1820. window.history.replaceState( null, '', searchLocation );
  1821. }
  1822. if ( ! $searchTab.length ) {
  1823. $searchTab = $( '<li class="plugin-install-search" />' )
  1824. .append( $( '<a />', {
  1825. 'class': 'current',
  1826. 'href': searchLocation,
  1827. 'text': wp.updates.l10n.searchResultsLabel
  1828. } ) );
  1829. $( '.wp-filter .filter-links .current' )
  1830. .removeClass( 'current' )
  1831. .parents( '.filter-links' )
  1832. .prepend( $searchTab );
  1833. $pluginFilter.prev( 'p' ).remove();
  1834. $( '.plugins-popular-tags-wrapper' ).remove();
  1835. }
  1836. if ( 'undefined' !== typeof wp.updates.searchRequest ) {
  1837. wp.updates.searchRequest.abort();
  1838. }
  1839. $( 'body' ).addClass( 'loading-content' );
  1840. wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) {
  1841. $( 'body' ).removeClass( 'loading-content' );
  1842. $pluginFilter.append( response.items );
  1843. delete wp.updates.searchRequest;
  1844. if ( 0 === response.count ) {
  1845. wp.a11y.speak( wp.updates.l10n.noPluginsFound );
  1846. } else {
  1847. wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
  1848. }
  1849. } );
  1850. }, 500 ) );
  1851. if ( $pluginSearch.length ) {
  1852. $pluginSearch.attr( 'aria-describedby', 'live-search-desc' );
  1853. }
  1854. /**
  1855. * Handles changes to the plugin search box on the Installed Plugins screen,
  1856. * searching the plugin list dynamically.
  1857. *
  1858. * @since 4.6.0
  1859. */
  1860. $pluginSearch.on( 'keyup input', _.debounce( function( event ) {
  1861. var data = {
  1862. _ajax_nonce: wp.updates.ajaxNonce,
  1863. s: event.target.value,
  1864. pagenow: pagenow,
  1865. plugin_status: 'all'
  1866. },
  1867. queryArgs;
  1868. // Clear on escape.
  1869. if ( 'keyup' === event.type && 27 === event.which ) {
  1870. event.target.value = '';
  1871. }
  1872. if ( wp.updates.searchTerm === data.s ) {
  1873. return;
  1874. } else {
  1875. wp.updates.searchTerm = data.s;
  1876. }
  1877. queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) {
  1878. if ( item ) return item.split( '=' );
  1879. } ) ) );
  1880. data.plugin_status = queryArgs.plugin_status || 'all';
  1881. if ( window.history && window.history.replaceState ) {
  1882. window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status );
  1883. }
  1884. if ( 'undefined' !== typeof wp.updates.searchRequest ) {
  1885. wp.updates.searchRequest.abort();
  1886. }
  1887. $bulkActionForm.empty();
  1888. $( 'body' ).addClass( 'loading-content' );
  1889. $( '.subsubsub .current' ).removeClass( 'current' );
  1890. wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) {
  1891. // Can we just ditch this whole subtitle business?
  1892. var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( wp.updates.l10n.searchResults.replace( '%s', _.escape( data.s ) ) ),
  1893. $oldSubTitle = $( '.wrap .subtitle' );
  1894. if ( ! data.s.length ) {
  1895. $oldSubTitle.remove();
  1896. $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' );
  1897. } else if ( $oldSubTitle.length ) {
  1898. $oldSubTitle.replaceWith( $subTitle );
  1899. } else {
  1900. $( '.wrap h1' ).append( $subTitle );
  1901. }
  1902. $( 'body' ).removeClass( 'loading-content' );
  1903. $bulkActionForm.append( response.items );
  1904. delete wp.updates.searchRequest;
  1905. if ( 0 === response.count ) {
  1906. wp.a11y.speak( wp.updates.l10n.noPluginsFound );
  1907. } else {
  1908. wp.a11y.speak( wp.updates.l10n.pluginsFound.replace( '%d', response.count ) );
  1909. }
  1910. } );
  1911. }, 500 ) );
  1912. /**
  1913. * Trigger a search event when the search form gets submitted.
  1914. *
  1915. * @since 4.6.0
  1916. */
  1917. $document.on( 'submit', '.search-plugins', function( event ) {
  1918. event.preventDefault();
  1919. $( 'input.wp-filter-search' ).trigger( 'input' );
  1920. } );
  1921. /**
  1922. * Trigger a search event when the search type gets changed.
  1923. *
  1924. * @since 4.6.0
  1925. */
  1926. $( '#typeselector' ).on( 'change', function() {
  1927. var $search = $( 'input[name="s"]' );
  1928. if ( $search.val().length ) {
  1929. $search.trigger( 'input', 'typechange' );
  1930. }
  1931. } );
  1932. /**
  1933. * Click handler for updating a plugin from the details modal on `plugin-install.php`.
  1934. *
  1935. * @since 4.2.0
  1936. *
  1937. * @param {Event} event Event interface.
  1938. */
  1939. $( '#plugin_update_from_iframe' ).on( 'click', function( event ) {
  1940. var target = window.parent === window ? null : window.parent,
  1941. update;
  1942. $.support.postMessage = !! window.postMessage;
  1943. if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) {
  1944. return;
  1945. }
  1946. event.preventDefault();
  1947. update = {
  1948. action: 'update-plugin',
  1949. data: {
  1950. plugin: $( this ).data( 'plugin' ),
  1951. slug: $( this ).data( 'slug' )
  1952. }
  1953. };
  1954. target.postMessage( JSON.stringify( update ), window.location.origin );
  1955. } );
  1956. /**
  1957. * Click handler for installing a plugin from the details modal on `plugin-install.php`.
  1958. *
  1959. * @since 4.6.0
  1960. *
  1961. * @param {Event} event Event interface.
  1962. */
  1963. $( '#plugin_install_from_iframe' ).on( 'click', function( event ) {
  1964. var target = window.parent === window ? null : window.parent,
  1965. install;
  1966. $.support.postMessage = !! window.postMessage;
  1967. if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) {
  1968. return;
  1969. }
  1970. event.preventDefault();
  1971. install = {
  1972. action: 'install-plugin',
  1973. data: {
  1974. slug: $( this ).data( 'slug' )
  1975. }
  1976. };
  1977. target.postMessage( JSON.stringify( install ), window.location.origin );
  1978. } );
  1979. /**
  1980. * Handles postMessage events.
  1981. *
  1982. * @since 4.2.0
  1983. * @since 4.6.0 Switched `update-plugin` action to use the queue.
  1984. *
  1985. * @param {Event} event Event interface.
  1986. */
  1987. $( window ).on( 'message', function( event ) {
  1988. var originalEvent = event.originalEvent,
  1989. expectedOrigin = document.location.protocol + '//' + document.location.hostname,
  1990. message;
  1991. if ( originalEvent.origin !== expectedOrigin ) {
  1992. return;
  1993. }
  1994. try {
  1995. message = $.parseJSON( originalEvent.data );
  1996. } catch ( e ) {
  1997. return;
  1998. }
  1999. if ( 'undefined' === typeof message.action ) {
  2000. return;
  2001. }
  2002. switch ( message.action ) {
  2003. // Called from `wp-admin/includes/class-wp-upgrader-skins.php`.
  2004. case 'decrementUpdateCount':
  2005. /** @property {string} message.upgradeType */
  2006. wp.updates.decrementCount( message.upgradeType );
  2007. break;
  2008. case 'install-plugin':
  2009. case 'update-plugin':
  2010. /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
  2011. window.tb_remove();
  2012. /* jscs:enable */
  2013. message.data = wp.updates._addCallbacks( message.data, message.action );
  2014. wp.updates.queue.push( message );
  2015. wp.updates.queueChecker();
  2016. break;
  2017. }
  2018. } );
  2019. /**
  2020. * Adds a callback to display a warning before leaving the page.
  2021. *
  2022. * @since 4.2.0
  2023. */
  2024. $( window ).on( 'beforeunload', wp.updates.beforeunload );
  2025. } );
  2026. })( jQuery, window.wp, window._wpUpdatesSettings );