http-file-upload.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. /*global mock, converse */
  2. const Strophe = converse.env.Strophe;
  3. const $iq = converse.env.$iq;
  4. const _ = converse.env._;
  5. const sizzle = converse.env.sizzle;
  6. const u = converse.env.utils;
  7. describe("XEP-0363: HTTP File Upload", function () {
  8. describe("Discovering support", function () {
  9. it("is done automatically",
  10. mock.initConverse(
  11. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  12. async function (done, _converse) {
  13. const IQ_stanzas = _converse.connection.IQ_stanzas;
  14. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
  15. let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
  16. let stanza = await u.waitUntil(() => IQ_stanzas.find(iq => iq.querySelector(selector)), 1000);
  17. /* <iq type='result'
  18. * from='plays.shakespeare.lit'
  19. * to='romeo@montague.net/orchard'
  20. * id='info1'>
  21. * <query xmlns='http://jabber.org/protocol/disco#info'>
  22. * <identity
  23. * category='server'
  24. * type='im'/>
  25. * <feature var='http://jabber.org/protocol/disco#info'/>
  26. * <feature var='http://jabber.org/protocol/disco#items'/>
  27. * </query>
  28. * </iq>
  29. */
  30. stanza = $iq({
  31. 'type': 'result',
  32. 'from': 'montague.lit',
  33. 'to': 'romeo@montague.lit/orchard',
  34. 'id': stanza.getAttribute('id'),
  35. }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
  36. .c('identity', {
  37. 'category': 'server',
  38. 'type': 'im'}).up()
  39. .c('feature', {
  40. 'var': 'http://jabber.org/protocol/disco#info'}).up()
  41. .c('feature', {
  42. 'var': 'http://jabber.org/protocol/disco#items'});
  43. _converse.connection._dataRecv(mock.createRequest(stanza));
  44. let entities = await _converse.api.disco.entities.get();
  45. expect(entities.length).toBe(2);
  46. expect(entities.pluck('jid').includes('montague.lit')).toBe(true);
  47. expect(entities.pluck('jid').includes('romeo@montague.lit')).toBe(true);
  48. expect(entities.get(_converse.domain).features.length).toBe(2);
  49. expect(entities.get(_converse.domain).identities.length).toBe(1);
  50. // Converse.js sees that the entity has a disco#items feature,
  51. // so it will make a query for it.
  52. selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]';
  53. await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length, 1000);
  54. /* <iq from='montague.tld'
  55. * id='step_01'
  56. * to='romeo@montague.tld/garden'
  57. * type='result'>
  58. * <query xmlns='http://jabber.org/protocol/disco#items'>
  59. * <item jid='upload.montague.tld' name='HTTP File Upload' />
  60. * <item jid='conference.montague.tld' name='Chatroom Service' />
  61. * </query>
  62. * </iq>
  63. */
  64. selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]';
  65. stanza = IQ_stanzas.find(iq => iq.querySelector(selector), 500);
  66. stanza = $iq({
  67. 'type': 'result',
  68. 'from': 'montague.lit',
  69. 'to': 'romeo@montague.lit/orchard',
  70. 'id': stanza.getAttribute('id'),
  71. }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
  72. .c('item', {
  73. 'jid': 'upload.montague.lit',
  74. 'name': 'HTTP File Upload'});
  75. _converse.connection._dataRecv(mock.createRequest(stanza));
  76. _converse.api.disco.entities.get().then(entities => {
  77. expect(entities.length).toBe(2);
  78. expect(entities.get('montague.lit').items.length).toBe(1);
  79. // Converse.js sees that the entity has a disco#info feature, so it will make a query for it.
  80. const selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
  81. return u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length > 0);
  82. });
  83. selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]';
  84. stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop(), 1000);
  85. expect(Strophe.serialize(stanza)).toBe(
  86. `<iq from="romeo@montague.lit/orchard" id="`+stanza.getAttribute('id')+`" to="upload.montague.lit" type="get" xmlns="jabber:client">`+
  87. `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
  88. `</iq>`);
  89. // Upload service responds and reports a maximum file size of 5MiB
  90. /* <iq from='upload.montague.tld'
  91. * id='step_02'
  92. * to='romeo@montague.tld/garden'
  93. * type='result'>
  94. * <query xmlns='http://jabber.org/protocol/disco#info'>
  95. * <identity category='store'
  96. * type='file'
  97. * name='HTTP File Upload' />
  98. * <feature var='urn:xmpp:http:upload:0' />
  99. * <x type='result' xmlns='jabber:x:data'>
  100. * <field var='FORM_TYPE' type='hidden'>
  101. * <value>urn:xmpp:http:upload:0</value>
  102. * </field>
  103. * <field var='max-file-size'>
  104. * <value>5242880</value>
  105. * </field>
  106. * </x>
  107. * </query>
  108. * </iq>
  109. */
  110. stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': stanza.getAttribute('id'), 'from': 'upload.montague.lit'})
  111. .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
  112. .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
  113. .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
  114. .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
  115. .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
  116. .c('value').t('urn:xmpp:http:upload:0').up().up()
  117. .c('field', {'var':'max-file-size'})
  118. .c('value').t('5242880');
  119. _converse.connection._dataRecv(mock.createRequest(stanza));
  120. entities = await _converse.api.disco.entities.get();
  121. expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1);
  122. const supported = await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
  123. expect(supported).toBe(true);
  124. const features = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain);
  125. expect(features.length).toBe(1);
  126. expect(features[0].get('jid')).toBe('upload.montague.lit');
  127. expect(features[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1);
  128. done();
  129. }));
  130. });
  131. describe("When not supported", function () {
  132. describe("A file upload toolbar button", function () {
  133. it("does not appear in private chats",
  134. mock.initConverse([], {}, async function (done, _converse) {
  135. await mock.waitForRoster(_converse, 'current', 3);
  136. mock.openControlBox(_converse);
  137. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  138. await mock.openChatBoxFor(_converse, contact_jid);
  139. await mock.waitUntilDiscoConfirmed(
  140. _converse, _converse.domain,
  141. [{'category': 'server', 'type':'IM'}],
  142. ['http://jabber.org/protocol/disco#items'], [], 'info');
  143. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
  144. const view = _converse.chatboxviews.get(contact_jid);
  145. expect(view.el.querySelector('.chat-toolbar .fileupload')).toBe(null);
  146. done();
  147. }));
  148. it("does not appear in MUC chats", mock.initConverse(
  149. ['rosterGroupsFetched'], {},
  150. async (done, _converse) => {
  151. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
  152. mock.waitUntilDiscoConfirmed(
  153. _converse, _converse.domain,
  154. [{'category': 'server', 'type':'IM'}],
  155. ['http://jabber.org/protocol/disco#items'], [], 'info');
  156. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items');
  157. const view = _converse.chatboxviews.get('lounge@montague.lit');
  158. await u.waitUntil(() => view.el.querySelector('.chat-toolbar .fileupload') === null);
  159. expect(1).toBe(1);
  160. done();
  161. }));
  162. });
  163. });
  164. describe("When supported", function () {
  165. describe("A file upload toolbar button", function () {
  166. it("appears in private chats", mock.initConverse(async (done, _converse) => {
  167. await mock.waitUntilDiscoConfirmed(
  168. _converse, _converse.domain,
  169. [{'category': 'server', 'type':'IM'}],
  170. ['http://jabber.org/protocol/disco#items'], [], 'info');
  171. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items')
  172. await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []);
  173. await mock.waitForRoster(_converse, 'current', 3);
  174. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  175. await mock.openChatBoxFor(_converse, contact_jid);
  176. const view = _converse.chatboxviews.get(contact_jid);
  177. const el = await u.waitUntil(() => view.el.querySelector('.chat-toolbar .fileupload'));
  178. expect(el).not.toEqual(null);
  179. done();
  180. }));
  181. it("appears in MUC chats", mock.initConverse(
  182. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  183. async (done, _converse) => {
  184. await mock.waitUntilDiscoConfirmed(
  185. _converse, _converse.domain,
  186. [{'category': 'server', 'type':'IM'}],
  187. ['http://jabber.org/protocol/disco#items'], [], 'info');
  188. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items');
  189. await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []);
  190. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
  191. await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').el.querySelector('.fileupload'));
  192. const view = _converse.chatboxviews.get('lounge@montague.lit');
  193. expect(view.el.querySelector('.chat-toolbar .fileupload')).not.toBe(null);
  194. done();
  195. }));
  196. describe("when clicked and a file chosen", function () {
  197. it("is uploaded and sent out", mock.initConverse(
  198. ['rosterGroupsFetched', 'chatBoxesFetched'], {} ,async (done, _converse) => {
  199. const base_url = 'https://conversejs.org';
  200. await mock.waitUntilDiscoConfirmed(
  201. _converse, _converse.domain,
  202. [{'category': 'server', 'type':'IM'}],
  203. ['http://jabber.org/protocol/disco#items'], [], 'info');
  204. const send_backup = XMLHttpRequest.prototype.send;
  205. const IQ_stanzas = _converse.connection.IQ_stanzas;
  206. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
  207. await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
  208. await mock.waitForRoster(_converse, 'current');
  209. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  210. await mock.openChatBoxFor(_converse, contact_jid);
  211. const view = _converse.chatboxviews.get(contact_jid);
  212. const file = {
  213. 'type': 'image/jpeg',
  214. 'size': '23456' ,
  215. 'lastModifiedDate': "",
  216. 'name': "my-juliet.jpg"
  217. };
  218. view.model.sendFiles([file]);
  219. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  220. await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
  221. const iq = IQ_stanzas.pop();
  222. expect(Strophe.serialize(iq)).toBe(
  223. `<iq from="romeo@montague.lit/orchard" `+
  224. `id="${iq.getAttribute("id")}" `+
  225. `to="upload.montague.tld" `+
  226. `type="get" `+
  227. `xmlns="jabber:client">`+
  228. `<request `+
  229. `content-type="image/jpeg" `+
  230. `filename="my-juliet.jpg" `+
  231. `size="23456" `+
  232. `xmlns="urn:xmpp:http:upload:0"/>`+
  233. `</iq>`);
  234. const message = base_url+"/logo/conversejs-filled.svg";
  235. const stanza = u.toStanza(`
  236. <iq from="upload.montague.tld"
  237. id="${iq.getAttribute("id")}"
  238. to="romeo@montague.lit/orchard"
  239. type="result">
  240. <slot xmlns="urn:xmpp:http:upload:0">
  241. <put url="https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg">
  242. <header name="Authorization">Basic Base64String==</header>
  243. <header name="Cookie">foo=bar; user=romeo</header>
  244. </put>
  245. <get url="${message}" />
  246. </slot>
  247. </iq>`);
  248. spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
  249. const message = view.model.messages.at(0);
  250. expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
  251. message.set('progress', 0.5);
  252. u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
  253. .then(() => {
  254. message.set('progress', 1);
  255. u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1')
  256. }).then(() => {
  257. message.save({
  258. 'upload': _converse.SUCCESS,
  259. 'oob_url': message.get('get'),
  260. 'message': message.get('get')
  261. });
  262. return new Promise(resolve => view.model.messages.once('rendered', resolve));
  263. });
  264. });
  265. let sent_stanza;
  266. spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
  267. _converse.connection._dataRecv(mock.createRequest(stanza));
  268. await u.waitUntil(() => sent_stanza, 1000);
  269. expect(sent_stanza.toLocaleString()).toBe(
  270. `<message from="romeo@montague.lit/orchard" `+
  271. `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
  272. `to="lady.montague@montague.lit" `+
  273. `type="chat" `+
  274. `xmlns="jabber:client">`+
  275. `<body>${message}</body>`+
  276. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  277. `<request xmlns="urn:xmpp:receipts"/>`+
  278. `<x xmlns="jabber:x:oob">`+
  279. `<url>${message}</url>`+
  280. `</x>`+
  281. `<origin-id id="${sent_stanza.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  282. `</message>`);
  283. const img_link_el = await u.waitUntil(() => view.el.querySelector('converse-chat-message-body .chat-image__link'), 1000);
  284. // Check that the image renders
  285. expect(img_link_el.outerHTML.replace(/<!---->/g, '').trim()).toEqual(
  286. `<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
  287. `<img class="chat-image img-thumbnail" src="${base_url}/logo/conversejs-filled.svg"></a>`);
  288. expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
  289. `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
  290. `Download image file "conversejs-filled.svg"</a>`);
  291. XMLHttpRequest.prototype.send = send_backup;
  292. done();
  293. }));
  294. it("is uploaded and sent out from a groupchat", mock.initConverse(async (done, _converse) => {
  295. const base_url = 'https://conversejs.org';
  296. await mock.waitUntilDiscoConfirmed(
  297. _converse, _converse.domain,
  298. [{'category': 'server', 'type':'IM'}],
  299. ['http://jabber.org/protocol/disco#items'], [], 'info');
  300. const send_backup = XMLHttpRequest.prototype.send;
  301. const IQ_stanzas = _converse.connection.IQ_stanzas;
  302. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
  303. await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
  304. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
  305. // Wait until MAM query has been sent out
  306. const sent_stanzas = _converse.connection.sent_stanzas;
  307. await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop());
  308. const view = _converse.chatboxviews.get('lounge@montague.lit');
  309. const file = {
  310. 'type': 'image/jpeg',
  311. 'size': '23456' ,
  312. 'lastModifiedDate': "",
  313. 'name': "my-juliet.jpg"
  314. };
  315. view.model.sendFiles([file]);
  316. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  317. await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length);
  318. const iq = IQ_stanzas.pop();
  319. expect(Strophe.serialize(iq)).toBe(
  320. `<iq from="romeo@montague.lit/orchard" `+
  321. `id="${iq.getAttribute("id")}" `+
  322. `to="upload.montague.tld" `+
  323. `type="get" `+
  324. `xmlns="jabber:client">`+
  325. `<request `+
  326. `content-type="image/jpeg" `+
  327. `filename="my-juliet.jpg" `+
  328. `size="23456" `+
  329. `xmlns="urn:xmpp:http:upload:0"/>`+
  330. `</iq>`);
  331. const message = base_url+"/logo/conversejs-filled.svg";
  332. const stanza = u.toStanza(`
  333. <iq from='upload.montague.tld'
  334. id="${iq.getAttribute('id')}"
  335. to='romeo@montague.lit/orchard'
  336. type='result'>
  337. <slot xmlns='urn:xmpp:http:upload:0'>
  338. <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg'>
  339. <header name='Authorization'>Basic Base64String==</header>
  340. <header name='Cookie'>foo=bar; user=romeo</header>
  341. </put>
  342. <get url="${message}" />
  343. </slot>
  344. </iq>`);
  345. spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () {
  346. const message = view.model.messages.at(0);
  347. expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
  348. message.set('progress', 0.5);
  349. u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5')
  350. .then(() => {
  351. message.set('progress', 1);
  352. u.waitUntil(() => view.el.querySelector('.chat-content progress')?.getAttribute('value') === '1')
  353. }).then(() => {
  354. message.save({
  355. 'upload': _converse.SUCCESS,
  356. 'oob_url': message.get('get'),
  357. 'message': message.get('get')
  358. });
  359. return new Promise(resolve => view.model.messages.once('rendered', resolve));
  360. });
  361. });
  362. let sent_stanza;
  363. spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
  364. _converse.connection._dataRecv(mock.createRequest(stanza));
  365. await u.waitUntil(() => sent_stanza, 1000);
  366. expect(sent_stanza.toLocaleString()).toBe(
  367. `<message `+
  368. `from="romeo@montague.lit/orchard" `+
  369. `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
  370. `to="lounge@montague.lit" `+
  371. `type="groupchat" `+
  372. `xmlns="jabber:client">`+
  373. `<body>${message}</body>`+
  374. `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
  375. `<x xmlns="jabber:x:oob">`+
  376. `<url>${message}</url>`+
  377. `</x>`+
  378. `<origin-id id="${sent_stanza.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
  379. `</message>`);
  380. const img_link_el = await u.waitUntil(() => view.el.querySelector('converse-chat-message-body .chat-image__link'), 1000);
  381. // Check that the image renders
  382. expect(img_link_el.outerHTML.replace(/<!---->/g, '').trim()).toEqual(
  383. `<a class="chat-image__link" target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
  384. `<img class="chat-image img-thumbnail" src="${base_url}/logo/conversejs-filled.svg"></a>`);
  385. expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.replace(/<!---->/g, '').trim()).toEqual(
  386. `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
  387. `Download image file "conversejs-filled.svg"</a>`);
  388. XMLHttpRequest.prototype.send = send_backup;
  389. done();
  390. }));
  391. it("shows an error message if the file is too large",
  392. mock.initConverse([], {}, async function (done, _converse) {
  393. const IQ_stanzas = _converse.connection.IQ_stanzas;
  394. const IQ_ids = _converse.connection.IQ_ids;
  395. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []);
  396. await u.waitUntil(() => _.filter(
  397. IQ_stanzas,
  398. iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length
  399. );
  400. let stanza = _.find(IQ_stanzas, function (iq) {
  401. return iq.querySelector(
  402. 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]');
  403. });
  404. const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
  405. stanza = $iq({
  406. 'type': 'result',
  407. 'from': 'montague.lit',
  408. 'to': 'romeo@montague.lit/orchard',
  409. 'id': info_IQ_id
  410. }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
  411. .c('identity', {
  412. 'category': 'server',
  413. 'type': 'im'}).up()
  414. .c('feature', {
  415. 'var': 'http://jabber.org/protocol/disco#info'}).up()
  416. .c('feature', {
  417. 'var': 'http://jabber.org/protocol/disco#items'});
  418. _converse.connection._dataRecv(mock.createRequest(stanza));
  419. let entities = await _converse.api.disco.entities.get();
  420. expect(entities.length).toBe(2);
  421. expect(_.includes(entities.pluck('jid'), 'montague.lit')).toBe(true);
  422. expect(_.includes(entities.pluck('jid'), 'romeo@montague.lit')).toBe(true);
  423. expect(entities.get(_converse.domain).features.length).toBe(2);
  424. expect(entities.get(_converse.domain).identities.length).toBe(1);
  425. await u.waitUntil(function () {
  426. // Converse.js sees that the entity has a disco#items feature,
  427. // so it will make a query for it.
  428. return _.filter(IQ_stanzas, function (iq) {
  429. return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]');
  430. }).length > 0;
  431. }, 300);
  432. stanza = _.find(IQ_stanzas, function (iq) {
  433. return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]');
  434. });
  435. var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
  436. stanza = $iq({
  437. 'type': 'result',
  438. 'from': 'montague.lit',
  439. 'to': 'romeo@montague.lit/orchard',
  440. 'id': items_IQ_id
  441. }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
  442. .c('item', {
  443. 'jid': 'upload.montague.lit',
  444. 'name': 'HTTP File Upload'});
  445. _converse.connection._dataRecv(mock.createRequest(stanza));
  446. entities = await _converse.api.disco.entities.get()
  447. expect(entities.length).toBe(2);
  448. expect(entities.get('montague.lit').items.length).toBe(1);
  449. await u.waitUntil(function () {
  450. // Converse.js sees that the entity has a disco#info feature,
  451. // so it will make a query for it.
  452. return _.filter(IQ_stanzas, function (iq) {
  453. return iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]');
  454. }).length > 0;
  455. }, 300);
  456. stanza = _.find(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'));
  457. const IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
  458. expect(Strophe.serialize(stanza)).toBe(
  459. `<iq from="romeo@montague.lit/orchard" id="${IQ_id}" to="upload.montague.lit" type="get" xmlns="jabber:client">`+
  460. `<query xmlns="http://jabber.org/protocol/disco#info"/>`+
  461. `</iq>`);
  462. // Upload service responds and reports a maximum file size of 5MiB
  463. stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': IQ_id, 'from': 'upload.montague.lit'})
  464. .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
  465. .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
  466. .c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
  467. .c('x', {'type':'result', 'xmlns':'jabber:x:data'})
  468. .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
  469. .c('value').t('urn:xmpp:http:upload:0').up().up()
  470. .c('field', {'var':'max-file-size'})
  471. .c('value').t('5242880');
  472. _converse.connection._dataRecv(mock.createRequest(stanza));
  473. entities = await _converse.api.disco.entities.get();
  474. expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1);
  475. await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain);
  476. await mock.waitForRoster(_converse, 'current');
  477. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  478. await mock.openChatBoxFor(_converse, contact_jid);
  479. const view = _converse.chatboxviews.get(contact_jid);
  480. var file = {
  481. 'type': 'image/jpeg',
  482. 'size': '5242881',
  483. 'lastModifiedDate': "",
  484. 'name': "my-juliet.jpg"
  485. };
  486. view.model.sendFiles([file]);
  487. await u.waitUntil(() => view.el.querySelectorAll('.message').length)
  488. const messages = view.el.querySelectorAll('.message.chat-error');
  489. expect(messages.length).toBe(1);
  490. expect(messages[0].textContent.trim()).toBe(
  491. 'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
  492. done();
  493. }));
  494. });
  495. });
  496. describe("While a file is being uploaded", function () {
  497. it("shows a progress bar", mock.initConverse(
  498. ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  499. async function (done, _converse) {
  500. await mock.waitUntilDiscoConfirmed(
  501. _converse, _converse.domain,
  502. [{'category': 'server', 'type':'IM'}],
  503. ['http://jabber.org/protocol/disco#items'], [], 'info');
  504. const IQ_stanzas = _converse.connection.IQ_stanzas;
  505. await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items');
  506. await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []);
  507. await mock.waitForRoster(_converse, 'current');
  508. const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  509. await mock.openChatBoxFor(_converse, contact_jid);
  510. const view = _converse.chatboxviews.get(contact_jid);
  511. const file = {
  512. 'type': 'image/jpeg',
  513. 'size': '23456' ,
  514. 'lastModifiedDate': "",
  515. 'name': "my-juliet.jpg"
  516. };
  517. view.model.sendFiles([file]);
  518. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  519. await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length)
  520. const iq = IQ_stanzas.pop();
  521. expect(Strophe.serialize(iq)).toBe(
  522. `<iq from="romeo@montague.lit/orchard" `+
  523. `id="${iq.getAttribute("id")}" `+
  524. `to="upload.montague.tld" `+
  525. `type="get" `+
  526. `xmlns="jabber:client">`+
  527. `<request `+
  528. `content-type="image/jpeg" `+
  529. `filename="my-juliet.jpg" `+
  530. `size="23456" `+
  531. `xmlns="urn:xmpp:http:upload:0"/>`+
  532. `</iq>`);
  533. const base_url = 'https://conversejs.org';
  534. const message = base_url+"/logo/conversejs-filled.svg";
  535. const stanza = u.toStanza(`
  536. <iq from="upload.montague.tld"
  537. id="${iq.getAttribute("id")}"
  538. to="romeo@montague.lit/orchard"
  539. type="result">
  540. <slot xmlns="urn:xmpp:http:upload:0">
  541. <put url="https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my-juliet.jpg">
  542. <header name="Authorization">Basic Base64String==</header>
  543. <header name="Cookie">foo=bar; user=romeo</header>
  544. </put>
  545. <get url="${message}" />
  546. </slot>
  547. </iq>`);
  548. spyOn(XMLHttpRequest.prototype, 'send').and.callFake(async () => {
  549. const message = view.model.messages.at(0);
  550. expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0');
  551. message.set('progress', 0.5);
  552. await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5');
  553. message.set('progress', 1);
  554. await u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1');
  555. expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB');
  556. done();
  557. });
  558. _converse.connection._dataRecv(mock.createRequest(stanza));
  559. }));
  560. });
  561. });
  562. });