define([ 'jquery', '/api/config', '/customize/messages.js', '/common/fsStore.js', '/common/common-util.js', '/common/common-hash.js', '/common/common-messaging.js', '/common/common-realtime.js', '/common/common-language.js', '/common/common-constants.js', '/common/common-feedback.js', '/common/outer/local-store.js', '/common/outer/store-rpc.js', '/common/pinpad.js', '/customize/application_config.js', '/bower_components/nthen/index.js', ], function ($, Config, Messages, Store, Util, Hash, Messaging, Realtime, Language, Constants, Feedback, LocalStore, AStore, Pinpad, AppConfig, Nthen) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata about pads to your local storage for future use and improved usability. Additionally, there is some basic functionality for import/export. */ var postMessage = function (cmd, data, cb) { setTimeout(function () { AStore.query(cmd, data, cb); }); }; var tryParsing = function (x) { try { return JSON.parse(x); } catch (e) { console.error(e); return null; } }; var origin = encodeURIComponent(window.location.hostname); var common = window.Cryptpad = { Messages: Messages, donateURL: 'https://accounts.cryptpad.fr/#/donate?on=' + origin, upgradeURL: 'https://accounts.cryptpad.fr/#/?on=' + origin, account: {}, }; var PINNING_ENABLED = AppConfig.enablePinning; var store; var rpc; var anon_rpc; var getProxy = common.getProxy = function () { if (store && store.getProxy()) { return store.getProxy().proxy; } }; var getNetwork = common.getNetwork = function () { if (store) { if (store.getProxy() && store.getProxy().info) { return store.getProxy().info.network; } } return; }; // RESTRICTED // Settings only common.getUserObject = function (cb) { postMessage("GET", [], function (obj) { cb(obj); }); }; common.resetDrive = function (cb) { postMessage("RESET_DRIVE", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.logoutFromAll = function (cb) { var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER); localStorage.setItem(Constants.tokenKey, token); postMessage("SET", { key: [Constants.tokenKey], value: token }, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; // Settings and auth common.getUserObject = function (cb) { postMessage("GET", [], function (obj) { cb(obj); }); }; // Settings and ready common.mergeAnonDrive = function (cb) { var data = { anonHash: LocalStore.getFSHash() }; postMessage("MIGRATE_ANON_DRIVE", data, cb); }; // Profile common.getProfileEditUrl = function (cb) { postMessage("GET", ['profile', 'edit'], function (obj) { cb(obj); }); }; common.setNewProfile = function (profile) { postMessage("SET", { key: ['profile'], value: profile }, function () {}); }; // REFACTOR pull language directly? common.getLanguage = function () { return Messages._languageUsed; }; common.setLanguage = function (l, cb) { Language.setLanguage(l, null, cb); }; // REAFCTOR store.getProfile should be store.get(['profile']) common.getMetadata = function (cb) { postMessage("GET_METADATA", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(null, obj); }); }; var getRealtime = common.getRealtime = function () { if (store && store.getProxy() && store.getProxy().info) { return store.getProxy().info.realtime; } return; }; // TODO not needed with async store common.hasSigningKeys = function (proxy) { return typeof(proxy) === 'object' && typeof(proxy.edPrivate) === 'string' && typeof(proxy.edPublic) === 'string'; }; // TODO not needed with async store common.hasCurveKeys = function (proxy) { return typeof(proxy) === 'object' && typeof(proxy.curvePrivate) === 'string' && typeof(proxy.curvePublic) === 'string'; }; var makePad = common.makePad = function (href, title) { var now = +new Date(); return { href: href, atime: now, ctime: now, title: title || Hash.getDefaultName(Hash.parsePadUrl(href)), }; }; common.setDisplayName = function (value, cb) { postMessage("SET_DISPLAY_NAME", value, cb); }; // STORAGE common.setPadAttribute = function (attr, value, cb, href) { href = Hash.getRelativeHref(href || window.location.href); postMessage("SET_PAD_ATTRIBUTE", { href: href, attr: attr, value: value }, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(); }); }; common.getPadAttribute = function (attr, cb) { var href = Hash.getRelativeHref(window.location.href); postMessage("GET_PAD_ATTRIBUTE", { href: href, attr: attr, }, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); }; common.setAttribute = function (attr, value, cb) { postMessage("SET_ATTRIBUTE", { attr: attr, value: value }, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(); }); }; common.getAttribute = function (attr, cb) { postMessage("GET_ATTRIBUTE", { attr: attr }, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); }; // Tags common.resetTags = function (href, tags, cb) { // set pad attribute cb = cb || $.noop; if (!Array.isArray(tags)) { return void cb('INVALID_TAGS'); } common.setPadAttribute('tags', tags.slice(), cb, href); }; common.tagPad = function (href, tag, cb) { if (typeof(cb) !== 'function') { return void console.error('EXPECTED_CALLBACK'); } if (typeof(tag) !== 'string') { return void cb('INVALID_TAG'); } common.getPadAttribute('tags', function (e, tags) { if (e) { return void cb(e); } var newTags; if (!tags) { newTags = [tag]; } else if (tags.indexOf(tag) === -1) { newTags = tags.slice(); newTags.push(tag); } common.setPadAttribute('tags', newTags, cb, href); }, href); }; common.untagPad = function (href, tag, cb) { if (typeof(cb) !== 'function') { return void console.error('EXPECTED_CALLBACK'); } if (typeof(tag) !== 'string') { return void cb('INVALID_TAG'); } common.getPadAttribute('tags', function (e, tags) { if (e) { return void cb(e); } if (!tags) { return void cb(); } var idx = tags.indexOf(tag); if (idx === -1) { return void cb(); } var newTags = tags.slice(); newTags.splice(idx, 1); common.setPadAttribute('tags', newTags, cb, href); }, href); }; common.getPadTags = function (href, cb) { if (typeof(cb) !== 'function') { return; } common.getPadAttribute('tags', function (e, tags) { if (e) { return void cb(e); } cb(void 0, tags ? tags.slice() : []); }, href); }; common.listAllTags = function (cb) { postMessage("LIST_ALL_TAGS", null, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(void 0, obj); }); }; // STORAGE - TEMPLATES common.listTemplates = function (type, cb) { postMessage("GET_TEMPLATES", null, function (obj) { if (obj && obj.error) { return void cb(obj.error); } if (!Array.isArray(obj)) { return void cb ('NOT_AN_ARRAY'); } if (!type) { return void cb(obj); } var templates = obj.filter(function (f) { var parsed = Hash.parsePadUrl(f.href); return parsed.type === type; }); cb(templates); }); }; common.saveAsTemplate = function (Cryptput, data, cb) { var p = Hash.parsePadUrl(window.location.href); if (!p.type) { return; } var hash = Hash.createRandomHash(); var href = '/' + p.type + '/#' + hash; Cryptput(hash, data.toSave, function (e) { if (e) { throw new Error(e); } postMessage("ADD_PAD", { href: href, title: data.title }, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(); }); }); }; common.isTemplate = function (href, cb) { var rhref = Hash.getRelativeHref(href); common.listTemplates(null, function (templates) { cb(void 0, templates.some(function (t) { return t.href === rhref; })); }); }; common.useTemplate = function (href, Crypt, cb) { var parsed = Hash.parsePadUrl(href); if(!parsed) { throw new Error("Cannot get template hash"); } Crypt.get(parsed.hash, function (err, val) { if (err) { throw new Error(err); } var p = Hash.parsePadUrl(window.location.href); Crypt.put(p.hash, val, cb); }); }; // Forget button common.moveToTrash = function (cb, href) { href = href || window.location.href; postMessage("MOVE_TO_TRASH", { href: href }, cb); }; // When opening a new pad or renaming it, store the new title common.setPadTitle = function (title, padHref, cb) { var href = padHref || window.location.href; var parsed = Hash.parsePadUrl(href); if (!parsed.hash) { return; } href = parsed.getUrl({present: parsed.present}); if (title === null) { return; } if (title.trim() === "") { title = Hash.getDefaultName(parsed); } postMessage("SET_PAD_TITLE", { href: href, title: title }, function (obj) { if (obj && obj.error) { console.log("unable to set pad title"); return void cb(obj.error); } cb(); }); }; // Needed for the secure filepicker app common.getSecureFilesList = function (query, cb) { postMessage("GET_SECURE_FILES_LIST", query, function (list) { cb(void 0, list); }); }; common.arePinsSynced = function (cb) { postMessage("ARE_PINS_SYNCED", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.resetPins = function (cb) { postMessage("RESET_PINS", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.pinPads = function (pads, cb) { postMessage("PIN_PADS", {pads: pads}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.unpinPads = function (pads, cb) { postMessage("UNPIN_PADS", {pads: pads}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.getPinnedUsage = function (cb) { postMessage("GET_PINNED_USAGE", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; // SFRAME: talk to anon_rpc from the iframe common.anonRpcMsg = function (msg, data, cb) { if (!msg) { return; } postMessage("ANON_RPC_MESSAGE", { msg: msg, data: data }, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.getFileSize = function (href, cb) { postMessage("GET_FILE_SIZE", {href: href}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(undefined, obj.size); }); }; // TODO not used anymore? common.getMultipleFileSize = function (files, cb) { postMessage("GET_MULTIPLE_FILE_SIZE", {files:files}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(undefined, obj.size); }); }; common.updatePinLimit = function (cb) { postMessage("UPDATE_PIN_LIMIT", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(undefined, obj.limit, obj.plan, obj.note); }); }; common.getPinLimit = function (cb) { postMessage("GET_PIN_LIMIT", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(undefined, obj.limit, obj.plan, obj.note); }); }; common.isOverPinLimit = function (cb) { if (!LocalStore.isLoggedIn()) { return void cb(null, false); } var usage; var andThen = function (e, limit, plan) { if (e) { return void cb(e); } var data = {usage: usage, limit: limit, plan: plan}; if (usage > limit) { return void cb (null, true, data); } return void cb (null, false, data); }; var todo = function (e, used) { if (e) { return void cb(e); } usage = used; common.getPinLimit(andThen); }; common.getPinnedUsage(todo); }; common.clearOwnedChannel = function (channel, cb) { postMessage("CLEAR_OWNED_CHANNEL", {channel: channel}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.uploadComplete = function (cb) { postMessage("UPLOAD_COMPLETE", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.uploadStatus = function (size, cb) { postMessage("UPLOAD_STATUS", {size: size}, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; common.uploadCancel = function (cb) { postMessage("UPLOAD_CANCEL", null, function (obj) { if (obj.error) { return void cb(obj.error); } cb(); }); }; // HERE common.getShareHashes = function (secret, cb) { if (!window.location.hash) { var hashes = Hash.getHashes(secret.channel, secret); return void cb(null, hashes); } var parsed = Hash.parsePadUrl(window.location.href); if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); } if (parsed.type === 'file') { secret.channel = Util.base64ToHex(secret.channel); } var hashes = Hash.getHashes(secret.channel, secret); if (!hashes.editHash && !hashes.viewHash && parsed.hashData && !parsed.hashData.mode) { // It means we're using an old hash hashes.editHash = window.location.hash.slice(1); return void cb(null, hashes); } postMessage("GET_STRONGER_HASH", { href: href }, function (hash) { if (hash) { hashes.editHash = hash; } cb(null, hashes); }); }; var CRYPTPAD_VERSION = 'cryptpad-version'; var updateLocalVersion = function () { // Check for CryptPad updates var urlArgs = Config.requireConf ? Config.requireConf.urlArgs : null; if (!urlArgs) { return; } var arr = /ver=([0-9.]+)(-[0-9]*)?/.exec(urlArgs); var ver = arr[1]; if (!ver) { return; } var verArr = ver.split('.'); verArr[2] = 0; if (verArr.length !== 3) { return; } var stored = localStorage[CRYPTPAD_VERSION] || '0.0.0'; var storedArr = stored.split('.'); storedArr[2] = 0; var shouldUpdate = parseInt(verArr[0]) > parseInt(storedArr[0]) || (parseInt(verArr[0]) === parseInt(storedArr[0]) && parseInt(verArr[1]) > parseInt(storedArr[1])); if (!shouldUpdate) { return; } localStorage[CRYPTPAD_VERSION] = ver; }; var _onMetadataChanged = []; common.onMetadataChanged = function (h) { if (typeof(h) !== "function") { return; } if (_onMetadataChanged.indexOf(h) !== -1) { return; } _onMetadataChanged.push(h); }; common.changeMetadata = function () { _onMetadataChanged.forEach(function (h) { h(); }); }; var requestLogin = function () { // log out so that you don't go into an endless loop... LocalStore.logout(); // redirect them to log in, and come back when they're done. sessionStorage.redirectTo = window.location.href; window.location.href = '/login/'; }; var onMessage = function (cmd, data, cb) { cb = cb || function () {}; switch (cmd) { case 'REQUEST_LOGIN': { requestLogin(); break; } case 'UPDATE_METADATA': { common.changeMetadata(); break; } case 'UPDATE_TOKEN': { var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); if (localToken !== data.token) { requestLogin(); } break; } } }; common.ready = (function () { var env = {}; var initialized = false; return function (f) { if (initialized) { return void setTimeout(function () { f(void 0, env); }); } // TODO if (sessionStorage[Constants.newPadPathKey]) { common.initialPath = sessionStorage[Constants.newPadPathKey]; delete sessionStorage[Constants.newPadPathKey]; } var provideFeedback = function () { if (typeof(window.Proxy) === 'undefined') { Feedback.send("NO_PROXIES"); } var shimPattern = /CRYPTPAD_SHIM/; if (shimPattern.test(Array.isArray.toString())) { Feedback.send("NO_ISARRAY"); } if (shimPattern.test(Array.prototype.fill.toString())) { Feedback.send("NO_ARRAYFILL"); } if (typeof(Symbol) === 'undefined') { Feedback.send('NO_SYMBOL'); } Feedback.reportScreenDimensions(); Feedback.reportLanguage(); }; var initFeedback = function (feedback) { // Initialize feedback Feedback.init(feedback); provideFeedback(); }; Nthen(function (waitFor) { var cfg = { query: onMessage, // TODO temporary, will be replaced by a webworker channel userHash: LocalStore.getUserHash(), anonHash: LocalStore.getFSHash(), localToken: localStorage.getItem(Constants.tokenKey) }; AStore.query("CONNECT", cfg, waitFor(function (data) { if (data.error) { throw new Error(data.error); } if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); } if (cfg.userHash && sessionStorage) { // copy User_hash into sessionStorage because cross-domain iframes // on safari replaces localStorage with sessionStorage or something sessionStorage.setItem(Constants.userHashKey, cfg.userHash); } if (cfg.userHash) { var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); if (localToken === null) { // if that number hasn't been set to localStorage, do so. localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]); } } // TODO ww //Messaging.addDirectMessageHandler(common); initFeedback(data.feedback); })); }).nThen(function (waitFor) { $(waitFor()); }).nThen(function (waitFor) { // Load the new pad when the hash has changed var oldHref = document.location.href; window.onhashchange = function () { var newHref = document.location.href; var parsedOld = Hash.parsePadUrl(oldHref).hashData; var parsedNew = Hash.parsePadUrl(newHref).hashData; if (parsedOld && parsedNew && ( parsedOld.type !== parsedNew.type || parsedOld.channel !== parsedNew.channel || parsedOld.mode !== parsedNew.mode || parsedOld.key !== parsedNew.key)) { if (!parsedOld.channel) { oldHref = newHref; return; } document.location.reload(); return; } if (parsedNew) { oldHref = newHref; } }; // Listen for login/logout in other tabs window.addEventListener('storage', function (e) { if (e.key !== Constants.userHashKey) { return; } var o = e.oldValue; var n = e.newValue; if (!o && n) { document.location.reload(); } else if (o && !n) { LocalStore.logout(); postMessage("DISCONNECT"); } }); if (PINNING_ENABLED && LocalStore.isLoggedIn()) { console.log("logged in. pads will be pinned"); postMessage("INIT_RPC", null, waitFor(function () { console.log('RPC handshake complete'); })); } else if (PINNING_ENABLED) { console.log('not logged in. pads will not be pinned'); } else { console.log('pinning disabled'); } postMessage("INIT_ANON_RPC", null, waitFor(function () { console.log('Anonymous RPC ready'); })); }).nThen(function (waitFor) { if (sessionStorage.createReadme) { var data = { driveReadme: Messages.driveReadme, driveReadmeTitle: Messages.driveReadmeTitle, }; postMessage("CREATE_README", data, waitFor(function (e) { if (e && e.error) { return void console.error(e.error); } delete sessionStorage.createReadme; })); } }).nThen(function (waitFor) { if (sessionStorage.migrateAnonDrive) { common.mergeAnonDrive(waitFor(function() { delete sessionStorage.migrateAnonDrive; })); } }).nThen(function () { updateLocalVersion(); f(void 0, env); if (typeof(window.onhashchange) === 'function') { window.onhashchange(); } }); }; }()); // MAGIC that happens implicitly $(function () { Language.applyTranslation(); }); return common; });