Render the OMEMO lock icon in MUC toolbars as well

updates #1180
This commit is contained in:
JC Brand 2018-12-13 09:48:43 +01:00
parent 55bb1826ea
commit f64fdb8088
4 changed files with 358 additions and 25 deletions

98
dist/converse.js vendored
View File

@ -56433,13 +56433,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this); this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this); this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
this.checkOMEMOSupported();
},
async checkOMEMOSupported() {
const _converse = this.__super__._converse;
const supported = await _converse.contactHasOMEMOSupport(this.model.get('jid'));
this.model.set('omemo_supported', supported);
}, },
showMessage(message) { showMessage(message) {
@ -56486,6 +56479,33 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
}); });
} }
},
ChatRoomView: {
events: {
'click .toggle-omemo': 'toggleOMEMO'
},
initialize() {
this.__super__.initialize.apply(this, arguments);
this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
},
toggleOMEMO(ev) {
const _converse = this.__super__._converse,
__ = _converse.__;
if (!this.model.get('omemo_supported')) {
return _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__('Cannot use end-to-end encryption in this groupchat, ' + 'either the groupchat has some anonymity or not all participants support OMEMO.')]);
}
ev.preventDefault();
this.model.save({
'omemo_active': !this.model.get('omemo_active')
});
}
} }
}, },
@ -56493,7 +56513,8 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
/* The initialize function gets called as soon as the plugin is /* The initialize function gets called as soon as the plugin is
* loaded by Converse.js's plugin machinery. * loaded by Converse.js's plugin machinery.
*/ */
const _converse = this._converse; const _converse = this._converse,
__ = _converse.__;
_converse.api.promises.add(['OMEMOInitialized']); _converse.api.promises.add(['OMEMOInitialized']);
@ -57134,6 +57155,49 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo_store.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo_store.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
} }
async function onOccupantAdded(chatroom, occupant) {
if (occupant.isSelf() || !chatroom.get('nonanonymous')) {
return;
}
if (chatroom.get('omemo_active')) {
const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
if (!supported) {
chatroom.messages.create({
'message': __("%1$s doesn't appear to have a client that supports OMEMO. " + "Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
'type': 'error'
});
chatroom.save({
'omemo_active': false,
'omemo_supported': false
});
}
}
}
async function checkOMEMOSupported(chatbox) {
let supported;
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
} else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
}
chatbox.set('omemo_supported', supported);
}
_converse.api.waitUntil('chatBoxesInitialized').then(() => _converse.chatboxes.on('add', chatbox => {
checkOMEMOSupported(chatbox);
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
chatbox.on('change:nonanonymous', checkOMEMOSupported);
chatbox.on('change:membersonly', checkOMEMOSupported);
}
}));
_converse.api.listen.on('afterTearDown', () => { _converse.api.listen.on('afterTearDown', () => {
if (_converse.devicelists) { if (_converse.devicelists) {
_converse.devicelists.reset(); _converse.devicelists.reset();
@ -65885,7 +65949,23 @@ Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig"); Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user"); Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].MUC_NICK_CHANGED_CODE = "303"; _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].MUC_NICK_CHANGED_CODE = "303";
_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES = ['passwordprotected', 'unsecured', 'hidden', 'publicroom', 'membersonly', 'open', 'persistent', 'temporary', 'nonanonymous', 'semianonymous', 'moderated', 'unmoderated', 'mam_enabled']; _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES = ['passwordprotected', 'unsecured', 'hidden', 'publicroom', 'membersonly', 'open', 'persistent', 'temporary', 'nonanonymous', 'semianonymous', 'moderated', 'unmoderated', 'mam_enabled']; // No longer used in code, but useful as reference.
//
// const ROOM_FEATURES_MAP = {
// 'passwordprotected': 'unsecured',
// 'unsecured': 'passwordprotected',
// 'hidden': 'publicroom',
// 'publicroom': 'hidden',
// 'membersonly': 'open',
// 'open': 'membersonly',
// 'persistent': 'temporary',
// 'temporary': 'persistent',
// 'nonanonymous': 'semianonymous',
// 'semianonymous': 'nonanonymous',
// 'moderated': 'unmoderated',
// 'unmoderated': 'moderated'
// };
_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOMSTATUS = { _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOMSTATUS = {
CONNECTED: 0, CONNECTED: 0,
CONNECTING: 1, CONNECTING: 1,

View File

@ -1,12 +1,8 @@
(function (root, factory) { (function (root, factory) {
define(["jasmine", "mock", "test-utils"], factory); define(["jasmine", "mock", "test-utils"], factory);
} (this, function (jasmine, mock, test_utils) { } (this, function (jasmine, mock, test_utils) {
var Strophe = converse.env.Strophe; const { $iq, $pres, $msg, _, Strophe } = converse.env;
var b64_sha1 = converse.env.b64_sha1; const u = converse.env.utils;
var $iq = converse.env.$iq;
var $msg = converse.env.$msg;
var _ = converse.env._;
var u = converse.env.utils;
function deviceListFetched (_converse, jid) { function deviceListFetched (_converse, jid) {
@ -229,7 +225,6 @@
done(); done();
})); }));
it("can receive a PreKeySignalMessage", it("can receive a PreKeySignalMessage",
mock.initConverseWithPromises( mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@ -824,6 +819,187 @@
done(); done();
})); }));
it("adds a toolbar button for starting an encrypted groupchat session",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
async function (done, _converse) {
// MEMO encryption works only in members-only conferences that are non-anonymous.
const features = [
'http://jabber.org/protocol/muc',
'jabber:iq:register',
'muc_passwordprotected',
'muc_hidden',
'muc_temporary',
'muc_membersonly',
'muc_unmoderated',
'muc_nonanonymous'
];
await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy', features);
const view = _converse.chatboxviews.get('lounge@localhost');
await test_utils.waitUntil(() => initializedOMEMO(_converse));
const toolbar = view.el.querySelector('.chat-toolbar');
let toggle = toolbar.querySelector('.toggle-omemo');
expect(view.model.get('omemo_active')).toBe(undefined);
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(false);
expect(view.model.get('omemo_supported')).toBe(true);
toggle.click();
toggle = toolbar.querySelector('.toggle-omemo');
expect(view.model.get('omemo_active')).toBe(true);
expect(u.hasClass('fa-unlock', toggle)).toBe(false);
expect(u.hasClass('fa-lock', toggle)).toBe(true);
expect(u.hasClass('disabled', toggle)).toBe(false);
expect(view.model.get('omemo_supported')).toBe(true);
let contact_jid = 'newguy@localhost';
let stanza = $pres({
to: 'dummy@localhost/resource',
from: 'lounge@localhost/newguy'
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'newguy@localhost/_converse.js-290929789',
'role': 'participant'
}).tree();
_converse.connection._dataRecv(test_utils.createRequest(stanza));
let iq_stanza = await test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid));
expect(iq_stanza.toLocaleString()).toBe(
`<iq from="dummy@localhost" id="${iq_stanza.nodeTree.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
`<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
`</pubsub>`+
`</iq>`);
stanza = $iq({
'from': contact_jid,
'id': iq_stanza.nodeTree.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result',
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
.c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await test_utils.waitUntil(() => _converse.omemo_store);
expect(_converse.devicelists.length).toBe(2);
const devicelist = _converse.devicelists.get(contact_jid);
expect(devicelist.devices.length).toBe(2);
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
expect(view.model.get('omemo_active')).toBe(true);
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(false);
expect(u.hasClass('fa-lock', toggle)).toBe(true);
expect(u.hasClass('disabled', toggle)).toBe(false);
expect(view.model.get('omemo_supported')).toBe(true);
// Test that the button gets disabled when the room becomes
// anonymous or semi-anonymous
view.model.save({'nonanonymous': false, 'semianonymous': true});
await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(true);
expect(view.model.get('omemo_supported')).toBe(false);
view.model.save({'nonanonymous': true, 'semianonymous': false});
await test_utils.waitUntil(() => view.model.get('omemo_supported'));
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(false);
// Test that the button gets disabled when the room becomes open
view.model.save({'membersonly': false, 'open': true});
await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(true);
expect(view.model.get('omemo_supported')).toBe(false);
view.model.save({'membersonly': true, 'open': false});
await test_utils.waitUntil(() => view.model.get('omemo_supported'));
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(false);
expect(view.model.get('omemo_supported')).toBe(true);
expect(view.model.get('omemo_active')).toBe(false);
toggle.click();
expect(view.model.get('omemo_active')).toBe(true);
// Someone enters the room who doesn't have OMEMO support, while we
// have OMEMO activated...
contact_jid = 'oldguy@localhost';
stanza = $pres({
to: 'dummy@localhost/resource',
from: 'lounge@localhost/oldguy'
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': `${contact_jid}/_converse.js-290929788`,
'role': 'participant'
}).tree();
_converse.connection._dataRecv(test_utils.createRequest(stanza));
iq_stanza = await test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid));
expect(iq_stanza.toLocaleString()).toBe(
`<iq from="dummy@localhost" id="${iq_stanza.nodeTree.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
`<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
`</pubsub>`+
`</iq>`);
stanza = $iq({
'from': contact_jid,
'id': iq_stanza.nodeTree.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'error'
}).c('error', {'type': 'cancel'})
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
expect(view.el.querySelector('.chat-error').textContent).toBe(
"oldguy doesn't appear to have a client that supports OMEMO. "+
"Encrypted chat will no longer be possible in this grouchat."
);
toggle = toolbar.querySelector('.toggle-omemo');
expect(_.isNull(toggle)).toBe(false);
expect(u.hasClass('fa-unlock', toggle)).toBe(true);
expect(u.hasClass('fa-lock', toggle)).toBe(false);
expect(u.hasClass('disabled', toggle)).toBe(true);
expect( _converse.chatboxviews.el.querySelector('.modal-body p')).toBe(null);
toggle.click();
const msg = _converse.chatboxviews.el.querySelector('.modal-body p');
expect(msg.textContent).toBe(
'Cannot use end-to-end encryption in this groupchat, '+
'either the groupchat has some anonymity or not all participants support OMEMO.');
done();
}));
it("shows OMEMO device fingerprints in the user details modal", it("shows OMEMO device fingerprints in the user details modal",
mock.initConverseWithPromises( mock.initConverseWithPromises(

View File

@ -87,7 +87,7 @@ converse.plugins.add('converse-omemo', {
beforeRender () { beforeRender () {
const { _converse } = this.__super__, const { _converse } = this.__super__,
device_id = _converse.omemo_store.get('device_id'); device_id = _converse.omemo_store.get('device_id');
if (device_id) { if (device_id) {
this.current_device = this.devicelist.devices.get(device_id); this.current_device = this.devicelist.devices.get(device_id);
} }
@ -458,14 +458,8 @@ converse.plugins.add('converse-omemo', {
this.__super__.initialize.apply(this, arguments); this.__super__.initialize.apply(this, arguments);
this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this); this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this); this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
this.checkOMEMOSupported();
}, },
async checkOMEMOSupported () {
const { _converse } = this.__super__;
const supported = await _converse.contactHasOMEMOSupport(this.model.get('jid'));
this.model.set('omemo_supported', supported);
},
showMessage (message) { showMessage (message) {
// We don't show a message if it's only keying material // We don't show a message if it's only keying material
@ -503,12 +497,38 @@ converse.plugins.add('converse-omemo', {
__('Error'), __('Error'),
[__("Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.", [__("Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
this.model.contact.getDisplayName() this.model.contact.getDisplayName()
)] )]
) )
} }
ev.preventDefault(); ev.preventDefault();
this.model.save({'omemo_active': !this.model.get('omemo_active')}); this.model.save({'omemo_active': !this.model.get('omemo_active')});
} }
},
ChatRoomView: {
events: {
'click .toggle-omemo': 'toggleOMEMO'
},
initialize () {
this.__super__.initialize.apply(this, arguments);
this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
},
toggleOMEMO (ev) {
const { _converse } = this.__super__, { __ } = _converse;
if (!this.model.get('omemo_supported')) {
return _converse.api.alert.show(
Strophe.LogLevel.ERROR,
__('Error'),
[__('Cannot use end-to-end encryption in this groupchat, '+
'either the groupchat has some anonymity or not all participants support OMEMO.')]
);
}
ev.preventDefault();
this.model.save({'omemo_active': !this.model.get('omemo_active')});
}
} }
}, },
@ -516,7 +536,8 @@ converse.plugins.add('converse-omemo', {
/* The initialize function gets called as soon as the plugin is /* The initialize function gets called as soon as the plugin is
* loaded by Converse.js's plugin machinery. * loaded by Converse.js's plugin machinery.
*/ */
const { _converse } = this; const { _converse } = this,
{ __ } = _converse;
_converse.api.promises.add(['OMEMOInitialized']); _converse.api.promises.add(['OMEMOInitialized']);
@ -1070,6 +1091,45 @@ converse.plugins.add('converse-omemo', {
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
} }
async function onOccupantAdded (chatroom, occupant) {
if (occupant.isSelf() || !chatroom.get('nonanonymous')) {
return;
}
if (chatroom.get('omemo_active')) {
const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
if (!supported) {
chatroom.messages.create({
'message': __("%1$s doesn't appear to have a client that supports OMEMO. " +
"Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
'type': 'error'
});
chatroom.save({'omemo_active': false, 'omemo_supported': false});
}
}
}
async function checkOMEMOSupported (chatbox) {
let supported;
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
} else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
}
chatbox.set('omemo_supported', supported);
}
_converse.api.waitUntil('chatBoxesInitialized').then(() =>
_converse.chatboxes.on('add', chatbox => {
checkOMEMOSupported(chatbox);
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
chatbox.on('change:nonanonymous', checkOMEMOSupported);
chatbox.on('change:membersonly', checkOMEMOSupported);
}
})
);
_converse.api.listen.on('afterTearDown', () => { _converse.api.listen.on('afterTearDown', () => {
if (_converse.devicelists) { if (_converse.devicelists) {
_converse.devicelists.reset(); _converse.devicelists.reset();

View File

@ -38,6 +38,23 @@ converse.ROOM_FEATURES = [
'moderated', 'unmoderated', 'mam_enabled' 'moderated', 'unmoderated', 'mam_enabled'
]; ];
// No longer used in code, but useful as reference.
//
// const ROOM_FEATURES_MAP = {
// 'passwordprotected': 'unsecured',
// 'unsecured': 'passwordprotected',
// 'hidden': 'publicroom',
// 'publicroom': 'hidden',
// 'membersonly': 'open',
// 'open': 'membersonly',
// 'persistent': 'temporary',
// 'temporary': 'persistent',
// 'nonanonymous': 'semianonymous',
// 'semianonymous': 'nonanonymous',
// 'moderated': 'unmoderated',
// 'unmoderated': 'moderated'
// };
converse.ROOMSTATUS = { converse.ROOMSTATUS = {
CONNECTED: 0, CONNECTED: 0,
CONNECTING: 1, CONNECTING: 1,