updates.js 93 KB

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