mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Merge branch 'staging' into invitations
This commit is contained in:
commit
2ac4995146
8
.github/ISSUE_TEMPLATE/bug_resolution.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_resolution.yml
vendored
@ -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
|
||||
|
||||
114
CHANGELOG.md
114
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
6
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
20
scripts/clean-activity.js
Normal 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');
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 () {};
|
||||
|
||||
@ -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(); }
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -1165,7 +1165,6 @@
|
||||
"calendar_update": "حَدِّثْ",
|
||||
"calendar_title": "عنوان",
|
||||
"calendar_loc": "موقع",
|
||||
"calendar_location": "الموقع: {0}",
|
||||
"calendar_hours": "ساعات",
|
||||
"calendar_noNotification": "بدون إشعار",
|
||||
"calendar_allDay": "طوال اليوم",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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}",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -940,7 +940,6 @@
|
||||
"calendar_hours": "時間",
|
||||
"calendar_minutes": "分",
|
||||
"calendar_allDay": "全日",
|
||||
"calendar_location": "場所: {0}",
|
||||
"calendar_loc": "場所",
|
||||
"calendar_title": "タイトル",
|
||||
"calendar_update": "更新",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1337,7 +1337,6 @@
|
||||
"calendar_hours": "Часа(ов)",
|
||||
"calendar_minutes": "Минут(ы)",
|
||||
"calendar_allDay": "Весь день",
|
||||
"calendar_location": "Место: {0}",
|
||||
"calendar_loc": "Место",
|
||||
"calendar_title": "Заголовок",
|
||||
"calendar_update": "Обновить",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1031,7 +1031,6 @@
|
||||
"calendar_hours": "Години",
|
||||
"calendar_minutes": "Хвилини",
|
||||
"calendar_allDay": "Весь день",
|
||||
"calendar_location": "Місце: {0}",
|
||||
"calendar_loc": "Місце",
|
||||
"calendar_title": "Заголовок",
|
||||
"calendar_update": "Оновити",
|
||||
|
||||
@ -1236,7 +1236,6 @@
|
||||
"calendar_hours": "小时",
|
||||
"calendar_minutes": "分钟",
|
||||
"calendar_allDay": "全天",
|
||||
"calendar_location": "位置:{0}",
|
||||
"calendar_loc": "地点",
|
||||
"calendar_title": "标题",
|
||||
"calendar_update": "更新",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user