define([ '/api/config', '/customize/messages.js?app=' + window.location.pathname.split('/').filter(function (x) { return x; }).join('.'), '/common/fsStore.js', '/bower_components/chainpad-crypto/crypto.js?v=0.1.5', '/bower_components/alertifyjs/dist/js/alertify.js', '/common/clipboard.js', '/customize/application_config.js', '/bower_components/jquery/dist/jquery.min.js', ], function (Config, Messages, Store, Crypto, Alertify, Clipboard, 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 $ = window.jQuery; var common = window.Cryptpad = { Messages: Messages, Alertify: Alertify, Clipboard: Clipboard }; var store; var find = common.find = function (map, path) { return (map && path.reduce(function (p, n) { return typeof(p[n]) !== 'undefined' && p[n]; }, map)); }; var getStore = common.getStore = function () { if (store) { return store; } throw new Error("Store is not ready!"); }; var getNetwork = common.getNetwork = function () { if (store) { if (store.getProxy() && store.getProxy().info) { return store.getProxy().info.network; } } return; }; var getUid = common.getUid = function () { if (store) { if (store.getProxy() && store.getProxy().proxy) { return store.getProxy().proxy.uid; } } }; var getRealtime = common.getRealtime = function () { if (store) { if (store.getProxy() && store.getProxy().info) { return store.getProxy().info.realtime; } } return; }; common.feedback = function (action) { if (!action) { return; } try { if (!getStore().getProxy().proxy.allowUserFeedback) { return; } } catch (e) { return void console.error(e); } $.ajax({ type: "HEAD", url: '/common/feedback.html?' + action + '=' + (+new Date()), }); }; 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); }; var getWebsocketURL = 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; }; var userHashKey = common.userHashKey = 'User_hash'; var userNameKey = common.userNameKey = 'User_name'; var fileHashKey = common.fileHashKey = 'FS_hash'; var displayNameKey = common.displayNameKey = 'cryptpad.username'; var login = common.login = function (hash, name, cb) { if (!hash) { throw new Error('expected a user hash'); } if (!name) { throw new Error('expected a user name'); } 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 = []; var logout = 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(); } }; var onLogout = 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; [sessionStorage, localStorage].some(function (s) { var h = s[userHashKey]; if (h) { return (hash = h); } }); return hash; }; var isLoggedIn = common.isLoggedIn = function () { return typeof getUserHash() === "string"; }; var isArray = common.isArray = $.isArray; var fixHTML = common.fixHTML = function (str) { if (!str) { return ''; } return str.replace(/[<>&"']/g, function (x) { return ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[x]; }); }; var truncate = common.truncate = function (text, len) { if (typeof(text) === 'string' && text.length > len) { return text.slice(0, len) + '…'; } return text; }; var hexToBase64 = common.hexToBase64 = function (hex) { var hexArray = hex .replace(/\r|\n/g, "") .replace(/([\da-fA-F]{2}) ?/g, "0x$1 ") .replace(/ +$/, "") .split(" "); var byteString = String.fromCharCode.apply(null, hexArray); return window.btoa(byteString).replace(/\//g, '-').slice(0,-2); }; var base64ToHex = common.base64ToHex = function (b64String) { var hexArray = []; atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){ var h = e.charCodeAt(0).toString(16); if (h.length === 1) { h = "0"+h; } hexArray.push(h); }); return hexArray.join(""); }; var deduplicate = common.deduplicateString = function (array) { var a = array.slice(); for(var i=0; i= 56) { // Old hash parsed.channel = hash.slice(0, 32); parsed.key = hash.slice(32); parsed.version = 0; return parsed; } var hashArr = hash.split('/'); if (hashArr[1] && hashArr[1] === '1') { parsed.version = 1; parsed.mode = hashArr[2]; parsed.channel = hashArr[3]; parsed.key = hashArr[4]; parsed.present = hashArr[5] && hashArr[5] === 'present'; return parsed; } return; }; var getEditHashFromKeys = common.getEditHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return chanKey + keys; } if (!keys.editKeyStr) { return; } return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); }; var getViewHashFromKeys = common.getViewHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return; } return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); }; var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys; var specialHashes = common.specialHashes = ['iframe']; /* * Returns all needed keys for a realtime channel * - no argument: use the URL hash or create one if it doesn't exist * - secretHash provided: use secretHash to find the keys */ var getSecrets = common.getSecrets = function (secretHash) { var secret = {}; var generate = function () { secret.keys = Crypto.createEditCryptor(); secret.key = Crypto.createEditCryptor().editKeyStr; }; if (!secretHash && !/#/.test(window.location.href)) { generate(); return secret; } else { var hash = secretHash || window.location.hash.slice(1); if (hash.length === 0 || specialHashes.indexOf(hash) !== -1) { generate(); return secret; } // old hash system : #{hexChanKey}{cryptKey} // new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey} if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Old hash secret.channel = hash.slice(0, 32); secret.key = hash.slice(32); } else { // New hash var hashArray = hash.split('/'); if (hashArray.length < 4) { common.alert("Unable to parse the key"); throw new Error("Unable to parse the key"); } var version = hashArray[1]; if (version === "1") { var mode = hashArray[2]; if (mode === 'edit') { secret.channel = base64ToHex(hashArray[3]); var keys = Crypto.createEditCryptor(hashArray[4].replace(/-/g, '/')); secret.keys = keys; secret.key = keys.editKeyStr; if (secret.channel.length !== 32 || secret.key.length !== 24) { common.alert("The channel key and/or the encryption key is invalid"); throw new Error("The channel key and/or the encryption key is invalid"); } } else if (mode === 'view') { secret.channel = base64ToHex(hashArray[3]); secret.keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/')); if (secret.channel.length !== 32) { common.alert("The channel key is invalid"); throw new Error("The channel key is invalid"); } } } } } return secret; }; var getHashes = common.getHashes = function (channel, secret) { var hashes = {}; if (secret.keys.editKeyStr) { hashes.editHash = getEditHashFromKeys(channel, secret.keys); } if (secret.keys.viewKeyStr) { hashes.viewHash = getViewHashFromKeys(channel, secret.keys); } return hashes; }; var uint8ArrayToHex = common.uint8ArrayToHex = function (a) { // call slice so Uint8Arrays work as expected return Array.prototype.slice.call(a).map(function (e, i) { var n = Number(e & 0xff).toString(16); if (n === 'NaN') { throw new Error('invalid input resulted in NaN'); } switch (n.length) { case 0: return '00'; // just being careful, shouldn't happen case 1: return '0' + n; case 2: return n; default: throw new Error('unexpected value'); } }).join(''); }; var createChannelId = common.createChannelId = function () { var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16)); if (id.length !== 32 || /[^a-f0-9]/.test(id)) { throw new Error('channel ids must consist of 32 hex characters'); } return id; }; var createRandomHash = common.createRandomHash = function () { // 16 byte channel Id var channelId = hexToBase64(createChannelId()); // 18 byte encryption key var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); return '/1/edit/' + [channelId, key].join('/'); }; var replaceHash = common.replaceHash = function (hash) { if (window.history && window.history.replaceState) { if (!/^#/.test(hash)) { hash = '#' + hash; } return void window.history.replaceState({}, window.document.title, hash); } window.location.hash = hash; }; var storageKey = common.storageKey = 'CryptPad_RECENTPADS'; /* * 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 migrateRecentPads = common.migrateRecentPads = function (pads) { return pads.map(function (pad) { var hash; if (isArray(pad)) { 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') { if (!pad.ctime) { pad.ctime = pad.atime; } if (!pad.title) { pad.href.replace(/#(.*)$/, function (x, hash) { pad.title = hash.slice(0,8); }); } if (/^https*:\/\//.test(pad.href)) { pad.href = common.getRelativeHref(pad.href); } hash = pad.href.slice(pad.href.indexOf('#')+1); 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; }); }; // Get the pads from localStorage to migrate them to the object store var getLegacyPads = common.getLegacyPads = function (cb) { require(['/customize/store.js'], function(Legacy) { Legacy.ready(function (err, legacy) { if (err) { cb(err, null); return; } legacy.get(storageKey, function (err2, recentPads) { if (err2) { cb(err2, null); return; } if (isArray(recentPads)) { cb(void 0, migrateRecentPads(recentPads)); return; } cb(void 0, []); }); }); }); }; var getHash = common.getHash = function () { return window.location.hash.slice(1); }; var getRelativeHref = common.getRelativeHref = function (href) { if (!href) { return; } if (href.indexOf('#') === -1) { return; } var parsed = common.parsePadUrl(href); return '/' + parsed.type + '/#' + parsed.hash; }; var parsePadUrl = common.parsePadUrl = function (href) { var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i; var ret = {}; if (!href) { return ret; } if (!/^https*:\/\//.test(href)) { var idx = href.indexOf('/#'); ret.type = href.slice(1, idx); ret.hash = href.slice(idx + 2); return ret; } var hash = href.replace(patt, function (a, domain, type, hash) { ret.domain = domain; ret.type = type; return ''; }); ret.hash = hash.replace(/#/g, ''); return ret; }; var isNameAvailable = function (title, parsed, pads) { return !pads.some(function (pad) { // another pad is already using that title if (pad.title === title) { return true; } }); }; // Create untitled documents when no name is given var getDefaultName = common.getDefaultName = function (parsed, recentPads) { var type = parsed.type; var untitledIndex = 1; 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 */ var mostRecent = common.mostRecent = function (a, b) { return new Date(b.atime).getTime() - new Date(a.atime).getTime(); }; // STORAGE var setPadAttribute = common.setPadAttribute = function (attr, value, cb) { getStore().setDrive([getHash(), attr].join('.'), value, function (err, data) { cb(err, data); }); }; var setAttribute = common.setAttribute = function (attr, value, cb) { getStore().set(["cryptpad", attr].join('.'), value, function (err, data) { cb(err, data); }); }; var setLSAttribute = common.setLSAttribute = function (attr, value) { localStorage[attr] = value; }; // STORAGE var getPadAttribute = common.getPadAttribute = function (attr, cb) { getStore().getDrive([getHash(), attr].join('.'), function (err, data) { cb(err, data); }); }; var getAttribute = common.getAttribute = function (attr, cb) { getStore().get(["cryptpad", attr].join('.'), function (err, data) { cb(err, data); }); }; var getLSAttribute = 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; }; var addTemplate = common.addTemplate = function (href) { getStore().addTemplate(href); }; // STORAGE /* fetch and migrate your pad history from localStorage */ var getRecentPads = common.getRecentPads = function (cb) { getStore().getDrive(storageKey, function (err, recentPads) { if (isArray(recentPads)) { cb(void 0, migrateRecentPads(recentPads)); return; } cb(void 0, []); }); }; // STORAGE /* commit a list of pads to localStorage */ var setRecentPads = common.setRecentPads = function (pads, cb) { getStore().setDrive(storageKey, pads, function (err, data) { cb(err, data); }); }; // STORAGE: Display Name var getLastName = common.getLastName = function (cb) { common.getAttribute('username', function (err, userName) { cb(err, userName); }); }; var _onDisplayNameChanged = []; var onDisplayNameChanged = common.onDisplayNameChanged = function (h) { if (typeof(h) !== "function") { return; } if (_onDisplayNameChanged.indexOf(h) !== -1) { return; } _onDisplayNameChanged.push(h); }; var changeDisplayName = common.changeDisplayName = function (newName) { _onDisplayNameChanged.forEach(function (h) { h(newName); }); }; // STORAGE var forgetPad = common.forgetPad = function (href, cb) { var parsed = parsePadUrl(href); var callback = function (err, data) { 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); } }; // STORAGE var findWeaker = common.findWeaker = function (href, recents) { var rHref = href || getRelativeHref(window.location.href); var parsed = parsePadUrl(rHref); if (!parsed.hash) { return false; } var weaker; recents.some(function (pad) { var p = parsePadUrl(pad.href); if (p.type !== parsed.type) { return; } // Not the same type if (p.hash === parsed.hash) { return; } // Same hash, not stronger var pHash = parseHash(p.hash); var parsedHash = parseHash(parsed.hash); if (!parsedHash || !pHash) { return; } if (pHash.version !== parsedHash.version) { return; } if (pHash.channel !== parsedHash.channel) { return; } if (pHash.mode === 'view' && parsedHash.mode === 'edit') { weaker = pad.href; return true; } return; }); return weaker; }; var findStronger = common.findStronger = function (href, recents) { var rHref = href || getRelativeHref(window.location.href); var parsed = parsePadUrl(rHref); if (!parsed.hash) { return false; } var stronger; recents.some(function (pad) { var p = parsePadUrl(pad.href); if (p.type !== parsed.type) { return; } // Not the same type if (p.hash === parsed.hash) { return; } // Same hash, not stronger var pHash = parseHash(p.hash); var parsedHash = parseHash(parsed.hash); if (!parsedHash || !pHash) { return; } if (pHash.version !== parsedHash.version) { return; } if (pHash.channel !== parsedHash.channel) { return; } if (pHash.mode === 'edit' && parsedHash.mode === 'view') { stronger = pad.href; return true; } return; }); return stronger; }; var isNotStrongestStored = common.isNotStrongestStored = function (href, recents) { return findStronger(href, recents); }; var setPadTitle = common.setPadTitle = function (name, cb) { var href = window.location.href; var parsed = parsePadUrl(href); href = getRelativeHref(href); getRecentPads(function (err, recent) { if (err) { cb(err); return; } var updateWeaker = []; var contains; var renamed = recent.map(function (pad) { var p = parsePadUrl(pad.href); if (p.type !== parsed.type) { return pad; } var shouldUpdate = p.hash === parsed.hash; // 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 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; } return pad; }); if (!contains) { var data = makePad(href, name); renamed.push(data); if (typeof(getStore().addPad) === "function") { getStore().addPad(href, common.initialPath, common.initialName || name); } } setRecentPads(renamed, function (err, data) { if (updateWeaker.length > 0) { updateWeaker.forEach(function (obj) { getStore().replaceHref(obj.o, obj.n); }); } cb(err, data); }); }); }; // STORAGE var getPadTitle = common.getPadTitle = function (cb) { var href = window.location.href; var parsed = parsePadUrl(window.location.href); var hashSlice = window.location.hash.slice(1,9); var title = ''; getRecentPads(function (err, pads) { if (err) { cb(err); return; } pads.some(function (pad) { var p = parsePadUrl(pad.href); if (p.hash === parsed.hash && p.type === parsed.type) { title = pad.title || hashSlice; return true; } }); if (title === '') { title = getDefaultName(parsed, pads); } cb(void 0, title); }); }; // STORAGE var causesNamingConflict = common.causesNamingConflict = function (title, cb) { var href = window.location.href; var parsed = parsePadUrl(href); getRecentPads(function (err, pads) { if (err) { cb(err); return; } var conflicts = pads.some(function (pad) { // another pad is already using that title if (pad.title === title) { var p = parsePadUrl(pad.href); if (p.type === parsed.type && p.hash === parsed.hash) { // the duplicate pad has the same type and hash // allow renames } else { // it's an entirely different pad... it conflicts return true; } } }); cb(void 0, conflicts); }); }; var newPadNameKey = common.newPadNameKey = "newPadName"; var newPadPathKey = common.newPadPathKey = "newPadPath"; // local name? common.ready = function (f) { var state = 0; var env = {}; var cb = function () { f(void 0, env); }; if (sessionStorage[newPadNameKey]) { common.initialName = sessionStorage[newPadNameKey]; delete sessionStorage[newPadNameKey]; } if (sessionStorage[newPadPathKey]) { common.initialPath = sessionStorage[newPadPathKey]; delete sessionStorage[newPadPathKey]; } Store.ready(function (err, storeObj) { store = common.store = env.store = storeObj; $(function() { // Race condition : if document.body is undefined when alertify.js is loaded, Alertify // won't work. We have to reset it now to make sure it uses a correct "body" Alertify.reset(); // Load the new pad when the hash has changed var oldHash = document.location.hash.slice(1); window.onhashchange = function () { var newHash = document.location.hash.slice(1); var parsedOld = parseHash(oldHash); var parsedNew = parseHash(newHash); if (parsedOld && parsedNew && ( parsedOld.channel !== parsedNew.channel || parsedOld.mode !== parsedNew.mode || parsedOld.key !== parsedNew.key)) { document.location.reload(); return; } if (parsedNew) { oldHash = newHash; } }; // Everything's ready, continue... if($('#pad-iframe').length) { var $iframe = $('#pad-iframe'); var iframe = $iframe[0]; var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc.readyState === 'complete') { cb(); return; } $iframe.load(cb); return; } cb(); }); }, common); }; 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"}); } }); }; var LOADING = 'loading'; var getRandomTip = function () { if (!Messages.tips || !Object.keys(Messages.tips).length) { return ''; } var keys = Object.keys(Messages.tips); var rdm = Math.floor(Math.random() * keys.length); return Messages.tips[keys[rdm]]; }; common.addLoadingScreen = function (loadingText, hideTips) { var $loading, $container; if ($('#' + LOADING).length) { $loading = $('#' + LOADING).show(); if (loadingText) { $('#' + LOADING).find('p').text(loadingText); } $container = $loading.find('.loadingContainer'); } else { $loading = $('
', {id: LOADING}); $container = $('
', {'class': 'loadingContainer'}); $container.append(''); var $spinner = $('
', {'class': 'spinnerContainer'}); common.spinner($spinner).show(); var $text = $('

').text(loadingText || Messages.loading); $container.append($spinner).append($text); $loading.append($container); $('body').append($loading); } if (Messages.tips && !hideTips) { var $loadingTip = $('

', {'id': 'loadingTip'}); var $tip = $('', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip); $loadingTip.css({ 'top': $('body').height()/2 + $container.height()/2 + 20 + 'px' }); $('body').append($loadingTip); } }; common.removeLoadingScreen = function (cb) { $('#' + LOADING).fadeOut(750, cb); $('#loadingTip').css('top', ''); window.setTimeout(function () { $('#loadingTip').fadeOut(750); }, 3000); }; common.errorLoadingScreen = function (error, transparent) { if (!$('#' + LOADING).is(':visible')) { common.addLoadingScreen(undefined, true); } $('.spinnerContainer').hide(); if (transparent) { $('#' + LOADING).css('opacity', 0.8); } $('#' + LOADING).find('p').html(error || Messages.error); }; /* * Saving files */ var fixFileName = common.fixFileName = function (filename) { return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_') .replace(/_+/g, '_'); }; var importContent = common.importContent = function (type, f) { return function () { var $files = $('').click(); $files.on('change', function (e) { var file = e.target.files[0]; var reader = new FileReader(); reader.onload = function (e) { f(e.target.result, file); }; reader.readAsText(file, type); }); }; }; /* * Buttons */ var renamePad = 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, data) { 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.getFilesDataFiles().map(function (href) { var parsed = common.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } parsed = common.parseHash(parsed.hash); if (parsed.version === 0) { return parsed.channel; } else if (parsed.version !== 1) { console.error("parsed href had no version"); console.error(parsed); return; } var channel = parsed.channel; if (!channel) { return; } var hex = common.base64ToHex(channel); return hex; }).filter(function (x) { return x; }); list.push(common.base64ToHex(userChannel)); list.sort(); return list; }; var createButton = common.createButton = function (type, rightside, data, callback) { var button; var size = "17px"; switch (type) { case 'export': button = $('