define([
'jquery',
'/api/config',
'/customize/messages.js',
'/common/fsStore.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-interface.js',
'/common/common-history.js',
'/common/common-userlist.js',
'/common/clipboard.js',
'/common/pinpad.js',
'/customize/application_config.js'
], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Clipboard, Pinpad, AppConfig) {
/* 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 common = window.Cryptpad = {
Messages: Messages,
Clipboard: Clipboard
};
// constants
var userHashKey = common.userHashKey = 'User_hash';
var userNameKey = common.userNameKey = 'User_name';
var fileHashKey = common.fileHashKey = 'FS_hash';
common.displayNameKey = 'cryptpad.username';
var newPadNameKey = common.newPadNameKey = "newPadName";
var newPadPathKey = common.newPadPathKey = "newPadPath";
var storageKey = common.storageKey = 'CryptPad_RECENTPADS';
var PINNING_ENABLED = AppConfig.enablePinning;
var store;
var rpc;
// import UI elements
common.findCancelButton = UI.findCancelButton;
common.findOKButton = UI.findOKButton;
common.listenForKeys = UI.listenForKeys;
common.stopListening = UI.stopListening;
common.prompt = UI.prompt;
common.confirm = UI.confirm;
common.alert = UI.alert;
common.log = UI.log;
common.warn = UI.warn;
common.spinner = UI.spinner;
common.addLoadingScreen = UI.addLoadingScreen;
common.removeLoadingScreen = UI.removeLoadingScreen;
common.errorLoadingScreen = UI.errorLoadingScreen;
// import common utilities for export
common.find = Util.find;
var fixHTML = common.fixHTML = Util.fixHTML;
common.hexToBase64 = Util.hexToBase64;
common.base64ToHex = Util.base64ToHex;
var deduplicateString = common.deduplicateString = Util.deduplicateString;
common.uint8ArrayToHex = Util.uint8ArrayToHex;
common.replaceHash = Util.replaceHash;
var getHash = common.getHash = Util.getHash;
common.fixFileName = Util.fixFileName;
common.bytesToMegabytes = Util.bytesToMegabytes;
common.bytesToKilobytes = Util.bytesToKilobytes;
// import hash utilities for export
var createRandomHash = common.createRandomHash = Hash.createRandomHash;
var parsePadUrl = common.parsePadUrl = Hash.parsePadUrl;
var isNotStrongestStored = common.isNotStrongestStored = Hash.isNotStrongestStored;
var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId;
var parseHash = common.parseHash = Hash.parseHash;
var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref;
common.getBlobPathFromHex = Hash.getBlobPathFromHex;
common.getEditHashFromKeys = Hash.getEditHashFromKeys;
common.getViewHashFromKeys = Hash.getViewHashFromKeys;
common.getSecrets = Hash.getSecrets;
common.getHashes = Hash.getHashes;
common.createChannelId = Hash.createChannelId;
common.findWeaker = Hash.findWeaker;
common.findStronger = Hash.findStronger;
common.serializeHash = Hash.serializeHash;
// Userlist
common.createUserList = UserList.create;
// History
common.getHistory = function (config) { return History.create(common, config); };
var getStore = common.getStore = function () {
if (store) { return store; }
throw new Error("Store is not ready!");
};
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;
};
var feedback = common.feedback = function (action, force) {
if (force !== true) {
if (!action) { return; }
try {
if (!getStore().getProxy().proxy.allowUserFeedback) { return; }
} catch (e) { return void console.error(e); }
}
var href = '/common/feedback.html?' + action + '=' + (+new Date());
console.log('[feedback] %s', href);
$.ajax({
type: "HEAD",
url: href,
});
};
common.reportAppUsage = function () {
var pattern = window.location.pathname.split('/')
.filter(function (x) { return x; }).join('.');
feedback(pattern);
};
common.getUid = function () {
if (store && store.getProxy() && store.getProxy().proxy) {
return store.getProxy().proxy.uid;
}
};
var getRealtime = common.getRealtime = function () {
if (store && store.getProxy() && store.getProxy().info) {
return store.getProxy().info.realtime;
}
return;
};
var whenRealtimeSyncs = common.whenRealtimeSyncs = function (realtime, cb) {
realtime.sync();
window.setTimeout(function () {
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
return void cb();
}
realtime.onSettle(function () {
cb();
});
}, 0);
};
common.getWebsocketURL = function () {
if (!Config.websocketPath) { return Config.websocketURL; }
var path = Config.websocketPath;
if (/^ws{1,2}:\/\//.test(path)) { return path; }
var protocol = window.location.protocol.replace(/http/, 'ws');
var host = window.location.host;
var url = protocol + '//' + host + path;
return url;
};
common.login = function (hash, name, cb) {
if (!hash) { throw new Error('expected a user hash'); }
if (!name) { throw new Error('expected a user name'); }
hash = common.serializeHash(hash);
localStorage.setItem(userHashKey, hash);
localStorage.setItem(userNameKey, name);
if (cb) { cb(); }
};
var eraseTempSessionValues = common.eraseTempSessionValues = function () {
// delete sessionStorage values that might have been left over
// from the main page's /user redirect
[
'login',
'login_user',
'login_pass',
'login_rmb',
'register'
].forEach(function (k) {
delete sessionStorage[k];
});
};
var logoutHandlers = [];
common.logout = function (cb) {
[
userNameKey,
userHashKey,
].forEach(function (k) {
sessionStorage.removeItem(k);
localStorage.removeItem(k);
delete localStorage[k];
delete sessionStorage[k];
});
// Make sure we have an FS_hash in localStorage before reloading all the tabs
// so that we don't end up with tabs using different anon hashes
if (!localStorage[fileHashKey]) {
localStorage[fileHashKey] = common.createRandomHash();
}
eraseTempSessionValues();
logoutHandlers.forEach(function (h) {
if (typeof (h) === "function") { h(); }
});
if (cb) { cb(); }
};
common.onLogout = function (h) {
if (typeof (h) !== "function") { return; }
if (logoutHandlers.indexOf(h) !== -1) { return; }
logoutHandlers.push(h);
};
var getUserHash = common.getUserHash = function () {
var hash = localStorage[userHashKey];
if (hash) {
var sHash = common.serializeHash(hash);
if (sHash !== hash) { localStorage[userHashKey] = sHash; }
}
return hash;
};
var isLoggedIn = common.isLoggedIn = function () {
return typeof getUserHash() === "string";
};
common.hasSigningKeys = function (proxy) {
return typeof(proxy) === 'object' &&
typeof(proxy.edPrivate) === 'string' &&
typeof(proxy.edPublic) === 'string';
};
common.isArray = $.isArray;
/*
* localStorage formatting
*/
/*
the first time this gets called, your local storage will migrate to a
new format. No more indices for values, everything is named now.
* href
* atime (access time)
* title
* ??? // what else can we put in here?
*/
var checkObjectData = function (pad, cb) {
if (!pad.ctime) { pad.ctime = pad.atime; }
if (/^https*:\/\//.test(pad.href)) {
pad.href = common.getRelativeHref(pad.href);
}
var parsed = common.parsePadUrl(pad.href);
if (!parsed || !parsed.hash) { return; }
if (typeof(cb) === 'function') {
cb(parsed);
}
if (!pad.title) {
pad.title = common.getDefaultname(parsed);
}
return parsed.hash;
};
// Migrate from legacy store (localStorage)
var migrateRecentPads = common.migrateRecentPads = function (pads) {
return pads.map(function (pad) {
var hash;
if (Array.isArray(pad)) { // TODO DEPRECATE_F
var href = pad[0];
href.replace(/\#(.*)$/, function (a, h) {
hash = h;
});
return {
href: pad[0],
atime: pad[1],
title: pad[2] || hash && hash.slice(0,8),
ctime: pad[1],
};
} else if (pad && typeof(pad) === 'object') {
hash = checkObjectData(pad);
if (!hash || !common.parseHash(hash)) { return; }
return pad;
} else {
console.error("[Cryptpad.migrateRecentPads] pad had unexpected value");
console.log(pad);
return;
}
}).filter(function (x) { return x; });
};
// Remove everything from RecentPads that is not an object and check the objects
var checkRecentPads = common.checkRecentPads = function (pads) {
pads.forEach(function (pad, i) {
if (pad && typeof(pad) === 'object') {
var hash = checkObjectData(pad);
if (!hash || !common.parseHash(hash)) {
console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad);
getStore().removeData(i);
return;
}
return pad;
}
console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad);
getStore().removeData(i);
});
};
// Get the pads from localStorage to migrate them to the object store
common.getLegacyPads = function (cb) {
require(['/customize/store.js'], function(Legacy) { // TODO DEPRECATE_F
Legacy.ready(function (err, legacy) {
if (err) { cb(err, null); return; }
legacy.get(storageKey, function (err2, recentPads) {
if (err2) { cb(err2, null); return; }
if (Array.isArray(recentPads)) {
feedback('MIGRATE_LEGACY_STORE');
cb(void 0, migrateRecentPads(recentPads));
return;
}
cb(void 0, []);
});
});
});
};
// Create untitled documents when no name is given
var getDefaultName = common.getDefaultName = function (parsed) {
var type = parsed.type;
var name = (Messages.type)[type] + ' - ' + new Date().toString().split(' ').slice(0,4).join(' ');
return name;
};
var isDefaultName = common.isDefaultName = function (parsed, title) {
var name = getDefaultName(parsed);
return title === name;
};
var makePad = function (href, title) {
var now = +new Date();
return {
href: href,
atime: now,
ctime: now,
title: title || window.location.hash.slice(1, 9),
};
};
/* Sort pads according to how recently they were accessed */
common.mostRecent = function (a, b) {
return new Date(b.atime).getTime() - new Date(a.atime).getTime();
};
// STORAGE
common.setPadAttribute = function (attr, value, cb) {
getStore().setDrive([getHash(), attr].join('.'), value, function (err, data) {
cb(err, data);
});
};
common.setAttribute = function (attr, value, cb) {
getStore().set(["cryptpad", attr].join('.'), value, function (err, data) {
cb(err, data);
});
};
common.setLSAttribute = function (attr, value) {
localStorage[attr] = value;
};
// STORAGE
common.getPadAttribute = function (attr, cb) {
getStore().getDrive([getHash(), attr].join('.'), function (err, data) {
cb(err, data);
});
};
common.getAttribute = function (attr, cb) {
getStore().get(["cryptpad", attr].join('.'), function (err, data) {
cb(err, data);
});
};
common.getLSAttribute = function (attr) {
return localStorage[attr];
};
// STORAGE - TEMPLATES
var listTemplates = common.listTemplates = function (type) {
var allTemplates = getStore().listTemplates();
if (!type) { return allTemplates; }
var templates = allTemplates.filter(function (f) {
var parsed = parsePadUrl(f.href);
return parsed.type === type;
});
return templates;
};
common.addTemplate = function (data) {
getStore().pushData(data);
getStore().addPad(data.href, ['template']);
};
common.isTemplate = function (href) {
var rhref = getRelativeHref(href);
var templates = listTemplates();
return templates.some(function (t) {
return t.href === rhref;
});
};
common.selectTemplate = function (type, rt, Crypt) {
if (!AppConfig.enableTemplates) { return; }
var temps = listTemplates(type);
if (temps.length === 0) { return; }
var $content = $('
');
$('
').text(Messages.selectTemplate).appendTo($content);
$('', {id:"selectTemplate"}).appendTo($content);
common.alert($content.html(), null, true);
var $p = $('#selectTemplate');
temps.forEach(function (t, i) {
$('', {href: t.href, title: t.title}).text(t.title).click(function (e) {
e.preventDefault();
var parsed = parsePadUrl(t.href);
if(!parsed) { throw new Error("Cannot get template hash"); }
common.addLoadingScreen(null, true);
Crypt.get(parsed.hash, function (err, val) {
if (err) { throw new Error(err); }
var p = parsePadUrl(window.location.href);
Crypt.put(p.hash, val, function () {
common.findOKButton().click();
common.removeLoadingScreen();
});
});
}).appendTo($p);
if (i !== temps.length) { $('
').appendTo($p); }
});
common.findOKButton().text(Messages.cancelButton);
};
// STORAGE
/* fetch and migrate your pad history from the store */
var getRecentPads = common.getRecentPads = function (cb) {
getStore().getDrive(storageKey, function (err, recentPads) {
if (Array.isArray(recentPads)) {
checkRecentPads(recentPads);
cb(void 0, recentPads);
return;
}
cb(void 0, []);
});
};
// STORAGE: Display Name
common.getLastName = function (cb) {
common.getAttribute('username', function (err, userName) {
cb(err, userName);
});
};
var _onDisplayNameChanged = [];
common.onDisplayNameChanged = function (h) {
if (typeof(h) !== "function") { return; }
if (_onDisplayNameChanged.indexOf(h) !== -1) { return; }
_onDisplayNameChanged.push(h);
};
common.changeDisplayName = function (newName) {
_onDisplayNameChanged.forEach(function (h) {
h(newName);
});
};
// STORAGE
common.forgetPad = function (href, cb) {
var parsed = parsePadUrl(href);
var callback = function (err) {
if (err) {
cb(err);
return;
}
getStore().keys(function (err, keys) {
if (err) {
cb(err);
return;
}
var toRemove = keys.filter(function (k) {
return k.indexOf(parsed.hash) === 0;
});
if (!toRemove.length) {
cb();
return;
}
getStore().removeBatch(toRemove, function (err, data) {
cb(err, data);
});
});
};
if (typeof(getStore().forgetPad) === "function") {
getStore().forgetPad(common.getRelativeHref(href), callback);
}
};
var updateFileName = function (href, oldName, newName) {
var fo = getStore().getProxy().fo;
var paths = fo.findFileInRoot(href);
paths.forEach(function (path) {
if (path.length !== 2) { return; }
var name = path[1].split('_')[0];
var parsed = parsePadUrl(href);
if (path.length === 2 && name === oldName && isDefaultName(parsed, name)) {
fo.rename(path, newName);
}
});
};
common.setPadTitle = function (name, cb) {
var href = window.location.href;
var parsed = parsePadUrl(href);
href = getRelativeHref(href);
// getRecentPads return the array from the drive, not a copy
// We don't have to call "set..." at the end, everything is stored with listmap
getRecentPads(function (err, recent) {
if (err) {
cb(err);
return;
}
var updateWeaker = [];
var contains;
recent.forEach(function (pad) {
var p = parsePadUrl(pad.href);
if (p.type !== parsed.type) { return pad; }
var shouldUpdate = p.hash.replace(/\/$/, '') === parsed.hash.replace(/\/$/, '');
// Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest :
// Edit > Edit (present) > View > View (present)
var pHash = parseHash(p.hash);
var parsedHash = parseHash(parsed.hash);
if (!pHash) { return; } // We may have a corrupted pad in our storage, abort here in that case
if (!shouldUpdate && pHash.version === 1 && parsedHash.version === 1 && pHash.channel === parsedHash.channel) {
if (pHash.mode === 'view' && parsedHash.mode === 'edit') { shouldUpdate = true; }
else if (pHash.mode === parsedHash.mode && pHash.present) { shouldUpdate = true; }
else {
// Editing a "weaker" version of a stored hash : update the date and do not push the current hash
pad.atime = +new Date();
contains = true;
return pad;
}
}
if (shouldUpdate) {
contains = true;
// update the atime
pad.atime = +new Date();
// set the name
var old = pad.title;
pad.title = name;
// If we now have a stronger version of a stored href, replace the weaker one by the strong one
if (pad && pad.href && href !== pad.href) {
updateWeaker.push({
o: pad.href,
n: href
});
}
pad.href = href;
updateFileName(href, old, name);
}
return pad;
});
if (!contains && href) {
var data = makePad(href, name);
getStore().pushData(data, function (e) {
if (e) {
if (e === 'E_OVER_LIMIT' && AppConfig.enablePinLimit) {
common.alert(Messages.pinLimitNotPinned, null, true);
return;
}
else { throw new Error("Cannot push this pad to CryptDrive", e); }
}
getStore().addPad(data, common.initialPath);
});
}
if (updateWeaker.length > 0) {
updateWeaker.forEach(function (obj) {
getStore().replaceHref(obj.o, obj.n);
});
}
cb(err, recent);
});
};
var errorHandlers = [];
common.onError = function (h) {
if (typeof h !== "function") { return; }
errorHandlers.push(h);
};
common.storeError = function () {
errorHandlers.forEach(function (h) {
if (typeof h === "function") {
h({type: "store"});
}
});
};
/*
* Buttons
*/
common.renamePad = function (title, callback) {
if (title === null) { return; }
if (title.trim() === "") {
var parsed = parsePadUrl(window.location.href);
title = getDefaultName(parsed);
}
common.setPadTitle(title, function (err) {
if (err) {
console.log("unable to set pad title");
console.log(err);
return;
}
callback(null, title);
});
};
var getUserChannelList = common.getUserChannelList = function () {
var store = common.getStore();
var proxy = store.getProxy();
var fo = proxy.fo;
// start with your userHash...
var userHash = localStorage && localStorage.User_hash;
if (!userHash) { return null; }
var userChannel = common.parseHash(userHash).channel;
if (!userChannel) { return null; }
var list = fo.getFiles([fo.FILES_DATA]).map(hrefToHexChannelId)
.filter(function (x) { return x; });
list.push(common.base64ToHex(userChannel));
list.sort();
return list;
};
var getCanonicalChannelList = common.getCanonicalChannelList = function () {
return deduplicateString(getUserChannelList()).sort();
};
var pinsReady = common.pinsReady = function () {
if (!isLoggedIn()) {
return false;
}
if (!PINNING_ENABLED) {
console.error('[PINNING_DISABLED]');
return false;
}
if (!rpc) {
console.error('[RPC_NOT_READY]');
return false;
}
return true;
};
common.arePinsSynced = function (cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
var list = getCanonicalChannelList();
var local = Hash.hashChannelList(list);
rpc.getServerHash(function (e, hash) {
if (e) { return void cb(e); }
cb(void 0, hash === local);
});
};
common.resetPins = function (cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
var list = getCanonicalChannelList();
rpc.reset(list, function (e, hash) {
if (e) { return void cb(e); }
cb(void 0, hash);
});
};
common.pinPads = function (pads, cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
rpc.pin(pads, function (e, hash) {
if (e) { return void cb(e); }
cb(void 0, hash);
});
};
common.unpinPads = function (pads, cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
rpc.unpin(pads, function (e, hash) {
if (e) { return void cb(e); }
cb(void 0, hash);
});
};
common.getPinnedUsage = function (cb) {
if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); }
rpc.getFileListSize(cb);
};
common.getFileSize = function (href, cb) {
var channelId = Hash.hrefToHexChannelId(href);
rpc.getFileSize(channelId, function (e, bytes) {
if (e) { return void cb(e); }
cb(void 0, bytes);
});
};
common.getPinLimit = function (cb) {
cb(void 0, typeof(AppConfig.pinLimit) === 'number'? AppConfig.pinLimit: 1000);
};
common.isOverPinLimit = function (cb) {
if (!common.isLoggedIn() || !AppConfig.enablePinLimit) { return void cb(null, false); }
var usage;
var andThen = function (e, limit) {
if (e) { return void cb(e); }
var data = {usage: usage, limit: limit};
if (usage > limit) {
return void cb (null, true, data);
}
return void cb (null, false, data);
};
var todo = function (e, used) {
usage = common.bytesToMegabytes(used);
if (e) { return void cb(e); }
common.getPinLimit(andThen);
};
common.getPinnedUsage(todo);
};
common.createButton = function (type, rightside, data, callback) {
var button;
var size = "17px";
switch (type) {
case 'export':
button = $('