diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f649883d..3330eb35b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +# YunnanLakeNewt (3.24.0) + +## Goals + +We are once again working to develop some significant new features. This release is fairly small but includes some significant changes to detect and handle a variety of errors. + +## Update notes + +This release includes some minor corrections the recommended NGINX configuration supplied in `cryptpad/docs/example.nginx.conf`. + +To update from 3.23.2 to 3.24.0: + +1. Update your NGINX config to replicate the most recent changes and reload NGINX to apply them. +2. Stop the nodejs server. +3. Pull the latest code from the `3.24.0` tag or the `main` branch using `git`. +4. Ensure you have the latest clientside and serverside dependencies with `bower update` and `npm install`. +5. Restart the nodejs server. + +## Features + +* A variety of CryptPad's pages now feature a much-improved loading screen which provides a more informative account of what is being loaded. It also implements some generic error handling to detect and report when something has failed in a catastrophic way. This is intended to both inform users that the page is in a broken state as well as to improve the quality of the debugging information they can provide to us so that we can fix the underlying cause. +* It is now possible to create spreadsheets from templates. Template functionality has existed for a long time in our other editors, however, OnlyOffice's architecture differs significantly and required the implementation of a wholly different system. +* One user reported some confusion regarding the use of the Kanban app's _tag_ functionality. We've updated the UI to be a little more informative. +* The "table of contents" in rich text pads now includes "anchors" created via the editor's toolbar. + +## Bug fixes + +* Recent changes to CryptPad's recommended CSP headers enabled Firefox to export spreadsheets to XLSX format, but they also triggered some regressions due to a number of incompatible APIs. + * Our usage of the `sessionStorage` for the purpose of passing important information to editors opened in a new tab stopped working. This meant that when you created a document in a folder, the resulting new tab would not receive the argument describing where it should be stored, and would instead save it to the default location. We've addressed this by replacing our usage of sessionStorage with a new format for passing the same arguments via the hash in the new document's URL. + * The `window.print` API also failed in a variety of cases. We've updated the relevant CSP headers to only be applied on the sheet editor (to support XSLX export) but allow printing elsewhere. We've also updated some print styles to provide more appealing results. +* The table of contents available in rich text pads failed to scroll when there were a sufficient number of heading to flow beyond the length of the page. Now a scrollbar appears when necessary. +* We discovered a number of cases where the presence of an allow list prevented some valid behaviour due to the server incorrectly concluding that users were not authenticated. We've improved the client's ability to detect these cases and re-authenticate when necessary. +* We also found that when the server was under very heavy load some database queries were timing out because they were slow (but not stopped). We've addressed this to only terminate such queries if they have been entirely inactive for several minutes. +* It was possible for "safe links" to include a mode ("edit" or "view") which did not match the rights of the user opening them. For example, if a user loaded a safe link with edit rights though they only had read-only access via their "viewer" role in a team. CryptPad will now recover from such cases and open the document with the closest set of access rights that they possess. +* We found that the server query `"IS_NEW_PAD"` could return an error but that clients would incorrectly interpret such a response as a `false`. This has been corrected. +* Finally, we've modified the "trash" UI for user and team drives such that when users attempt to empty their trash of owned shared folders they are prompted to remove the items or delete them from the server entirely, as they would be with other owned assets. + # XerusDaamsi reloaded (3.23.2) A number of instance administrators reported issues following our 3.23.1 release. We suspect the issues were caused by applying the recommended update steps out of order which would result in the incorrect HTTP header values getting cached for the most recent version of a file. Since the most recently updated headers modified some security settings, this caused a catastrophic error on clients receiving the incorrect headers which caused them to fail to load under certain circumstances. diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css index 000162c00..a7939839d 100644 --- a/customize.dist/ckeditor-contents.css +++ b/customize.dist/ckeditor-contents.css @@ -213,3 +213,33 @@ media-tag * { width: 100%; height: 100%; } +media-tag button.btn { + background-color: #fff; + box-sizing: border-box; + outline: 0; + display: inline-flex; + align-items: center; + padding: 0 6px; + min-height: 36px; + line-height: 22px; + white-space: nowrap; + text-align: center; + text-transform: uppercase; + font-size: 14px; + text-decoration: none; + cursor: pointer; + border-radius: 0; + transition: none; + color: #3F4141; + border: 1px solid #3F4141; +} +media-tag button.btn:hover, media-tag button.btn:active, media-tag button.btn:focus { + background-color: #ccc; +} +media-tag button b { + margin-left: 5px; +} +media-tag button .fa { + display: inline; + margin-right: 5px; +} diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 344c61942..dc073aff8 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -312,8 +312,9 @@ button.primary:hover{ return bar; }; + var hasErrored = false; var updateLoadingProgress = function (data) { - if (!built) { return; } + if (!built || !data) { return; } var c = types.indexOf(data.type); if (c < current) { return console.error(data); } try { @@ -323,18 +324,33 @@ button.primary:hover{ if (el2) { el2.innerHTML = makeList(data); } var el3 = document.querySelector('.cp-loading-progress-container'); if (el3) { el3.innerHTML = makeBar(data); } - } catch (e) { console.error(e); } + } catch (e) { + if (!hasErrored) { console.error(e); } + } }; window.CryptPad_updateLoadingProgress = updateLoadingProgress; + window.CryptPad_loadingError = function (err) { if (!built) { return; } + + if (err === 'Error: XDR encoding failure') { + console.warn(err); + return; + } + + hasErrored = true; + var err2; + if (err === 'Script error.') { + err2 = Messages.error_unhelpfulScriptError; + } + try { var node = document.querySelector('.cp-loading-progress'); if (!node) { return; } if (node.parentNode) { node.parentNode.removeChild(node); } document.querySelector('.cp-loading-spinner-container').setAttribute('style', 'display:none;'); document.querySelector('#cp-loading-message').setAttribute('style', 'display:block;'); - document.querySelector('#cp-loading-message').innerText = err; + document.querySelector('#cp-loading-message').innerText = err2 || err; } catch (e) { console.error(e); } }; return function () { diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 40dbbfb95..303375e4e 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -26,7 +26,9 @@ var getStoredLanguage = function () { return localStorage && localStorage.getIte var getBrowserLanguage = function () { return navigator.language || navigator.userLanguage || ''; }; var getLanguage = messages._getLanguage = function () { if (window.cryptpadLanguage) { return window.cryptpadLanguage; } - if (getStoredLanguage()) { return getStoredLanguage(); } + try { + if (getStoredLanguage()) { return getStoredLanguage(); } + } catch (e) { console.log(e); } var l = getBrowserLanguage(); // Edge returns 'fr-FR' --> transform it to 'fr' and check again return map[l] ? l : @@ -65,7 +67,9 @@ define(req, function(AppConfig, Default, Language) { if (AppConfig.availableLanguages.indexOf(language) === -1) { language = defaultLanguage; Language = Default; - localStorage.setItem(LS_LANG, language); + try { + localStorage.setItem(LS_LANG, language); + } catch (e) { console.log(e); } } Object.keys(map).forEach(function (l) { if (l === defaultLanguage) { return; } diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 1db764bcc..74c46c4ec 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -62,7 +62,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "CryptPad v3.23.2 (XerusDaamsi reloaded)"; + Pages.versionString = "CryptPad v3.24.0 (YunnanLakeNewt)"; // used for the about menu Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 0acbd49b4..65eedf263 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -2,6 +2,10 @@ @import (reference) "./variables.less"; .forms_main() { + --LessLoader_require: LessLoader_currentFile(); +} + +& { @alertify-fore: @colortheme_modal-fg; @alertify-btn-fg: @alertify-fore; @alertify-light-bg: fade(@alertify-fore, 25%); @@ -124,6 +128,14 @@ font-weight: bold; } + &.btn-default { + border-color: @cryptpad_text_col; + color: @cryptpad_text_col; + &:hover, &:active, &:focus { + background-color: #ccc; + } + } + &.danger, &.btn-danger { background-color: @colortheme_alertify-red; border-color: @colortheme_alertify-red-border; diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 23eb056b0..d7fe13f43 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -94,6 +94,15 @@ height: 80vh; max-height: 90vh; } + button.btn-default { + display: inline-flex; + .fa { + margin-right: 5px; + } + b { + margin-left: 5px; + } + } } media-tag:empty { width: 100px; diff --git a/docs/cryptpad.service b/docs/cryptpad.service index eee8b2af5..43d8652f6 100644 --- a/docs/cryptpad.service +++ b/docs/cryptpad.service @@ -17,7 +17,7 @@ SyslogIdentifier=cryptpad User=cryptpad Group=cryptpad # modify to match your working directory -Environment='PWD="/home/cryptpad/cryptpad/cryptpad"' +Environment='PWD="/home/cryptpad/cryptpad"' # systemd sets the open file limit to 4000 unless you override it # cryptpad stores its data with the filesystem, so you should increase this to match the value of `ulimit -n` diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index d7f22825d..26e574307 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -167,12 +167,19 @@ var archiveDocument = function (Env, Server, cb, data) { // Env.blobStore.archive.proof(userSafeKey, blobId, cb) }; -var restoreArchivedDocument = function (Env, Server, cb) { - // Env.msgStore.restoreArchivedChannel(channelName, cb) - // Env.blobStore.restore.blob(blobId, cb) - // Env.blobStore.restore.proof(userSafekey, blobId, cb) +var restoreArchivedDocument = function (Env, Server, cb, data) { + var id = Array.isArray(data) && data[1]; + if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - cb("NOT_IMPLEMENTED"); + switch (id.length) { + case 32: + return void Env.msgStore.restoreArchivedChannel(id, cb); + case 48: + // Env.blobStore.restore.proof(userSafekey, id, cb) // XXX .... + return void Env.blobStore.restore.blob(id, cb); + default: + return void cb("INVALID_ID_LENGTH"); + } }; // CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_INDEX', documentID], console.log) diff --git a/lib/commands/upload.js b/lib/commands/upload.js index 7286caa93..6dc0aa911 100644 --- a/lib/commands/upload.js +++ b/lib/commands/upload.js @@ -75,21 +75,9 @@ Upload.upload = function (Env, safeKey, chunk, cb) { Env.blobStore.upload(safeKey, chunk, cb); }; -var reportStatus = function (Env, label, safeKey, err, id) { - var data = { - safeKey: safeKey, - err: err && err.message || err, - id: id, - }; - var method = err? 'error': 'info'; - Env.Log[method](label, data); -}; - Upload.complete = function (Env, safeKey, arg, cb) { - Env.blobStore.complete(safeKey, arg, function (err, id) { - reportStatus(Env, 'UPLOAD_COMPLETE', safeKey, err, id); - cb(err, id); - }); + Env.blobStore.closeBlobstage(safeKey); + Env.completeUpload(safeKey, arg, false, cb); }; Upload.cancel = function (Env, safeKey, arg, cb) { @@ -97,9 +85,7 @@ Upload.cancel = function (Env, safeKey, arg, cb) { }; Upload.complete_owned = function (Env, safeKey, arg, cb) { - Env.blobStore.completeOwned(safeKey, arg, function (err, id) { - reportStatus(Env, 'UPLOAD_COMPLETE_OWNED', safeKey, err, id); - cb(err, id); - }); + Env.blobStore.closeBlobstage(safeKey); + Env.completeUpload(safeKey, arg, true, cb); }; diff --git a/lib/storage/blob.js b/lib/storage/blob.js index dfbc802b4..044eeaeaa 100644 --- a/lib/storage/blob.js +++ b/lib/storage/blob.js @@ -139,6 +139,15 @@ var upload = function (Env, safeKey, content, cb) { } }; +var closeBlobstage = function (Env, safeKey) { + var session = Env.getSession(safeKey); + if (!(session && session.blobstage && typeof(session.blobstage.close) === 'function')) { + return; + } + session.blobstage.close(); + delete session.blobstage; +}; + // upload_cancel var upload_cancel = function (Env, safeKey, fileSize, cb) { var session = Env.getSession(safeKey); @@ -159,27 +168,22 @@ var upload_cancel = function (Env, safeKey, fileSize, cb) { // upload_complete var upload_complete = function (Env, safeKey, id, cb) { - var session = Env.getSession(safeKey); - - if (session.blobstage && session.blobstage.close) { - session.blobstage.close(); - delete session.blobstage; - } + closeBlobstage(Env, safeKey); var oldPath = makeStagePath(Env, safeKey); var newPath = makeBlobPath(Env, id); nThen(function (w) { // make sure the path to your final location exists - Fse.mkdirp(Path.dirname(newPath), function (e) { + Fse.mkdirp(Path.dirname(newPath), w(function (e) { if (e) { w.abort(); return void cb('RENAME_ERR'); } - }); + })); }).nThen(function (w) { // make sure there's not already something in that exact location - isFile(newPath, function (e, yes) { + isFile(newPath, w(function (e, yes) { if (e) { w.abort(); return void cb(e); @@ -188,8 +192,8 @@ var upload_complete = function (Env, safeKey, id, cb) { w.abort(); return void cb('RENAME_ERR'); } - cb(void 0, newPath, id); - }); + cb(void 0, id); + })); }).nThen(function () { // finally, move the old file to the new path // FIXME we could just move and handle the EEXISTS instead of the above block @@ -217,15 +221,7 @@ var tryId = function (path, cb) { // owned_upload_complete var owned_upload_complete = function (Env, safeKey, id, cb) { - var session = Env.getSession(safeKey); - - // the file has already been uploaded to the staging area - // close the pending writestream - if (session.blobstage && session.blobstage.close) { - session.blobstage.close(); - delete session.blobstage; - } - + closeBlobstage(Env, safeKey); if (!isValidId(id)) { return void cb('EINVAL_ID'); } @@ -582,6 +578,9 @@ BlobStore.create = function (config, _cb) { }, }, + closeBlobstage: function (safeKey) { + closeBlobstage(Env, safeKey); + }, complete: function (safeKey, id, _cb) { var cb = Util.once(Util.mkAsync(_cb)); if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); } diff --git a/lib/storage/file.js b/lib/storage/file.js index b1ccfde0f..03bbaa8b4 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -681,9 +681,9 @@ var unarchiveChannel = function (env, channelName, cb) { // restore the metadata log Fse.move(archiveMetadataPath, metadataPath, w(function (err) { // if there's nothing to move, you're done. - if (err && err.code === 'ENOENT') { + /*if (err && err.code === 'ENOENT') { return CB(); - } + }*/ // XXX make sure removing this part won't break anything // call back with an error if something goes wrong if (err) { w.abort(); diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index 42c75fdca..9d5abf386 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -457,6 +457,38 @@ const evictInactive = function (data, cb) { Eviction(Env, cb); }; +var reportStatus = function (Env, label, safeKey, err, id) { + var data = { + safeKey: safeKey, + err: err && err.message || err, + id: id, + }; + var method = err? 'error': 'info'; + Env.Log[method](label, data); +}; + +const completeUpload = function (data, cb) { + if (!data) { return void cb('INVALID_ARGS'); } + var owned = data.owned; + var safeKey = data.safeKey; + var arg = data.arg; + + var method; + var label; + if (owned) { + method = 'completeOwned'; + label = 'UPLOAD_COMPLETE_OWNED'; + } else { + method = 'complete'; + label = 'UPLOAD_COMPLETE'; + } + + Env.blobStore[method](safeKey, arg, function (err, id) { + reportStatus(Env, label, safeKey, err, id); + cb(err, id); + }); +}; + const COMMANDS = { COMPUTE_INDEX: computeIndex, COMPUTE_METADATA: computeMetadata, @@ -471,6 +503,7 @@ const COMMANDS = { RUN_TASKS: runTasks, WRITE_TASK: writeTask, EVICT_INACTIVE: evictInactive, + COMPLETE_UPLOAD: completeUpload, }; COMMANDS.INLINE = function (data, cb) { @@ -568,7 +601,7 @@ process.on('message', function (data) { const cb = function (err, value) { process.send({ - error: err, + error: Util.serializeError(err), txid: data.txid, pid: data.pid, value: value, @@ -577,7 +610,7 @@ process.on('message', function (data) { if (!ready) { return void init(data.config, function (err) { - if (err) { return void cb(err); } + if (err) { return void cb(Util.serializeError(err)); } ready = true; cb(); }); diff --git a/lib/workers/index.js b/lib/workers/index.js index 6e9f57e88..fe868e250 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -9,6 +9,7 @@ const PID = process.pid; const DB_PATH = 'lib/workers/db-worker'; const MAX_JOBS = 16; +const DEFAULT_QUERY_TIMEOUT = 60000 * 15; // increased from three to fifteen minutes because queries for very large files were taking as long as seven minutes Workers.initialize = function (Env, config, _cb) { var cb = Util.once(Util.mkAsync(_cb)); @@ -113,6 +114,7 @@ Workers.initialize = function (Env, config, _cb) { const txid = guid(); var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) { if (err !== 'TIMEOUT') { return; } + Log.debug("WORKER_TIMEOUT_CAUSE", msg); // in the event of a timeout the user will receive an error // but the state used to resend a query in the event of a worker crash // won't be cleared. This also leaks a slot that could be used to keep @@ -132,7 +134,7 @@ Workers.initialize = function (Env, config, _cb) { state.tasks[txid] = msg; // default to timing out affter 180s if no explicit timeout is passed - var timeout = typeof(opt.timeout) !== 'undefined'? opt.timeout: 180000; + var timeout = typeof(opt.timeout) !== 'undefined'? opt.timeout: DEFAULT_QUERY_TIMEOUT; response.expect(txid, cb, timeout); state.worker.send(msg); }; @@ -422,6 +424,15 @@ Workers.initialize = function (Env, config, _cb) { }, cb); }; + Env.completeUpload = function (safeKey, arg, owned, cb) { + sendCommand({ + command: "COMPLETE_UPLOAD", + owned: owned, // Boolean + safeKey: safeKey, // String (public key) + arg: arg, // String (file id) + }, cb); + }; + cb(void 0); }); }; diff --git a/package-lock.json b/package-lock.json index a9ee6e5e7..775f42f68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.23.2", + "version": "3.24.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a849b2c2d..37fd6c686 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "3.23.2", + "version": "3.24.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/server.js b/server.js index 60247f47a..3869af509 100644 --- a/server.js +++ b/server.js @@ -136,6 +136,20 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) { }); }()); +app.use('/blob', function (req, res, next) { + if (req.method === 'HEAD') { + Express.static(Path.join(__dirname, (config.blobPath || './blob')), { + setHeaders: function (res, path, stat) { + res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Headers', 'Content-Length'); + res.set('Access-Control-Expose-Headers', 'Content-Length'); + } + })(req, res, next); + return; + } + next(); +}); + app.use(function (req, res, next) { if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -202,6 +216,7 @@ var serveConfig = (function () { adminKeys: Env.admins, inactiveTime: Env.inactiveTime, supportMailbox: Env.supportMailbox, + defaultStorageLimit: Env.defaultStorageLimit, maxUploadSize: Env.maxUploadSize, premiumUploadSize: Env.premiumUploadSize, }, null, '\t'), diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 5112fd9e2..651e9dddd 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -97,5 +97,15 @@ color: @colortheme_logo-2; } } + + input.cp-admin-inval { + border-color: red !important; + } + .cp-admin-nopassword { + .cp-admin-pw { + display: none !important; + } + } + } diff --git a/www/admin/inner.html b/www/admin/inner.html index 01bda5fab..eeb234d0c 100644 --- a/www/admin/inner.html +++ b/www/admin/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/admin/inner.js b/www/admin/inner.js index db5afca54..d9bebc097 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -43,6 +43,8 @@ define([ 'general': [ 'cp-admin-flush-cache', 'cp-admin-update-limit', + 'cp-admin-archive', + 'cp-admin-unarchive', // 'cp-admin-registration', ], 'quota': [ @@ -107,6 +109,141 @@ define([ }); return $div; }; + Messages.admin_archiveTitle = "Archive documents"; // XXX + Messages.admin_archiveHint = "Make a document unavailable without deleting it permanently. It will be placed in an 'archive' directory and deleted after a few days (configurable in the server configuration file)."; // XXX + Messages.admin_archiveButton = "Archive"; + + Messages.admin_unarchiveTitle = "Restore archived documents"; // XXX + Messages.admin_unarchiveHint = "Restore a document that has previously been archived"; + Messages.admin_unarchiveButton = "Restore"; + + Messages.admin_archiveInput = "Document URL"; + Messages.admin_archiveInput2 = "Document password"; + Messages.admin_archiveInval = "Invalid document"; + Messages.restoredFromServer = "Pad restored"; + + var archiveForm = function (archive, $div, $button) { + var label = h('label', { for: 'cp-admin-archive' }, Messages.admin_archiveInput); + var input = h('input#cp-admin-archive', { + type: 'text' + }); + + var label2 = h('label.cp-admin-pw', { + for: 'cp-admin-archive-pw' + }, Messages.admin_archiveInput2); + var input2 = UI.passwordInput({ + id: 'cp-admin-archive-pw', + placeholder: Messages.login_password + }); + var $pw = $(input2); + $pw.addClass('cp-admin-pw'); + var $pwInput = $pw.find('input'); + + + $button.before(h('div.cp-admin-setlimit-form', [ + label, + input, + label2, + input2 + ])); + + $div.addClass('cp-admin-nopassword'); + + var parsed; + var $input = $(input).on('keypress change paste', function () { + setTimeout(function () { + $input.removeClass('cp-admin-inval'); + var val = $input.val().trim(); + if (!val) { + $div.toggleClass('cp-admin-nopassword', true); + return; + } + + parsed = Hash.isValidHref(val); + $pwInput.val(''); + + if (!parsed || !parsed.hashData) { + $div.toggleClass('cp-admin-nopassword', true); + return void $input.addClass('cp-admin-inval'); + } + + var pw = parsed.hashData.version !== 3 && parsed.hashData.password; + $div.toggleClass('cp-admin-nopassword', !pw); + }); + }); + $pw.on('keypress change', function () { + setTimeout(function () { + $pw.toggleClass('cp-admin-inval', !$pwInput.val()); + }); + }); + + var clicked = false; + $button.click(function () { + if (!parsed || !parsed.hashData) { + UI.warn(Messages.admin_archiveInval); + return; + } + var pw = parsed.hashData.password ? $pwInput.val() : undefined; + var channel; + if (parsed.hashData.version === 3) { + channel = parsed.hashData.channel; + } else { + var secret = Hash.getSecrets(parsed.type, parsed.hash, pw); + channel = secret && secret.channel; + } + + if (!channel) { + UI.warn(Messages.admin_archiveInval); + return; + } + + if (clicked) { return; } + clicked = true; + + nThen(function (waitFor) { + if (!archive) { return; } + common.getFileSize(channel, waitFor(function (err, size) { + if (!err && size === 0) { + clicked = false; + waitFor.abort(); + return void UI.warn(Messages.admin_archiveInval); + } + })); + }).nThen(function () { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: archive ? 'ARCHIVE_DOCUMENT' : 'RESTORE_ARCHIVED_DOCUMENT', + data: channel + }, function (err, obj) { + var e = err || (obj && obj.error); + clicked = false; + if (e) { + UI.warn(Messages.error); + console.error(e); + return; + } + UI.log(archive ? Messages.deletedFromServer : Messages.restoredFromServer); + $input.val(''); + $pwInput.val(''); + }); + }); + }); + }; + + create['archive'] = function () { + var key = 'archive'; + var $div = makeBlock(key, true); + var $button = $div.find('button'); + archiveForm(true, $div, $button); + return $div; + }; + create['unarchive'] = function () { + var key = 'unarchive'; + var $div = makeBlock(key, true); + var $button = $div.find('button'); + archiveForm(false, $div, $button); + return $div; + }; + create['registration'] = function () { var key = 'registration'; var $div = makeBlock(key, true); diff --git a/www/admin/main.js b/www/admin/main.js index 817d2bd2e..8a6ec7a70 100644 --- a/www/admin/main.js +++ b/www/admin/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js', -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/admin/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { var addRpc = function (sframeChan, Cryptpad/*, Utils*/) { // Adding a new avatar from the profile: pin it and store it in the object diff --git a/www/code/inner.html b/www/code/inner.html index a4ea56206..b25534297 100644 --- a/www/code/inner.html +++ b/www/code/inner.html @@ -2,7 +2,7 @@ - + '; + mediaObject.tag.appendChild(container); + mediaObject.tag.appendChild(text); + }; + var makeDownloadButton = function (cfg, mediaObject, size, cb) { + var btn = document.createElement('button'); + btn.setAttribute('class', 'btn btn-default'); + btn.innerHTML = '' + + cfg.download.textDl + ' (' + size + 'MB)'; + btn.addEventListener('click', function () { + makeProgressBar(cfg, mediaObject); + cb(); + }); + mediaObject.tag.innerHTML = ''; + mediaObject.tag.appendChild(btn); + }; + + var getFileSize = function (src, _cb) { + var cb = function (e, res) { + _cb(e, res); + cb = function () {}; + }; + // XXX Cache + var xhr = new XMLHttpRequest(); + xhr.open("HEAD", src); + xhr.onerror = function () { return void cb("XHR_ERROR"); }; + xhr.onreadystatechange = function() { + if (this.readyState === this.DONE) { + cb(null, Number(xhr.getResponseHeader("Content-Length"))); + } + }; + xhr.onload = function () { + if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); } + }; + xhr.send(); + }; // Download a blob from href - var download = function (src, _cb) { + var download = function (src, _cb, progressCb) { var cb = function (e, res) { _cb(e, res); cb = function () {}; @@ -140,6 +227,16 @@ var factory = function (Cache) { xhr.open('GET', src, true); xhr.responseType = 'arraybuffer'; + var progress = function (offset) { + progressCb(offset * 100); + }; + xhr.addEventListener("progress", function (evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + progress(percentComplete); + } + }, false); + xhr.onerror = function () { return void cb("XHR_ERROR"); }; xhr.onload = function () { // Error? @@ -164,7 +261,6 @@ var factory = function (Cache) { Cache.getBlobCache(cacheKey, function (err, u8) { if (err || !u8) { return void fetch(); } - console.error('using cache', cacheKey); cb(null, u8); }); @@ -443,6 +539,7 @@ var factory = function (Cache) { // End media-tag rendering: display the tag and emit the event var end = function (decrypted) { + mediaObject.complete = true; process(mediaObject, decrypted, cfg, function (err) { if (err) { return void emit('error', err); } mediaObject._blob = decrypted; @@ -451,32 +548,54 @@ var factory = function (Cache) { }; // If we have the blob in our cache, don't download & decrypt it again, just display + // XXX Store in the cache the pending mediaobject: make sure we don't download and decrypt twice the same element at the same time if (cache[uid]) { end(cache[uid]); return mediaObject; } - // Download the encrypted blob - download(src, function (err, u8Encrypted) { + var dl = function () { + // Download the encrypted blob + download(src, function (err, u8Encrypted) { + if (err) { + if (err === "XHR_ERROR 404") { + mediaObject.tag.innerHTML = ''; + } + return void emit('error', err); + } + // Decrypt the blob + decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { + if (errDecryption) { + return void emit('error', errDecryption); + } + // Cache and display the decrypted blob + cache[uid] = u8Decrypted; + end(u8Decrypted); + }, function (progress) { + emit('progress', { + progress: 50+0.5*progress + }); + }); + }, function (progress) { + emit('progress', { + progress: 0.5*progress + }); + }); + }; + + if (cfg.force) { dl(); return mediaObject; } + + var maxSize = 5 * 1024 * 1024; + getFileSize(src, function (err, size) { if (err) { if (err === "XHR_ERROR 404") { mediaObject.tag.innerHTML = ''; } return void emit('error', err); } - // Decrypt the blob - decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) { - if (errDecryption) { - return void emit('error', errDecryption); - } - // Cache and display the decrypted blob - cache[uid] = u8Decrypted; - end(u8Decrypted); - }, function (progress) { - emit('progress', { - progress: progress - }); - }); + if (!size || size < maxSize) { return void dl(); } + var sizeMb = Math.round(10 * size / 1024 / 1024) / 10; + makeDownloadButton(cfg, mediaObject, sizeMb, dl); }); return mediaObject; diff --git a/www/common/notify.js b/www/common/notify.js index a3c8cd2b8..81dee662d 100644 --- a/www/common/notify.js +++ b/www/common/notify.js @@ -16,6 +16,8 @@ define(['/api/config'], function (ApiConfig) { var getPermission = Module.getPermission = function (f) { f = f || function () {}; + // "Notification.requestPermission is not a function" on Firefox 68.11.0esr + if (!Notification || typeof(Notification.requestPermission) !== 'function') { return void f(false); } Notification.requestPermission(function (permission) { if (permission === "granted") { f(true); } else { f(false); } diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index ce0dbcc3f..c9efb1eda 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -1310,6 +1310,16 @@ define([ if (APP.migrate && !readOnly) { onMigrateRdy.fire(); } + + // Check if history can/should be trimmed + var cp = getLastCp(); + if (cp && cp.file && cp.hash) { + var channels = [{ + channel: content.channel, + lastKnownHash: cp.hash + }]; + common.checkTrimHistory(channels); + } } } }; @@ -2070,7 +2080,9 @@ define([ // Import template var $template = common.createButton('importtemplate', true, {}, openTemplatePicker); - $template.appendTo(toolbar.$drawer); + if ($template && typeof($template.appendTo) === 'function') { + $template.appendTo(toolbar.$drawer); + } })(); } diff --git a/www/common/onlyoffice/main.js b/www/common/onlyoffice/main.js index 0560b95bd..db83ef456 100644 --- a/www/common/onlyoffice/main.js +++ b/www/common/onlyoffice/main.js @@ -3,54 +3,23 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/common-hash.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, Hash, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, Hash, SFCommonO) { // Loaded in load #2 var hash, href, version; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - - // Hidden hash - hash = window.location.hash; - href = window.location.href; - if (window.history && window.history.replaceState && hash) { - window.history.replaceState({}, window.document.title, '#'); - } - + var obj = SFCommonO.initIframe(waitFor, true, true); + href = obj.href; + hash = obj.hash; var parsed = Hash.parsePadUrl(href); if (parsed && parsed.hashData) { var opts = parsed.getOptions(); version = opts.versionHash; } - - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' + - requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { var addData = function (obj) { obj.ooType = window.location.pathname.replace(/^\//, '').replace(/\/$/, ''); diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 11c912a44..803d8bc93 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -297,6 +297,7 @@ proxy.mailboxes = { msg: msg, hash: hash }; + var notify = box.ready; Handlers.add(ctx, box, message, function (dismissed, toDismiss) { if (toDismiss) { // List of other messages to remove dismiss(ctx, toDismiss, '', function () { @@ -314,8 +315,7 @@ proxy.mailboxes = { } box.content[hash] = msg; showMessage(ctx, type, message, null, function (obj) { - if (!box.ready) { return; } - if (!obj || !obj.msg) { return; } + if (!obj || !obj.msg || !notify) { return; } Notify.system(undefined, obj.msg); }); }); diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 50565cc0b..f2a5996cf 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -598,6 +598,8 @@ define([ Thumb.initPadThumbnails(common, options.thumbnail); } } + + common.checkTrimHistory(); }); }; var onConnectionChange = function (info) { diff --git a/www/common/sframe-app-outer.js b/www/common/sframe-app-outer.js index d85266ca7..b836e2a4a 100644 --- a/www/common/sframe-app-outer.js +++ b/www/common/sframe-app-outer.js @@ -3,46 +3,16 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { var hash, href; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - - // Hidden hash - hash = window.location.hash; - href = window.location.href; - if (window.history && window.history.replaceState && hash) { - window.history.replaceState({}, window.document.title, '#'); - } - - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' + - requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + var obj = SFCommonO.initIframe(waitFor, true); + href = obj.href; + hash = obj.hash; }).nThen(function (/*waitFor*/) { SFCommonO.start({ hash: hash, diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js index 43b424624..0da095589 100644 --- a/www/common/sframe-boot.js +++ b/www/common/sframe-boot.js @@ -21,6 +21,7 @@ var afterLoaded = function (req) { window.parent.postMessage(JSON.stringify({ q: 'READY', txid: txid }), '*'); }, 1); }; + window.cryptpadLanguage = req.lang; if (req.req) { require(req.req, ready); } else { ready(); } var onReply = function (msg) { var data = JSON.parse(msg.data); @@ -61,7 +62,6 @@ var afterLoaded = function (req) { updated: lsUpdated, store: data.localStore }; - window.cryptpadLanguage = data.language; require(['/common/sframe-boot2.js'], function () { }); }; window.addEventListener('message', onReply); diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 5a68237ad..66a93545f 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -43,7 +43,7 @@ define([ return void console.log(); } if (window.CryptPad_loadingError) { - window.CryptPad_loadingError(e); + return void window.CryptPad_loadingError(e); } throw e; }; diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index c9e2eeacd..8a5be3764 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -47,8 +47,9 @@ define([ return 'cp-fileupload-element-' + String(Math.random()).substring(2); }; + Messages.fileTableHeader = "Downloads and uploads"; // XXX var tableHeader = h('div.cp-fileupload-header', [ - h('div.cp-fileupload-header-title', h('span', Messages.fileuploadHeader || 'Uploaded files')), + h('div.cp-fileupload-header-title', h('span', Messages.fileTableHeader)), h('div.cp-fileupload-header-close', h('span.fa.fa-times')), ]); @@ -262,7 +263,8 @@ define([ // name $('').append($link).appendTo($tr); // size - $('').text(UIElements.prettySize(estimate)).appendTo($tr); + var size = estimate ? UIElements.prettySize(estimate) : ''; + $(h('td.cp-fileupload-size')).text(size).appendTo($tr); // progress $('', {'class': 'cp-fileupload-table-progress'}).append($progressContainer).appendTo($tr); // cancel @@ -590,12 +592,11 @@ define([ queue.next(); }; - /* var cancelled = function () { $row.find('.cp-fileupload-table-cancel').addClass('cancelled').html('').append(h('span.fa.fa-minus')); queue.inProgress = false; queue.next(); - };*/ + }; /** * Update progress in the download panel, for downloading a file @@ -627,8 +628,21 @@ define([ * As updateDLProgress but for folders * @param {number} progressValue Progression of download, between 0 and 1 */ + Messages.download_zip = "Building ZIP file..."; // XXX + Messages.download_zip_file = "File {0}/{1}"; // XXX var updateProgress = function (progressValue) { var text = Math.round(progressValue*100) + '%'; + if (Array.isArray(data.list)) { + text = Messages._getKey('download_zip_file', [Math.round(progressValue * data.list.length), data.list.length]); + } + if (progressValue === 2) { + text = Messages.download_zip; + progressValue = 1; + } + if (progressValue === 3) { + text = "100%"; + progressValue = 1; + } $pv.text(text); $pb.css({ width: (progressValue * 100) + '%' @@ -641,7 +655,8 @@ define([ get: common.getPad, sframeChan: sframeChan, }; - downloadFunction(ctx, data, function (err, obj) { + + var dl = downloadFunction(ctx, data, function (err, obj) { $link.prepend($('', {'class': 'fa fa-external-link'})) .attr('href', '#') .click(function (e) { @@ -657,19 +672,17 @@ define([ folderProgress: updateProgress, }); -/* - var $cancel = $('', {'class': 'cp-fileupload-table-cancel-button fa fa-times'}).click(function () { - dl.cancel(); - $cancel.remove(); - $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); - cancelled(); - }); -*/ - - $row.find('.cp-fileupload-table-cancel') - .html('') - .append(h('span.fa.fa-minus')); - //.append($cancel); + var $cancel = $row.find('.cp-fileupload-table-cancel').html(''); + if (dl && dl.cancel) { + $('', { + 'class': 'cp-fileupload-table-cancel-button fa fa-times' + }).click(function () { + dl.cancel(); + $cancel.remove(); + $row.find('.cp-fileupload-table-progress-value').text(Messages.upload_cancelled); + cancelled(); + }).appendTo($cancel); + } }; File.downloadFile = function (fData, cb) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index a53171150..ededee769 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -2,10 +2,57 @@ define([ '/bower_components/nthen/index.js', '/api/config', + '/common/requireconfig.js', + '/customize/messages.js', 'jquery', -], function (nThen, ApiConfig, $) { +], function (nThen, ApiConfig, RequireConfig, Messages, $) { var common = {}; + common.initIframe = function (waitFor, isRt) { + var requireConfig = RequireConfig(); + var lang = Messages._languageUsed; + var req = { + cfg: requireConfig, + req: [ '/common/loading.js' ], + pfx: window.location.origin, + lang: lang + }; + window.rc = requireConfig; + window.apiconf = ApiConfig; + + var hash, href; + if (isRt) { + // Hidden hash + hash = window.location.hash; + href = window.location.href; + if (window.history && window.history.replaceState && hash) { + window.history.replaceState({}, window.document.title, '#'); + } + } + + document.getElementById('sbox-iframe').setAttribute('src', + ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' + + requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req))); + + // This is a cheap trick to avoid loading sframe-channel in parallel with the + // loading screen setup. + var done = waitFor(); + var onMsg = function (msg) { + var data = JSON.parse(msg.data); + if (data.q !== 'READY') { return; } + window.removeEventListener('message', onMsg); + var _done = done; + done = function () { }; + _done(); + }; + window.addEventListener('message', onMsg); + + return { + hash: hash, + href: href + }; + }; + common.start = function (cfg) { cfg = cfg || {}; var realtime = !cfg.noRealtime; @@ -76,6 +123,7 @@ define([ Utils.LocalStore = _LocalStore; Utils.Cache = _Cache; Utils.UserObject = _UserObject; + Utils.currentPad = currentPad; AppConfig = _AppConfig; Test = _Test; @@ -523,6 +571,7 @@ define([ isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, + notifications: Notification && Notification.permission === "granted", accounts: { donateURL: Cryptpad.donateURL, upgradeURL: Cryptpad.upgradeURL @@ -559,7 +608,7 @@ define([ for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; } if (cfg.addData) { - cfg.addData(metaObj.priv, Cryptpad, metaObj.user); + cfg.addData(metaObj.priv, Cryptpad, metaObj.user, Utils); } sframeChan.event('EV_METADATA_UPDATE', metaObj); @@ -1523,9 +1572,13 @@ define([ }); }); - if (cfg.messaging) { - Notifier.getPermission(); + sframeChan.on('Q_ASK_NOTIFICATION', function (data, cb) { + Notification.requestPermission(function (s) { + cb(s === "granted"); + }); + }); + if (cfg.messaging) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { Cryptpad.universal.execCommand({ type: 'messenger', diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 3eb86ff93..4198a649a 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -264,6 +264,66 @@ define([ return teamChatChannel; }; + // When opening a pad, if were an owner check the history size and prompt for trimming if + // necessary + funcs.checkTrimHistory = function (channels, isDrive) { + channels = channels || []; + var priv = ctx.metadataMgr.getPrivateData(); + + var limit = 100 * 1024 * 1024; // 100MB + limit = 100 * 1024; // XXX 100KB + + var owned; + nThen(function (w) { + if (isDrive) { + funcs.getAttribute(['drive', 'trim'], w(function (err, val) { + if (err || typeof(val) !== "number") { return; } + if (val < (+new Date())) { return; } + w.abort(); + })); + return; + } + funcs.getPadAttribute('trim', w(function (err, val) { + if (err || typeof(val) !== "number") { return; } + if (val < (+new Date())) { return; } + w.abort(); + })); + }).nThen(function (w) { + // Check ownership + // DRIVE + if (isDrive) { + if (!priv.isDriveOwned) { return void w.abort(); } + return; + } + // PAD + channels.push({ channel: priv.channel }); + funcs.getPadMetadata({ + channel: priv.channel + }, w(function (md) { + if (md && md.error) { return void w.abort(); } + var owners = md.owners; + owned = funcs.isOwned(owners); + if (!owned) { return void w.abort(); } + })); + }).nThen(function () { + // We're an owner: check the history size + var history = funcs.makeUniversal('history'); + history.execCommand('GET_HISTORY_SIZE', { + account: isDrive, + pad: !isDrive, + channels: channels, + teamId: typeof(owned) === "number" && owned + }, function (obj) { + if (obj && obj.error) { return; } // can't get history size: abort + var bytes = obj.size; + if (!bytes || typeof(bytes) !== "number") { return; } // no history: abort + if (bytes < limit) { return; } + obj.drive = isDrive; + UIElements.displayTrimHistoryPrompt(funcs, obj); + }); + }); + }; + var cursorChannel; // common-ui-elements needs to be able to get the cursor channel to put it in metadata when // importing a template diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 0927cde21..c441f9e0f 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -990,6 +990,30 @@ MessengerUI, Messages) { h('div.cp-notifications-empty', Messages.notifications_empty) ]); var pads_options = [div]; + + var metadataMgr = config.metadataMgr; + var privateData = metadataMgr.getPrivateData(); + if (!privateData.notifications) { + Messages.allowNotifications = "Allow notifications"; // XXX + var allowNotif = h('div.cp-notifications-gotoapp', h('p', Messages.allowNotifications)); + pads_options.unshift(h("hr")); + pads_options.unshift(allowNotif); + var $allow = $(allowNotif).click(function () { + Common.getSframeChannel().event('Q_ASK_NOTIFICATION', null, function (e, allow) { + if (!allow) { return; } + $(allowNotif).remove(); + }); + }); + var onChange = function () { + var privateData = metadataMgr.getPrivateData(); + if (!privateData.notifications) { return; } + $allow.remove(); + metadataMgr.off('change', onChange); + }; + metadataMgr.onChange(onChange); + } + + if (Common.isLoggedIn()) { pads_options.unshift(h("hr")); pads_options.unshift(openNotifsApp); diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 781c20b65..b8aa108ed 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -1459,5 +1459,14 @@ "admin_setlimitHint": "Lege individuelle Begrenzungen für Benutzer anhand ihrer öffentlichen Schlüssel fest. Du kannst bestehende Regeln aktualisieren oder entfernen.", "access_destroyPad": "Dokument oder Ordner endgültig zerstören", "fm_shareFolderPassword": "Diesen Ordner mit einem Passwort schützen (optional)", - "fm_deletedFolder": "Gelöschter Ordner" + "fm_deletedFolder": "Gelöschter Ordner", + "tag_edit": "Ändern", + "tag_add": "Hinzufügen", + "loading_state_4": "Teams laden", + "loading_state_3": "Geteilte Ordner laden", + "loading_state_2": "Inhalte aktualisieren", + "loading_state_1": "Drive laden", + "loading_state_0": "Oberfläche vorbereiten", + "loading_state_5": "Dokument rekonstruieren", + "error_unhelpfulScriptError": "Skriptfehler: Siehe Konsole im Browser für Details" } diff --git a/www/common/translations/messages.fi.json b/www/common/translations/messages.fi.json index fe4ee1f64..ac5ace154 100644 --- a/www/common/translations/messages.fi.json +++ b/www/common/translations/messages.fi.json @@ -1459,5 +1459,14 @@ "history_cantRestore": "Palauttaminen epäonnistui. Yhteytesi on katkennut.", "history_close": "Sulje", "history_restore": "Palauta", - "share_bar": "Luo linkki" + "share_bar": "Luo linkki", + "error_unhelpfulScriptError": "Skriptivirhe: Lisätietoja selaimen kehittäjäkonsolissa", + "tag_edit": "Muokkaa", + "tag_add": "Lisää", + "loading_state_5": "Uudelleenrakenna asiakirja", + "loading_state_4": "Lataa Teams", + "loading_state_3": "Lataa jaetut kansiot", + "loading_state_2": "Päivitä sisältö", + "loading_state_1": "Lataa Drive", + "loading_state_0": "Rakenna käyttöliittymä" } diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 7d8a84efb..1d1779035 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1465,5 +1465,8 @@ "loading_state_3": "Chargement des dossiers partagés", "loading_state_2": "Mise à jour du contenu", "loading_state_1": "Chargement du drive", - "loading_state_0": "Construction de l'interface" + "loading_state_0": "Construction de l'interface", + "tag_edit": "Modifier", + "tag_add": "Ajouter", + "error_unhelpfulScriptError": "Erreur de script : consultez la console du navigateur pour plus de détails" } diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index fa96fc5a7..c76b9a7c1 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1467,5 +1467,6 @@ "loading_state_4": "Load Teams", "loading_state_5": "Reconstruct document", "tag_add": "Add", - "tag_edit": "Edit" + "tag_edit": "Edit", + "error_unhelpfulScriptError": "Script Error: See browser console for details" } diff --git a/www/contacts/inner.html b/www/contacts/inner.html index b99bbc5b4..797d09d3b 100644 --- a/www/contacts/inner.html +++ b/www/contacts/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/contacts/main.js b/www/contacts/main.js index 38d6c5e71..faf92f94e 100644 --- a/www/contacts/main.js +++ b/www/contacts/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/contacts/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { SFCommonO.start({ noRealtime: true, diff --git a/www/debug/inner.html b/www/debug/inner.html index 2ac53948c..7936c04f2 100644 --- a/www/debug/inner.html +++ b/www/debug/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/notifications/main.js b/www/notifications/main.js index 20c8653f9..785fb3b5d 100644 --- a/www/notifications/main.js +++ b/www/notifications/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js', -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/notifications/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { var category; if (window.location.hash) { diff --git a/www/oodoc/inner.html b/www/oodoc/inner.html index 529c5a8d9..884ae5a00 100644 --- a/www/oodoc/inner.html +++ b/www/oodoc/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/ooslide/inner.html b/www/ooslide/inner.html index d06820db2..e7c4e111f 100644 --- a/www/ooslide/inner.html +++ b/www/ooslide/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index b413eed6a..489cbb17d 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -27,6 +27,7 @@ body.cp-app-pad { #cp-app-pad-toc { @toc-level-indent: 15px; + overflow-y: auto; margin-top: 10px; margin-left: 10px; width: 200px; diff --git a/www/pad/inner.html b/www/pad/inner.html index e4dbcdf95..17bfec308 100644 --- a/www/pad/inner.html +++ b/www/pad/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/profile/main.js b/www/profile/main.js index 92b24b3fc..b041d926a 100644 --- a/www/profile/main.js +++ b/www/profile/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js', -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/profile/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { var getSecrets = function (Cryptpad, Utils, cb) { var Hash = Utils.Hash; diff --git a/www/secureiframe/inner.html b/www/secureiframe/inner.html index 29c3cf797..97bfb3930 100644 --- a/www/secureiframe/inner.html +++ b/www/secureiframe/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/settings/main.js b/www/settings/main.js index bbc0f87d3..750423a1d 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/settings/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_THUMBNAIL_CLEAR', function (d, cb) { diff --git a/www/sheet/inner.html b/www/sheet/inner.html index 07d21904d..68949568f 100644 --- a/www/sheet/inner.html +++ b/www/sheet/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/slide/inner.html b/www/slide/inner.html index c04091cf7..f067e8a2f 100644 --- a/www/slide/inner.html +++ b/www/slide/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/support/main.js b/www/support/main.js index b5ca65126..1dc8c0e56 100644 --- a/www/support/main.js +++ b/www/support/main.js @@ -3,40 +3,16 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js', '/common/outer/local-store.js', '/common/outer/login-block.js', -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO, LocalStore, Block) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO, LocalStore, Block) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/support/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { var category; if (window.location.hash) { diff --git a/www/teams/inner.html b/www/teams/inner.html index 243a74edf..5ec12c287 100644 --- a/www/teams/inner.html +++ b/www/teams/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/whiteboard/inner.html b/www/whiteboard/inner.html index 533b4568f..4b56440e3 100644 --- a/www/whiteboard/inner.html +++ b/www/whiteboard/inner.html @@ -2,7 +2,7 @@ - + diff --git a/www/worker/main.js b/www/worker/main.js index 04dffa748..633982146 100644 --- a/www/worker/main.js +++ b/www/worker/main.js @@ -3,38 +3,14 @@ define([ '/bower_components/nthen/index.js', '/api/config', '/common/dom-ready.js', - '/common/requireconfig.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { - var requireConfig = RequireConfig(); +], function (nThen, ApiConfig, DomReady, SFCommonO) { // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var req = { - cfg: requireConfig, - req: [ '/common/loading.js' ], - pfx: window.location.origin - }; - window.rc = requireConfig; - window.apiconf = ApiConfig; - document.getElementById('sbox-iframe').setAttribute('src', - ApiConfig.httpSafeOrigin + '/worker/inner.html?' + requireConfig.urlArgs + - '#' + encodeURIComponent(JSON.stringify(req))); - - // This is a cheap trick to avoid loading sframe-channel in parallel with the - // loading screen setup. - var done = waitFor(); - var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } - window.removeEventListener('message', onMsg); - var _done = done; - done = function () { }; - _done(); - }; - window.addEventListener('message', onMsg); + SFCommonO.initIframe(waitFor); }).nThen(function (/*waitFor*/) { SFCommonO.start({ noRealtime: true,