Merge branch 'staging' into invitations

This commit is contained in:
yflory 2024-01-18 16:35:41 +01:00
commit 2ac4995146
40 changed files with 388 additions and 119 deletions

View File

@ -55,7 +55,7 @@ body:
- type: dropdown
id: operating-system
attributes:
label: What opreating system are you using?
label: Which operating system are you using?
multiple: true
options:
- Linux/BSD/UNIX
@ -89,6 +89,7 @@ body:
label: Version
description: What version of CryptPad are you running?
options:
- 5.6.0
- 5.5.0
- 5.4.1
- 5.4.0
@ -99,11 +100,6 @@ body:
- 5.0.0
- 4.14.1
- 4.14.0
- 4.13.0
- 4.12.1
- 4.12.0
- 4.11.0
- 4.10.0
- Other
validations:
required: true

View File

@ -4,6 +4,120 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# 5.6.0
## Goals
This release introduces support for integrating CryptPad instances with Single-Sign On authentication. It brings a lot of improvements and fixes to Form, Calendar, and other parts of CryptPad. This release begins to improve the accessibility of the toolbar towards full WCAG compliance which we hope to achieve in the near future.
## Features
- Authentication
- This version paves the way for SSO authentication for a CryptPad instance via a plugin (est. release Jan. 2024) [#1320](https://github.com/cryptpad/cryptpad/pull/1320)
- New setting to make Two-Factor Authentication mandatory for all user accounts on an instance [#1341](https://github.com/cryptpad/cryptpad/pull/1341)
- Form
- New button to duplicate a question [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Calendar
- New description field for calendar events [#1299](https://github.com/cryptpad/cryptpad/pull/1299)
## Improvements
- Accessibility of toolbars and some drop-down menus [#1290](https://github.com/cryptpad/cryptpad/pull/1290)
- "+ New" drop-down menu in Drive and Team Drive #1191
- New `Ctrl + e` modal #1192
- Code contact request notifications as headings #1197
- DOM order of toolbar #1198
- Notifications menu not accessible via Keyboard #1201
- Sidebar "tabs" not accessible via keyboard #1203
- Implement keyboard navigation of toolbar menus #1209
- CryptDrive page needs a logical tab order #1151
- Elements not accessible using the keyboard #1162
- Calendar event modal date-picker is cut-off at some screen resolutions #1280
- Visible focus #1206
- Rich Text
- Improvements to the Rich Text toolbar and layout for mobile usage [#1296](https://github.com/cryptpad/cryptpad/pull/1296)
- Calendar
- Handling the move of repeating events from a calendar to another [#1308](https://github.com/cryptpad/cryptpad/pull/1308)
- Kanban
- Changed positioning of kanban tag container on smaller screens [#1307](https://github.com/cryptpad/cryptpad/pull/1307)
- New option to increase the number of teams slots for premium users only [#1315](https://github.com/cryptpad/cryptpad/pull/1315)
- Improve licensing information, CryptPad code now complies with the [REUSE](https://reuse.software/) specifications [#1300](https://github.com/cryptpad/cryptpad/pull/1300)
- Deployment
- Basic configuration for Apache HTTPd [#1332](https://github.com/cryptpad/cryptpad/pull/1332)
- Add Docker health check [#1287](https://github.com/cryptpad/cryptpad/pull/1287)
- Cleanup
- Old // XXX comments [#1334](https://github.com/cryptpad/cryptpad/pull/1334)
- Outdated/misplaced files [#1327](https://github.com/cryptpad/cryptpad/pull/1327)
## Fixes
- Fix browser autocomplete issues (password, numbers, etc.) [#1342](https://github.com/cryptpad/cryptpad/pull/1342)
- Drive
- Container height fills screen [#1304](https://github.com/cryptpad/cryptpad/pull/1304)
- Context menu on mobile [#1301](https://github.com/cryptpad/cryptpad/pull/1301)
- OnlyOffice applications
- Use correct mime type for .wasm files (export functionality) [#1288](https://github.com/cryptpad/cryptpad/pull/1288)
- Fix filter functionality in Sheets [#1319](https://github.com/cryptpad/cryptpad/issues/1319)
- Form
- Fix an error upon importing a template in forms [#1316](https://github.com/cryptpad/cryptpad/pull/1316)
- Can now set form closing date/time on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Can now edit time options for poll questions on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Dates in CSV exports of forms are now in ISO (not timestamp) format [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Page breaks are no longer visible in conditional sections when condition is not met [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Final submission page now has margins [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Question blocks on mobile are now only draggable at the top of the block to make scrolling possible [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Whiteboard
- Fix a few export-related issues [#1328](https://github.com/cryptpad/cryptpad/pull/1328)
- Calendar
- Reformat `www/calendar/export.js` [#1314](https://github.com/cryptpad/cryptpad/pull/1314)
- Fix a bug with stopping the recurrence of a calendar event [#1312](https://github.com/cryptpad/cryptpad/pull/1312)
- Calendar creates itself twice when navigating with the keyboard [#1250](https://github.com/cryptpad/cryptpad/issues/1250)
- Fix timezone in Daylight Saving Time issues [#1317](https://github.com/cryptpad/cryptpad/pull/1317)
- Translations
- Revise the translation of `zh` [#1329](https://github.com/cryptpad/cryptpad/pull/1329)
## Dependencies
- Added [Moment.js](http://momentjs.com/) for improved handling of dates in Calendar (added as part of [#1317](https://github.com/cryptpad/cryptpad/pull/1317))
## Deployment
We [fixed an issue with the Systemd service file and logging](https://github.com/cryptpad/cryptpad/commit/078095c3e25d39707bdaab7ec066ceed6cb7158b), you'll need to add the following lines to your `cryptpad.service` before continuing by following the upgrade notes below.
```diff
# Restart service after 10 seconds if node service crashes
RestartSec=2
+ # Proper logging to journald
+ StandardOutput=journal
+ StandardError=journal+console
User=cryptpad
Group=cryptpad
```
## Upgrade notes
If you are upgrading from a version older than `5.5.0` please read the upgrade notes of all versions between yours and `5.5.0` to avoid configuration issues.
To upgrade:
1. Reload the Systemd daemon, required due to the changes in the **Deployment** section
```bash
sudo systemctl daemon-reload
```
2. Stop your server
3. Get the latest code with git
```bash
git fetch origin --tags
git checkout 5.6.0
```
4. Restart your server
5. Review your instance's checkup page to ensure that you are passing all tests
# 5.5.0
## Features

View File

@ -61,7 +61,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at contact@cryptpad.fr. All
reported by contacting the project team at contact@cryptpad.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@ -12,14 +12,9 @@ WORKDIR /cryptpad
# Copy CryptPad source code to the container
COPY . /cryptpad
RUN sed -i "s@//httpAddress: '::'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
RUN sed -i "s@//httpAddress: 'localhost'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
# Install wget for healthcheck
RUN apt-get update && apt-get install --no-install-recommends -y wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN npm install --production \
&& npm run install:components
@ -31,6 +26,11 @@ FROM node:lts-slim
RUN groupadd cryptpad -g 4001
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
# Install wget for healthcheck
RUN apt-get update && apt-get install --no-install-recommends -y wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy cryptpad with installed modules
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
USER cryptpad

View File

@ -365,10 +365,9 @@
.cp-toolbar {
* {
outline-width: 0;
outline-style: none;
&:focus {
// color shows on chrome/edge, but not firefox
outline-color: @cryptpad_color_brand;
outline: @cryptpad_color_brand solid 2px;
}
}
@ -780,6 +779,7 @@
padding: 10px;
color: @toolbar-bg-color;
color: var(--toolbar-bg-color);
border-radius: 5px;
span {
font-size: 45px;

View File

@ -200,6 +200,11 @@ var evictArchived = function (Env, cb) {
// but if it's been stored for the configured time...
// expire it
if (Env.DRY_RUN) {
if (item.channel.length === 32) { removed++; }
else if (item.channel.length === 44) { accounts++; }
return void Log.info("EVICT_ARCHIVED_CHANNEL_DRY_RUN", item.channel, cb);
}
store.removeArchivedChannel(item.channel, w(function (err) {
if (err) {
return Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', {
@ -245,7 +250,11 @@ var evictArchived = function (Env, cb) {
Log.error("EVICT_BLOB_LIST_ARCHIVED_PROOF_ERROR", err);
return void next();
}
if (item && item.mtime > retentionTime) { return void next(); }
if (item && item.ctime > retentionTime) { return void next(); }
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVED_BLOB_PROOF_DRY_RUN", item, next);
}
blobs.remove.archived.proof(item.safeKey, item.blobId, (function (err) {
if (err) {
Log.error("EVICT_ARCHIVED_BLOB_PROOF_ERROR", item);
@ -272,7 +281,11 @@ var evictArchived = function (Env, cb) {
Log.error("EVICT_BLOB_LIST_ARCHIVED_BLOBS_ERROR", err);
return void next();
}
if (item && item.mtime > retentionTime) { return void next(); }
if (item && item.ctime > retentionTime) { return void next(); }
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVED_BLOB_DRY_RUN", item, next);
}
blobs.remove.archived.blob(item.blobId, function (err) {
if (err) {
Log.error("EVICT_ARCHIVED_BLOB_ERROR", item);
@ -288,6 +301,7 @@ var evictArchived = function (Env, cb) {
}));
};
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
nThen(loadStorage)
.nThen(migrateIncorrectBlobs)
.nThen(removeArchivedChannels)
@ -544,6 +558,9 @@ module.exports = function (Env, cb) {
}
// remove the pin logs of inactive accounts if inactive account removal is configured
if (Env.DRY_RUN) {
return void Log.info("EVICT_INACTIVE_ACCOUNT_DRY_RUN", id, next);
}
pinStore.archiveChannel(id, undefined, function (err) {
if (err) {
return Log.error('EVICT_INACTIVE_ACCOUNT_PIN_LOG', err, next);
@ -602,7 +619,12 @@ module.exports = function (Env, cb) {
// unless we address this race condition with this last-minute double-check
if (item.mtime > inactiveTime) { return void next(); }
removed++;
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVE_BLOB_DRY_RUN", {
item: item,
}, next);
}
blobs.archive.blob(item.blobId, 'INACTIVE', function (err) {
if (err) {
return Log.error("EVICT_ARCHIVE_BLOB_ERROR", {
@ -610,6 +632,7 @@ module.exports = function (Env, cb) {
item: item,
}, next);
}
removed++;
Log.info("EVICT_ARCHIVE_BLOB", {
item: item,
}, next);
@ -658,6 +681,10 @@ module.exports = function (Env, cb) {
}
}));
}).nThen(function () {
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_BLOB_PROOF_LONELY_DRY_RUN", item, next);
}
blobs.remove.proof(item.safeKey, item.blobId, function (err) {
if (err) {
return Log.error("EVICT_BLOB_PROOF_LONELY_ERROR", item, next);
@ -698,6 +725,9 @@ module.exports = function (Env, cb) {
// check if the database has any ephemeral channels
// if it does it's because of a bug, and they should be removed
if (item.channel.length === 34) {
if (Env.DRY_RUN) {
return void Log.info("EVICT_EPHEMERAL_DRY_RUN", item.channel, cb);
}
return void store.removeChannel(item.channel, w(function (err) {
if (err) {
return Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', {
@ -728,6 +758,11 @@ module.exports = function (Env, cb) {
// else fall through to the archival
}));
}).nThen(function (w) {
if (Env.DRY_RUN) {
archived++;
w.abort();
return void Log.info("EVICT_CHANNEL_ARCHIVAL_DRY_RUN", item.channel, cb);
}
return void store.archiveChannel(item.channel, 'INACTIVE', w(function (err) {
if (err) {
Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', {
@ -736,8 +771,8 @@ module.exports = function (Env, cb) {
}, w());
return;
}
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
archived++;
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
}));
}).nThen(cb);
};
@ -754,6 +789,7 @@ module.exports = function (Env, cb) {
store.listChannels(handler, w(done), true); // using a hacky "fast mode" since we only need the channel id
};
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
nThen(loadStorage)
// iterate over all documents and add them to a bloom filter if they have been active

View File

@ -153,8 +153,12 @@ var clearActivity = function (Env, blobId, cb) {
};
var updateActivity = function (Env, blobId, cb) {
var path = makeActivityPath(Env, blobId);
var s_data = String(+new Date());
Fs.writeFile(path, s_data, cb);
var blobPath = makeBlobPath(Env, blobId);
isFile(blobPath, (err, state) => {
if (err || !state) { return void cb(); }
var s_data = String(+new Date());
Fs.writeFile(path, s_data, cb);
});
};
var archiveActivity = function (Env, blobId, cb) {
@ -464,7 +468,7 @@ var makeWalker = function (n, handleChild, done) {
// do no more than 20 jobs at a time
var tasks = Semaphore.create(n);
var recurse = function (path) {
var recurse = function (path, dir) {
tasks.take(function (give) {
var next = give(W());
@ -477,7 +481,19 @@ var makeWalker = function (n, handleChild, done) {
}
if (!stats.isDirectory()) {
w.abort();
return void handleChild(void 0, path, next);
if (/\.activity$/.test(path)) {
// NOTE: some activity files were created for deleted blobs due to
// a bug. We're going to detect them here in order to be able to clean
// them.
if (!dir.includes(Path.basename(path.replace(/\.activity$/, '')))) {
return void handleChild(void 0, path, next, true);
}
// Ignore valid activity files
return next();
}
// Ignore placeholder files
if (/\.placeholder$/.test(path)) { return next(); }
return void handleChild(void 0, path, next, false);
}
// fall through
}));
@ -487,7 +503,7 @@ var makeWalker = function (n, handleChild, done) {
if (err) { return next(); }
// everything is fine and it's a directory...
dir.forEach(function (d) {
recurse(Path.join(path, d));
recurse(Path.join(path, d), dir);
});
next();
});
@ -502,7 +518,8 @@ var listProofs = function (root, handler, cb) {
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next) {
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (loneActivity) { return void next(); }
// path is the path to a child node on the filesystem
// next handles the next job in a queue
@ -537,12 +554,20 @@ var listProofs = function (root, handler, cb) {
});
};
var getActivityStat = function (path, base, cb) {
var suffix = base ? '' : '.activity';
Fs.stat(path+suffix, function (err, stats) {
if (err && err.code === 'ENOENT' && !base) { return getActivityStat(path, true, cb); }
cb(err, stats);
});
};
var listBlobs = function (root, handler, cb) {
// iterate over files
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next) {
Fs.stat(path, function (err, stats) {
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (loneActivity) { return void next(); }
getActivityStat(path, false, function (err, stats) {
if (err) {
return void handler(err, void 0, next);
}
@ -565,6 +590,30 @@ var listBlobs = function (root, handler, cb) {
});
};
var cleanLoneActivity = function (root, cb) {
// iterate over files
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (!loneActivity) { return void next(); }
Fs.unlink(path, function (err) {
if (err) {
return console.error('ERROR', path, err);
}
console.log('DELETED', path);
next();
});
}, function () {
cb();
});
dir.forEach(function (d) {
if (d.length !== 2) { return; }
walk(Path.join(root, d));
});
});
};
BlobStore.create = function (config, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(config.getSession) !== 'function') {
@ -651,6 +700,10 @@ BlobStore.create = function (config, _cb) {
removeArchivedProof(Env, safeKey, blobId, cb);
},
},
loneActivity: function (_cb) {
var cb = Util.once(Util.mkAsync(_cb));
cleanLoneActivity(Env.blobPath, cb);
}
},
archive: {

6
package-lock.json generated
View File

@ -2028,9 +2028,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",

View File

@ -32,7 +32,7 @@ You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files
Previously, Docker images were community maintained, had their own repository and weren't official supported. We changed that with v5.4.0 during July 2023. Thanks to @promasu for all the work on the community images.
# Security
# Privacy / Security
CryptPad offers a variety of collaborative tools that encrypt your data in your browser
before it is sent to the server and your collaborators. In the event that the server is
@ -95,7 +95,7 @@ This project is tested with [BrowserStack](https://www.browserstack.com/).
This software is and will always be available under the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the License, or (at your option)
any later version. If you wish to use this technology in a proprietary product, please contact
sales@xwiki.com.
sales@cryptpad.org
[Tor browser]: https://www.torproject.org/download/
[active attack]: https://en.wikipedia.org/wiki/Attack_(computing)#Types_of_attack

20
scripts/clean-activity.js Normal file
View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* Some .activity file were created for deleted blob due to a bug.
* This script can be run once to remove these invalid activity file.
**/
var config = require("../lib/load-config");
var BlobStore = require("../lib/storage/blob");
config.getSession = function () {};
BlobStore.create(config, function (err, store) {
if (err) { return console.error('ERROR', err); }
console.log('Cleaning lone .activity files...');
store.remove.loneActivity(function (err) {
if (err) { return console.error('ERROR', err); }
console.log('Done');
});
});

View File

@ -15,6 +15,10 @@ var config = require("../lib/load-config");
var Env = Environment.create(config);
// Set DRY_RUN to true to run the script without deleting anything. A log file
// will be created.
Env.DRY_RUN = false;
var loadPremiumAccounts = function (Env, cb) {
nThen(function (w) {
// load premium accounts

View File

@ -15,6 +15,10 @@ var config = require("../lib/load-config");
var Env = Environment.create(config);
// Set DRY_RUN to true to run the script without deleting anything. A log file
// will be created.
Env.DRY_RUN = false;
var loadPremiumAccounts = function (Env, cb) {
nThen(function (w) {
// load premium accounts

View File

@ -53,7 +53,7 @@ special_rules.fr = function (s) {
ignore instances where the following character is a '/'
because this is probably a URL (http(s)://)
*/
return /\S[:;\?\!][^\/]{1,}/.test(s);
return /\S[:;\?\!][^\/]{1,}/.test(s.replace(/mailto:/g, " :"));
};
var noop = function () {};

View File

@ -230,17 +230,17 @@ define([
Thumb.fromDOM = function (opts, cb) {
var element = opts.getContainer();
if (!element) { return; }
var todo = function () {
var todo = function (html2canvas) {
if (!window.html2canvas) { window.html2canvas = html2canvas; }
if (opts.filter) { opts.filter(element, true); }
window.html2canvas(element, {
allowTaint: true,
onrendered: function (canvas) {
if (opts.filter) { opts.filter(element, false); }
setTimeout(function () {
var D = getResizedDimensions(canvas, 'pad');
Thumb.fromCanvas(canvas, D, cb);
}, 10);
}
}).then(function (canvas) {
if (opts.filter) { opts.filter(element, false); }
setTimeout(function () {
var D = getResizedDimensions(canvas, 'pad');
Thumb.fromCanvas(canvas, D, cb);
}, 10);
});
};
if (window.html2canvas) { return void todo(); }

View File

@ -743,6 +743,11 @@
return !(typeof(Atomics) === "undefined" || !supportsSharedArrayBuffers() || typeof(WebAssembly) === 'undefined');
};
//Returns an array of integers in range 0 to (length-1)
Util.getKeysArray = function (length) {
return [...Array(length).keys()];
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Util;
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {

View File

@ -636,7 +636,7 @@ define([
// UI containers
var $tree = APP.$tree = $("#cp-app-drive-tree");
var $content = APP.$content = $("#cp-app-drive-content");
var $contentContainer = APP.$content = $("#cp-app-drive-content-container");
var $contentContainer = $("#cp-app-drive-content-container");
var $appContainer = $(".cp-app-drive-container");
var $driveToolbar = APP.toolbar.$bottom;
var $contextMenu = createContextMenu(common).appendTo($appContainer);

View File

@ -1028,7 +1028,7 @@ define([
// If this is a form wiht a answer channel, delete it too
var p = priv.propChannels;
if (p.answersChannel) {
if (p && p.answersChannel) {
sframeChan.query('Q_DELETE_OWNED', {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
channel: p.answersChannel

View File

@ -1403,7 +1403,7 @@ define([
// Hidden hash: if a pad is deleted, we may have to switch back to full hash
// in some tabs
Store.checkDeletedPad = function (channel) {
Store.checkDeletedPad = function (channel, cb) {
if (!channel) { return; }
// Check if the pad is still stored in one of our drives
@ -1411,6 +1411,7 @@ define([
channel: channel,
isFile: true // we don't care if it's view or edit
}, function (res) {
if (typeof(cb) === "function") { setTimeout(cb); }
// If it is stored, abort
if (Object.keys(res).length) { return; }
// Otherwise, tell all the tabs that this channel was deleted and give them the hrefs

View File

@ -586,7 +586,7 @@ define([
// 1. add the shared folder to our list of shared folders
// NOTE: pushSharedFolder will encrypt the href directly in the object if needed
Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) {
if (err === "EEXISTS" && folderData.href && folderId) {
if (err === "EEXISTS" && folderData.href && folderId) { // Check upgrade
var parsed = Hash.parsePadUrl(folderData.href);
var secret = Hash.getSecrets('drive', parsed.hash, folderData.password);
SF.upgrade(secret.channel, secret);
@ -594,6 +594,10 @@ define([
waitFor.abort();
return void cb(folderId);
}
if (err === "EEXISTS" && folderId) { // Exists but no upgrade, return folderId
waitFor.abort();
return void cb(folderId);
}
if (err) {
waitFor.abort();
return void cb(err);
@ -1015,41 +1019,52 @@ define([
var owned = Env.user.userObject.ownedInTrash(function (owners) {
return _ownedByMe(Env, owners);
});
var n = nThen;
owned.forEach(function (chan) {
Env.removeOwnedChannel(chan, waitFor(function (obj) {
// If the error is that the file is already removed, nothing to
// report, it's a normal behavior (pad expired probably)
if (obj && obj.error && obj.error !== "ENOENT") {
// RPC may not be responding
// Send a report that can be handled manually
console.error(obj.error, chan);
Feedback.send('ERROR_EMPTYTRASH_OWNED=' + chan + '|' + obj.error, true);
}
console.warn('DELETED', chan);
}));
n = n(function (w) {
Env.removeOwnedChannel(chan, w(function (obj) {
setTimeout(w(), 50);
// If the error is that the file is already removed, nothing to
// report, it's a normal behavior (pad expired probably)
if (obj && obj.error && obj.error !== "ENOENT") {
// RPC may not be responding
// Send a report that can be handled manually
console.error(obj.error, chan);
Feedback.send('ERROR_EMPTYTRASH_OWNED=' + chan + '|' + obj.error, true);
}
console.warn('DELETED', chan);
}));
}).nThen;
});
n(waitFor());
}
// Empty the trash
Env.user.userObject.emptyTrash(waitFor(function (err, toClean) {
cb();
var nn = nThen;
// Don't block nThen for the lower-priority tasks
setTimeout(function () {
setTimeout(waitFor(function () {
// Unpin deleted pads if needed
// Check if we need to restore a full hash (hidden hash deleted from drive)
if (!Array.isArray(toClean)) { return; }
var done = waitFor();
var toCheck = Util.deduplicateString(toClean);
var toUnpin = [];
toCheck.forEach(function (channel) {
// Check unpin
var data = findChannel(Env, channel, true);
if (!data.length) { toUnpin.push(channel); }
// Check hidden hash
Env.Store.checkDeletedPad(channel);
nn = nn(function (w) {
var data = findChannel(Env, channel, true);
if (!data.length) { toUnpin.push(channel); }
// Check hidden hash, one at a time, asynchronously
Env.Store.checkDeletedPad(channel, w());
}).nThen;
});
Env.unpinPads(toUnpin, function () {});
});
nn(function () {
Env.unpinPads(toUnpin, function () {
done();
});
});
}));
}));
}).nThen(cb);
};

View File

@ -1165,7 +1165,6 @@
"calendar_update": "حَدِّثْ",
"calendar_title": "عنوان",
"calendar_loc": "موقع",
"calendar_location": "الموقع: {0}",
"calendar_hours": "ساعات",
"calendar_noNotification": "بدون إشعار",
"calendar_allDay": "طوال اليوم",

View File

@ -1421,7 +1421,6 @@
"calendar_deleteTeamConfirm": "Esteu segur que voleu suprimir aquest calendari de l'equip?",
"calendar_deleteOwned": "Continuarà sent visible per a altres usuaris amb els quals s'ha compartit.",
"calendar_newEvent": "Esdeveniment nou",
"calendar_location": "Ubicació: {0}",
"calendar_more": "{0} més",
"reminder_missed": "<b>{0}</b> va tenir lloc el {1}",
"reminder_now": "<b>{0}</b> ha començat",

View File

@ -635,7 +635,6 @@
"calendar_hours": "Hodin",
"calendar_minutes": "Minut",
"calendar_allDay": "Celý den",
"calendar_location": "Místo: {0}",
"calendar_loc": "Místo",
"calendar_title": "Nadpis",
"calendar_update": "Uložit",

View File

@ -479,7 +479,7 @@
"mdToolbar_list": "Aufzählung",
"mdToolbar_check": "Aufgabenliste",
"mdToolbar_code": "Code",
"home_host": "Dies ist eine unabhängige Installation der CrypPad-Software.",
"home_host": "Dies ist eine unabhängige Instanz von CryptPad.",
"main_catch_phrase": "Suite zur Zusammenarbeit<br>Ende-zu-Ende-verschlüsselt und quelloffen",
"about": "Über uns",
"privacy": "Datenschutzerklärung",
@ -1167,7 +1167,6 @@
"calendar_hours": "Stunden",
"calendar_minutes": "Minuten",
"calendar_allDay": "Ganztägig",
"calendar_location": "Ort: {0}",
"calendar_loc": "Ort",
"calendar_title": "Titel",
"calendar_update": "Aktualisieren",
@ -1667,5 +1666,20 @@
"status": "Statusseite",
"access_passwordUsed": "Dieses Passwort wurde bereits für dieses Dokument verwendet. Es kann nicht erneut verwendet werden.",
"admin_diskUsageWarning": "Mit Vorsicht zu verwenden! Je nach Größe der auf der Instanz gespeicherten Daten kann die Erstellung dieses Berichts den gesamten auf dem Server verfügbaren Speicher verbrauchen und zu einem Absturz führen.",
"dph_sf_destroyed": "Geteilter Ordner <b>{0}</b> wurde vom Eigentümer zerstört"
"dph_sf_destroyed": "Geteilter Ordner <b>{0}</b> wurde vom Eigentümer zerstört",
"calendar_desc": "Beschreibung",
"admin_forcemfaHint": "Alle Benutzer dieser Instanz werden aufgefordert, eine Zwei-Faktor-Authentifizierung für die Anmeldung bei ihrem Account einzurichten.",
"ssoauth_form_hint_register": "Füge ein CryptPad-Passwort für zusätzliche Sicherheit hinzu oder lasse es leer und fahre fort. Wenn du kein Passwort hinzufügst, können die Administratoren der Instanz auf die Schlüssel zum Schutz deiner Daten zugreifen.",
"ssoauth_form_hint_login": "Bitte gib dein CryptPad-Passwort ein",
"loading_mfa_required": "Auf dieser Instanz ist eine Zwei-Faktor-Authentifizierung erforderlich. Bitte aktualisiere deinen Account mit einer Authentifizierungs-App und dem folgenden Formular.",
"calendar_description": "Beschreibung:{0}{1}",
"duplicate": "Duplizieren",
"kanban_hideTags": "Weniger Tags anzeigen",
"kanban_showTags": "Alle Tags anzeigen",
"sso_login_description": "Einloggen mit",
"sso_register_description": "Registrieren mit",
"ssoauth_header": "CryptPad-Passwort",
"calendar_rec_change": "Ein sich wiederholendes Ereignis wird in einen anderen Kalender verschoben. Du kannst diese Änderung nur auf dieses Ereignis oder alle wiederholten Ereignisse anwenden.",
"calendar_rec_change_first": "Das erste sich wiederholende Ereignis wird in einen anderen Kalender verschoben. Alle wiederholten Ereignisse werden ebenfalls verschoben.",
"admin_forcemfaTitle": "Verpflichtende Zwei-Faktor-Authentifizierung"
}

View File

@ -950,7 +950,6 @@
"calendar_hours": "Horas",
"calendar_minutes": "Minutos",
"calendar_allDay": "Todo el día",
"calendar_location": "Localización: {0}",
"calendar_loc": "Localización",
"calendar_title": "Título",
"calendar_dateTimeRange": "{0} {1} - {2}",

View File

@ -1048,7 +1048,6 @@
"calendar_hours": "Orduak",
"calendar_minutes": "Minutuak",
"calendar_allDay": "Egunero",
"calendar_location": "Kokapena: {0}",
"calendar_loc": "Kokapena",
"calendar_title": "Izena",
"calendar_update": "Eguneratu",

View File

@ -1173,7 +1173,6 @@
"calendar_update": "Päivitä",
"calendar_title": "Otsikko",
"calendar_loc": "Sijainti",
"calendar_location": "Sijainti: {0}",
"calendar_allDay": "Koko päivä",
"calendar_minutes": "Minuutit",
"calendar_hours": "Tunnit",

View File

@ -1155,7 +1155,6 @@
"calendar_hours": "Heures",
"calendar_minutes": "Minutes",
"calendar_allDay": "Journée entière",
"calendar_location": "Lieu : {0}",
"calendar_loc": "Lieu",
"calendar_title": "Titre",
"calendar_update": "Modifier",
@ -1671,13 +1670,16 @@
"calendar_rec_change_first": "Déplacer le premier événement répété vers un autre calendrier. Tous les autres événements seront aussi déplacés.",
"calendar_desc": "Description",
"sso_register_description": "S'enregistrer avec",
"calendar_description": "Description:{0}{1}",
"calendar_description": "Description : {0}{1}",
"ssoauth_header": "Mot de passe CryptPad",
"ssoauth_form_hint_login": "Veuillez entrer votre mot de passe CryptPad",
"ssoauth_form_hint_register": "Ajouter un mot de passe CryptPad pour plus de sécurité ou laisser vide et continuer. Si vous n'ajoutez pas de mot de passe, les clés protégeant vos données seront accessibles aux administrateur·ices de l'instance.",
"calendar_rec_change": "Déplacer un événement répété vers un autre calendrier. Vous pouvez appliquer ce changement seulement pour cet événement ou pour tous les événements répétés.",
"sso_login_description": "Se connecter avec",
"duplicate": "Dupliquer",
"kanban_showTags": "Voir toutes les étiquettes",
"kanban_hideTags": "Voir moins d'étiquettes"
"kanban_showTags": "Voir tous les mots-clés",
"kanban_hideTags": "Voir moins de mots-clés",
"admin_forcemfaTitle": "Authentification à deux facteurs obligatoire",
"admin_forcemfaHint": "Tous les utilisateurs de cette instance seront obligés d'activer l'authentification à deux facteurs pour se connecter à leur compte.",
"loading_mfa_required": "L'authentification à deux facteurs est requise pour cette instance. Veuillez mettre à jour votre compte en utilisant une application d'authentification et le formulaire ci-dessous."
}

View File

@ -915,7 +915,6 @@
"calendar_default": "Kalender saya",
"calendar_new": "Kalender baru",
"calendar_newEvent": "Acara baru",
"calendar_location": "Lokasi: {0}",
"calendar_allDay": "Seharian",
"reminder_inProgressAllDay": "Hari ini: <b>{0}</b>",
"admin_supportPrivButton": "Tampilkan kunci",

View File

@ -1098,7 +1098,6 @@
"broadcast_end": "Termina",
"broadcast_maintenance": "Una manutenzione è programmata tra <b>{0}</b> e <b>{1}</b>. CryptPad potrebbe non essere disponibile in quel lasso di tempo.",
"admin_surveyTitle": "Sondaggio",
"calendar_location": "Posizione: {0}",
"admin_surveyButton": "Salva sondaggio",
"admin_surveyActive": "Apri sondaggio",
"calendar_minutes": "Minuti",

View File

@ -940,7 +940,6 @@
"calendar_hours": "時間",
"calendar_minutes": "分",
"calendar_allDay": "全日",
"calendar_location": "場所: {0}",
"calendar_loc": "場所",
"calendar_title": "タイトル",
"calendar_update": "更新",

View File

@ -522,7 +522,6 @@
"calendar_import": "Voeg toe aan mijn agenda's",
"calendar_dateTimeRange": "{0} {1} - {2}",
"calendar_title": "Titel",
"calendar_location": "Plaats: {0}",
"calendar_minutes": "Minuten",
"calendar_addNotification": "Voeg herinnering toe",
"register_registrationIsClosed": "Registratie is gesloten.",

View File

@ -899,7 +899,6 @@
"calendar_hours": "Godziny",
"calendar_minutes": "Minuty",
"calendar_allDay": "Cały dzień",
"calendar_location": "Lokalizacja: {0}",
"calendar_loc": "Lokalizacja",
"calendar_title": "Tytuł",
"calendar_update": "Zaktualizuj",

View File

@ -1235,7 +1235,6 @@
"calendar_hours": "Horas",
"calendar_minutes": "Minutos",
"calendar_allDay": "O dia todo",
"calendar_location": "Localização: {0}",
"calendar_loc": "Local",
"calendar_title": "Título",
"calendar_update": "Atualizar",

View File

@ -1234,7 +1234,6 @@
"calendar_hours": "Horas",
"calendar_minutes": "Minutos",
"calendar_allDay": "O dia todo",
"calendar_location": "Localização: {0}",
"calendar_loc": "Local",
"calendar_title": "Título",
"calendar_update": "Atualizar",

View File

@ -1337,7 +1337,6 @@
"calendar_hours": "Часа(ов)",
"calendar_minutes": "Минут(ы)",
"calendar_allDay": "Весь день",
"calendar_location": "Место: {0}",
"calendar_loc": "Место",
"calendar_title": "Заголовок",
"calendar_update": "Обновить",

View File

@ -650,7 +650,6 @@
"calendar_month": "Månad",
"calendar_title": "Rubrik",
"calendar_noNotification": "Ingen",
"calendar_location": "Plats: {0}",
"settings_notifCalendarTitle": "Kalenderaviseringar",
"mediatag_defaultImageName": "bild",
"share_formAuditor": "Granskare",

View File

@ -1031,7 +1031,6 @@
"calendar_hours": "Години",
"calendar_minutes": "Хвилини",
"calendar_allDay": "Весь день",
"calendar_location": "Місце: {0}",
"calendar_loc": "Місце",
"calendar_title": "Заголовок",
"calendar_update": "Оновити",

View File

@ -1236,7 +1236,6 @@
"calendar_hours": "小时",
"calendar_minutes": "分钟",
"calendar_allDay": "全天",
"calendar_location": "位置:{0}",
"calendar_loc": "地点",
"calendar_title": "标题",
"calendar_update": "更新",

View File

@ -2065,6 +2065,7 @@ define([
var tag = h('input');
var picker = Flatpickr(tag, {
disableMobile: true,
enableTime: true,
time_24hr: is24h,
dateFormat: dateFormat,
@ -3295,7 +3296,7 @@ define([
});
sorted.forEach(function (uid) {
var answer = answers[uid];
var viewOnly = content.answers.cantEdit || APP.isClosed;
var action = h(viewOnly ? 'button.btn.btn-secondary' : 'button.btn.btn-primary', [
@ -4109,9 +4110,27 @@ define([
idx = obj.idx;
_uid = Util.uid();
var opts = Util.clone(content.form[uid].opts);
if (type === 'multiradio' || type === 'multicheck') {
var itemKeys = Util.getKeysArray(content.form[uid].opts.items.length);
var valueKeys = Util.getKeysArray(content.form[uid].opts.values.length);
opts.items = itemKeys.map(function (i) {
return {
uid: Util.uid(),
v: content.form[uid].opts.items[i].v
};
});
opts.values = valueKeys.map(function (i) {
return {
uid: Util.uid(),
v: content.form[uid].opts.values[i].v
};
});
}
content.form[_uid] = {
q: content.form[uid].q,
opts: content.form[uid].opts,
opts: opts,
type: type,
};

View File

@ -1137,6 +1137,33 @@ define([
if (framework.isReadOnly() || framework.isLocked()) {
$container.addClass('cp-app-readonly');
}
var cleanData = function (boards) {
if (typeof(boards) !== "object") { return; }
var items = boards.items || {};
var data = boards.data || {};
var list = boards.list || [];
// Remove duplicate boards
list = boards.list = Util.deduplicateString(list);
Object.keys(data).forEach(function (id) {
if (list.indexOf(Number(id)) === -1) {
list.push(Number(id));
}
// Remove duplicate items
var b = data[id];
b.item = Util.deduplicateString(b.item || []);
});
Object.keys(items).forEach(function (eid) {
var exists = Object.keys(data).some(function (id) {
return (data[id].item || []).indexOf(Number(eid)) !== -1;
});
if (!exists) { delete items[eid]; }
});
framework.localChange();
};
framework.setFileImporter({accept: ['.json', 'application/json']}, function (content /*, file */) {
var parsed;
try { parsed = JSON.parse(content); }
@ -1150,6 +1177,8 @@ define([
});
framework.setFileExporter('.json', function () {
var content = kanban.getBoardsJSON();
cleanData(content);
return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], {
type: 'application/json',
});
@ -1291,32 +1320,6 @@ define([
};
});
var cleanData = function (boards) {
if (typeof(boards) !== "object") { return; }
var items = boards.items || {};
var data = boards.data || {};
var list = boards.list || [];
// Remove duplicate boards
list = boards.list = Util.deduplicateString(list);
Object.keys(data).forEach(function (id) {
if (list.indexOf(Number(id)) === -1) {
list.push(Number(id));
}
// Remove duplicate items
var b = data[id];
b.item = Util.deduplicateString(b.item || []);
});
Object.keys(items).forEach(function (eid) {
var exists = Object.keys(data).some(function (id) {
return (data[id].item || []).indexOf(Number(eid)) !== -1;
});
if (!exists) { delete items[eid]; }
});
framework.localChange();
};
framework.onReady(function () {
$("#cp-app-kanban-content").focus();
var content = kanban.getBoardsJSON();