define([ 'jquery', '/bower_components/nthen/index.js', '/customize/messages.js', '/common/sframe-chainpad-netflux-inner.js', '/common/outer/worker-channel.js', '/common/sframe-common-title.js', '/common/common-ui-elements.js', '/common/sframe-common-history.js', '/common/sframe-common-file.js', '/common/sframe-common-codemirror.js', '/common/sframe-common-cursor.js', '/common/sframe-common-mailbox.js', '/common/inner/common-mediatag.js', '/common/metadata-manager.js', '/customize/application_config.js', '/common/common-realtime.js', '/common/common-util.js', '/common/common-hash.js', '/common/common-thumbnail.js', '/common/common-interface.js', '/common/common-feedback.js', '/common/common-language.js', '/bower_components/localforage/dist/localforage.min.js' ], function ( $, nThen, Messages, CpNfInner, SFrameChannel, Title, UIElements, History, File, CodeMirror, Cursor, Mailbox, MT, MetadataMgr, AppConfig, CommonRealtime, Util, Hash, Thumb, UI, Feedback, Language, localForage ) { // Chainpad Netflux Inner var funcs = {}; var ctx = {}; funcs.Messages = Messages; var evRealtimeSynced = Util.mkEvent(true); funcs.startRealtime = function (options) { if (ctx.cpNfInner) { return ctx.cpNfInner; } options.sframeChan = ctx.sframeChan; options.metadataMgr = ctx.metadataMgr; ctx.cpNfInner = CpNfInner.start(options); ctx.cpNfInner.metadataMgr.onChangeLazy(options.onLocal); ctx.cpNfInner.whenRealtimeSyncs(function () { evRealtimeSynced.fire(); }); return ctx.cpNfInner; }; funcs.getMetadataMgr = function () { return ctx.metadataMgr; }; funcs.getSframeChannel = function () { return ctx.sframeChan; }; funcs.getAppConfig = function () { return AppConfig; }; funcs.isLoggedIn = function () { return ctx.metadataMgr.getPrivateData().loggedIn; }; // MISC // Call the selected function with 'funcs' as a (new) first parameter var callWithCommon = function (f) { return function () { [].unshift.call(arguments, funcs); return f.apply(null, arguments); }; }; // UI window.CryptPad_UI = UI; window.CryptPad_UIElements = UIElements; window.CryptPad_common = funcs; funcs.createUserAdminMenu = callWithCommon(UIElements.createUserAdminMenu); funcs.openFilePicker = callWithCommon(UIElements.openFilePicker); funcs.openTemplatePicker = callWithCommon(UIElements.openTemplatePicker); funcs.displayMediatagImage = callWithCommon(MT.displayMediatagImage); funcs.displayAvatar = callWithCommon(MT.displayAvatar); funcs.createButton = callWithCommon(UIElements.createButton); funcs.createUsageBar = callWithCommon(UIElements.createUsageBar); funcs.updateTags = callWithCommon(UIElements.updateTags); funcs.createLanguageSelector = callWithCommon(UIElements.createLanguageSelector); funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar); funcs.createHelpMenu = callWithCommon(UIElements.createHelpMenu); funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen); funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning); funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal); funcs.onServerError = callWithCommon(UIElements.onServerError); funcs.addMentions = callWithCommon(UIElements.addMentions); funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu); funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview); funcs.getMediaTag = callWithCommon(MT.getMediaTag); // Thumb funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail); funcs.addThumbnail = Thumb.addThumbnail; // History funcs.getHistory = callWithCommon(History.create); // Title module funcs.createTitle = callWithCommon(Title.create); // Cursor funcs.createCursor = callWithCommon(Cursor.create); // Files funcs.uploadFile = callWithCommon(File.uploadFile); funcs.createFileManager = callWithCommon(File.create); funcs.getMediatagScript = function () { var origin = ctx.metadataMgr.getPrivateData().origin; return ''; }; funcs.getMediatagFromHref = function (obj) { if (!obj || !obj.hash) { return; } var data = ctx.metadataMgr.getPrivateData(); var secret = Hash.getSecrets('file', obj.hash, obj.password); if (secret.keys && secret.channel) { var key = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var hexFileName = secret.channel; var origin = data.fileHost || data.origin; var src = origin + Hash.getBlobPathFromHex(hexFileName); return '' + ''; } return; }; funcs.importMediaTag = function ($mt) { if (!$mt || !$mt.is('media-tag')) { return; } var chanStr = $mt.attr('src'); var keyStr = $mt.attr('data-crypto-key'); // Remove origin var a = document.createElement('a'); a.href = chanStr; var src = a.pathname; // Get channel id var channel = src.replace(/\/blob\/[0-9a-f]{2}\//i, ''); // Get key var key = keyStr.replace(/cryptpad:/i, ''); var metadata = $mt[0]._mediaObject._blob.metadata; ctx.sframeChan.query('Q_IMPORT_MEDIATAG', { channel: channel, key: key, name: metadata.name, type: metadata.type, owners: metadata.owners }, function () { UI.log(Messages.saved); }); }; funcs.getFileSize = function (channelId, cb) { funcs.sendAnonRpcMsg("GET_FILE_SIZE", channelId, function (data) { if (!data) { return void cb("No response"); } if (data.error) { return void cb(data.error); } if (data.response && data.response.length && typeof(data.response[0]) === 'number') { return void cb(void 0, data.response[0]); } else { cb('INVALID_RESPONSE'); } }); }; // Universal direct channel var modules = {}; funcs.makeUniversal = function (type, cfg) { if (cfg && cfg.onEvent) { modules[type] = { onEvent: cfg.onEvent || function () {} }; } var sframeChan = funcs.getSframeChannel(); return { execCommand: function (cmd, data, cb) { sframeChan.query("Q_UNIVERSAL_COMMAND", { type: type, data: { cmd: cmd, data: data } }, function (err, obj) { if (err) { return void cb({error: err}); } cb(obj); }); } }; }; funcs.getAuthorId = function () { }; var authorUid = function(existing) { if (!Array.isArray(existing)) { existing = []; } var n; var i = 0; while (!n || existing.indexOf(n) !== -1 && i++ < 1000) { n = Math.floor(Math.random() * 1000000); } // If we can't find a valid number in 1000 iterations, use 0... if (existing.indexOf(n) !== -1) { n = 0; } return n; }; funcs.getAuthorId = function(authors, curve) { var existing = Object.keys(authors || {}).map(Number); if (!funcs.isLoggedIn()) { return authorUid(existing); } var uid; existing.some(function(id) { var author = authors[id] || {}; if (author.curvePublic !== curve) { return; } uid = Number(id); return true; }); return uid || authorUid(existing); }; // Chat var padChatChannel; // common-ui-elements needs to be able to get the chat channel to put it in metadata when // importing a template funcs.getPadChat = function () { return padChatChannel; }; funcs.openPadChat = function (saveChanges) { var md = JSON.parse(JSON.stringify(ctx.metadataMgr.getMetadata())); //if (md.chat) { delete md.chat; } // Old channel without signing key // NOTE: "chat2" is also used in cryptpad-common's "useTemplate" var channel = md.chat2 || Hash.createChannelId(); if (!md.chat2) { md.chat2 = channel; ctx.metadataMgr.updateMetadata(md); setTimeout(saveChanges); } padChatChannel = channel; console.debug('Chat ID:', channel); ctx.sframeChan.query('Q_CHAT_OPENPADCHAT', channel, function (err, obj) { if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); } }); }; // Team Chat var teamChatChannel; funcs.setTeamChat = function (channel) { teamChatChannel = channel; }; funcs.getTeamChat = function () { return teamChatChannel; }; var cursorChannel; // common-ui-elements needs to be able to get the cursor channel to put it in metadata when // importing a template funcs.getCursorChannel = function () { return cursorChannel; }; funcs.openCursorChannel = function (saveChanges) { var md = JSON.parse(JSON.stringify(ctx.metadataMgr.getMetadata())); var channel = md.cursor; if (typeof(channel) !== 'string' || channel.length !== Hash.ephemeralChannelLength) { channel = Hash.createChannelId(true); // true indicates that it's an ephemeral channel } if (md.cursor !== channel) { md.cursor = channel; ctx.metadataMgr.updateMetadata(md); setTimeout(saveChanges); } cursorChannel = channel; ctx.sframeChan.query('Q_CURSOR_OPENCHANNEL', channel, function (err, obj) { if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); } }); }; // CodeMirror funcs.initCodeMirrorApp = callWithCommon(CodeMirror.create); // Window funcs.logout = function (cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_LOGOUT', null, cb); }; funcs.notify = function (data) { ctx.sframeChan.event('EV_NOTIFY', data); }; funcs.setTabTitle = function (newTitle) { ctx.sframeChan.event('EV_SET_TAB_TITLE', newTitle); }; funcs.setHash = function (hash) { ctx.sframeChan.event('EV_SET_HASH', hash); }; funcs.setLoginRedirect = function (cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_SET_LOGIN_REDIRECT', null, cb); }; funcs.isPresentUrl = function (cb) { ctx.sframeChan.query('Q_PRESENT_URL_GET_VALUE', null, cb); }; funcs.setPresentUrl = function (value) { ctx.sframeChan.event('EV_PRESENT_URL_SET_VALUE', value); }; // Store funcs.handleNewFile = function (waitFor, config) { if (window.__CRYPTPAD_TEST__) { return; } var priv = ctx.metadataMgr.getPrivateData(); if (priv.isNewFile) { var c = (priv.settings.general && priv.settings.general.creation) || {}; // If this is a new file but we have a hash in the URL and pad creation screen is // not displayed, then display an error... if (priv.isDeleted && !funcs.isLoggedIn()) { UI.errorLoadingScreen(Messages.inactiveError, false, function () { UI.addLoadingScreen(); return void funcs.createPad({}, waitFor()); }); return; } // Otherwise, if we don't display the screen, it means it is not a deleted pad // so we can continue and start realtime... if (!funcs.isLoggedIn()) { return void funcs.createPad(c, waitFor()); } // If we display the pad creation screen, it will handle deleted pads directly funcs.getPadCreationScreen(c, config, waitFor()); return; } if (priv.burnAfterReading) { UIElements.displayBurnAfterReadingPage(funcs, waitFor(function () { UI.addLoadingScreen(); ctx.sframeChan.event('EV_BURN_AFTER_READING'); })); } }; funcs.createPad = function (cfg, cb) { ctx.sframeChan.query("Q_CREATE_PAD", { owned: cfg.owned, expire: cfg.expire, password: cfg.password, team: cfg.team, template: cfg.template, templateId: cfg.templateId }, cb); }; funcs.isOwned = function (owners) { var priv = ctx.metadataMgr.getPrivateData(); var edPublic = priv.edPublic; var owned = false; if (Array.isArray(owners) && owners.length) { if (owners.indexOf(edPublic) !== -1) { owned = true; } else { Object.keys(priv.teams || {}).some(function (id) { var team = priv.teams[id] || {}; if (team.viewer) { return; } if (owners.indexOf(team.edPublic) === -1) { return; } owned = Number(id); return true; }); } } return owned; }; funcs.isPadStored = function (cb) { ctx.sframeChan.query("Q_IS_PAD_STORED", null, function (err, obj) { cb (err || (obj && obj.error), obj); }); }; funcs.sendAnonRpcMsg = function (msg, content, cb) { ctx.sframeChan.query('Q_ANON_RPC_MESSAGE', { msg: msg, content: content }, function (err, data) { if (cb) { cb(data); } }); }; funcs.getPinUsage = function (teamId, cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_PIN_GET_USAGE', teamId, function (err, data) { cb(err || data.error, data.data); }); }; funcs.isOverPinLimit = function (cb) { ctx.sframeChan.query('Q_GET_PIN_LIMIT_STATUS', null, function (err, data) { cb(data.error, data.overLimit, data.limits); }); }; // href is optional here: if not provided, we use the href of the current tab funcs.getPadAttribute = function (key, cb, href) { ctx.sframeChan.query('Q_GET_PAD_ATTRIBUTE', { key: key, href: href }, function (err, res) { cb(err || res.error, res && res.data); }); }; funcs.setPadAttribute = function (key, value, cb, href) { cb = cb || $.noop; ctx.sframeChan.query('Q_SET_PAD_ATTRIBUTE', { key: key, href: href, value: value }, cb); }; funcs.getAttribute = function (key, cb) { ctx.sframeChan.query('Q_GET_ATTRIBUTE', { key: key }, function (err, res) { cb (err || res.error, res.data); }); }; funcs.setAttribute = function (key, value, cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_SET_ATTRIBUTE', { key: key, value: value }, cb); }; // Thumbnails funcs.setThumbnail = function (key, value, cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_THUMBNAIL_SET', { key: key, value: value }, cb); }; funcs.getThumbnail = function (key, cb) { ctx.sframeChan.query('Q_THUMBNAIL_GET', { key: key }, function (err, res) { cb (err || res.error, res.data); }); }; funcs.sessionStorage = { put: function (key, value, cb) { ctx.sframeChan.query('Q_SESSIONSTORAGE_PUT', { key: key, value: value }, cb); } }; funcs.setDisplayName = function (name, cb) { cb = cb || $.noop; ctx.sframeChan.query('Q_SETTINGS_SET_DISPLAY_NAME', name, cb); }; funcs.mergeAnonDrive = function (cb) { ctx.sframeChan.query('Q_MERGE_ANON_DRIVE', null, cb); }; // Create friend request funcs.getPendingFriends = function () { return ctx.metadataMgr.getPrivateData().pendingFriends; }; funcs.sendFriendRequest = function (data, cb) { ctx.sframeChan.query('Q_SEND_FRIEND_REQUEST', data, cb); }; // Friend requests received var friendRequests = {}; funcs.addFriendRequest = function (data) { var curve = Util.find(data, ['content', 'msg', 'author']); friendRequests[curve] = data; }; funcs.removeFriendRequest = function (hash) { Object.keys(friendRequests).some(function (curve) { var h = Util.find(friendRequests[curve], ['content', 'hash']); if (h === hash) { delete friendRequests[curve]; return true; } }); }; funcs.getFriendRequests = function () { return JSON.parse(JSON.stringify(friendRequests)); }; funcs.getFriends = function (meIncluded) { var priv = ctx.metadataMgr.getPrivateData(); var friends = priv.friends; var goodFriends = {}; Object.keys(friends).forEach(function (curve) { if (curve.length !== 44 && !meIncluded) { return; } var data = friends[curve]; if (!data.notifications) { return; } goodFriends[curve] = friends[curve]; }); return goodFriends; }; // Feedback funcs.prepareFeedback = function (key) { if (typeof(key) !== 'string') { return $.noop; } var type = ctx.metadataMgr.getMetadata().type; return function () { Feedback.send((key + (type? '_' + type: '')).toUpperCase()); }; }; // RESTRICTED // Filepicker app funcs.getFilesList = function (types, cb) { ctx.sframeChan.query('Q_GET_FILES_LIST', types, function (err, data) { cb(err || data.error, data.data); }); }; /* funcs.storeLinkToClipboard = function (readOnly, cb) { ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) { if (cb) { cb(err); } }); }; */ funcs.getPad = function (data, cb) { ctx.sframeChan.query("Q_CRYPTGET", data, function (err, obj) { if (err) { return void cb(err); } if (obj.error) { return void cb(obj.error); } cb(null, obj.data); }, { timeout: 60000 }); }; funcs.getPadMetadata = function (data, cb) { ctx.sframeChan.query('Q_GET_PAD_METADATA', data, function (err, val) { if (err || (val && val.error)) { return void cb({error: err || val.error}); } cb(val); }); }; funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); }; funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); }; funcs.openUnsafeURL = function (url) { var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(url); window.open(bounceHref); }; funcs.fixLinks = function (domElement) { var origin = ctx.metadataMgr.getPrivateData().origin; $(domElement).find('a[target="_blank"]').click(function (e) { e.preventDefault(); e.stopPropagation(); var href = $(this).attr('href'); var absolute = /^https?:\/\//i; if (!absolute.test(href)) { if (href.slice(0,1) !== '/') { href = '/' + href; } href = origin + href; } funcs.openUnsafeURL(href); }); $(domElement).find('a[target!="_blank"]').click(function (e) { e.preventDefault(); e.stopPropagation(); funcs.gotoURL($(this).attr('href')); }); return $(domElement)[0]; }; funcs.whenRealtimeSyncs = evRealtimeSynced.reg; var logoutHandlers = []; funcs.onLogout = function (h) { if (typeof (h) !== "function") { return; } if (logoutHandlers.indexOf(h) !== -1) { return; } logoutHandlers.push(h); }; var shortcuts = []; funcs.addShortcuts = function (w, isApp) { w = w || window; if (shortcuts.indexOf(w) !== -1) { return; } shortcuts.push(w); $(w).keydown(function (e) { // Ctrl || Meta (mac) if (e.ctrlKey || (navigator.platform === "MacIntel" && e.metaKey)) { // Ctrl+E: New pad modal if (e.which === 69 && isApp) { e.preventDefault(); return void funcs.createNewPadModal(); } // Ctrl+S: prevent default (save) if (e.which === 83) { return void e.preventDefault(); } } }); }; funcs.mailbox = {}; Object.freeze(funcs); return { create: function (cb) { if (window.CryptPad_sframe_common) { throw new Error("Sframe-common should only be created once"); } window.CryptPad_sframe_common = true; if (window.CryptPad_updateLoadingProgress) { window.CryptPad_updateLoadingProgress({ type: 'drive', progress: 0 }); } nThen(function (waitFor) { var msgEv = Util.mkEvent(); var iframe = window.parent; window.addEventListener('message', function (msg) { if (msg.source !== iframe) { return; } msgEv.fire(msg); }); var postMsg = function (data) { iframe.postMessage(data, '*'); }; SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) { ctx.sframeChan = sfc; })); }).nThen(function (waitFor) { localForage.clear(); Language.applyTranslation(); ctx.metadataMgr = MetadataMgr.create(ctx.sframeChan); ctx.sframeChan.whenReg('EV_CACHE_PUT', function () { if (Object.keys(window.cryptpadCache.updated).length) { ctx.sframeChan.event('EV_CACHE_PUT', window.cryptpadCache.updated); } window.cryptpadCache._put = window.cryptpadCache.put; window.cryptpadCache.put = function (k, v, cb) { window.cryptpadCache._put(k, v, cb); var x = {}; x[k] = v; ctx.sframeChan.event('EV_CACHE_PUT', x); }; }); ctx.sframeChan.whenReg('EV_LOCALSTORE_PUT', function () { if (Object.keys(window.cryptpadStore.updated).length) { ctx.sframeChan.event('EV_LOCALSTORE_PUT', window.cryptpadStore.updated); } window.cryptpadStore._put = window.cryptpadStore.put; window.cryptpadStore.put = function (k, v, cb) { window.cryptpadStore._put(k, v, cb); var x = {}; x[k] = v; ctx.sframeChan.event('EV_LOCALSTORE_PUT', x); }; }); UI.addTooltips(); ctx.sframeChan.on("EV_PAD_NODATA", function () { UI.errorLoadingScreen(Messages.safeLinks_error); }); ctx.sframeChan.on("EV_PAD_PASSWORD", function (cfg) { UIElements.displayPasswordPrompt(funcs, cfg); }); ctx.sframeChan.on("EV_RESTRICTED_ERROR", function () { UI.errorLoadingScreen(Messages.restrictedError); }); ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () { UI.errorLoadingScreen(Messages.password_error_seed); }); ctx.sframeChan.on("EV_EXPIRED_ERROR", function () { funcs.onServerError({ type: 'EEXPIRED' }); }); ctx.sframeChan.on('EV_LOADING_INFO', function (data) { //UI.updateLoadingProgress(data, 'drive'); UI.updateLoadingProgress(data); }); ctx.sframeChan.on('EV_NEW_VERSION', function () { // TODO lock the UI and do the same in non-framework apps var $err = $('
').append(Messages.newVersionError); $err.find('a').click(function () { funcs.gotoURL(); }); UI.findOKButton().click(); UI.errorLoadingScreen($err, true, true); }); ctx.sframeChan.on('EV_AUTOSTORE_DISPLAY_POPUP', function (data) { UIElements.displayStorePadPopup(funcs, data); }); ctx.sframeChan.on('EV_LOADING_ERROR', function (err) { var msg = err; if (err === 'DELETED') { msg = Messages.deletedError + '
' + Messages.errorRedirectToHome; } if (err === "INVALID_HASH") { msg = Messages.invalidHashError; } UI.errorLoadingScreen(msg, false, function () { funcs.gotoURL('/drive/'); }); }); ctx.sframeChan.on('EV_UNIVERSAL_EVENT', function (obj) { var type = obj.type; if (!type || !modules[type]) { return; } modules[type].onEvent(obj.data); }); ctx.metadataMgr.onReady(waitFor()); }).nThen(function () { var privateData = ctx.metadataMgr.getPrivateData(); funcs.addShortcuts(window, Boolean(privateData.app)); try { var feedback = privateData.feedbackAllowed; Feedback.init(feedback); } catch (e) { Feedback.init(false); } try { var forbidden = privateData.disabledApp; if (forbidden) { UI.alert(Messages.disabledApp, function () { funcs.gotoURL('/drive/'); }, {forefront: true}); return; } var mustLogin = privateData.registeredOnly; if (mustLogin) { UI.alert(Messages.mustLogin, function () { funcs.setLoginRedirect(function () { funcs.gotoURL('/login/'); }); }, {forefront: true}); return; } } catch (e) { console.error("Can't check permissions for the app"); } try { window.CP_DEV_MODE = privateData.devMode; } catch (e) {} ctx.sframeChan.on('EV_LOGOUT', function () { $(window).on('keyup', function (e) { if (e.keyCode === 27) { UI.removeLoadingScreen(); } }); UI.addLoadingScreen({hideTips: true}); var origin = privateData.origin; var href = origin + "/login/"; var onLogoutMsg = Messages._getKey('onLogout', ['', '']); UI.errorLoadingScreen(onLogoutMsg, true); logoutHandlers.forEach(function (h) { if (typeof (h) === "function") { h(); } }); }); ctx.sframeChan.on('EV_WORKER_TIMEOUT', function () { UI.errorLoadingScreen(Messages.timeoutError, false, function () { funcs.gotoURL(''); }); }); ctx.sframeChan.on('EV_CHROME_68', function () { UI.alert(Messages.chrome68); }); funcs.isPadStored(function (err, val) { if (err || !val) { return; } UIElements.displayCrowdfunding(funcs); }); ctx.sframeChan.ready(); Mailbox.create(funcs); cb(funcs); }); } }; });