// Load #1, load as little as possible because we are in a race to get the loading screen up. define([ '/bower_components/nthen/index.js', '/api/config', 'jquery', ], function (nThen, ApiConfig, $) { var common = {}; common.start = function (cfg) { cfg = cfg || {}; var realtime = !cfg.noRealtime; var secret; var hashes; var isNewFile; var CpNfOuter; var Cryptpad; var Crypto; var Cryptget; var SFrameChannel; var sframeChan; var FilePicker; var Share; var Messaging; var Notifier; var Utils = { nThen: nThen }; var AppConfig; var Test; var password; var initialPathInDrive; nThen(function (waitFor) { // Load #2, the loading screen is up so grab whatever you need... require([ '/common/sframe-chainpad-netflux-outer.js', '/common/cryptpad-common.js', '/bower_components/chainpad-crypto/crypto.js', '/common/cryptget.js', '/common/outer/worker-channel.js', '/filepicker/main.js', '/share/main.js', '/common/common-messaging.js', '/common/common-notifier.js', '/common/common-hash.js', '/common/common-util.js', '/common/common-realtime.js', '/common/common-constants.js', '/common/common-feedback.js', '/common/outer/local-store.js', '/customize/application_config.js', '/common/test.js', '/common/userObject.js', ], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel, _FilePicker, _Share, _Messaging, _Notifier, _Hash, _Util, _Realtime, _Constants, _Feedback, _LocalStore, _AppConfig, _Test, _UserObject) { CpNfOuter = _CpNfOuter; Cryptpad = _Cryptpad; Crypto = Utils.Crypto = _Crypto; Cryptget = _Cryptget; SFrameChannel = _SFrameChannel; FilePicker = _FilePicker; Share = _Share; Messaging = _Messaging; Notifier = _Notifier; Utils.Hash = _Hash; Utils.Util = _Util; Utils.Realtime = _Realtime; Utils.Constants = _Constants; Utils.Feedback = _Feedback; Utils.LocalStore = _LocalStore; Utils.UserObject = _UserObject; AppConfig = _AppConfig; Test = _Test; if (localStorage.CRYPTPAD_URLARGS !== ApiConfig.requireConf.urlArgs) { console.log("New version, flushing cache"); Object.keys(localStorage).forEach(function (k) { if (k.indexOf('CRYPTPAD_CACHE|') !== 0) { return; } delete localStorage[k]; }); localStorage.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; } var cache = {}; var localStore = {}; Object.keys(localStorage).forEach(function (k) { if (k.indexOf('CRYPTPAD_CACHE|') === 0) { cache[k.slice(('CRYPTPAD_CACHE|').length)] = localStorage[k]; return; } if (k.indexOf('CRYPTPAD_STORE|') === 0) { localStore[k.slice(('CRYPTPAD_STORE|').length)] = localStorage[k]; return; } }); // The inner iframe tries to get some data from us every ms (cache, store...). // It will send a "READY" message and wait for our answer with the correct txid. // First, we have to answer to this message, otherwise we're going to block // sframe-boot.js. Then we can start the channel. var msgEv = _Util.mkEvent(); var iframe = $('#sbox-iframe')[0].contentWindow; var postMsg = function (data) { iframe.postMessage(data, '*'); }; var whenReady = waitFor(function (msg) { if (msg.source !== iframe) { return; } var data = JSON.parse(msg.data); if (!data.txid) { return; } // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data postMsg(JSON.stringify({ txid: data.txid, cache: cache, localStore: localStore, language: Cryptpad.getLanguage() })); // Then start the channel window.addEventListener('message', function (msg) { if (msg.source !== iframe) { return; } msgEv.fire(msg); }); SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) { Utils.sframeChan = sframeChan = sfc; })); }); window.addEventListener('message', whenReady); Cryptpad.loading.onDriveEvent.reg(function (data) { if (sframeChan) { sframeChan.event('EV_LOADING_INFO', data); } }); Cryptpad.ready(waitFor(function () { if (sframeChan) { sframeChan.event('EV_LOADING_INFO', { state: -1 }); } }), { driveEvents: cfg.driveEvents }); })); }).nThen(function (waitFor) { if (!Utils.Hash.isValidHref(window.location.href)) { waitFor.abort(); return void sframeChan.event('EV_LOADING_ERROR', 'INVALID_HASH'); } $('#sbox-iframe').focus(); sframeChan.on('EV_CACHE_PUT', function (x) { Object.keys(x).forEach(function (k) { localStorage['CRYPTPAD_CACHE|' + k] = x[k]; }); }); sframeChan.on('EV_LOCALSTORE_PUT', function (x) { Object.keys(x).forEach(function (k) { if (typeof(x[k]) === "undefined") { delete localStorage['CRYPTPAD_STORE|' + k]; return; } localStorage['CRYPTPAD_STORE|' + k] = x[k]; }); }); if (cfg.getSecrets) { var w = waitFor(); // No password for drive, profile and todo cfg.getSecrets(Cryptpad, Utils, waitFor(function (err, s) { secret = Utils.secret = s; Cryptpad.getShareHashes(secret, function (err, h) { hashes = h; w(); }); })); } else { var parsed = Utils.Hash.parsePadUrl(window.location.href); var todo = function () { secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, void 0, password); Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; if (password && !parsed.hashData.password) { var ohc = window.onhashchange; window.onhashchange = function () {}; window.location.hash = h.fileHash || h.editHash || h.viewHash || window.location.hash; window.onhashchange = ohc; ohc({reset: true}); } })); }; if (!parsed.hashData) { // No hash, no need to check for a password return void todo(); } // We now need to check if there is a password and if we know the correct password. // We'll use getFileSize and isNewChannel to detect incorrect passwords. // First we'll get the password value from our drive (getPadAttribute), and we'll check // if the channel is valid. If the pad is not stored in our drive, we'll test with an // empty password instead. // If this initial check returns a valid channel, open the pad. // If the channel is invalid: // Option 1: this is a password-protected pad not stored in our drive --> password prompt // Option 2: this is a pad stored in our drive // 2a: 'edit' pad or file --> password-prompt // 2b: 'view' pad no '/p/' --> the seed is incorrect // 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect // 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt var askPassword = function (wrongPasswordStored, cfg) { // Ask for the password and check if the pad exists // If the pad doesn't exist, it means the password isn't correct // or the pad has been deleted var correctPassword = waitFor(); sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { password = data; var next = function (e, isNew) { if (Boolean(isNew)) { // Ask again in the inner iframe // We should receive a new Q_PAD_PASSWORD_VALUE cb(false); } else { todo(); if (wrongPasswordStored) { // Store the correct password nThen(function (w) { Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); if (parsed.hashData.mode === 'edit') { var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret); Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret); Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); } }).nThen(correctPassword); } else { correctPassword(); } cb(true); } }; if (parsed.type === "file") { // `isNewChannel` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(window.location.href, password, function (e, size) { next(e, size === 0); }); return; } // Not a file, so we can use `isNewChannel` Cryptpad.isNewChannel(window.location.href, password, next); }); sframeChan.event("EV_PAD_PASSWORD", cfg); }; var done = waitFor(); var stored = false; var passwordCfg = { value: '' }; nThen(function (w) { Cryptpad.getPadAttribute('title', w(function (err, data) { stored = (!err && typeof (data) === "string"); })); Cryptpad.getPadAttribute('password', w(function (err, val) { password = val; }), parsed.getUrl()); }).nThen(function (w) { if (!password && !stored && sessionStorage.newPadPassword) { passwordCfg.value = sessionStorage.newPadPassword; delete sessionStorage.newPadPassword; } if (parsed.type === "file") { // `isNewChannel` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(window.location.href, password, w(function (e, size) { if (size !== 0) { return void todo(); } // Wrong password or deleted file? askPassword(true, passwordCfg); })); return; } // Not a file, so we can use `isNewChannel` Cryptpad.isNewChannel(window.location.href, password, w(function(e, isNew) { if (!isNew) { return void todo(); } if (parsed.hashData.mode === 'view' && (password || !parsed.hashData.password)) { // Error, wrong password stored, the view seed has changed with the password // password will never work sframeChan.event("EV_PAD_PASSWORD_ERROR"); waitFor.abort(); return; } if (!stored && !parsed.hashData.password) { // We've received a link without /p/ and it doesn't work without a password: abort return void todo(); } // Wrong password or deleted file? askPassword(true, passwordCfg); })); }).nThen(done); } }).nThen(function (waitFor) { if (cfg.afterSecrets) { cfg.afterSecrets(Cryptpad, Utils, secret, waitFor()); } }).nThen(function (waitFor) { // Check if the pad exists on server if (!window.location.hash) { isNewFile = true; return; } if (realtime) { Cryptpad.isNewChannel(window.location.href, password, waitFor(function (e, isNew) { if (e) { return console.error(e); } isNewFile = Boolean(isNew); })); } }).nThen(function () { var readOnly = secret.keys && !secret.keys.editKeyStr; var isNewHash = true; if (!secret.keys) { isNewHash = false; secret.keys = secret.key; readOnly = false; } Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys); var parsed = Utils.Hash.parsePadUrl(window.location.href); if (!parsed.type) { throw new Error(); } var defaultTitle = Utils.UserObject.getDefaultName(parsed); var edPublic, curvePublic, notifications, isTemplate; var forceCreationScreen = cfg.useCreationScreen && sessionStorage[Utils.Constants.displayPadCreationScreen]; delete sessionStorage[Utils.Constants.displayPadCreationScreen]; var updateMeta = function () { //console.log('EV_METADATA_UPDATE'); var metaObj; nThen(function (waitFor) { Cryptpad.getMetadata(waitFor(function (err, m) { if (err) { console.log(err); } metaObj = m; edPublic = metaObj.priv.edPublic; // needed to create an owned pad curvePublic = metaObj.user.curvePublic; notifications = metaObj.user.notifications; })); if (typeof(isTemplate) === "undefined") { Cryptpad.isTemplate(window.location.href, waitFor(function (err, t) { if (err) { console.log(err); } isTemplate = t; })); } }).nThen(function (/*waitFor*/) { metaObj.doc = { defaultTitle: defaultTitle, type: cfg.type || parsed.type }; var additionalPriv = { app: parsed.type, accountName: Utils.LocalStore.getAccountName(), origin: window.location.origin, pathname: window.location.pathname, fileHost: ApiConfig.fileHost, readOnly: readOnly, isTemplate: isTemplate, feedbackAllowed: Utils.Feedback.state, isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, accounts: { donateURL: Cryptpad.donateURL, upgradeURL: Cryptpad.upgradeURL }, isNewFile: isNewFile, isDeleted: isNewFile && window.location.hash.length > 0, forceCreationScreen: forceCreationScreen, password: password, channel: secret.channel, enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default devMode: localStorage.CryptPad_dev === "1", fromFileData: Cryptpad.fromFileData ? { title: Cryptpad.fromFileData.title } : undefined, storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined) }; if (window.CryptPad_newSharedFolder) { additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; } if (Utils.Constants.criticalApps.indexOf(parsed.type) === -1 && AppConfig.availablePadTypes.indexOf(parsed.type) === -1) { additionalPriv.disabledApp = true; } if (!Utils.LocalStore.isLoggedIn() && AppConfig.registeredOnlyTypes.indexOf(parsed.type) !== -1 && parsed.type !== "file") { additionalPriv.registeredOnly = true; } if (['debug', 'profile'].indexOf(parsed.type) !== -1) { additionalPriv.hashes = hashes; } for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; } if (cfg.addData) { cfg.addData(metaObj.priv, Cryptpad, metaObj.user); } sframeChan.event('EV_METADATA_UPDATE', metaObj); }); }; Cryptpad.onMetadataChanged(updateMeta); sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); Utils.LocalStore.onLogout(function () { sframeChan.event('EV_LOGOUT'); }); Test.registerOuter(sframeChan); Cryptpad.onNewVersionReconnect.reg(function () { sframeChan.event("EV_NEW_VERSION"); }); // Put in the following function the RPC queries that should also work in filepicker var addCommonRpc = function (sframeChan) { sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) { Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) { cb({error: err, response: response}); }); }); sframeChan.on('Q_GET_PINNED_USAGE', function (data, cb) { Cryptpad.getPinnedUsage({}, function (e, used) { cb({ error: e, quota: used }); }); }); sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) { Cryptpad.isOverPinLimit(null, function (e, overLimit, limits) { cb({ error: e, overLimit: overLimit, limits: limits }); }); }); sframeChan.on('Q_THUMBNAIL_GET', function (data, cb) { Utils.LocalStore.getThumbnail(data.key, function (e, data) { cb({ error: e, data: data }); }); }); sframeChan.on('Q_THUMBNAIL_SET', function (data, cb) { Utils.LocalStore.setThumbnail(data.key, data.value, function (e) { cb({error:e}); }); }); sframeChan.on('Q_GET_ATTRIBUTE', function (data, cb) { Cryptpad.getAttribute(data.key, function (e, data) { cb({ error: e, data: data }); }); }); sframeChan.on('Q_SET_ATTRIBUTE', function (data, cb) { Cryptpad.setAttribute(data.key, data.value, function (e) { cb({error:e}); }); }); Cryptpad.mailbox.onEvent.reg(function (data, cb) { sframeChan.query('EV_MAILBOX_EVENT', data, function (err, obj) { if (!cb) { return; } if (err) { return void cb({error: err}); } cb(obj); }); }); sframeChan.on('Q_MAILBOX_COMMAND', function (data, cb) { Cryptpad.mailbox.execCommand(data, cb); }); sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) { sessionStorage.redirectTo = window.location.href; cb(); }); sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) { Cryptpad.storeInTeam(data, cb); }); }; addCommonRpc(sframeChan); var currentTitle; var currentTabTitle; var setDocumentTitle = function () { if (!currentTabTitle) { document.title = currentTitle || 'CryptPad'; return; } var title = currentTabTitle.replace(/\{title\}/g, currentTitle || 'CryptPad'); document.title = title; }; sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newData, cb) { var newTitle = newData.title || newData.defaultTitle; currentTitle = newTitle; setDocumentTitle(); var data = { password: password, title: newTitle, channel: secret.channel, path: initialPathInDrive // Where to store the pad if we don't have it in our drive }; Cryptpad.setPadTitle(data, function (err) { cb({error: err}); }); }); sframeChan.on('EV_SET_TAB_TITLE', function (newTabTitle) { currentTabTitle = newTabTitle; setDocumentTitle(); }); sframeChan.on('EV_SET_HASH', function (hash) { window.location.hash = hash; }); Cryptpad.autoStore.onStoreRequest.reg(function (data) { sframeChan.event("EV_AUTOSTORE_DISPLAY_POPUP", data); }); sframeChan.on('Q_AUTOSTORE_STORE', function (obj, cb) { var data = { password: password, title: currentTitle, channel: secret.channel, path: initialPathInDrive, // Where to store the pad if we don't have it in our drive forceSave: true }; Cryptpad.setPadTitle(data, function (err) { cb({error: err}); }); }); sframeChan.on('Q_IS_PAD_STORED', function (data, cb) { Cryptpad.getPadAttribute('title', function (err, data) { cb (!err && typeof (data) === "string"); }); }); sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) { var parsed = Utils.Hash.parsePadUrl(data.href); if (parsed.type === 'drive') { // Shared folder var secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, data.password); Cryptpad.addSharedFolder(null, secret, cb); } else { var _data = { password: data.password, href: data.href, channel: data.channel, title: data.title, owners: data.metadata.owners, expire: data.metadata.expire, forceSave: true }; Cryptpad.setPadTitle(_data, function (err) { cb({error: err}); }); } // Also add your mailbox to the metadata object var padParsed = Utils.Hash.parsePadUrl(data.href); var padSecret = Utils.Hash.getSecrets(padParsed.type, padParsed.hash, data.password); var padCrypto = Utils.Crypto.createEncryptor(padSecret.keys); try { var value = {}; value[edPublic] = padCrypto.encrypt(JSON.stringify({ notifications: notifications, curvePublic: curvePublic })); var msg = { channel: data.channel, command: 'ADD_MAILBOX', value: value }; Cryptpad.setPadMetadata(msg, function (res) { if (res.error) { console.error(res.error); } }); } catch (err) { return void console.error(err); } }); sframeChan.on('Q_IMPORT_MEDIATAG', function (obj, cb) { var key = obj.key; var channel = obj.channel; var hash = Utils.Hash.getFileHashFromKeys({ version: 1, channel: channel, keys: { fileKeyStr: key } }); var href = '/file/#' + hash; var data = { title: obj.name, href: href, channel: channel, owners: obj.owners, forceSave: true, }; Cryptpad.setPadTitle(data, function (err) { Cryptpad.setPadAttribute('fileType', obj.type, null, href); cb(err); }); }); sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) { Cryptpad.setDisplayName(newName, function (err) { if (err) { console.log("Couldn't set username"); console.error(err); cb('ERROR'); return; } Cryptpad.changeMetadata(); cb(); }); }); sframeChan.on('Q_LOGOUT', function (data, cb) { Utils.LocalStore.logout(cb); }); sframeChan.on('EV_NOTIFY', function (data) { Notifier.notify(data); }); sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) { cb = cb || $.noop; if (readOnly && hashes.editHash) { var appPath = window.location.pathname; Cryptpad.moveToTrash(cb, appPath + '#' + hashes.editHash); return; } Cryptpad.moveToTrash(cb); }); sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) { Cryptpad.saveAsTemplate(Cryptget.put, data, cb); }); // Messaging sframeChan.on('Q_SEND_FRIEND_REQUEST', function (data, cb) { Cryptpad.messaging.sendFriendRequest(data, cb); }); sframeChan.on('Q_ANSWER_FRIEND_REQUEST', function (data, cb) { Cryptpad.messaging.answerFriendRequest(data, cb); }); // History sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) { var crypto = Crypto.createEncryptor(secret.keys); Cryptpad.getFullHistory({ channel: secret.channel, validateKey: secret.keys.validateKey }, function (encryptedMsgs) { var nt = nThen; var decryptedMsgs = []; var total = encryptedMsgs.length; encryptedMsgs.forEach(function (msg, i) { nt = nt(function (waitFor) { // The 3rd parameter "true" means we're going to skip signature validation. // We don't need it since the message is already validated serverside by hk decryptedMsgs.push(crypto.decrypt(msg, true, true)); setTimeout(waitFor(function () { sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total); })); }).nThen; }); nt(function () { cb(decryptedMsgs); }); }); }); sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) { var nSecret = secret; if (cfg.isDrive) { // Shared folder or user hash or fs hash var hash = Utils.LocalStore.getUserHash() || Utils.LocalStore.getFSHash(); if (data.sharedFolder) { hash = data.sharedFolder.hash; } if (hash) { var password = (data.sharedFolder && data.sharedFolder.password) || undefined; nSecret = Utils.Hash.getSecrets('drive', hash, password); } } var channel = nSecret.channel; var validate = nSecret.keys.validateKey; var crypto = Crypto.createEncryptor(nSecret.keys); Cryptpad.getHistoryRange({ channel: channel, validateKey: validate, lastKnownHash: data.lastKnownHash }, function (data) { cb({ isFull: data.isFull, messages: data.messages.map(function (msg) { // The 3rd parameter "true" means we're going to skip signature validation. // We don't need it since the message is already validated serverside by hk return crypto.decrypt(msg, true, true); }), lastKnownHash: data.lastKnownHash }); }); }); // Store sframeChan.on('Q_GET_PAD_ATTRIBUTE', function (data, cb) { var href; if (readOnly && hashes.editHash) { // If we have a stronger hash, use it for pad attributes href = window.location.pathname + '#' + hashes.editHash; } if (data.href) { href = data.href; } Cryptpad.getPadAttribute(data.key, function (e, data) { cb({ error: e, data: data }); }, href); }); sframeChan.on('Q_SET_PAD_ATTRIBUTE', function (data, cb) { var href; if (readOnly && hashes.editHash) { // If we have a stronger hash, use it for pad attributes href = window.location.pathname + '#' + hashes.editHash; } if (data.href) { href = data.href; } Cryptpad.setPadAttribute(data.key, data.value, function (e) { cb({error:e}); }, href); }); sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) { Cryptpad.getDeletedPads(data, function (err, obj) { if (err) { return void console.error(err); } cb(obj); }); }); sframeChan.on('Q_SESSIONSTORAGE_PUT', function (data, cb) { if (typeof (data.value) === "undefined") { delete sessionStorage[data.key]; } else { sessionStorage[data.key] = data.value; } cb(); }); sframeChan.on('Q_IS_ONLY_IN_SHARED_FOLDER', function (data, cb) { Cryptpad.isOnlyInSharedFolder(secret.channel, function (err, t) { if (err) { return void cb({error: err}); } cb(t); }); }); // Present mode URL sframeChan.on('Q_PRESENT_URL_GET_VALUE', function (data, cb) { var parsed = Utils.Hash.parsePadUrl(window.location.href); cb(parsed.hashData && parsed.hashData.present); }); sframeChan.on('EV_PRESENT_URL_SET_VALUE', function (data) { var parsed = Utils.Hash.parsePadUrl(window.location.href); window.location.href = parsed.getUrl({ embed: parsed.hashData.embed, present: data }); }); // File upload var onFileUpload = function (sframeChan, data, cb) { require(['/common/outer/upload.js'], function (Files) { var sendEvent = function (data) { sframeChan.event("EV_FILE_UPLOAD_STATE", data); }; var updateProgress = function (progressValue) { sendEvent({ uid: data.uid, progress: progressValue }); }; var onComplete = function (href) { sendEvent({ complete: true, uid: data.uid, href: href }); }; var onError = function (e) { sendEvent({ uid: data.uid, error: e }); }; var onPending = function (cb) { sframeChan.query('Q_CANCEL_PENDING_FILE_UPLOAD', { uid: data.uid }, function (err, data) { if (data) { cb(); } }); }; data.blob = Crypto.Nacl.util.decodeBase64(data.blob); Files.upload(data, data.noStore, Cryptpad, updateProgress, onComplete, onError, onPending); cb(); }); }; sframeChan.on('Q_UPLOAD_FILE', function (data, cb) { onFileUpload(sframeChan, data, cb); }); // File picker var FP = {}; var initFilePicker = function (cfg) { // cfg.hidden means pre-loading the filepicker while keeping it hidden. // if cfg.hidden is true and the iframe already exists, do nothing if (!FP.$iframe) { var config = {}; config.onFilePicked = function (data) { sframeChan.event('EV_FILE_PICKED', data); }; config.onClose = function () { FP.$iframe.hide(); }; config.onFileUpload = onFileUpload; config.types = cfg; config.addCommonRpc = addCommonRpc; config.modules = { Cryptpad: Cryptpad, SFrameChannel: SFrameChannel, Utils: Utils }; FP.$iframe = $('