UNPKG

@jsxc/jsxc

Version:

Real-time XMPP chat application with video calls, file transfer and encrypted communication

330 lines (261 loc) 10.7 kB
import Contact from '../../Contact'; import Message from '../../Message'; import { AbstractPlugin, IMetaData } from '../../plugin/AbstractPlugin'; import PluginAPI from '../../plugin/PluginAPI'; import JID from '../../JID'; import * as Namespace from '../../connection/xmpp/namespace'; import Attachment from '../../Attachment'; import HttpUploadService from './HttpUploadService'; import { IConnection } from '../../connection/Connection.interface'; import { $iq } from '../../vendor/Strophe'; import Translation from '../../util/Translation'; import { IContact } from '@src/Contact.interface'; import { IMessage } from '@src/Message.interface'; import Client from '@src/Client'; const MIN_VERSION = '4.0.0'; const MAX_VERSION = '99.0.0'; const IMAGE_SUFFIXES = ['jpeg', 'jpg', 'png', 'svg', 'gif']; export default class HttpUploadPlugin extends AbstractPlugin { public static getId(): string { return 'http-upload'; } public static getName(): string { return 'HTTP File Upload'; } public static getMetaData(): IMetaData { return { description: Translation.t('setting-http-upload-enable'), xeps: [ { id: 'XEP-0363', name: 'HTTP File Upload', version: '1.0.0', }, ], }; } private services: HttpUploadService[]; constructor(pluginAPI: PluginAPI) { super(MIN_VERSION, MAX_VERSION, pluginAPI); Namespace.register('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); pluginAPI.addPreSendMessageProcessor(this.preSendMessageProcessor, 20); pluginAPI.addPreSendMessageStanzaProcessor(this.addBitsOfBinary); pluginAPI.addAfterReceiveMessageProcessor(this.extractAttachmentFromStanza); pluginAPI.addAfterReceiveGroupMessageProcessor(this.extractAttachmentFromStanza); let connection = pluginAPI.getConnection(); connection.registerHandler(this.onBitsOfBinary, 'urn:xmpp:bob', 'iq'); } private preSendMessageProcessor = (contact: Contact, message: Message): Promise<[Contact, Message]> => { if (!message.hasAttachment()) { return Promise.resolve([contact, message]); } let attachment = message.getAttachment(); return this.getServices() .then(services => { for (let service of services) { if (service.isSuitable(attachment)) { return service; } this.pluginAPI.Log.debug( `${service.getJid()} only supports files up to ${service.getMaxFileSize()} bytes` ); } throw new Error('Found no suitable http upload service. File probably too large.'); }) .then(service => { return service.sendFile(attachment.getFile(), (transferred, total) => { message.updateProgress(transferred, total); }); }) .then(downloadUrl => { this.addUrlToMessage(downloadUrl, attachment, message); attachment.setProcessed(true); if (!attachment.setData(downloadUrl)) { message.setErrorMessage(Translation.t('Attachment_too_large_to_store')); } }) .catch(err => { this.pluginAPI.Log.debug(err); if (err) { setTimeout(() => { contact.addSystemMessage(err.toString()); }, 500); } }) .then(() => { return [contact, message]; }); }; private getServices(): Promise<HttpUploadService[]> { if (this.services) { return Promise.resolve(this.services); } return this.requestServices().then(services => { this.services = services; return services; }); } private requestServices(): Promise<HttpUploadService[]> { let connection = this.getConnection(); let ownJid = connection.getJID(); let serverJid = new JID('', ownJid.domain, ''); let discoInfoRepository = this.pluginAPI.getDiscoInfoRepository(); return connection .getDiscoService() .getDiscoItems(serverJid) .then(stanza => { let promises = []; $(stanza) .find('item') .each((index, element) => { let jid = new JID('', $(element).attr('jid'), ''); //@TODO cache let promise = discoInfoRepository.requestDiscoInfo(jid).then(discoInfo => { let hasFeature = discoInfo.hasFeature(Namespace.get('HTTPUPLOAD')); if (hasFeature) { let maxFileSize = 0; let form = discoInfo.getFormByType(Namespace.get('HTTPUPLOAD')); if (form) { let values = form.getValues('max-file-size') || []; if (values.length === 1) { maxFileSize = parseInt(values[0], 10); } } return new HttpUploadService(this.pluginAPI, jid, maxFileSize); } }); promises.push(promise); }); return Promise.all(promises).then(results => { return results.filter(service => typeof service !== 'undefined'); }); }); } private getConnection(): IConnection { return this.pluginAPI.getConnection(); } private addUrlToMessage(downloadUrl: string, attachment: Attachment, message: Message) { let plaintext = message.getPlaintextMessage(); message.setPlaintextMessage(downloadUrl + '\n' + plaintext); let html = $('<div>').append(message.getHtmlMessage()); let linkElement = $('<a>'); linkElement.attr('href', downloadUrl); let imageElement = $('<img>'); imageElement.attr('src', 'cid:' + attachment.getUid()); imageElement.attr('alt', attachment.getName()); linkElement.append(imageElement); html.append($('<p>').append(linkElement)); //@TODO html !== empty ??? if (plaintext) { html.append($('<p>').text(plaintext)); } message.setHtmlMessage(html.html()); } private onBitsOfBinary = (stanza: string): boolean => { let stanzaElement = $(stanza); let from = new JID(stanzaElement.attr('from')); let type = stanzaElement.attr('type'); let id = stanzaElement.attr('id'); let cid = stanzaElement.find('data[xmlns="urn:xmpp:bob"]').attr('cid'); if (type !== 'get') { return true; } let attachment = new Attachment(cid); //@REVIEW security if (attachment.hasThumbnailData()) { let iq = $iq({ to: from.full, id, type: 'result', }) .c('data', { xmlns: 'urn:xmpp:bob', cid: attachment.getUid(), type: attachment.getMimeType(), }) .t(attachment.getThumbnailData().replace(/^[^,],+/, '')); this.pluginAPI.sendIQ(iq); } return true; }; private addBitsOfBinary = (message: Message, xmlStanza: Strophe.Builder): Promise<any> => { //@TODO check if element with cid exists if (message.hasAttachment() && message.getAttachment().hasThumbnailData()) { let attachment = message.getAttachment(); let thumbnailData = attachment.getThumbnailData(); xmlStanza .c('data', { xmlns: 'urn:xmpp:bob', cid: attachment.getUid(), type: thumbnailData.match(/data:(\w+\/[\w-+\d.]+)(?=;|,)/)[1], }) .t(thumbnailData.replace(/^[^,],+/, '')) .up(); } return Promise.resolve([message, xmlStanza]); }; private extractAttachmentFromStanza = ( contact: IContact, message: IMessage, stanza: Element ): Promise<[IContact, IMessage, Element]> => { let element = $(stanza); let bodyElement = element.find('html body[xmlns="' + Strophe.NS.XHTML + '"]').first(); let dataElement = element.find('data[xmlns="urn:xmpp:bob"]'); if (bodyElement.length && dataElement.length === 1 && !message.isEncrypted()) { let cid = dataElement.attr('cid'); let mimeType = dataElement.attr('type'); if (!/^[a-z]+\/[a-z0-9.\-+]+$/.test(mimeType)) { return Promise.resolve([contact, message, stanza]); } let linkElement = bodyElement.find('a'); let imageElement = linkElement.find('img[src^="cid:"]'); if (imageElement.length === 1 && 'cid:' + cid === imageElement.attr('src')) { let url = linkElement.attr('href'); let name = imageElement.attr('alt'); let thumbnailData = dataElement.text(); if ( /^data:image\/(jpeg|jpg|gif|png|svg);base64,[/+=a-z0-9]+$/i.test(thumbnailData) && /^https?:\/\//.test(url) ) { let attachment = new Attachment(name, mimeType, url); attachment.setThumbnailData(thumbnailData); attachment.setData(url); message.setAttachment(attachment); message.setPlaintextMessage(bodyElement.text()); } } } else { this.processLinks(message); } return Promise.resolve([contact, message, stanza]); }; private processLinks(message: IMessage) { let plaintext = message.getPlaintextMessage(); if (!plaintext) { return; } let pattern = new RegExp(/^(https?:\/\/[^\s]+)/); let match = plaintext.match(pattern); if (match) { let url = match[0]; let extension = this.getFileExtensionFromUrl(url); if (IMAGE_SUFFIXES.includes(extension)) { let fileName = this.getFileNameFromUrl(url) || 'image'; let attachment = new Attachment(decodeURIComponent(fileName), 'image/' + extension, url); attachment.setData(url); if (Client.isTrustedDomain(new URL(url))) { attachment.generateThumbnail(); } message.setAttachment(attachment); message.setPlaintextMessage(plaintext.replace(url, '')); } } } private getFileExtensionFromUrl(url: string): string { return url.split(/[#?]/)[0].split('.').pop().trim().toLowerCase(); } private getFileNameFromUrl(url: string): string { let parsedUrl = new URL(url); return parsedUrl.pathname.substring(parsedUrl.pathname.lastIndexOf('/') + 1); } }