Honor the filesize restrictions of the XMPP server

updates #161
This commit is contained in:
JC Brand 2018-04-18 10:03:21 +02:00
parent 95e648e79f
commit 133df99aec
8 changed files with 268 additions and 76 deletions

6
package-lock.json generated
View File

@ -1929,6 +1929,12 @@
"integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
"dev": true
},
"filesize": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
"dev": true
},
"fill-range": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",

View File

@ -37,16 +37,17 @@
"backbone": "1.3.3",
"backbone.browserStorage": "0.0.3",
"backbone.nativeview": "^0.3.3",
"bootstrap": "^4.0.0",
"bootstrap.native": "^2.0.21",
"backbone.overview": "1.0.2",
"backbone.vdomview": "1.0.1",
"bootstrap": "^4.0.0",
"bootstrap.native": "^2.0.21",
"bourbon": "^4.3.2",
"clean-css-cli": "^4.0.10",
"emojione": "^3.0.3",
"es6-promise": "^4.1.0",
"eslint": "4.19.0",
"eslint-plugin-lodash": "^2.3.3",
"filesize": "^3.6.1",
"font-awesome": "^4.7.0",
"http-server": "^0.10.0",
"install": "^0.9.5",

View File

@ -765,7 +765,7 @@
// We send another message, for which an error will
// not be received, to test that errors appear
// after the relevant message.
msg_text = 'This message will be sent, and not receive an error';
msg_text = 'This message will be sent, and also receive an error';
message = view.model.messages.create({
'msgid': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104',
'fullname': fullname,
@ -802,12 +802,6 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect($chat_content.find('.chat-error').text()).toEqual(error_txt);
/* Incoming error messages that are not tied to a
* certain show message (via the msgid attribute),
* are not shown at all. The reason for this is
* that we may get error messages for chat state
* notifications as well.
*/
stanza = $msg({
'to': _converse.connection.jid,
'type':'error',
@ -819,7 +813,36 @@
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
.t('Server-to-server connection failed: Connecting failed: connection timeout');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect($chat_content.find('.chat-error').length).toEqual(1);
expect($chat_content.find('.chat-error').length).toEqual(2);
// If the last message is already an error message,
// then we don't render it another time.
stanza = $msg({
'to': _converse.connection.jid,
'type':'error',
'id':'another-unused-id',
'from': sender_jid
})
.c('error', {'type': 'cancel'})
.c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
.t('Server-to-server connection failed: Connecting failed: connection timeout');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect($chat_content.find('.chat-error').length).toEqual(2);
// A different error message will however render
stanza = $msg({
'to': _converse.connection.jid,
'type':'error',
'id':'another-id',
'from': sender_jid
})
.c('error', {'type': 'cancel'})
.c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
.t('Something else went wrong as well');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
expect($chat_content.find('.chat-error').length).toEqual(3);
done();
}));
});

View File

