説明なし

updates.js 92KB

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