diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd0f5091..361cee09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,35 @@ -# Pending +# 4.3.0 (D) -* PRs - * comment config.js about supporting multiple domains in httpUnsafeOrigin - * add decreePath - * explicitly pass archivePath when initializing stores - * fix incorrect API in `scripts/migrations/migrate-tasks-v1.js` -* login/register - * delete login block when deleting account - * more careful checks when changing user password -* checkup page - * fixed typo - * progress bar - * test - * websockets - * sandbox CSP - * login block -* admin page - * support responses to closed tickets - * collapse very long messages -* open properties menu for pads that aren't stored in your drive -* help menu that only links to docs - * remove unused (nested) keys -* display survey URL -* support 'KB' in Util.magnitudeOfBytes -* degraded mode - * decide on a number -* sheets - * fix naming collisions between images in spreadsheets - * degraded mode not supported - * getPropChannels - * pinning? - * oo rebuild - * OnlyOffice v6.2 - * some buttons that we were hiding have new ids and needed to be hidden again -* translations - * updated catch-phrase (Collaboration suite\nend-to-end-encrypted and open-source -* CKEditor - * cursor jump when clicking on a comment bubble - * keybindings for common styles - * test if this affects scroll position (it shouldn't) - * check that CTRL-space doesn't mess with anything and that it is what Google uses - * test on Mac -* nodrive - * load anonymous accounts without creating a drive - * faster load time, less junk on the server +## Goals +This release is a continuation of our recent efforts to stabilize the platform, fixing small bugs and inconsistencies that we missed when developing larger features. In the meantime we've received reports of the platform performing poorly under various unusual circumstances, so we've developed some targeted fixes to both improve user experience and decrease the load on our server. + +## Update notes + +## Features + +* We're introducing a "degraded mode" for most of our editors (all except polls and sheets). This follows reports we received that CryptPad performed poorly in settings where a relatively large number of users with *edit* rights were connected simultaneously. To alleviate this, some non-essential features will be disabled when a number of concurrent editors is reached, in order to save computing power on client devices. The user-list will stop being updated as users join and leave, users cursors will stop being displayed, and the chat will not be disabled. Sessions will enter this mode when 8 or more editors are present. This threshold can be configured via `customize/application_config.js` by setting a `degradedLimit` attribute. +* CryptPad was recently used to distribute some high-profile documents. For the first time we were able to observe our server supporting more than 1000 concurrent viewers in a single pad and around 350000 unique visitors over the course of a few days. While the distributed document incurred very little load, CryptPad created a drive for each visitor the first time they visited. Most of these drives were presumably abandoned as these users did not return to create or edit their own documents. Such users that directly load an existing document without having previously visited the platform will no longer create a drive automatically, unless they explicitly visit a page which requires it. This behaviour is supported in most of our editors except sheets and polls. This should result in faster load times for new users, but just in case it causes any issues we've made it easy to disable. Instance admins can disable "no-drive mode" via `customize/application_config.js` by setting `allowDrivelessMode` to `false`. +* We've updated our sheet editor to use OnlyOffice 6.2, which includes support for pivot tables, among a range of other improvements. +* Our rich text editor now features some keyboard shortcuts to apply some commonly used styles: + * heading size 1-6: ctrl+alt+1-6 + * "div": ctrl+alt+8 + * "preformatted": ctrl+alt+9 + * paragraph: ctrl+alt+0 + * remove styles from selection: ctrl+space +* We've removed a large number of strings that were included in the "Getting started" box that was displayed to new users in each of our editors. Instead, this box simply contains a link to the relevant page in our documentation. Our intent is to both simplify the interface for newcomers and reduce the number of strings that require translation. +* We've continued to progress on our "checkup page" which performs some routine checks to see whether the host instance is correctly configured. While its hints are not especially helpful for admins without reading the code to understand what they are testing, they do detect a fairly wide range of issues and have already helped us to identify some inconsistencies in our recommended configuration. We plan to link directly from this page to the relevant sections of a configuration guide an in upcoming release. +* The admin support ticket interface has been updated to collapse very long messages in response to some ticket threads submitted in the last few weeks. We also found that sometimes we needed more information after a ticket had been closed, so we added the ability to re-open closed tickets. +* Some time ago we removed the "Survey link" option from the user admin dropdown menu (found in the top-right corner of the page). This release re-enables it for instances that explicitly provide a link to a survey, however, we no longer provide a link to a survey by default. + +## Bug fixes + +* We finally reviewed and merged a number of pull-requests that had been pending for some time. Collectively, they fixed some configuration issues and type errors in some of our older scripts. +* Sheets can now contain multiple images with the same name, whereas before they would conflict and one would be displayed multiple times. +* A recent change in our code to conditionally display size measurements in different magnitudes (GB, MB) removed support for Kilobytes (KB). This release restores the previous behaviour. +* We believe we've identified and corrected an issue that caused the rich text editor to scroll to the top of the document when the button to add a comment was clicked. +* We recently made it such that documents owned by a particular user would not be automatically re-added to that user's drive when they viewed them. This change revealed a number of odd cases where various commands (destroy, add password, get document size, etc.) did not work as expected unless the document was first added to their drive. We reviewed many of these features and corrected the underlying issues that caused these commands to fail. +* We performed a similar review of various commands related to user accounts and identified a number of issues that caused account deletion to fail. # 4.2.1 diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index ae1096946..e74b67cb0 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -184,6 +184,7 @@ } &.btn-register { margin-top: 10px !important; + white-space: normal; } diff --git a/lib/hk-util.js b/lib/hk-util.js index 8780addcd..860435c86 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -661,6 +661,8 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { if (txid) { msg[0] = txid; } Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(msg)], readMore); }, (err) => { + // Any error but ENOENT: abort + // ENOENT is allowed in case we want to create a new pad if (err && err.code !== 'ENOENT') { if (err.message === "EUNKNOWN") { Log.error("HK_GET_HISTORY", { @@ -675,11 +677,28 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { err: err && err.message || err, stack: err && err.stack, }); } + // FIXME err.message isn't useful for users const parsedMsg = {error:err.message, channel: channelName, txid: txid}; Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]); return; } + // If we're asking for a specific version (lastKnownHash) but we receive an + // ENOENT, this is not a pad creation so we need to abort. + if (err && err.code === 'ENOENT' && lastKnownHash) { +/* + This informs clients that the pad they're trying to load was deleted by its owner. + The user in question might be reconnecting or might have loaded the document from their cache. + The owner that deleted it could be another user or the same user from a different device. + Either way, the respectful thing to do is display an error screen informing them that the content + is no longer on the server so they don't abuse the data and so that they don't unintentionally continue + to edit it in a broken state. +*/ + const parsedMsg2 = {error:'EDELETED', channel: channelName, txid: txid}; + Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg2)]); + return; + } + if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) { handleFirstMessage(Env, channelName, metadata); Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]); diff --git a/scripts/find-html-translations.js b/scripts/find-html-translations.js new file mode 100644 index 000000000..8c93a6f8f --- /dev/null +++ b/scripts/find-html-translations.js @@ -0,0 +1,27 @@ +var EN = require("../www/common/translations/messages.json"); + +var simpleTags = [ + '
', + '
', +]; + +['a', 'b', 'em', 'p', 'i'].forEach(function (tag) { + simpleTags.push('<' + tag + '>'); + simpleTags.push(''); +}); + +Object.keys(EN).forEach(function (k) { + var s = EN[k]; + if (typeof(s) !== 'string') { return; } + var usesHTML; + + s.replace(/<.*?>/g, function (html) { + if (simpleTags.indexOf(html) !== -1) { return; } + usesHTML = true; + console.log("{%s}", html); + }); + + if (usesHTML) { + console.log("[%s] %s\n", k, s); + } +}); diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index e119b3e5e..0f91ea447 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -162,7 +162,7 @@ define(function() { // making it much faster to open new tabs. config.disableWorkers = false; - config.surveyURL = "https://survey.cryptpad.fr/index.php/672782"; + //config.surveyURL = ""; // Teams are always loaded during the initial loading screen (for the first tab only if // SharedWorkers are available). Allowing users to be members of multiple teams can @@ -196,7 +196,6 @@ define(function() { // a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable // the driveless mode by changing the following value to "false" config.allowDrivelessMode = true; - config.allowDrivelessMode = true; return config; }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 08c84d1e6..1cef92b51 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2537,9 +2537,11 @@ define([ $creation.focus(); }; + var autoStoreModal = {}; UIElements.onServerError = function (common, err, toolbar, cb) { //if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; } var priv = common.getMetadataMgr().getPrivateData(); + var sframeChan = common.getSframeChannel(); var msg = err.type; if (err.type === 'EEXPIRED') { msg = Messages.expiredError; @@ -2549,10 +2551,36 @@ define([ if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } } else if (err.type === 'EDELETED') { if (priv.burnAfterReading) { return void cb(); } + + if (autoStoreModal[priv.channel]) { + autoStoreModal[priv.channel].delete(); + delete autoStoreModal[priv.channel]; + } + + if (err.ownDeletion) { + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } + (cb || function () {})(); + return; + } + + // View users have the wrong seed, thay can't retireve access directly + // Version 1 hashes don't support passwords + if (!priv.readOnly && !priv.oldVersionHash) { + sframeChan.event('EV_SHARE_OPEN', {hidden: true}); // Close share modal + UIElements.displayPasswordPrompt(common, { + fromServerError: true, + loaded: err.loaded, + }); + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } + (cb || function () {})(); + return; + } + msg = Messages.deletedError; if (err.loaded) { msg += Messages.errorCopy; } + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } } else if (err.type === 'ERESTRICTED') { msg = Messages.restrictedError; @@ -2561,7 +2589,6 @@ define([ msg = Messages.oo_deletedVersion; if (toolbar && typeof toolbar.failed === "function") { toolbar.failed(true); } } - var sframeChan = common.getSframeChannel(); sframeChan.event('EV_SHARE_OPEN', {hidden: true}); UI.errorLoadingScreen(msg, Boolean(err.loaded), Boolean(err.loaded)); (cb || function () {})(); @@ -2570,7 +2597,10 @@ define([ UIElements.displayPasswordPrompt = function (common, cfg, isError) { var error; if (isError) { error = setHTML(h('p.cp-password-error'), Messages.password_error); } + var info = h('p.cp-password-info', Messages.password_info); + var info_loaded = setHTML(h('p.cp-password-info'), Messages.errorCopy); + var password = UI.passwordInput({placeholder: Messages.password_placeholder}); var $password = $(password); var button = h('button.btn.btn-primary', Messages.password_submit); @@ -2582,6 +2612,21 @@ define([ var submit = function () { var value = $password.find('.cp-password-input').val(); + + // Password-prompt called from UIElements.onServerError + if (cfg.fromServerError) { + common.getSframeChannel().query('Q_PASSWORD_CHECK', value, function (err, obj) { + if (obj && obj.error) { + console.error(obj.error); + return void UI.warn(Messages.error); + } + // On success, outer will reload the page: this is a wrong password + UIElements.displayPasswordPrompt(common, cfg, true); + }); + return; + } + + // Initial load UI.addLoadingScreen({newProgress: true}); if (window.CryptPad_updateLoadingProgress) { window.CryptPad_updateLoadingProgress({ @@ -2595,6 +2640,8 @@ define([ } }); }; + + $password.find('.cp-password-input').on('keydown', function (e) { if (e.which === 13) { submit(); } }); $(button).on('click', function () { submit(); }); @@ -2602,12 +2649,13 @@ define([ var block = h('div#cp-loading-password-prompt', [ error, info, + cfg.loaded ? info_loaded : undefined, h('p.cp-password-form', [ password, button - ]) + ]), ]); - UI.errorLoadingScreen(block); + UI.errorLoadingScreen(block, Boolean(cfg.loaded), Boolean(cfg.loaded)); $password.find('.cp-password-input').focus(); }; @@ -2700,7 +2748,6 @@ define([ }; var storePopupState = false; - var autoStoreModal = {}; UIElements.displayStorePadPopup = function (common, data) { if (storePopupState) { return; } storePopupState = true; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index f4419d2cb..8e8ec8f82 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -481,10 +481,20 @@ define([ }); }; + common.isNewChannel = function (href, password, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var channel = Hash.hrefToHexChannelId(href, password); + postMessage('IS_NEW_CHANNEL', {channel: channel}, function (obj) { + var error = obj && obj.error; + if (error) { return void cb(error); } + if (!obj) { return void cb('ERROR'); } + cb (null, obj.isNew); + }, {timeout: -1}); + }; // This function is used when we want to open a pad. We first need // to check if it exists. With the cached drive, we need to wait for // the network to be available before we can continue. - common.isNewChannel = function (href, password, _cb) { + common.hasChannelHistory = function (href, password, _cb) { var cb = Util.once(Util.mkAsync(_cb)); var channel = Hash.hrefToHexChannelId(href, password); var error; @@ -1182,7 +1192,6 @@ define([ } else if (mailbox && typeof(mailbox) === "object") { m = {}; Object.keys(mailbox).forEach(function (ed) { - console.log(mailbox[ed]); try { m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true)); } catch (e) { @@ -1217,6 +1226,7 @@ define([ cryptgetVal = JSON.stringify(parsed); } }), optsGet); + Cache.clearChannel(newSecret.channel, waitFor()); }).nThen(function (waitFor) { optsPut.metadata.restricted = oldMetadata.restricted; optsPut.metadata.allowed = oldMetadata.allowed; @@ -1435,7 +1445,6 @@ define([ var oldMetadata; var oldRtChannel; var privateData; - var padData; var newSecret; if (parsed.hashData.version >= 2) { @@ -1462,9 +1471,8 @@ define([ Nthen(function (waitFor) { common.getPadAttribute('', waitFor(function (err, _data) { - padData = _data; - if (!oldPassword) { - optsGet.password = padData.password; + if (!oldPassword && _data) { + optsGet.password = _data.password; } }), href); common.getAccessKeys(waitFor(function (keys) { @@ -1472,7 +1480,7 @@ define([ optsPut.accessKeys = keys; })); }).nThen(function (waitFor) { - oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password); + oldSecret = Hash.getSecrets(parsed.type, parsed.hash, optsGet.password); require([ '/common/cryptget.js', @@ -1600,6 +1608,7 @@ define([ } })); })); + Cache.clearChannel(newSecret.channel, waitFor()); }).nThen(function (waitFor) { // The new rt channel is ready // The blob uses its own encryption and doesn't need to be reencrypted @@ -2508,6 +2517,24 @@ define([ } if (parsedNew.hashData) { oldHref = newHref; } }; + // If you're in noDrive mode, check if an FS_hash is added and reload if that's the case + if (rdyCfg.noDrive && !localStorage[Constants.fileHashKey]) { + window.addEventListener('storage', function (e) { + if (e.key !== Constants.fileHashKey) { return; } + // New entry added to FS_hash: drive created in another tab, reload + var o = e.oldValue; + var n = e.newValue; + if (!o && n) { + postMessage('HAS_DRIVE', null, function(obj) { + // If we're still in noDrive mode, reload + if (!obj.state) { + LocalStore.loginReload(); + } + // Otherwise this worker is connected, nothing to do + }); + } + }); + } // Listen for login/logout in other tabs window.addEventListener('storage', function (e) { if (e.key !== Constants.userHashKey) { return; } diff --git a/www/common/inner/access.js b/www/common/inner/access.js index a09f98bfb..cb3fc57c9 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -25,10 +25,12 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); - var channel = data.channel; + var priv = metadataMgr.getPrivateData(); + var channel = data.channel || priv.channel; var owners = data.owners || []; var pending_owners = data.pending_owners || []; var teamOwner = data.teamId; + var title = opts.title; opts = opts || {}; var redrawAll = function () {}; @@ -115,7 +117,7 @@ define([ if (!friend) { return; } common.mailbox.sendTo("RM_OWNER", { channel: channel, - title: data.title, + title: data.title || title, pending: pending }, { channel: friend.notifications, @@ -271,7 +273,7 @@ define([ href: data.href || data.rohref, password: data.password, path: isTemplate ? ['template'] : undefined, - title: data.title || '', + title: data.title || title || "", teamId: obj.id }, waitFor(function (err) { if (err) { return void console.error(err); } @@ -320,6 +322,12 @@ define([ })); } }).nThen(function (waitFor) { + var href = data.href; + var hashes = priv.hashes || {}; + var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash; + if (data.fakeHref) { + href = Hash.hashToHref(bestHash, priv.app); + } sel.forEach(function (el) { var curve = $(el).attr('data-curve'); if (curve === user.curvePublic) { return; } @@ -327,9 +335,9 @@ define([ if (!friend) { return; } common.mailbox.sendTo("ADD_OWNER", { channel: channel, - href: data.href, - password: data.password, - title: data.title + href: href, + password: data.password || priv.password, + title: data.title || title }, { channel: friend.notifications, curvePublic: friend.curvePublic @@ -398,7 +406,8 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); - var channel = data.channel; + var priv = metadataMgr.getPrivateData(); + var channel = data.channel || priv.channel; var owners = data.owners || []; var restricted = data.restricted || false; var allowed = data.allowed || []; @@ -894,6 +903,7 @@ define([ if (data.fakeHref) { href = Hash.hashToHref(bestHash, priv.app); } + var isNotStored = Boolean(data.fakeHref); sframeChan.query(q, { teamId: typeof(owned) !== "boolean" ? owned : undefined, href: href, @@ -931,22 +941,26 @@ define([ // Pad password changed: update the href // Use hidden hash if needed (we're an owner of this pad so we know it is stored) var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']); - var href = (priv.readOnly && data.roHref) ? data.roHref : data.href; + if (isNotStored) { useUnsafe = true; } + var _href = (priv.readOnly && data.roHref) ? data.roHref : data.href; if (useUnsafe !== true) { - var newParsed = Hash.parsePadUrl(href); + var newParsed = Hash.parsePadUrl(_href); var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass); var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {}); - href = Hash.hashToHref(newHash, parsed.type); + _href = Hash.hashToHref(newHash, parsed.type); } + // Trigger a page reload if the href didn't change + if (_href === href) { _href = undefined; } + if (data.warning) { return void UI.alert(Messages.properties_passwordWarning, function () { - common.gotoURL(href); + common.gotoURL(_href); }, {force: true}); } return void UI.alert(Messages.properties_passwordSuccess, function () { if (!isSharedFolder) { - common.gotoURL(href); + common.gotoURL(_href); } }, {force: true}); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 8ac1a77c3..af5ac9047 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -319,6 +319,7 @@ define([ }); }; + var myDeletions = {}; Store.removeOwnedChannel = function (clientId, data, cb) { // "data" used to be a string (channelID), now it can also be an object // data.force tells us we can safely remove the drive ID @@ -331,8 +332,6 @@ define([ teamId = data.teamId; } - // XXX CLEAR CACHE - if (channel === store.driveChannel && !force) { return void cb({error: 'User drive removal blocked!'}); } @@ -341,7 +340,11 @@ define([ if (!s) { return void cb({ error: 'ENOTFOUND' }); } if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + // If this channel is loaded, remember that we deleted it ourselves + if (Store.channels[channel]) { myDeletions[channel] = true; } + s.rpc.removeOwnedChannel(channel, function (err) { + if (err) { delete myDeletions[channel]; } cb({error:err}); }); }; @@ -452,7 +455,7 @@ define([ account.note = obj.note; cb(obj); }); - }); + }, Cache); }; ////////////////////////////////////////////////////////////////// @@ -1708,8 +1711,17 @@ define([ return; } var onError = function (err) { + // If it's a deletion started from this worker, different UI message + if (err && err.type === "EDELETED" && myDeletions[data.channel]) { + delete myDeletions[channel]; + err.ownDeletion = true; + } channel.bcast("PAD_ERROR", err); + if (err && err.type === "EDELETED" && Cache && Cache.clearChannel) { + Cache.clearChannel(data.channel); + } + // If this is a DELETED, EXPIRED or RESTRICTED pad, leave the channel if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; } Store.leavePad(null, data, function () {}); @@ -2610,22 +2622,28 @@ define([ // "cb" is wrapped in Util.once() and may have already been called // if we have a local cache var onReady = function (clientId, returned, cb) { - console.error('READY'); store.ready = true; var proxy = store.proxy; var manager = store.manager; var userObject = store.userObject; nThen(function (waitFor) { - if (manager) { return; } if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; } if (!proxy.friends_pending) { proxy.friends_pending = {}; } - onCacheReady(clientId, waitFor()); - manager = store.manager; - userObject = store.userObject; - }).nThen(function (waitFor) { + + // Call onCacheReady if the manager is not yet defined + if (!manager) { + onCacheReady(clientId, waitFor()); + manager = store.manager; + userObject = store.userObject; + } + + // Initialize RPC in parallel of onCacheReady in case a shared folder + // is RESTRICTED and requires RPC authentication initAnonRpc(null, null, waitFor()); initRpc(null, null, waitFor()); + + // Update loading progress postMessage(clientId, 'LOADING_DRIVE', { type: 'migrate', progress: 0 @@ -2941,6 +2959,13 @@ define([ */ var initialized = false; + // Are we still in noDrive mode? + Store.hasDrive = function (clientId, data, cb) { + cb({ + state: Boolean(store.proxy) + }); + }; + // If we load CryptPad for the first time from an existing pad, don't create a // drive automatically. var onNoDrive = function (clientId, cb) { diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index 28c181079..11024c609 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -573,6 +573,14 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback) var ready = false; var onCacheReady = function () { if (!config.onCacheReady) { return; } + var state = ref.state; + if (!Object.keys(state.members || {}).length) { + // No member, corrupted cache + try { + ref.internal.cpNetflux.resetCache(); + } catch (e) { console.error(e); } + return void config.onCacheReady({error: "CORRUPTED"}); + } config.onCacheReady(roster); }; var onReady = function (info) { diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index e444f47f5..c4d1d1301 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -15,6 +15,7 @@ define([ MIGRATE_ANON_DRIVE: Store.migrateAnonDrive, PING: function (cId, data, cb) { cb(); }, CACHE_DISABLE: Store.disableCache, + HAS_DRIVE: Store.hasDrive, // RPC UPDATE_PIN_LIMIT: Store.updatePinLimit, GET_PIN_LIMIT: Store.getPinLimit, diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 31785e26b..932e53e9f 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -187,7 +187,7 @@ define([ team.rpc = call; team.onRpcReadyEvt.fire(); cb(); - }); + }, Cache); }); }; @@ -351,10 +351,9 @@ define([ var team; if (!ctx.store.proxy.teams[id]) { return; } nThen(function (waitFor) { - if (ctx.cache[id]) { return; } onCacheReady(ctx, id, lm, roster, keys, cId, waitFor()); - }).nThen(function (waitFor) { - team = ctx.teams[id]; + team = ctx.teams[id] || ctx.cache[id]; + // Init Team RPC if (!keys.drive.edPrivate) { return; } initRpc(ctx, team, keys.drive, waitFor(function () {})); @@ -547,6 +546,14 @@ define([ store: ctx.store, lastKnownHash: rosterData.lastKnownHash, onCacheReady: function (_roster) { + if (!cache) { return; } + console.error('Corrupted roster cache, cant load this team offline', teamData); + if (_roster && _roster.error === "CORRUPTED") { + if (lm && typeof(lm.stop) === "function") { lm.stop(); } + waitFor.abort(); + cb({error: 'CACHE_CORRUPTED_ROSTER'}); + return; + } roster = _roster; cacheRdy.roster = true; cacheRdy.check(); diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 20d57fd07..32ef8ed41 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -213,10 +213,6 @@ define([ evStart.reg(function () { toolbar.forgotten(); }); break; } - case STATE.FORBIDDEN: { - evStart.reg(function () { toolbar.deleted(); }); - break; - } case STATE.DELETED: { evStart.reg(function () { toolbar.deleted(); }); break; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 687d3a54b..61588b9aa 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -78,7 +78,7 @@ define([ }; var AppConfig; var Test; - var password, newPadPassword; + var password, newPadPassword, newPadPasswordForce; var initialPathInDrive; var burnAfterReading; @@ -312,6 +312,7 @@ define([ newPadPassword = Crypto.decrypt(newPad.pw, uKey); } catch (e) { console.error(e); } } + if (newPad.f) { newPadPasswordForce = 1; } if (newPad.d) { Cryptpad.fromFileData = newPad.d; var _parsed1 = Utils.Hash.parsePadUrl(Cryptpad.fromFileData.href); @@ -319,6 +320,7 @@ define([ delete Cryptpad.fromFileData; } } + } catch (e) { console.error(e, parsed.hashData.newPadOpts); } @@ -349,7 +351,7 @@ define([ } // 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. + // We'll use getFileSize and hasChannelHistory 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 @@ -397,15 +399,15 @@ define([ } }; if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) + // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, function (e, size) { next(e, size === 0); }); return; } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(currentPad.href, password, next); + // Not a file, so we can use `hasChannelHistory` + Cryptpad.hasChannelHistory(currentPad.href, password, next); }); sframeChan.event("EV_PAD_PASSWORD", cfg); }; @@ -467,19 +469,32 @@ define([ currentPad.href = parsed.getUrl(opts); currentPad.hash = parsed.hashData && parsed.hashData.getHash(opts); } - Cryptpad.getPadAttribute('title', w(function (err, data) { + Cryptpad.getPadAttribute('channel', w(function (err, data) { stored = (!err && typeof (data) === "string"); })); Cryptpad.getPadAttribute('password', w(function (err, val) { password = val; }), parsed.getUrl()); }).nThen(function (w) { + // If we've already tested this password and this is a redirect, force + if (typeof(newPadPassword) !== "undefined" && newPadPasswordForce) { + password = newPadPassword; + return void todo(); + } + + // If the pad is not stored and we have a newPadPassword, it probably + // comes from a notification: password prompt pre-filled if (!password && !stored && newPadPassword) { passwordCfg.value = newPadPassword; } + // Pad not stored && password required: always ask for the password + if (!stored && parsed.hashData.password && !newPadPasswordForce) { + return void askPassword(true, passwordCfg); + } + if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) + // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, w(function (e, size) { if (size !== 0) { return void todo(); } @@ -488,8 +503,8 @@ define([ })); return; } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(currentPad.href, password, w(function(e, isNew) { + // Not a file, so we can use `hasChannelHistory` + Cryptpad.hasChannelHistory(currentPad.href, password, w(function(e, isNew) { if (isNew && expire && expire < (+new Date())) { sframeChan.event("EV_EXPIRED_ERROR"); waitFor.abort(); @@ -503,10 +518,6 @@ define([ 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); })); @@ -540,8 +551,7 @@ define([ if (realtime) { // TODO we probably don't need to check again for password-protected pads - // (we use isNewChannel to test the password...) - Cryptpad.isNewChannel(currentPad.href, password, waitFor(function (e, isNew) { + Cryptpad.hasChannelHistory(currentPad.href, password, waitFor(function (e, isNew) { if (e) { return console.error(e); } isNewFile = Boolean(isNew); })); @@ -607,6 +617,7 @@ define([ feedbackAllowed: Utils.Feedback.state, isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, + oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, notifications: notifs, accounts: { @@ -1673,6 +1684,43 @@ define([ }); }); + sframeChan.on('Q_PASSWORD_CHECK', function (pw, cb) { + Cryptpad.isNewChannel(currentPad.href, pw, function (e, isNew) { + if (isNew === false) { + nThen(function (w) { + // If the pad is stored, update its data + var _secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, pw); + var chan = _secret.channel; + var editH = Utils.Hash.getEditHashFromKeys(_secret); + var viewH = Utils.Hash.getViewHashFromKeys(_secret); + var href = Utils.Hash.hashToHref(editH, parsed.type); + var roHref = Utils.Hash.hashToHref(viewH, parsed.type); + Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('channel', chan, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); + }).nThen(function () { + // Get redirect URL + var uHash = Utils.LocalStore.getUserHash(); + var uSecret = Utils.Hash.getSecrets('drive', uHash); + var uKey = uSecret.keys.cryptKey; + var url = Utils.Hash.getNewPadURL(currentPad.href, { + pw: Crypto.encrypt(pw, uKey), + f: 1 + }); + // redirect + window.location.href = url; + document.location.reload(); + }); + + return; + } + cb({ + error: e + }); + }); + }); + if (cfg.messaging) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { Cryptpad.universal.execCommand({ diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index c46a0641d..77f60fdb7 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -801,7 +801,7 @@ define([ $err.find('a').click(function () { funcs.gotoURL(); }); - UI.findOKButton().click(); + UI.findOKButton().click(); // FIXME this might be randomly clicking something dangerous... UI.errorLoadingScreen($err, true, true); }); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index aafc356a5..e034304f7 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -213,7 +213,11 @@ MessengerUI, Messages) { .appendTo($editUsers); var degradedLimit = Config.degradedLimit || 8; - if (!online) { + if (toolbar.isDeleted) { + $('').text(Messages.deletedFromServer).appendTo($editUsersList); + numberOfEditUsers = '?'; + numberOfViewUsers = '?'; + } else if (!online) { $('').text(Messages.userlist_offline).appendTo($editUsersList); numberOfEditUsers = '?'; numberOfViewUsers = '?'; @@ -229,7 +233,7 @@ MessengerUI, Messages) { var $spansmall = $('').html(fa_editusers + ' ' + numberOfEditUsers + '   ' + fa_viewusers + ' ' + numberOfViewUsers); $userButtons.find('.cp-dropdown-button-title').html('').append($spansmall); - if (!online) { return; } + if (!online || toolbar.isDeleted) { return; } if (metadataMgr.isDegraded() === true) { return; } @@ -542,6 +546,9 @@ MessengerUI, Messages) { hidden: true }); $shareBlock.click(function () { + if (toolbar.isDeleted) { + return void UI.warn(Messages.deletedFromServer); + } var title = (config.title && config.title.getTitle && config.title.getTitle()) || (config.title && config.title.defaultName) || ""; @@ -565,7 +572,15 @@ MessengerUI, Messages) { h('span.cp-button-name', Messages.accessButton) ])); $accessBlock.click(function () { - Common.getSframeChannel().event('EV_ACCESS_OPEN'); + if (toolbar.isDeleted) { + return void UI.warn(Messages.deletedFromServer); + } + var title = (config.title && config.title.getTitle && config.title.getTitle()) + || (config.title && config.title.defaultName) + || ""; + Common.getSframeChannel().event('EV_ACCESS_OPEN', { + title: title + }); }); toolbar.$bottomM.append($accessBlock); @@ -1378,7 +1393,9 @@ MessengerUI, Messages) { toolbar.deleted = function (/*userId*/) { toolbar.isErrorState = true; toolbar.connected = false; + toolbar.isDeleted = true; updateUserList(toolbar, config, true); + toolbar.title.toggleClass('cp-toolbar-unsync', true); // "read only" next to the title if (toolbar.spinner) { toolbar.spinner.text(Messages.deletedFromServer); } diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index c282362b6..992213969 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -27,10 +27,10 @@ "padNotPinned": "Dieses Pad wird nach 3 Monaten ohne Aktivität auslaufen, {0}logge dich ein{1} oder {2}registriere dich{3}, um das Auslaufen zu verhindern.", "anonymousStoreDisabled": "Der Webmaster dieses CryptPad-Servers hat die anonyme Verwendung des Speichers deaktiviert. Du musst dich einloggen, um CryptDrive zu verwenden.", "expiredError": "Dieses Pad ist abgelaufen und ist nicht mehr verfügbar.", - "deletedError": "Dieses Pad wurde von seinem Eigentümer gelöscht und ist nicht mehr verfügbar.", + "deletedError": "Dieses Dokument wurde gelöscht und ist nicht mehr verfügbar.", "inactiveError": "Dieses Pad ist wegen Inaktivität gelöscht worden. Drücke Esc, um ein neues Pad zu erstellen.", "chainpadError": "Ein kritischer Fehler ist beim Aktualisieren deines Inhalts aufgetreten. Diese Seite ist schreibgeschützt, damit du sicherstellen kannst, dass kein Inhalt verloren geht.
Drücke Esc, um das Pad schreibgeschützt zu lesen oder lade es neu, um die Bearbeitung fortzusetzen.", - "errorCopy": " Du kannst noch auf den Inhalt zugreifen, indem du Esc drückst.
Wenn du das Fenster schließt, kannst du darauf nicht mehr zugreifen.", + "errorCopy": " Du kannst die aktuelle Version weiterhin schreibgeschützt verwenden, indem du Esc drückst.", "errorRedirectToHome": "Drücke Esc, um zu deinem CryptDrive zurückzukehren.", "newVersionError": "Eine neue Version von CryptPad ist verfügbar.
Lade die Seite neu, um die neue Version zu benutzen. Drücke Esc, um im Offline-Modus weiterzuarbeiten.", "loading": "Laden...", @@ -597,8 +597,8 @@ "creation_expiration": "Ablaufdatum", "creation_passwordValue": "Passwort", "creation_newPadModalDescription": "Klicke auf einen Dokument-Typ, um das entsprechende Dokument zu erstellen. Du kannst auch die Tab-Taste für die Auswahl und die Enter-Taste zum Bestätigen benutzen.", - "password_info": "Das Pad, das du öffnen möchtest, existiert nicht mehr oder ist mit einem Passwort geschützt. Gib das richtige Passwort ein, um den Inhalt anzuzeigen.", - "password_error": "Pad nicht gefunden!
Dieser Fehler kann zwei Ursachen haben: Entweder ist das Passwort ungültig oder das Pad wurde vom Server gelöscht.", + "password_info": "Das Dokument, das du öffnen möchtest, existiert nicht mehr oder ist mit einem neuen Passwort geschützt. Gib das richtige Passwort ein, um den Inhalt anzuzeigen.", + "password_error": "Dokument nicht gefunden!
Dieser Fehler kann zwei Ursachen haben: Entweder ist das Passwort ungültig oder das Dokument wurde zerstört.", "password_placeholder": "Gib das Passwort hier ein...", "password_submit": "Abschicken", "properties_addPassword": "Passwort hinzufügen", diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 322c7cf7b..f3eff56bc 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -27,11 +27,11 @@ "padNotPinned": "Ce pad va expirer après 3 mois d'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.", "anonymousStoreDisabled": "L'administrateur de cette instance de CryptPad a désactivé le drive pour les utilisateurs non enregistrés. Vous devez vous connecter pour pouvoir utiliser CryptDrive.", "expiredError": "Ce pad a atteint sa date d'expiration est n'est donc plus disponible.", - "deletedError": "Ce pad a été supprimé par son propriétaire et n'est donc plus disponible.", + "deletedError": "Ce document a été supprimé et n'est plus disponible.", "inactiveError": "Ce pad a été supprimé en raison de son inactivité. Appuyez sur Échap pour créer un nouveau pad.", "chainpadError": "Une erreur critique est survenue lors de la mise à jour du contenu. Le pad est désormais en mode lecture seule afin de s'assurer que vous ne perdiez pas davantage de données.
Appuyez sur Échap pour voir le pad ou rechargez la page pour pouvoir le modifier à nouveau.", "invalidHashError": "L'URL du document demandé n'est pas valide.", - "errorCopy": " Vous pouvez accéder au contenu en appuyant sur Échap.
Quand vous fermerez cette page, il sera définitivement supprimé.", + "errorCopy": " Vous pouvez toujours utiliser la version actuelle en mode lecture seule en appuyant sur Échap.", "errorRedirectToHome": "Appuyez sur Échap pour retourner vers votre CryptDrive.", "newVersionError": "Une nouvelle version de CryptPad est disponible.
Rechargez la page pour utiliser la nouvelle version, ou appuyez sur Échap pour accéder au contenu actuel en mode hors-ligne.", "loading": "Chargement...", @@ -604,8 +604,8 @@ "creation_expiration": "Date d'expiration", "creation_passwordValue": "Mot de passe", "creation_newPadModalDescription": "Cliquer sur le type de document à créer. Vous pouvez aussi utiliser les touches Tab pour sélectionner un type et Entrée pour valider.", - "password_info": "Le pad auquel vous essayez d'accéder n'existe plus ou est protégé par un mot de passe. Entrez le bon mot de passe pour accéder à son contenu.", - "password_error": "Pad introuvable !
Cette erreur peut provenir de deux facteurs. Soit le mot de passe est faux, soit le pad a été supprimé du serveur.", + "password_info": "Le document auquel vous essayez d'accéder n'existe plus ou est protégé par un nouveau mot de passe. Entrez le bon mot de passe pour accéder au contenu.", + "password_error": "Document introuvable
Cette erreur peut provenir de deux facteurs : soit le mot de passe est faux, soit le document a été détruit.", "password_placeholder": "Tapez le mot de passe ici...", "password_submit": "Valider", "properties_addPassword": "Ajouter un mot de passe", diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index d0e4d20cb..ceec7b35f 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -29,11 +29,11 @@ "padNotPinnedVariable": "This pad will expire after {4} days of inactivity, {0}login{1} or {2}register{3} to preserve it.", "anonymousStoreDisabled": "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive.", "expiredError": "This pad has reached its expiration time and is no longer available.", - "deletedError": "This pad has been deleted by its owner and is no longer available.", + "deletedError": "This document has been deleted and is no longer available.", "inactiveError": "This pad has been deleted due to inactivity. Press Esc to create a new pad.", "chainpadError": "A critical error occurred when updating your content. This page is in read-only mode to make sure you won't lose your work.
Hit Esc to continue to view this pad, or reload to try editing again.", "invalidHashError": "The document you've requested has an invalid URL.", - "errorCopy": " You can still access the content by pressing Esc.
Once you close this window you will not be able to access it again.", + "errorCopy": " You can still use the current version in read-only mode by pressing Esc.", "errorRedirectToHome": "Press Esc to be redirected to your CryptDrive.", "newVersionError": "A new version of CryptPad is available.
Reload to use the new version, or press escape to access your content in offline mode.", "loading": "Loading...", @@ -619,8 +619,8 @@ "creation_expiration": "Expiration date", "creation_passwordValue": "Password", "creation_newPadModalDescription": "Click on a document type to create it. You can also press Tab to select the type and press Enter to confirm.", - "password_info": "The pad you're trying to open no longer exist or is protected with a password. Enter the correct password to access its content.", - "password_error": "Pad not found!
This error can be caused by two factors: either the password in invalid, or the pad has been deleted from the server.", + "password_info": "The document you are trying to open no longer exists or is protected with a new password. Enter the correct password to access the content.", + "password_error": "Document not found
This error can be caused by two factors: either the password is invalid, or the document has been destroyed.", "password_placeholder": "Type the password here...", "password_submit": "Submit", "properties_addPassword": "Add a password", diff --git a/www/common/translations/messages.pt-br.json b/www/common/translations/messages.pt-br.json index d9ebe6794..bd78dbf1b 100644 --- a/www/common/translations/messages.pt-br.json +++ b/www/common/translations/messages.pt-br.json @@ -12,8 +12,8 @@ "kanban": "Kanban", "todo": "A Fazer", "contacts": "Contactos", - "sheet": "SpreadSheet (Beta)", - "teams": "" + "sheet": "Planilha (Beta)", + "teams": "Times" }, "button_newpad": "Novo bloco RTF", "button_newcode": "Novo bloco de código", @@ -26,7 +26,7 @@ "loading": "Carregando...", "error": "Erro", "saved": "Salvo", - "deleted": "Bloco deletado do seu CryptDrive", + "deleted": "Deletado", "disconnected": "Desconectado", "synchronizing": "Sincronizando", "reconnecting": "Reconectando...", @@ -260,32 +260,32 @@ "feedback_about": "If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions", "feedback_privacy": "We care about your privacy, and at the same time we want CryptPad to be very easy to use. We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken.", "feedback_optout": "If you would like to opt out, visit your user settings page, where you'll find a checkbox to enable or disable user feedback", - "button_newkanban": "", + "button_newkanban": "Novo Kanban", "button_newsheet": "Nova Planilha", "padNotPinned": "Esse pad vai expirar depois de 3 meses de inatividade. {0}login{1} ou {2}registrar-se{3} para preservá-lo.", - "anonymousStoreDisabled": "", - "expiredError": "", - "deletedError": "", - "inactiveError": "", - "chainpadError": "", - "invalidHashError": "", - "errorCopy": "", - "errorRedirectToHome": "", - "newVersionError": "", - "deletedFromServer": "", - "mustLogin": "", - "disabledApp": "", - "realtime_unrecoverableError": "", - "typing": "", - "initializing": "", - "forgotten": "", - "errorState": "", - "userlist_offline": "", - "pinLimitReachedAlertNoAccounts": "", - "importButton": "", - "exportButton": "", - "saveTitle": "", - "forgetButton": "", + "anonymousStoreDisabled": "O webmaster desta instância do CryptPad desabilitou o armazenamento para usuários anônimos. Você precisa fazer login para habilitar o uso do CryptDrive.", + "expiredError": "Este pad alcançou o tempo de expiração e não está mais disponível.", + "deletedError": "Este documento foi deletado e não está mais disponível.", + "inactiveError": "Este pad foi deletado por inatividade. Tecle Esc para criar um novo pad.", + "chainpadError": "Um erro crítico ocorreu enquanto atualizava seu conteúdo. Esta página está em modo somente leitura para ter certeza de que você não perderá seu trabalho.
Tecle Esc para continuar vendo este pad, ou recarregue para tentar edita-lo novamente.", + "invalidHashError": "O documento requerido tem uma URL inválida.", + "errorCopy": " Você pode continuar usando esta versão em modo somente leitura teclando Esc.", + "errorRedirectToHome": "Tecle Esc para ser redirecionado para seu CryptDrive.", + "newVersionError": "Uma nova versão do CryptPad está disponível.
Recarregued para usar a nova versão, ou tecle Esc para acessar seu conteúdo em modo offline.", + "deletedFromServer": "Documento destruído", + "mustLogin": "Você precisa estar logado para acessar esta página", + "disabledApp": "Esta aplicação foi desabilitada. Contate o administrador deste CryptPad para mais informações.", + "realtime_unrecoverableError": "Um erro irrecuperável ocorreu. Click em OK para recarregar.", + "typing": "Editando", + "initializing": "Inicializando...", + "forgotten": "Movido para a lixeira", + "errorState": "Erro crítico: {0}", + "userlist_offline": "Você está desconectado, a lista do usuário não está disponível.", + "pinLimitReachedAlertNoAccounts": "Você alcançou o seu limite de armazenamento", + "importButton": "importar", + "exportButton": "Exportar", + "saveTitle": "Salve o título (enter)", + "forgetButton": "Deletar", "userListButton": "", "chatButton": "", "userAccountButton": "", @@ -357,5 +357,8 @@ "oo_cantUpload": "", "oo_uploaded": "", "canvas_opacityLabel": "", - "canvas_widthLabel": "" + "canvas_widthLabel": "", + "storageStatus": "Armazenamento:
{0} usados do total {1}", + "upgradeAccount": "Atualizar conta", + "padNotPinnedVariable": "Este pad vai expirar em {4} dias de inatividade, {0} faça login{1} ou {2}registre-se{3} para preserva-lo." } diff --git a/www/login/main.js b/www/login/main.js index 768c012a8..5023e0b19 100644 --- a/www/login/main.js +++ b/www/login/main.js @@ -70,7 +70,8 @@ define([ if ($uname.val()) { localStorage.login_user = $uname.val(); } - window.location.href = '/register/'; + var hash = (window.location.hash || '').replace(/\/login\//, '/register/'); + window.location.href = '/register/' + hash; }); Test(function (t) { diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index 01635cbaa..1c5af4a57 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -89,9 +89,10 @@ define([ }; // Access modal - create['access'] = function () { + create['access'] = function (data) { require(['/common/inner/access.js'], function (Access) { Access.getAccessModal(common, { + title: data.title, onClose: function () { hideIframe(); }