@ -204,9 +204,9 @@
done();
}));
describe("when clicked", function () {
describe("when clicked and a file chosen", function () {
it("a file upload slot is requested", mock.initConverseWithAsync(function (done, _converse) {
it("is uploaded and sent out", mock.initConverseWithAsync(function (done, _converse) {
test_utils.waitUntilDiscoConfirmed(
_converse, _converse.domain,
[{'category': 'server', 'type':'IM'}],
@ -311,6 +311,135 @@
});
});
}));
it("shows and error message if the file is too large", mock.initConverseWithAsync(function (done, _converse) {
var IQ_stanzas = _converse.connection.IQ_stanzas;
var IQ_ids = _converse.connection.IQ_ids;
var send_backup = XMLHttpRequest.prototype.send;
test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []).then(function () {
test_utils.waitUntil(function () {
return _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector(
'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
}).length > 0;
}, 300).then(function () {
var stanza = _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector(
'iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
})[0];
var info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
stanza = $iq({
'type': 'result',
'from': 'localhost',
'to': 'dummy@localhost/resource',
'id': info_IQ_id
}).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
.c('identity', {
'category': 'server',
'type': 'im'}).up()
.c('feature', {
'var': 'http://jabber.org/protocol/disco#info'}).up()
.c('feature', {
'var': 'http://jabber.org/protocol/disco#items'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
_converse.api.disco.entities.get().then(function(entities) {
expect(entities.length).toBe(2);
expect(_.includes(entities.pluck('jid'), 'localhost')).toBe(true);
expect(_.includes(entities.pluck('jid'), 'dummy@localhost')).toBe(true);
expect(entities.get(_converse.domain).features.length).toBe(2);
expect(entities.get(_converse.domain).identities.length).toBe(1);
return test_utils.waitUntil(function () {
// Converse.js sees that the entity has a disco#items feature,
// so it will make a query for it.
return _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
}).length > 0;
}, 300);
});
}).then(function () {
var stanza = _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector('iq[to="localhost"] query[xmlns="http://jabber.org/protocol/disco#items"]');
})[0];
var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
stanza = $iq({
'type': 'result',
'from': 'localhost',
'to': 'dummy@localhost/resource',
'id': items_IQ_id
}).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'})
.c('item', {
'jid': 'upload.localhost',
'name': 'HTTP File Upload'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
_converse.api.disco.entities.get().then(function (entities) {
expect(entities.length).toBe(2);
expect(entities.get('localhost').items.length).toBe(1);
return test_utils.waitUntil(function () {
// Converse.js sees that the entity has a disco#info feature,
// so it will make a query for it.
return _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
}).length > 0;
}, 300);
});
}).then(function () {
var stanza = _.filter(IQ_stanzas, function (iq) {
return iq.nodeTree.querySelector('iq[to="upload.localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]');
})[0];
var IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)];
expect(stanza.toLocaleString()).toBe(
"<iq from='dummy@localhost/resource' to='upload.localhost' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='http://jabber.org/protocol/disco#info'/>"+
"</iq>");
// Upload service responds and reports a maximum file size of 5MiB
stanza = $iq({'type': 'result', 'to': 'dummy@localhost/resource', 'id': IQ_id, 'from': 'upload.localhost'})
.c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'})
.c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up()
.c('feature', {'var':'urn:xmpp:http:upload:0'}).up()
.c('x', {'type':'result', 'xmlns':'jabber:x:data'})
.c('field', {'var':'FORM_TYPE', 'type':'hidden'})
.c('value').t('urn:xmpp:http:upload:0').up().up()
.c('field', {'var':'max-file-size'})
.c('value').t('5242880');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
_converse.api.disco.entities.get().then(function (entities) {
expect(entities.get('localhost').items.get('upload.localhost').identities.where({'category': 'store'}).length).toBe(1);
_converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(
function (result) {
test_utils.createContacts(_converse, 'current');
var contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid);
var view = _converse.chatboxviews.get(contact_jid);
var file = {
'type': 'image/jpeg',
'size': '5242881',
'lastModifiedDate': "",
'name': "my-juliet.jpg"
};
view.model.sendFiles([file]);
return test_utils.waitUntil(function () {
return view.el.querySelectorAll('.message').length;
}).then(function () {
const messages = view.el.querySelectorAll('.message.chat-error');
expect(messages.length).toBe(1);
expect(messages[0].textContent).toBe(
'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.');
done();
});
}
);
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
})
});
}));
});
});
});

View File

@ -29,6 +29,7 @@ require.config({
"emojione": "node_modules/emojione/lib/js/emojione",
"es6-promise": "node_modules/es6-promise/dist/es6-promise.auto",
"eventemitter": "node_modules/otr/build/dep/eventemitter",
"filesize": "node_modules/filesize/lib/filesize",
"form-utils": "src/utils/form",
"i18n": "src/i18n",
"jed": "node_modules/jed/jed",

View File

@ -8,10 +8,11 @@
define([
"converse-core",
"emojione",
"filesize",
"tpl!chatboxes",
"backbone.overview"
], factory);
}(this, function (converse, emojione, tpl_chatboxes) {
}(this, function (converse, emojione, filesize, tpl_chatboxes) {
"use strict";
const { $msg, Backbone, Promise, Strophe, b64_sha1, moment, utils, _ } = converse.env;
@ -289,21 +290,36 @@
sendFiles (files) {
_converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
const slot_request_url = _.get(result.pop(), 'id');
const item = result.pop(),
data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
max_file_size = window.parseInt(_.get(data, 'attributes.max-file-size.value')),
slot_request_url = _.get(item, 'id');
if (!slot_request_url) {
const err_msg = __("Sorry, looks like file upload is not supported by your server.");
return this.trigger('showHelpMessages', [err_msg], 'error');
this.messages.create({
'message': __("Sorry, looks like file upload is not supported by your server."),
'type': 'error',
});
return;
}
_.each(files, (file) => {
this.messages.create(
_.extend(
this.getOutgoingMessageAttributes(), {
'file': file,
'progress': 0,
'slot_request_url': slot_request_url,
'type': this.get('message_type'),
})
);
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
return this.messages.create({
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
file.name, filesize(max_file_size)),
'type': 'error',
});
} else {
this.messages.create(
_.extend(
this.getOutgoingMessageAttributes(), {
'file': file,
'progress': 0,
'slot_request_url': slot_request_url,
'type': this.get('message_type'),
})
);
}
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},

View File

@ -529,33 +529,6 @@
}
},
showMessage (message) {
/* Inserts a chat message into the content area of the chat box.
* Will also insert a new day indicator if the message is on a
* different day.
*
* The message to show may either be newer than the newest
* message, or older than the oldest message.
*
* Parameters:
* (Backbone.Model) message: The message object
*/
const view = new _converse.MessageView({'model': message}),
current_msg_date = moment(message.get('time')) || moment,
previous_msg_date = this.getLastMessageDate(current_msg_date),
message_el = view.el;
if (_.isNull(previous_msg_date)) {
this.content.insertAdjacentElement('afterbegin', message_el);
} else {
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
previous_msg_el.insertAdjacentElement('afterend', message_el);
}
this.insertDayIndicator(message_el);
this.clearChatStateNotification(message.get('from'));
this.setScrollPosition(message_el);
},
setScrollPosition (message_el) {
/* Given a newly inserted message, determine whether we
* should keep the scrollbar in place (so as to not scroll
@ -655,8 +628,50 @@
return !u.isVisible(this.el);
},
handleTextMessage (message) {
this.showMessage(message);
insertMessage (view) {
/* Given a view representing a message, insert it inot the
* content area of the chat box.
*
* Parameters:
* (Backbone.View) message: The message Backbone.View
*/
if (view.model.get('type') === 'error') {
const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
if (previous_msg_el) {
return previous_msg_el.insertAdjacentElement('afterend', view.el);
}
}
const current_msg_date = moment(view.model.get('time')) || moment,
previous_msg_date = this.getLastMessageDate(current_msg_date);
if (_.isNull(previous_msg_date)) {
this.content.insertAdjacentElement('afterbegin', view.el);
} else {
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
if (view.model.get('type') === 'error' &&
u.hasClass('chat-error', previous_msg_el) &&
previous_msg_el.textContent === view.model.get('message')) {
// We don't show a duplicate error message
return;
}
previous_msg_el.insertAdjacentElement('afterend', view.el);
}
},
showMessage (message) {
/* Inserts a chat message into the content area of the chat box.
*
* Will also insert a new day indicator if the message is on a
* different day.
*
* Parameters:
* (Backbone.Model) message: The message object
*/
const view = new _converse.MessageView({'model': message});
this.insertMessage(view);
this.insertDayIndicator(view.el);
this.clearChatStateNotification(message.get('from'));
this.setScrollPosition(view.el);
if (u.isNewMessage(message)) {
if (message.get('sender') === 'me') {
@ -676,21 +691,6 @@
}
},
handleErrorMessage (message) {
const message_el = this.content.querySelector(`[data-msgid="${message.get('msgid')}"]`);
if (!_.isNull(message_el)) {
message_el.insertAdjacentHTML(
'afterend',
tpl_info({
'extra_classes': 'chat-error',
'message': message.get('message'),
'isodate': moment().format(),
'data': ''
}));
this.scrollDown();
}
},
onMessageAdded (message) {
/* Handler that gets called when a new message object is created.
*
@ -702,13 +702,13 @@
delete this.clear_status_timeout;
}
if (message.get('type') === 'error') {
this.handleErrorMessage(message);
this.showMessage(message);
} else {
if (message.get('chat_state')) {
this.showChatStateNotification(message);
}
if (message.get('file') || message.get('message')) {
this.handleTextMessage(message);
this.showMessage(message);
}
}
_converse.emit('messageAdded', {

View File

@ -11,6 +11,7 @@
"emojione",
"tpl!action",
"tpl!file",
"tpl!info",
"tpl!message",
"tpl!spoiler_message"
], factory);
@ -20,6 +21,7 @@
emojione,
tpl_action,
tpl_file,
tpl_info,
tpl_message,
tpl_spoiler_message
) {
@ -53,7 +55,10 @@
render () {
if (this.model.get('file') && !this.model.get('message')) {
return this.renderFileUploadProgresBar();
} else if (this.model.get('type') === 'error') {
return this.renderErrorMessage();
}
let template, username,
text = this.model.get('message');
@ -91,6 +96,10 @@
u.renderImageURLs(msg_content).then(() => {
this.model.collection.trigger('rendered');
});
return this.replaceElement(msg);
},
replaceElement (msg) {
if (!_.isNil(this.el.parentElement)) {
this.el.parentElement.replaceChild(msg, this.el);
}
@ -98,13 +107,20 @@
return this.el;
},
renderErrorMessage () {
const moment_time = moment(this.model.get('time')),
msg = u.stringToElement(
tpl_info(_.extend(this.model.toJSON(), {
'extra_classes': 'chat-error',
'isodate': moment_time.format(),
'data': ''
})));
return this.replaceElement(msg);
},
renderFileUploadProgresBar () {
const msg = u.stringToElement(tpl_file(this.model.toJSON()));
if (!_.isNil(this.el.parentElement)) {
this.el.parentElement.replaceChild(msg, this.el);
}
this.setElement(msg);
return this.el;
return this.replaceElement(msg);
},
isMeCommand () {