Merge branch '5.7-rc'

This commit is contained in:
David Benque 2024-02-02 10:45:38 +00:00
commit dd28f2a2c9
567 changed files with 132631 additions and 678 deletions

View file

@ -13,7 +13,7 @@ CKEDITOR.editorConfig = function( config ) {
config.removeButtons= 'Source,Maximize';
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize,elementspath,contextmenu,liststyle,tabletools,tableselection';
config.removePlugins= 'resize,elementspath,liststyle';
config.resize_enabled= false; //bottom-bar
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
config.toolbarGroups= [

View file

@ -49,9 +49,11 @@ define([
Exports.ssoAuth = function (provider, cb) {
var keys = Nacl.sign.keyPair();
var inviteToken = window.location.hash.slice(1);
localStorage.CP_sso_auth = JSON.stringify({
s: Nacl.util.encodeBase64(keys.secretKey),
p: Nacl.util.encodeBase64(keys.publicKey)
p: Nacl.util.encodeBase64(keys.publicKey),
token: inviteToken
});
ServerCommand(keys, {
command: 'SSO_AUTH',
@ -90,7 +92,9 @@ define([
};
var hashing;
Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, onOTP, testing, test) {
Exports.loginOrRegisterUI = function (config) {
let { uname, token, shouldImport, cb } = config;
if (hashing) { return void console.log("hashing is already in progress"); }
hashing = true;
@ -98,8 +102,7 @@ define([
var proceed = function (result) {
hashing = false;
// NOTE: test is also use as a cb for the install page
if (test && typeof test === "function" && test(result)) { return; }
if (cb && typeof cb === "function" && cb(result)) { return; }
LocalStore.clearLoginToken();
Realtime.whenRealtimeSyncs(result.realtime, function () {
Exports.redirect();
@ -117,16 +120,12 @@ define([
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
// after hashing the password
window.setTimeout(function () {
Login.loginOrRegister({
uname,
passwd,
isRegister,
onOTP
}, function (err, result) {
var proxy;
Login.loginOrRegister(config, function (err, result) {
var proxy = {};
if (result) { proxy = result.proxy; }
if (err) {
console.warn(err);
switch (err) {
case 'NO_SUCH_USER':
UI.removeLoadingScreen(function () {
@ -196,6 +195,9 @@ define([
});
break;
case 'E_RESTRICTED':
if (token) {
return UI.errorLoadingScreen(Messages.register_invalidToken);
}
UI.errorLoadingScreen(Messages.register_registrationIsClosed);
break;
default: // UNHANDLED ERROR
@ -205,8 +207,6 @@ define([
return;
}
//if (testing) { return void proceed(result); }
if (!(proxy.curvePrivate && proxy.curvePublic &&
proxy.edPrivate && proxy.edPublic)) {

View file

@ -98,7 +98,7 @@ define([
return h('a', attrs, [icon, text]);
};
Pages.versionString = "5.6.0";
Pages.versionString = "5.7.0";
var customURLs = Pages.customURLs = {};
(function () {

View file

@ -60,7 +60,7 @@ define([
h('button.login', Msg.login_login),
]),
]),
h('div.col-md-3'),
h('div.col-md-3'+ssoEnforced),
h('div.col-md-3'+ssoEnabled),
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
h('div.cp-login-sso', Msg.sso_login_description)

View file

@ -36,25 +36,27 @@ define([
];
};
if (Config.restrictRegistration) {
return frame([
h('div.cp-restricted-registration', [
h('p', Msg.register_registrationIsClosed),
])
]);
}
var termsCheck;
if (termsLink) {
termsCheck = h('div.checkbox-container', tos);
}
var closed = Config.restrictRegistration;
if (closed) {
$('body').addClass('cp-register-closed');
}
return frame([
h('div.cp-restricted-registration', [
h('p', Msg.register_registrationIsClosed),
]),
h('div.row.cp-register-det', [
h('div#data.hidden.col-md-6', [
h('h2', Msg.register_notes_title),
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
]),
h('div.col-md-3.cp-closed-filler'+ssoEnabled, h('div')),
h('div.cp-reg-form.col-md-6', [
h('div#userForm.form-group'+ssoEnforced, [
h('div.cp-register-instance', [
@ -104,6 +106,7 @@ define([
h('div.cp-register-sso', Msg.sso_register_description)
]),
]),
h('div.col-md-3.cp-closed-filler'+ssoEnabled),
])
]);
};

View file

@ -54,7 +54,9 @@
width: 100%;
height: 24px;
margin: 0;
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
//align-items: center;
//justify-content: center;
@ -667,6 +669,17 @@
font-size: 18px;
}
}
.cp-app-drive-element-icon {
font-size: 0.9rem;
margin: 0;
margin-right: 0.3rem;
}
.cp-app-drive-element-name-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cp-app-drive-element-state {
left: 3px;
}
@ -678,7 +691,7 @@
max-height: 100px;
background: @cp_drive-thumb-bg;
& ~ .fa, & ~ .cptools {
display: inline;
display: none;
font-size: 17px;
position: absolute;
top: 3px;
@ -701,6 +714,9 @@
}
div.cp-app-drive-content-list {
.cp-app-drive-element-icon {
display: none !important;
}
.cp-app-drive-element-grid {
display: none;
}

View file

@ -18,6 +18,20 @@
.alertify_main();
.checkmark_main(20px);
&:not(.cp-register-closed) {
.cp-restricted-registration {
display: none;
}
.cp-closed-filler {
display: none;
}
}
&.cp-register-closed {
div.cp-register-det {
display: none;
}
}
.cp-container {
.form-group {
.cp-register-instance {

View file

@ -14,6 +14,8 @@ server {
# Let's Encrypt webroot
include letsencrypt-webroot;
# Include mime.types to be able to support .mjs files (see "types" below)
include mime.types;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
@ -166,11 +168,6 @@ server {
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
# draw.io uses inline script tags in it's index.html. The hashes are added here.
if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
set $scriptSrc "'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
}
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
if ($unsafe) {
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
@ -179,6 +176,11 @@ server {
# Finally, set all the rules you composed above.
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
# Add support for .mjs files used by pdfjs
types {
application/javascript mjs;
}
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
# We prefer to serve static content from nginx directly and to leave the API server to handle
# the dynamic content that only it can manage. This is primarily an optimization

View file

@ -82,8 +82,8 @@ const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) {
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey, reason } = body;
Block.removeLoginBlock(Env, publicKey, reason, cb);
const { publicKey, edPublic, reason } = body;
Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb);
};

View file

@ -513,10 +513,10 @@ const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) {
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey, reason } = body;
const { publicKey, edPublic, reason } = body;
nThen(function (w) {
// Remove the block
Block.removeLoginBlock(Env, publicKey, reason, w((err) => {
Block.removeLoginBlock(Env, publicKey, reason, edPublic, w((err) => {
if (err) {
w.abort();
return void cb(err);

View file

@ -12,6 +12,8 @@ const Decrees = require("../decrees");
const Pinning = require("./pin-rpc");
const Core = require("./core");
const Channel = require("./channel");
const Invitation = require("./invitation");
const Users = require("./users");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
const ArchiveAccount = require('../archive-account');
@ -459,6 +461,10 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) {
var instanceStatus = function (Env, Server, cb) {
cb(void 0, {
restrictRegistration: Env.restrictRegistration,
restrictSsoRegistration: Env.restrictSsoRegistration,
dontStoreSSOUsers: Env.dontStoreSSOUsers,
dontStoreInvitedUsers: Env.dontStoreInvitedUsers,
enableEmbedding: Env.enableEmbedding,
launchTime: Env.launchTime,
currentTime: +new Date(),
@ -862,6 +868,48 @@ var getMetadataHistory = function (Env, Server, cb, data) {
});
};
var getKnownUsers = (Env, Server, cb) => {
Users.getAll(Env, cb);
};
var addKnownUser = (Env, Server, cb, data, unsafeKey) => {
var obj = Array.isArray(data) && data[1];
var edPublic = obj.edPublic;
var block = obj.block;
var alias = obj.alias;
var userData = {
edPublic,
block,
alias,
email: obj.email,
name: obj.name,
type: 'manual'
};
Users.add(Env, edPublic, userData, unsafeKey, cb);
};
var deleteKnownUser = (Env, Server, cb, data) => {
var id = Array.isArray(data) && data[1];
Users.delete(Env, id, cb);
};
var updateKnownUser = (Env, Server, cb, data) => {
var args = Array.isArray(data) && data[1];
var edPublic = args.edPublic;
var changes = args.changes;
Users.update(Env, edPublic, changes, cb);
};
var getInvitations = (Env, Server, cb) => {
Invitation.getAll(Env, cb);
};
var createInvitation = (Env, Server, cb, data, unsafeKey) => {
const args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
Invitation.create(Env, args.alias, args.email, cb, unsafeKey);
};
var deleteInvitation = (Env, Server, cb, data) => {
var id = Array.isArray(data) && data[1];
Invitation.delete(Env, id, cb);
};
var commands = {
ACTIVE_SESSIONS: getActiveSessions,
ACTIVE_PADS: getActiveChannelCount,
@ -920,6 +968,15 @@ var commands = {
GET_USER_TOTAL_SIZE: getUserTotalSize,
REMOVE_DOCUMENT: removeDocument,
GET_ALL_INVITATIONS: getInvitations,
CREATE_INVITATION: createInvitation,
DELETE_INVITATION: deleteInvitation,
GET_ALL_USERS: getKnownUsers,
ADD_KNOWN_USER: addKnownUser,
DELETE_KNOWN_USER: deleteKnownUser,
UPDATE_KNOWN_USER: updateKnownUser,
};
// addFirstAdmin is an anon_rpc command

View file

@ -9,6 +9,8 @@ const Nacl = require("tweetnacl/nacl-fast");
const nThen = require("nthen");
const Util = require("../common-util");
const BlockStore = require("../storage/block");
const Invitation = require("./invitation");
const Users = require("./users");
var isString = s => typeof(s) === 'string';
Block.isValidBlockId = id => {
@ -109,13 +111,21 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
Block.writeLoginBlock = function (Env, msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const { publicKey, signature, ciphertext, registrationProof } = msg;
const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO } = msg;
var previousKey;
var validatedBlock, path;
var validatedInvite;
nThen(function (w) {
if (!inviteToken) { return; }
Invitation.check(Env, inviteToken, w((err, state) => {
if (err || !state) { return; } // Invalid token, don't abort, check registration proof
validatedInvite = true;
}));
}).nThen(function (w) {
if (!Env.restrictRegistration) { return; }
if (!registrationProof) {
var ssoAllowed = isSSO && !Env.restrictSsoRegistration;
if (!(registrationProof || validatedInvite || ssoAllowed)) {
// we allow users with existing blocks to create new ones
// call back with error if registration is restricted and no proof of an existing block was provided
w.abort();
@ -124,6 +134,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
});
return cb("E_RESTRICTED");
}
if (!registrationProof) { return; }
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
if (err || !provenKey) { // double check that a key was validated
w.abort();
@ -162,7 +173,46 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
path: path,
});
cb(err);
if (!err && registrationProof) {
Users.checkUpdate(Env, userData, publicKey, (err) => {
if (!err) { return; }
Env.Log.error('UPDATE_KNOWN_USER', {
userData,
publicKey
});
});
}
});
if (validatedInvite) {
Invitation.use(Env, inviteToken, publicKey, userData, (err) => {
if (!err) { return; }
Env.Log.error('USE_INVITATION_LINK', {
inviteToken,
userData,
publicKey
});
});
} else if (isSSO && !Env.dontStoreSSOUsers && !registrationProof) {
let edPublic = Array.isArray(userData) && userData[1];
let name = Array.isArray(userData) && userData[0];
if (!edPublic) { return; }
let data = {
block: publicKey,
name,
edPublic,
type: 'sso',
alias: name
};
Users.add(Env, edPublic, data, null, (err) => {
if (err) {
Env.Log.error('INVITATION_ADD_USER', {
error: err,
data: data
});
}
});
}
});
};
@ -176,7 +226,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
information, we can just sign some constant and use that as proof.
*/
Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
Block.removeLoginBlock = function (Env, publicKey, reason, edPublic, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
BlockStore.archive(Env, publicKey, reason, function (err) {
@ -187,6 +237,11 @@ Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
cb(err);
});
if (edPublic && reason !== 'PASSWORD_CHANGE') {
Users.delete(Env, edPublic, (err) => {
if (err) { Env.Log.error('KNOWN_USER_DELETION_ERROR', { error: err, key: edPublic }); }
});
}
// We should also try to remove the SSO data. Errors will be logged
// but they don't have to be shown to the user. The account data

View file

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Invitation = module.exports;
const Invite = require('../storage/invite');
const Util = require("../common-util");
const Users = require("./users");
const getUid = () => {
return Util.uid() + Util.uid() + Util.uid();
};
Invitation.getAll = (Env, cb) => {
Invite.getAll(Env, (err, data) => {
if (err) { return void cb(err); }
cb(null, data);
});
};
Invitation.create = (Env, alias, email, _cb, unsafeKey) => {
const cb = Util.once(Util.mkAsync(_cb));
const id = getUid();
const invitation = {
alias,
email,
createdBy: unsafeKey,
time: +new Date()
};
Invite.write(Env, id, invitation, (err) => {
if (err) { return void cb(err); }
cb(null, id);
});
};
Invitation.delete = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.delete(Env, id, (err) => {
if (err && err !== 'ENOENT') { return void cb(err); }
cb(void 0, true);
});
};
Invitation.check = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.read(Env, id, (err) => {
if (err) { return void cb(err); }
cb(void 0, true);
});
};
Invitation.use = (Env, id, blockId, userData, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.read(Env, id, (err, _data) => {
if (err) { return void cb(err); }
let data = Util.clone(_data);
if (!Array.isArray(userData)) { userData = []; }
let name = userData[0];
let edPublic = userData[1];
data.block = blockId;
data.name = name;
data.edPublic = edPublic;
data.type = 'invite:' + id;
let adminKey = data.createdBy;
if (!Env.dontStoreInvitedUsers) {
Users.add(Env, edPublic, data, adminKey, (err) => {
if (err) {
Env.Log.error('INVITATION_ADD_USER', {
error: err,
data: data
});
}
});
}
Invite.delete(Env, id, (err) => {
if (err) {
Env.Log.error('INVITATION_DELETE_USE', {
error: err,
id: id
});
}
});
});
};

80
lib/commands/users.js Normal file
View file

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Users = module.exports;
const User = require('../storage/user');
const Util = require("../common-util");
Users.getAll = (Env, cb) => {
User.getAll(Env, (err, data) => {
if (err) { return void cb(err); }
cb(null, data);
});
};
Users.add = (Env, edPublic, data, adminKey, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
data.createdBy = adminKey;
data.time = +new Date();
const safeKey = Util.escapeKeyCharacters(edPublic);
User.write(Env, safeKey, data, (err) => {
if (err) { return void cb(err); }
cb();
});
};
Users.delete = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
User.delete(Env, id, (err) => {
if (err && err !== 'ENOENT') { return void cb(err); }
cb(void 0, true);
});
};
Users.read = (Env, edPublic, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
User.read(Env, edPublic, (err, data) => {
if (err) { return void cb(err); }
cb(void 0, data);
});
};
Users.update = (Env, edPublic, changes, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Users.read(Env, edPublic, (err, data) => {
if (err === 'ENOENT') { return void cb(); }
if (err) { return void cb(err); }
if (typeof(changes) !== "object") { return void cb('EINVAL'); }
// User exists, update their data
var aborted = Object.keys(changes || {}).some((key) => {
if (changes[key] === false) {
delete data[key];
return;
}
if (String(changes[key]).length > 300) {
cb('E_TOO_LONG');
return true;
}
data[key] = changes[key];
});
if (aborted) { return; }
User.update(Env, edPublic, data, cb);
});
};
// On password change, update the block
Users.checkUpdate = (Env, userData, newBlock, cb) => {
if (!Array.isArray(userData)) { userData = []; }
let edPublic = userData[1];
if (!edPublic) { return void cb('INVALID_PUBLIC_KEY'); }
Users.read(Env, edPublic, (err, data) => {
if (err === 'ENOENT') { return void cb(); }
if (err) { return void cb(err); }
// User exists, update their block
data.block = newBlock;
User.update(Env, edPublic, data, cb);
});
};

View file

@ -10,6 +10,7 @@ var Core = require("./commands/core");
IMPLEMENTED:
RESTRICT_REGISTRATION(<boolean>)
RESTRICT_SSO_REGISTRATION(<boolean>)
UPDATE_DEFAULT_STORAGE(<number>)
// QUOTA MANAGEMENT
@ -112,6 +113,9 @@ commands.ENFORCE_MFA = makeBooleanSetter('enforceMFA');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
commands.RESTRICT_SSO_REGISTRATION = makeBooleanSetter('restrictSsoRegistration');
commands.DISABLE_STORE_INVITED_USERS = makeBooleanSetter('dontStoreInvitedUsers');
commands.DISABLE_STORE_SSO_USERS = makeBooleanSetter('dontStoreSSOUsers');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_EVICTION', [true]]], console.log)
commands.DISABLE_INTEGRATED_EVICTION = makeBooleanSetter('disableIntegratedEviction');

View file

@ -52,10 +52,6 @@ Default.padContentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.diagramContentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.httpHeaders = function (Env) {
return {
"X-XSS-Protection": "1; mode=block",

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

@ -134,8 +134,6 @@ var getHeaders = function (Env, type) {
var csp;
if (type === 'office') {
csp = Default.padContentSecurity(Env);
} else if (type === 'diagram') {
csp = Default.diagramContentSecurity(Env);
} else {
csp = Default.contentSecurity(Env);
}
@ -158,8 +156,6 @@ var setHeaders = function (req, res) {
type = 'office';
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
type = 'api';
} else if (/^\/components\/drawio\/src\/main\/webapp\/index.html.*$/.test(req.url)) {
type = 'diagram';
} else {
type = 'standard';
}
@ -579,6 +575,7 @@ var serveConfig = makeRouteCache(function () {
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration,
restrictSsoRegistration: Env.restrictSsoRegistration,
httpSafeOrigin: Env.httpSafeOrigin,
enableEmbedding: Env.enableEmbedding,
fileHost: Env.fileHost,

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: {

82
lib/storage/invite.js Normal file
View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Basic = require("./basic.js");
const Path = require("node:path");
const nThen = require('nthen');
const Util = require('../common-util');
const Invite = module.exports;
/* This module manages storage used to implement instance invitations when registration
is closed. This "database" will store individual invitation and their state.
An invitation is created with a random uid and an alias (username, email, etc.)
Once it is used by the user, their newly created blockId is added which will mark
it as completed.
*/
const pathFromId = function (Env, id) {
if (!id || typeof(id) !== 'string') { return void console.error('INVITE_BAD_ID', id); }
return Path.join(Env.paths.base, "invitations", id.slice(0, 2), id);
};
Invite.read = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.read(Env, path, (err, data) => {
if (err) { return void cb(err.code); }
cb(void 0, Util.tryParse(data));
});
};
Invite.getAll = function (Env, cb) {
let invitations = {};
nThen((waitFor) => {
let dirPath = Path.join(Env.paths.base, "invitations");
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
if (err) { waitFor.abort(); return void cb(err.code); }
prefixes.forEach((prefix) => {
var dirPath2 = Path.join(Env.paths.base, "invitations", prefix);
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
if (err) { waitFor.abort(); return void cb(err.code); }
files.forEach((id) => {
Invite.read(Env, id, waitFor((err, data) => {
invitations[id] = data || { error: err };
}));
});
}));
});
}));
}).nThen(() => {
cb(null, invitations);
});
};
Invite.write = function (Env, id, data, cb) {
var path = pathFromId(Env, id);
Basic.write(Env, path, JSON.stringify(data), (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
Invite.delete = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.delete(Env, path, (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
Invite.update = function (Env, id, data, cb) {
Invite.delete(Env, id, (err) => {
if (err) { return void cb(err); }
Invite.write(Env, id, data, cb);
});
};

79
lib/storage/user.js Normal file
View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Basic = require("./basic.js");
const Path = require("node:path");
const nThen = require('nthen');
const Util = require('../common-util');
const User = module.exports;
/* This module manages storage used to implement user management. "Known users" can
be added here in order to store their public key, their block ID and an alias
used to recognize them.
*/
const pathFromId = function (Env, id) {
if (!id || typeof(id) !== 'string') { return void console.error('KNWONUSER_BAD_ID', id); }
return Path.join(Env.paths.base, "users", id.slice(0, 2), id);
};
User.read = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.read(Env, path, (err, data) => {
if (err) { return void cb(err.code); }
cb(void 0, Util.tryParse(data));
});
};
User.getAll = function (Env, cb) {
let users = {};
nThen((waitFor) => {
let dirPath = Path.join(Env.paths.base, "users");
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
if (err) { waitFor.abort(); return void cb(err.code); }
prefixes.forEach((prefix) => {
var dirPath2 = Path.join(Env.paths.base, "users", prefix);
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
if (err) { waitFor.abort(); return void cb(err.code); }
files.forEach((id) => {
User.read(Env, id, waitFor((err, data) => {
users[id] = data || { error: err };
}));
});
}));
});
}));
}).nThen(() => {
cb(null, users);
});
};
User.write = function (Env, id, data, cb) {
var path = pathFromId(Env, id);
Basic.write(Env, path, JSON.stringify(data), (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
User.delete = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.delete(Env, path, (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
User.update = function (Env, id, data, cb) {
User.delete(Env, id, (err) => {
if (err) { return void cb(err); }
User.write(Env, id, data, cb);
});
};

499
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cryptpad",
"version": "5.6.0",
"version": "5.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cryptpad",
"version": "5.6.0",
"version": "5.7.0",
"license": "AGPL-3.0+",
"dependencies": {
"@mcrowe/minibloom": "^0.2.0",
@ -26,7 +26,7 @@
"cookie-parser": "^1.4.6",
"croppie": "^2.5.0",
"dragula": "3.7.2",
"drawio": "cryptpad/drawio-npm#npm",
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+4",
"express": "~4.18.2",
"file-saver": "1.3.1",
"fs-extra": "^7.0.0",
@ -204,9 +204,9 @@
"integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA=="
},
"node_modules/@types/http-proxy": {
"version": "1.17.12",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz",
"integrity": "sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==",
"version": "1.17.14",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz",
"integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==",
"dependencies": {
"@types/node": "*"
}
@ -228,9 +228,12 @@
"integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
},
"node_modules/@types/node": {
"version": "20.6.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.3.tgz",
"integrity": "sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA=="
"version": "20.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
"integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/passport": {
"version": "1.0.14",
@ -962,12 +965,13 @@
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -1115,66 +1119,17 @@
"node": ">=0.10.0"
}
},
"node_modules/class-utils/node_modules/is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/class-utils/node_modules/is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/class-utils/node_modules/is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
"integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
"dev": true,
"dependencies": {
"is-accessor-descriptor": "^0.1.6",
"is-data-descriptor": "^0.1.4",
"kind-of": "^5.0.0"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/cli": {
@ -1205,9 +1160,9 @@
}
},
"node_modules/codemirror": {
"version": "5.65.15",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.15.tgz",
"integrity": "sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g=="
"version": "5.65.16",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
"integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
},
"node_modules/collection-visit": {
"version": "1.0.0",
@ -1228,9 +1183,12 @@
"dev": true
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/components-font-awesome": {
"version": "4.7.0",
@ -1411,6 +1369,19 @@
"node": ">=0.10"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
@ -1534,9 +1505,8 @@
}
},
"node_modules/drawio": {
"name": "drawio-cp",
"version": "21.7.5",
"resolved": "git+ssh://git@github.com/cryptpad/drawio-npm.git#b430ab78be9944f19721c3489c9ce95bc1abe2ca"
"version": "21.8.2+4",
"resolved": "git+ssh://git@github.com/cryptpad/drawio-npm.git#5555df31ce3a1c6220cefa3aaa16b9a7880bf8b8"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
@ -1651,66 +1621,17 @@
"node": ">=0.10.0"
}
},
"node_modules/expand-brackets/node_modules/is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/expand-brackets/node_modules/is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/expand-brackets/node_modules/is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
"integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
"dev": true,
"dependencies": {
"is-accessor-descriptor": "^0.1.6",
"is-data-descriptor": "^0.1.4",
"kind-of": "^5.0.0"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/express": {
@ -2028,9 +1949,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",
@ -2113,9 +2034,12 @@
"dev": true
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gar": {
"version": "1.0.4",
@ -2135,14 +2059,14 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -2241,20 +2165,31 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz",
"integrity": "sha512-zGRpnr2l5w/s8PxkrquUJoVeR06KvqPelrYqiSyQV7QEBqCYivpb6UzXYWC6JDBVtNFOT0rzJRFhkfJgxzmILA=="
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"function-bind": "^1.1.1"
"get-intrinsic": "^1.2.2"
},
"engines": {
"node": ">= 0.4.0"
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
@ -2342,6 +2277,17 @@
"node": ">=0.10.0"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@ -2499,22 +2445,14 @@
}
},
"node_modules/is-accessor-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
"integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==",
"dependencies": {
"kind-of": "^6.0.0"
"hasown": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-accessor-descriptor/node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"engines": {
"node": ">=0.10.0"
"node": ">= 0.10"
}
},
"node_modules/is-arrayish": {
@ -2529,43 +2467,26 @@
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"node_modules/is-data-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz",
"integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==",
"dependencies": {
"kind-of": "^6.0.0"
"hasown": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-data-descriptor/node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/is-descriptor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz",
"integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==",
"dependencies": {
"is-accessor-descriptor": "^1.0.0",
"is-data-descriptor": "^1.0.0",
"kind-of": "^6.0.2"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-descriptor/node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/is-directory": {
@ -3363,47 +3284,16 @@
"node": ">=0.10.0"
}
},
"node_modules/object-copy/node_modules/is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-copy/node_modules/is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-copy/node_modules/is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
"integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
"dependencies": {
"is-accessor-descriptor": "^0.1.6",
"is-data-descriptor": "^0.1.4",
"kind-of": "^5.0.0"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
"integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/object-copy/node_modules/kind-of": {
@ -3426,9 +3316,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -4210,6 +4100,20 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-getter": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz",
@ -4351,66 +4255,17 @@
"node": ">=0.10.0"
}
},
"node_modules/snapdragon/node_modules/is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snapdragon/node_modules/is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snapdragon/node_modules/is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
"integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
"dev": true,
"dependencies": {
"is-accessor-descriptor": "^0.1.6",
"is-data-descriptor": "^0.1.4",
"kind-of": "^5.0.0"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/snapdragon/node_modules/source-map": {
@ -4423,9 +4278,9 @@
}
},
"node_modules/sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.1.tgz",
"integrity": "sha512-P5Cjvb0UG1ZVNiDPj/n4V+DinttXG6K8n7vM/HQf0C25K3YKQTQY6fsr/sEGsJGpQ9exmPxluHxKBc0mLKU1lQ=="
},
"node_modules/sortify": {
"version": "1.0.4",
@ -4531,61 +4386,16 @@
"node": ">=0.10.0"
}
},
"node_modules/static-extend/node_modules/is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
"integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==",
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/static-extend/node_modules/is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
"integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==",
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/static-extend/node_modules/is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
"integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
"integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
"dependencies": {
"is-accessor-descriptor": "^0.1.6",
"is-data-descriptor": "^0.1.4",
"kind-of": "^5.0.0"
"is-accessor-descriptor": "^1.0.1",
"is-data-descriptor": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
"node": ">= 0.4"
}
},
"node_modules/statuses": {
@ -4859,6 +4669,11 @@
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",

View file

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "5.6.0",
"version": "5.7.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -63,7 +63,7 @@
"requirejs-plugins": "^1.0.2",
"scrypt-async": "1.2.0",
"sortablejs": "^1.6.0",
"drawio": "cryptpad/drawio-npm#npm",
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+4",
"pako": "^2.1.0",
"x2js": "^3.4.4"
},

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

@ -50,5 +50,6 @@ Fse.rmSync(oldComponentsPath, { recursive: true, force: true });
].forEach(l => {
const source = Path.join("node_modules", l);
const destination = Path.join(componentsPath, l);
Fs.rmSync(destination, { recursive: true, force: true });
Fs.cpSync(source, destination, { recursive: true });
});

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

@ -37,12 +37,12 @@
}
}
.cp-admin-setlimit-form, .cp-admin-broadcast-form {
label {
.cp-sidebarlayout-element {
label:not(.cp-admin-label) {
font-weight: normal !important;
}
input {
max-width: 400px;
max-width: 25rem;
}
nav {
display: flex;
@ -246,6 +246,12 @@
}
}
.cp-admin-users {
.cp-admin-store-invited, .cp-admin-store-sso {
margin-bottom: 0 !important;
}
}
.cp-admin-broadcast-form {
input.flatpickr-input {
width: 307.875px !important; // same width as flatpickr calendar

View file

@ -61,7 +61,6 @@ define([
'general': [ // Msg.admin_cat_general
'cp-admin-flush-cache',
'cp-admin-update-limit',
'cp-admin-registration',
'cp-admin-enableembeds',
'cp-admin-forcemfa',
'cp-admin-email',
@ -73,6 +72,11 @@ define([
'cp-admin-jurisdiction',
'cp-admin-notice',
],
'users': [ // Msg.admin_cat_quota
'cp-admin-registration',
'cp-admin-invitation',
'cp-admin-users',
],
'quota': [ // Msg.admin_cat_quota
'cp-admin-defaultlimit',
'cp-admin-setlimit',
@ -132,7 +136,7 @@ define([
// Convert to camlCase for translation keys
var safeKey = keyToCamlCase(key);
var $div = $('<div>', {'class': 'cp-admin-' + key + ' cp-sidebarlayout-element'});
$('<label>', {'id': 'cp-admin-' + key}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<label>', {'id': 'cp-admin-' + key, 'class':'cp-admin-label'}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['admin_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
@ -1204,6 +1208,25 @@ define([
return tableObj.table;
};
var getBlockId = (val) => {
var url;
try {
url = new URL(val, ApiConfig.httpUnsafeOrigin);
} catch (err) { }
var getKey = function () {
var parts = val.split('/');
return parts[parts.length - 1];
};
var isValidBlockURL = function (url) {
if (!url) { return; }
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
};
if (isValidBlockURL(url)) {
return getKey();
}
return;
};
create['block-metadata'] = function () {
var key = 'block-metadata';
var $div = makeBlock(key, true); // Msg.admin_blockMetadataHint.admin_blockMetadataTitle
@ -1236,21 +1259,10 @@ define([
key: '',
};
var url;
try {
url = new URL(val, ApiConfig.httpUnsafeOrigin);
} catch (err) { }
var getKey = function () {
var parts = val.split('/');
return parts[parts.length - 1];
};
var isValidBlockURL = function (url) {
if (!url) { return; }
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
};
if (isValidBlockURL(url)) {
var key = getBlockId(val);
if (key) {
state.valid = true;
state.key = getKey();
state.key = key;
}
return state;
};
@ -1483,27 +1495,474 @@ Example
};
// Msg.admin_registrationHint, .admin_registrationTitle
create['registration'] = makeAdminCheckbox({
key: 'registration',
getState: function () {
return APP.instanceStatus.restrictRegistration;
},
query: function (val, setState) {
// Msg.admin_registrationSsoTitle
create['registration'] = function () {
var refresh = function () {};
var $div = makeAdminCheckbox({
key: 'registration',
getState: function () {
return APP.instanceStatus.restrictRegistration;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_REGISTRATION', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictRegistration);
refresh();
flushCacheNotice();
});
});
}
})();
var $sso = makeAdminCheckbox({
key: 'registration-sso',
getState: function () {
return APP.instanceStatus.restrictSsoRegistration;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_SSO_REGISTRATION', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictSsoRegistration);
flushCacheNotice();
});
});
}
})();
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
if (ssoEnabled) {
$sso.find('#cp-admin-registration-sso').hide();
$sso.find('> span.cp-sidebarlayout-description').hide();
$div.append($sso);
}
refresh = () => {
var closed = APP.instanceStatus.restrictRegistration;
if (closed) {
$sso.show();
} else {
$sso.hide();
}
};
refresh();
return $div;
};
create['invitation'] = function () {
var key = 'invitation';
var $div = makeBlock(key); // Msg.admin_invitationHint, admin_invitationTitle
var list = h('table.cp-admin-all-limits');
var input = h('input#cp-admin-invitation-alias');
var inputEmail = h('input#cp-admin-invitation-email');
var button = h('button.btn.btn-primary', Messages.admin_invitationCreate);
var $b = $(button);
var refreshInvite = function () {};
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
Util.onClickEnter($(refresh), function () {
refreshInvite();
});
var add = h('div', [
h('label', { for: 'cp-admin-invitation-alias' }, Messages.admin_invitationAlias),
input,
h('label', { for: 'cp-admin-invitation-email' }, Messages.admin_invitationEmail),
inputEmail,
h('nav', [button, refresh])
]);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var deleteInvite = function (id) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_REGISTRATION', [val]]
cmd: 'DELETE_INVITATION',
data: id
}, function (e, response) {
$b.prop('disabled', false);
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
refreshInvite();
});
};
var $list = $(list);
refreshInvite = function () {
$list.empty();
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_ALL_INVITATIONS',
}, function (e, response) {
if (e || response.error) {
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
console.error(e, response);
return;
}
if (!Array.isArray(response)) { return; }
var all = response[0];
Object.keys(all).forEach(function (key, i) {
if (!i) { // First item: add header to table
var trHead = h('tr', [
h('th', Messages.admin_invitationLink),
h('th', Messages.admin_invitationAlias),
h('th', Messages.admin_invitationEmail),
h('th', Messages.admin_documentCreationTime),
h('th')
]);
$list.append(trHead);
}
var data = all[key];
var url = privateData.origin + Hash.hashToHref(key, 'register');
var del = h('button.btn.btn-danger', [
h('i.fa.fa-trash'),
h('span', Messages.kanban_delete)
]);
var $del = $(del);
Util.onClickEnter($del, function () {
$del.attr('disabled', 'disabled');
UI.confirm(Messages.admin_invitationDeleteConfirm, function (yes) {
$del.attr('disabled', '');
if (!yes) { return; }
deleteInvite(key);
});
});
var copy = h('button.btn.btn-secondary', [
h('i.fa.fa-clipboard'),
h('span', Messages.admin_invitationCopy)
]);
Util.onClickEnter($(copy), function () {
Clipboard.copy(url, () => {
UI.log(Messages.genericCopySuccess);
});
});
var line = h('tr', [
h('td', UI.dialog.selectable(url)),
h('td', data.alias),
h('td', data.email),
h('td', new Date(data.time).toLocaleString()),
//h('td', data.createdBy),
h('td', [
copy,
del
])
]);
$list.append(line);
});
});
};
refreshInvite();
$b.on('click', () => {
var alias = $(input).val().trim();
if (!alias) { return void UI.warn(Messages.error); } // FIXME better error message
$b.prop('disabled', true);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'CREATE_INVITATION',
data: {
alias,
email: $(inputEmail).val()
}
}, function (e, response) {
$b.prop('disabled', false);
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
$(input).val('').focus();
$(inputEmail).val('');
refreshInvite();
});
});
$div.append([add, list]);
return $div;
};
create['users'] = function () {
var key = 'users';
var $div = makeBlock(key); // Msg.admin_usersHint, admin_usersTitle
var list = h('table.cp-admin-all-limits');
var userAlias = h('input#cp-admin-users-alias');
var userEmail = h('input#cp-admin-users-email');
var userEdPublic = h('input#cp-admin-users-key');
var userBlock = h('input#cp-admin-users-block');
var button = h('button.btn.btn-primary', Messages.admin_usersAdd);
var $b = $(button);
var refreshUsers = function () {};
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
Util.onClickEnter($(refresh), function () {
refreshUsers();
});
var add = h('div', [
h('label', { for: 'cp-admin-users-alias' }, Messages.admin_invitationAlias),
userAlias,
h('label', { for: 'cp-admin-users-email' }, Messages.admin_invitationEmail),
userEmail,
h('label', { for: 'cp-admin-users-key' }, Messages.admin_limitUser),
userEdPublic,
h('label', { for: 'cp-admin-users-block' }, Messages.admin_usersBlock),
userBlock,
h('nav', [button, refresh])
]);
var $invited = makeAdminCheckbox({
key: 'store-invited',
getState: function () {
return !APP.instanceStatus.dontStoreInvitedUsers;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_STORE_INVITED_USERS', [!val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(!APP.instanceStatus.dontStoreInvitedUsers);
flushCacheNotice();
});
});
}
})();
$invited.find('#cp-admin-store-invited').hide();
$invited.find('> span.cp-sidebarlayout-description').hide();
$div.append($invited);
var $sso = makeAdminCheckbox({
key: 'store-sso',
getState: function () {
return !APP.instanceStatus.dontStoreSSOUsers;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_STORE_SSO_USERS', [!val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(!APP.instanceStatus.dontStoreSSOUsers);
flushCacheNotice();
});
});
}
})();
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
if (ssoEnabled) {
$sso.find('#cp-admin-store-sso').hide();
$sso.find('> span.cp-sidebarlayout-description').hide();
$div.append($sso);
}
var deleteUser = function (id) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'DELETE_KNOWN_USER',
data: id
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
return void console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictRegistration);
flushCacheNotice();
refreshUsers();
});
};
var updateUser = function (key, changes) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'UPDATE_KNOWN_USER',
data: {
edPublic: key,
changes: changes
}
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
refreshUsers();
});
};
var $list = $(list);
refreshUsers = function () {
$list.empty();
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_ALL_USERS',
}, function (e, response) {
if (e || response.error) {
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
console.error(e, response);
return;
}
if (!Array.isArray(response)) { return; }
var all = response[0];
Object.keys(all).forEach(function (key, i) {
if (!i) { // First item: add header to table
var trHead = h('tr', [
h('th', Messages.admin_invitationAlias),
h('th', Messages.admin_invitationEmail),
h('th', Messages.admin_limitUser),
h('th', Messages.admin_documentCreationTime),
h('th')
]);
$list.append(trHead);
}
var data = all[key];
var editUser = () => {};
var del = h('button.btn.btn-danger', [
h('i.fa.fa-trash'),
Messages.admin_usersRemove
]);
var $del = $(del);
Util.onClickEnter($del, function () {
$del.attr('disabled', 'disabled');
UI.confirm(Messages.admin_usersRemoveConfirm, function (yes) {
$del.attr('disabled', '');
if (!yes) { return; }
deleteUser(key);
});
});
var edit = h('button.btn.btn-secondary', [
h('i.fa.fa-pencil'),
h('span', Messages.tag_edit)
]);
Util.onClickEnter($(edit), function () {
editUser();
});
var alias = h('td', data.alias);
var email = h('td', data.email);
var actions = h('td', [edit, del]);
var $alias = $(alias);
var $email = $(email);
var $actions = $(actions);
editUser = () => {
var aliasInput = h('input');
var emailInput = h('input');
$(aliasInput).val(data.alias);
$(emailInput).val(data.email);
var save = h('button.btn.btn-primary', Messages.settings_save);
var cancel = h('button.btn.btn-secondary', Messages.cancel);
Util.onClickEnter($(save), function () {
var aliasVal = $(aliasInput).val().trim();
if (!aliasVal) { return void UI.warn(Messages.error); }
var changes = {
alias: aliasVal,
email: $(emailInput).val().trim()
};
updateUser(key, changes);
});
Util.onClickEnter($(cancel), function () {
refreshUsers();
});
$alias.html('').append(aliasInput);
$email.html('').append(emailInput);
$actions.html('').append([save, cancel]);
console.warn(alias, email, $alias, $email, aliasInput);
};
var infoButton = h('button.btn.primary.cp-report', {
style: 'margin-left: 10px; cursor: pointer;',
}, [
h('i.fa.fa-database'),
h('span', Messages.admin_diskUsageButton)
]);
$(infoButton).click(() => {
getAccountData(key, (err, data) => {
if (err) { return void console.error(err); }
var table = renderAccountData(data);
UI.alert(table, () => {
}, {
wide: true,
});
});
});
var line = h('tr', [
alias,
email,
h('td', [
h('code', key),
infoButton
]),
h('td', new Date(data.time).toLocaleString()),
//h('td', data.createdBy),
actions
]);
$list.append(line);
});
});
},
});
};
refreshUsers();
$b.on('click', () => {
var alias = $(userAlias).val().trim();
if (!alias) { return void UI.warn(Messages.error); }
$b.prop('disabled', true);
var done = () => { $b.prop('disabled', false); };
// TODO Get "block" from pin log?
var keyStr = $(userEdPublic).val().trim();
var edPublic = keyStr && Keys.canonicalize(keyStr);
if (!edPublic) {
done();
return void UI.warn(Messages.admin_invalKey);
}
var block = getBlockId($(userBlock).val());
var obj = {
alias,
email: $(userEmail).val(),
block: block,
edPublic: edPublic,
};
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADD_KNOWN_USER',
data: obj
}, function (e, response) {
done();
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
$(userAlias).val('').focus();
$(userEmail).val('');
$(userBlock).val('');
$(userEdPublic).val('');
refreshUsers();
});
});
$div.append([add, list]);
return $div;
};
// Msg.admin_enableembedsHint, .admin_enableembedsTitle
create['enableembeds'] = makeAdminCheckbox({
@ -1913,19 +2372,23 @@ Example
var key = 'setlimit';
var $div = makeBlock(key); // Msg.admin_setlimitHint, .admin_setlimitTitle
var user = h('input.cp-setlimit-key', { id: 'user-input' });
var user = h('input.cp-setlimit-key#cp-admin-setlimit-user');
var $key = $(user);
var limit = h('input.cp-setlimit-quota', { type: 'number', min: 0, value: 0, id: 'limit-input' });
var note = h('input.cp-setlimit-note', { id: 'note-input' });
var limit = h('input.cp-setlimit-quota#cp-admin-setlimit-value', {
type: 'number',
min: 0,
value: 0
});
var note = h('input.cp-setlimit-note#cp-admin-setlimit-note');
var remove = h('button.btn.btn-danger', Messages.fc_remove);
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
var form = h('div.cp-admin-setlimit-form', [
h('label', { for: 'user-input' }, Messages.admin_limitUser),
h('label', { for: 'cp-admin-setlimit-user' }, Messages.admin_limitUser),
user,
h('label', { for: 'limit-input' }, Messages.admin_limitMB),
h('label', { for: 'cp-admin-setlimit-value' }, Messages.admin_limitMB),
limit,
h('label', { for: 'note-input' }, Messages.admin_limitSetNote),
h('label', { for: 'cp-admin-setlimit-note' }, Messages.admin_limitSetNote),
note,
h('nav', [set, remove])
]);
@ -3445,6 +3908,7 @@ Example
var SIDEBAR_ICONS = {
general: 'fa fa-user-o',
stats: 'fa fa-line-chart',
users: 'fa fa-address-card-o',
quota: 'fa fa-hdd-o',
support: 'fa fa-life-ring',
broadcast: 'fa fa-bullhorn',

View file

@ -1307,23 +1307,20 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
updateDateRange();
updateRecurring();
});
var f = Flatpickr(goDate, {
enableTime: false,
defaultDate: APP.calendar.getDate()._date,
clickOpens: false,
//dateFormat: dateFormat,
onChange: function (date) {
date[0].setHours(12);
APP.moveToDate(+date[0]);
updateDateRange();
updateRecurring();
},
});
$(goDate).click(function () {
var f = Flatpickr(goDate, {
enableTime: false,
defaultDate: APP.calendar.getDate()._date,
//dateFormat: dateFormat,
onChange: function (date) {
date[0].setHours(12);
f.destroy();
APP.moveToDate(+date[0]);
updateDateRange();
updateRecurring();
},
onClose: function () {
setTimeout(f.destroy);
}
});
f.open();
return f.isOpen ? f.close() : f.open();
});
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
goLeft, goToday, goRight, goDate
@ -1380,6 +1377,8 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
}
if (m) {
m = m.map(function (n) {
tmp.setDate(15);
tmp.setHours(12);
tmp.setMonth(n-1);
return tmp.toLocaleDateString(getDateLanguage(), { month: 'long' });
});
@ -1466,7 +1465,7 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
dayStr,
monthStr
]),
last: last ? '-1' + dayStr : undefined,
last: last ? '-1' + dayCode : undefined,
lastStr: Messages._getKey('calendar_rec_'+key+'_nth', [
Messages['calendar_nth_last'],
dayStr,

View file

@ -245,6 +245,10 @@ define([
return void cb('NO_SUCH_USER');
}
if (!isProxyEmpty(rt.proxy) && res.auth_token && res.auth_token.bearer) {
LocalStore.setSessionToken(res.auth_token.bearer);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
//rt.network.disconnect();
@ -256,9 +260,6 @@ define([
var LS_LANG = "CRYPTPAD_LANG";
if (l) { localStorage.setItem(LS_LANG, l); }
if (res.auth_token && res.auth_token.bearer) {
LocalStore.setSessionToken(res.auth_token.bearer);
}
return void LocalStore.login(undefined, res.blockHash, res.uname, function () {
cb(void 0, res, RT);
});
@ -287,7 +288,7 @@ define([
};
Exports.loginOrRegister = function (config, cb) {
let { uname, passwd, isRegister, onOTP, ssoAuth } = config;
let { uname, passwd, token, isRegister, onOTP, ssoAuth } = config;
if (typeof(cb) !== 'function') { return; }
// Usernames are all lowercase. No going back on this one
@ -481,7 +482,7 @@ define([
legacyLogin(opt, isRegister, waitFor(function (err, data) {
if (err) {
waitFor.abort();
return void cb(err);
return void cb(err, res);
}
if (!data) { return; } // Go to next block (modern registration)
@ -496,7 +497,7 @@ define([
modernLoginRegister(opt, isRegister, waitFor(function (err, data, _RT) {
if (err) {
waitFor.abort();
return void cb(err);
return void cb(err, res);
}
RT = _RT;
if (!data) { return; } // Go to next block (modern registration)
@ -516,17 +517,23 @@ define([
// FIXME We currently can't create an account with OTP by default
// NOTE If we ever want to do that for SSO accounts it will require major changes
// because writeLoginBlock only supports one type of authentication at a time
// XXX Get server config to know if the user data should be sent or not
// Only SSO users and invited users can be stored by the server and it needs to be configured
var userData = (token || ssoAuth) ? [uname, RT.proxy.edPublic] : undefined;
Block.writeLoginBlock({
pw: Boolean(passwd),
auth: ssoAuth,
blockKeys: blockKeys,
token: token,
content: toPublish,
userData
}, waitFor(function (e, res) {
if (e === 'SSO_NO_SESSION') { return; } // account created, need re-login
if (e) {
console.error(e);
console.error(e, res);
waitFor.abort();
return void cb(e);
return void cb(res ? (res.errorCode || res.error) : e);
}
if (res && res.bearer) {
LocalStore.setSessionToken(res.bearer);

View file

@ -150,7 +150,7 @@ define([
video.src = url;
};
Thumb.fromPdfBlob = function (blob, cb) {
require.config({paths: {'pdfjs-dist': '/lib/pdfjs'}});
require.config({paths: {'pdfjs-dist': '/lib/pdfjs/legacy'}});
require(['pdfjs-dist/build/pdf'], function (PDFJS) {
var url = URL.createObjectURL(blob);
var makeThumb = function (page) {
@ -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

@ -107,6 +107,20 @@
});
};
Util.onClickEnter = function ($element, handler, cfg) {
$element.on('click keydown', function (e) {
var isClick = e.type === 'click';
var isEnter = e.type === 'keydown' && e.which === 13;
var isSpace = e.type === 'keydown' && e.which === 32 && cfg && cfg.space;
if (!isClick && !isEnter && !isSpace) { return; }
// "enter" on a button triggers a click, disable it
if (e.type === 'keydown') { e.preventDefault(); }
handler();
});
};
Util.response = function (errorHandler) {
var pending = {};
var timeouts = {};
@ -733,6 +747,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

@ -1527,6 +1527,7 @@ define([
hash: newHash,
href: newHref,
roHref: newRoHref,
channel: newSecret.channel
});
});
};
@ -1912,6 +1913,7 @@ define([
common.deleteAccount = function (data, cb) {
data = data || {};
common.CP_onAccountDeletion = true;
var bytes = data.bytes; // From Scrypt
var auth = data.auth; // MFA data
@ -2058,9 +2060,11 @@ define([
User_hash: newHash,
edPublic: edPublic,
};
var userData = [undefined, edPublic];
var sessionToken = LocalStore.getSessionToken() || undefined;
Block.writeLoginBlock({
auth: auth,
userData: userData,
blockKeys: blockKeys,
oldBlockKeys: oldBlockKeys,
content: content,
@ -2123,6 +2127,7 @@ define([
Block.removeLoginBlock({
reason: 'PASSWORD_CHANGE',
auth: auth,
edPublic: edPublic,
blockKeys: oldBlockKeys,
}, waitFor(function (err) {
if (err) { return void console.error(err); }
@ -2338,7 +2343,7 @@ define([
LocalStore.logout(function () {
common.stopWorker();
common.drive.onDeleted.fire(data.reason);
});
}, true);
};
var lastPing = +new Date();
@ -2896,13 +2901,17 @@ define([
if (!o && n) {
LocalStore.loginReload();
} else if (o && !n) {
LocalStore.logout();
if (!common.CP_onAccountDeletion) { LocalStore.logout(); }
} else if (o && n && o !== n) {
common.passwordUpdated = true;
window.location.reload();
}
});
common.drive.onDeleted.reg(function () {
common.CP_onAccountDeletion = true;
});
LocalStore.onLogout(function () {
if (common.CP_onAccountDeletion) { return; }
console.log('onLogout: disconnect');
common.stopWorker();
});

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);
@ -2150,6 +2150,22 @@ define([
} */
};
var thumbsUrls = {};
// This is duplicated in cryptpad-common, it should be unified
var getFileIcon = function (id) {
var data = manager.getFileData(id);
return UI.getFileIcon(data);
};
var getIcon = UI.getIcon;
var addTitleIcon = function (element, $name) {
var icon = getFileIcon(element);
$(icon).addClass('cp-app-drive-element-icon');
$name.addClass('cp-app-drive-element-name-icon');
$name.prepend($(icon));
};
var addFileData = function (element, $element) {
if (!manager.isFile(element)) { return; }
@ -2201,13 +2217,15 @@ define([
var name = manager.getTitle(element);
// The element with the class '.name' is underlined when the 'li' is hovered
var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(name);
var $name = $(h('span.cp-app-drive-element-name', [
h('span.cp-app-drive-element-name-text', name)
]));
$element.append($name);
$element.append($state);
if (APP.mobile()) {
$element.append($menu);
}
if (getViewMode() === 'grid') {
$element.attr('title', name);
}
@ -2220,8 +2238,8 @@ define([
$element.prepend(img);
$(img).addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail');
$(img).attr("draggable", false);
}
else {
addTitleIcon(element, $name);
} else {
common.displayThumbnail(href || data.roHref, data.channel, data.password, $element, function ($thumb) {
// Called only if the thumbnail exists
// Remove the .hide() added by displayThumnail() because it hides the icon in list mode too
@ -2229,6 +2247,7 @@ define([
$thumb.addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail');
$thumb.attr("draggable", false);
thumbsUrls[element] = $thumb[0].src;
addTitleIcon(element, $name);
});
}
@ -2289,7 +2308,9 @@ define([
var sf = manager.hasSubfolder(element);
var hasFiles = manager.hasFile(element);
var $name = $('<span>', {'class': 'cp-app-drive-element-name'}).text(key);
var $name = $(h('span.cp-app-drive-element-name', [
h('span.cp-app-drive-element-name-text', key)
]));
var $subfolders = $('<span>', {
'class': 'cp-app-drive-element-folders cp-app-drive-element-list'
}).text(sf);
@ -2308,13 +2329,6 @@ define([
}
};
// This is duplicated in cryptpad-common, it should be unified
var getFileIcon = function (id) {
var data = manager.getFileData(id);
return UI.getFileIcon(data);
};
var getIcon = UI.getIcon;
var createShareButton = function (id, $container) {
var $shareBlock = $('<button>', {
'class': 'cp-toolbar-share-button',

View file

@ -30,7 +30,6 @@ define([
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
var channel = data.channel || priv.channel;
var owners = data.owners || [];
var pending_owners = data.pending_owners || [];
var teamOwner = data.teamId;
@ -105,7 +104,7 @@ define([
}).nThen(function (waitFor) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
value: [ed],
@ -127,7 +126,7 @@ define([
var friend = friends[curve];
if (!friend) { return; }
common.mailbox.sendTo("RM_OWNER", {
channel: channel,
channel: data.channel || priv.channel,
title: data.title || title,
pending: pending
}, {
@ -265,7 +264,7 @@ define([
if (toAddTeams.length) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'ADD_OWNERS',
value: toAddTeams.map(function (obj) { return obj.edPublic; }),
@ -301,7 +300,7 @@ define([
if (toAdd.length) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'ADD_PENDING_OWNERS',
value: toAdd,
@ -322,7 +321,7 @@ define([
if (addMe) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'ADD_OWNERS',
value: [priv.edPublic],
@ -352,7 +351,7 @@ define([
var friend = friends[curve];
if (!friend) { return; }
common.mailbox.sendTo("ADD_OWNER", {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
href: href,
calendar: opts.calendar,
@ -427,7 +426,6 @@ define([
var metadataMgr = common.getMetadataMgr();
var priv = metadataMgr.getPrivateData();
var channel = data.channel || priv.channel;
var owners = data.owners || [];
var restricted = data.restricted || false;
var allowed = data.allowed || [];
@ -516,7 +514,7 @@ define([
*/
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'RM_ALLOWED',
value: [ed],
@ -546,7 +544,7 @@ define([
spinner.spin();
var val = $checkbox.is(':checked');
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'RESTRICT_ACCESS',
value: [Boolean(val)],
@ -646,7 +644,6 @@ define([
return $div;
};
$(addBtn).click(function () {
var priv = metadataMgr.getPrivateData();
var user = metadataMgr.getUserData();
@ -657,12 +654,14 @@ define([
var $sel = $div.find('.cp-usergrid-user.cp-selected');
var sel = $sel.toArray();
if (!sel.length) { return; }
var dataToAdd = [];
var toAdd = sel.map(function (el) {
var curve = $(el).attr('data-curve');
var teamId = $(el).attr('data-teamid');
// If the pad is woned by a team, we can transfer ownership to ourselves
if (curve === user.curvePublic && teamOwner) { return priv.edPublic; }
var data = friends[curve] || teamsData[teamId];
dataToAdd.push(data);
if (!data) { return; }
return data.edPublic;
}).filter(function (x) { return x; });
@ -682,7 +681,7 @@ define([
if (toAdd.length) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channel: data.channel || priv.channel,
channels: otherChan,
command: 'ADD_ALLOWED',
value: toAdd,
@ -690,6 +689,19 @@ define([
}, waitFor(function (err, res) {
err = err || (res && res.error);
redrawAll(true);
dataToAdd.forEach(function(mailbox) {
if (mailbox.notifications && mailbox.curvePublic) {
common.mailbox.sendTo("ADD_TO_ACCESS_LIST", {
channel: data.channel || priv.channel,
}, {
channel: mailbox.notifications,
curvePublic: mailbox.curvePublic
});
} else {
return;
}
});
if (err) {
waitFor.abort();
var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
@ -939,12 +951,12 @@ define([
sframeChan.query(q, {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
href: href,
oldPassword: priv.password,
oldPassword: data.password || priv.password,
password: newPass
}, function (err, data) {
}, function (err, res) {
$(passwordOk).text(Messages.properties_changePasswordButton);
pLocked = false;
err = err || data.error;
err = err || res.error;
if (err) {
if (err === "PASSWORD_ALREADY_USED") {
return void UI.alert(Messages.access_passwordUsed);
@ -954,6 +966,11 @@ define([
}
UI.findOKButton().click();
data.password = newPass;
data.href = res.href;
data.roHref = res.roHref;
data.channel = res.channel;
$pwInput.val(newPass);
if (newPass) {
$password.show();
@ -969,7 +986,7 @@ define([
if (isFile || priv.app !== parsed.type) {
if (onProgress && onProgress.stop) { onProgress.stop(); }
$(passwordOk).text(Messages.properties_changePasswordButton);
var alertMsg = data.warning ? Messages.properties_passwordWarningFile
var alertMsg = res.warning ? Messages.properties_passwordWarningFile
: Messages.properties_passwordSuccessFile;
return void UI.alert(alertMsg, undefined, {force: true});
}
@ -978,7 +995,7 @@ define([
// Use hidden hash if needed (we're an owner of this pad so we know it is stored)
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
if (isNotStored) { useUnsafe = true; }
var _href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
var _href = (priv.readOnly && res.roHref) ? res.roHref : res.href;
if (useUnsafe !== true) {
var newParsed = Hash.parsePadUrl(_href);
var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
@ -989,7 +1006,7 @@ define([
// Trigger a page reload if the href didn't change
if (_href === href) { _href = undefined; }
if (data.warning) {
if (res.warning) {
return void UI.alert(Messages.properties_passwordWarning, function () {
if (isNotStored) {
return sframeChan.query('Q_PASSWORD_CHECK', newPass, () => { common.gotoURL(_href); });
@ -1028,7 +1045,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

@ -24,8 +24,29 @@ define([
// Configure MediaTags to use our local viewer
// This file is loaded by sframe-common so the following config is used in all the inner apps
if (MediaTag) {
// Firefox 121 introduces an issue with ligatures that requires an update to PDFjs
// See: https://github.com/cryptpad/cryptpad/issues/1362
// Unfortunately this updated PDFjs doesn't work with older browsers
let isModernFirefox = false;
try {
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
if (isFirefox) {
let version = +navigator.userAgent.match(/rv:([0-9.]+)/)[1];
isModernFirefox = version >= 100;
}
} catch (e) {}
let isModernChromium = false;
try {
isModernChromium = navigator.userAgentData.brands.some(data => {
return data.brand === 'Chromium' && data.version >= 100;
});
} catch (e) {}
let path = 'legacy';
if (isModernFirefox || isModernChromium) { path = 'modern'; }
MediaTag.setDefaultConfig('pdf', {
viewer: '/lib/pdfjs/web/viewer.html'
viewer: `/lib/pdfjs/${path}/web/viewer.html`
});
MediaTag.setDefaultConfig('download', {
text: Messages.mediatag_saveButton,

View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([], function() {
const openImageDialog = function(common, integrationChannel, data, cb) {
if (integrationChannel) {
const handleImage = (_, image) => {
cb(image);
};
integrationChannel.query('Q_INTEGRATION_ON_INSERT_IMAGE', data, handleImage, {raw: true});
return;
}
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
}, cb);
};
return {
openImageDialog,
};
});

View file

@ -121,6 +121,12 @@ var factory = function () {
if (cfg.pdf.viewer) { // PDFJS
var viewerUrl = cfg.pdf.viewer + '?file=' + url;
iframe.src = viewerUrl + '#' + window.encodeURIComponent(metadata.name);
iframe.onload = function () {
if (!metadata.name) { return; }
try {
iframe.contentWindow.PDFViewerApplication.setTitleUsingUrl(metadata.name);
} catch (e) { console.warn(e); }
};
return void cb (void 0, iframe);
}
iframe.src = url + '#' + window.encodeURIComponent(metadata.name);

View file

@ -498,6 +498,7 @@ define([
if (!toShow) { return defaultDismiss(common, data)(); }
var slice = toShow.length > 200;
var unsafe = toShow;
toShow = Util.fixHTML(toShow);
content.getFormatText = function () {
@ -510,7 +511,7 @@ define([
content.handler = function () {
var content = h('div', [
h('h4', Messages.broadcast_newCustom),
h('div.cp-admin-message', toShow)
h('div.cp-admin-message', unsafe) // Use unsafe string, hyperscript is safe
]);
UI.alert(content);
};

View file

@ -98,7 +98,7 @@ define(['/api/config'], function (ApiConfig) {
if(!document.getElementById("favicon-ico")) {
var faviconLink = document.createElement('link');
attrs.href = attrs.href.replaceAll(".png", ".ico");
attrs.href = attrs.href.replace(/\.png/g, ".ico");
attrs.id = 'favicon-ico';
attrs.type = 'image/x-icon';

View file

@ -861,7 +861,6 @@ define([
// Owned drive
if (metadata && metadata.owners && metadata.owners.length === 1 &&
metadata.owners.indexOf(edPublic) !== -1) {
var token;
nThen(function (waitFor) {
Block.checkRights({
auth: auth,
@ -876,8 +875,7 @@ define([
}).nThen(function (waitFor) {
self.accountDeletion = clientId;
// Log out from other workers
var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
store.proxy[Constants.tokenKey] = token;
store.proxy[Constants.tokenKey] = 'DELETED';
onSync(null, waitFor());
}).nThen(function (waitFor) {
// Delete Pin Store
@ -896,6 +894,7 @@ define([
Block.removeLoginBlock({
reason: 'ARCHIVE_OWNED',
auth: auth,
edPublic: edPublic,
blockKeys: blockKeys,
}, waitFor(function (err) {
if (err) { console.error(err); }
@ -904,7 +903,8 @@ define([
removeOwnedPads(true, waitFor);
}).nThen(function () {
// Log out current worker
postMessage(clientId, "DELETE_ACCOUNT", token, function () {});
broadcast([clientId], "DRIVE_DELETED", 'ARCHIVE_OWNED');
postMessage(clientId, "DELETE_ACCOUNT", 'DELETED', function () {});
store.network.disconnect();
cb({
state: true
@ -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
@ -2399,6 +2400,8 @@ define([
Store.loadSharedFolder = function (teamId, id, data, cb, isNew) {
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
var parsed = Hash.parsePadUrl(data.href || data.roHref);
if (!parsed && !parsed.hashData) { return void cb({error: 'EINVAL'}); }
SF.load({
isNew: isNew,
network: store.network || store.networkPromise,
@ -2926,6 +2929,7 @@ define([
broadcast([], "UPDATE_METADATA");
});
proxy.on('change', [Constants.tokenKey], function () {
if (store.isDeleted || proxy[Constants.tokenKey] === 'DELETED') { return; }
broadcast([], "UPDATE_TOKEN", { token: proxy[Constants.tokenKey] });
});
@ -3061,9 +3065,8 @@ define([
.on('error', function (info) {
if (info.error && info.error === 'EDELETED') {
if (store.ownDeletion) { return; }
broadcast([], "LOGOUT", {
reason: info.message
});
store.isDeleted = true;
broadcast([], "DRIVE_DELETED", info.message);
}
});

View file

@ -177,7 +177,7 @@ define([
}, cb);
};
Block.writeLoginBlock = function (data, cb) {
const { content, blockKeys, oldBlockKeys, auth, pw, session } = data;
const { content, blockKeys, oldBlockKeys, auth, pw, session, token, userData } = data;
var command = 'WRITE_BLOCK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
@ -186,6 +186,8 @@ define([
block.auth = auth && auth.data;
block.hasPassword = pw;
block.registrationProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
if (token) { block.inviteToken = token; }
if (userData) { block.userData = userData; }
ServerCommand(blockKeys.sign, {
command: command,
@ -194,7 +196,7 @@ define([
}, cb);
};
Block.removeLoginBlock = function (data, cb) {
const { reason, blockKeys, auth } = data;
const { reason, blockKeys, auth, edPublic } = data;
var command = 'REMOVE_BLOCK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
@ -202,6 +204,7 @@ define([
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data,
edPublic: edPublic,
reason: reason
}, cb);
};

View file

@ -347,6 +347,26 @@ define([
cb(false);
};
handlers['ADD_TO_ACCESS_LIST'] = function(ctx, common, data, cb) {
var msg = data.msg;
var content = msg.content;
var channel = content.channel;
ctx.Store.getAllStores().forEach(function (store) {
var res = store.manager.findChannel(channel);
if (!res.length) { return; }
var data = res[0].data;
var id = res[0].id;
var teamId = store.id;
ctx.Store.loadSharedFolder(teamId, id, data, function () {
}, false);
});
cb(true);
};
// Hide duplicates when receiving an ADD_OWNER notification:
var addOwners = {};
handlers['ADD_OWNER'] = function (ctx, box, data, cb) {

View file

@ -104,7 +104,7 @@ var init = function (client, cb) {
cfg.broadcast = function (excludes, cmd, data, cb) {
cb = cb || function () {};
Object.keys(self.tabs).forEach(function (cId) {
if (excludes.indexOf(cId) !== -1) { return; }
if (excludes.indexOf(+cId) !== -1) { return; }
self.tabs[cId].chan.query(cmd, data, function (err, data2) {
if (err) { return void cb({error: err}); }
cb(data2);

View file

@ -130,7 +130,6 @@ define([
var ext = s.pop() || 'bin';
var name = s.join('');
var replacement = '';
console.error(name);
var sanitized = name
.replace(illegalRe, replacement)
.replace(controlRe, replacement)

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

@ -88,6 +88,7 @@ define([
var state = STATE.DISCONNECTED;
var firstConnection = true;
var integration;
let integrationChannel;
var toolbarContainer = options.toolbarContainer ||
(function () { throw new Error("toolbarContainer must be specified"); }());
@ -623,12 +624,12 @@ define([
if (privateDat.integration) {
common.openIntegrationChannel(onLocal);
var sframeChan = common.getSframeChannel();
integrationChannel = common.getSframeChannel();
var integrationSave = function (cb) {
var ext = privateDat.integrationConfig.fileType;
var upload = Util.once(function (_blob) {
sframeChan.query('Q_INTEGRATION_SAVE', {
integrationChannel.query('Q_INTEGRATION_SAVE', {
blob: _blob
}, cb, {
raw: true
@ -645,7 +646,7 @@ define([
}
};
const integrationHasUnsavedChanges = function(unsavedChanges, cb) {
sframeChan.query('Q_INTEGRATION_HAS_UNSAVED_CHANGES', unsavedChanges, cb);
integrationChannel.query('Q_INTEGRATION_HAS_UNSAVED_CHANGES', unsavedChanges, cb);
};
var inte = common.createIntegration(onLocal, cpNfInner.chainpad,
integrationSave, integrationHasUnsavedChanges);
@ -656,7 +657,7 @@ define([
});
}
if (firstConnection) {
sframeChan.on('Q_INTEGRATION_NEEDSAVE', function (data, cb) {
integrationChannel.on('Q_INTEGRATION_NEEDSAVE', function (data, cb) {
integrationSave(function (obj) {
if (obj && obj.error) { console.error(obj.error); }
cb();
@ -1124,9 +1125,18 @@ define([
// Call this after all of the handlers are setup.
start: evStart.fire,
// Call this, when the user wants to add an image from drive.
insertImage: function(data, cb) {
require(['/common/inner/image-dialog.js'], function(imageDialog) {
imageDialog.openImageDialog(common, integrationChannel, data, cb);
});
},
// Determine the internal state of the framework.
getState: function () { return state; },
isIntegrated: function() { return cpNfInner.metadataMgr.getPrivateData().integration; },
// Internals
_: {
sfCommon: common,

View file

@ -96,6 +96,7 @@ define([
var data = {};
data.name = file.metadata.name;
data.fileType = file.metadata.type;
data.url = href;
data.password = file.password;
if (file.metadata.type.slice(0,6) === 'image/') {

View file

@ -136,6 +136,7 @@ define([
}, dismissIcon);
$(dismiss).addClass("cp-clickable")
.on('click keypress', function (event) {
event.stopPropagation();
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
data.content.dismissHandler();
}

View file

@ -2081,6 +2081,11 @@ define([
cfg.integrationUtils.onHasUnsavedChanges(obj, cb);
}
});
sframeChan.on('Q_INTEGRATION_ON_INSERT_IMAGE', function (data, cb) {
if (cfg.integrationUtils && cfg.integrationUtils.onInsertImage) {
cfg.integrationUtils.onInsertImage(data, cb);
}
});
integrationSave = function (cb) {
sframeChan.query('Q_INTEGRATION_NEEDSAVE', null, cb);
};

View file

@ -1012,6 +1012,7 @@ define([
} catch (e) {}
ctx.sframeChan.on('EV_LOGOUT', function () {
if (window.CP_ownAccountDeletion) { return; }
$(window).on('keyup', function (e) {
if (e.keyCode === 27) {
UI.removeLoadingScreen();

View file

@ -314,5 +314,10 @@
"settings_backupHint": "Архивирайте или възстановете цялото си съдържание на CryptDrive. Той няма да има съдържанието на вашите документи, а само ключовете за достъп до тях.",
"login_hashing": "Хеширането на вашата парола, може да отнеме известно време.",
"settings_restore": "Възстановяване",
"settings_backupHint2": "Изтеглете всички документи във вашето устройство. Документите ще бъдат изтеглени във формати, които могат да се четат от други приложения, когато такъв формат е наличен. Когато такъв формат не е наличен, документите ще бъдат изтеглени във формат, който може да се чете от CryptPad."
"settings_backupHint2": "Изтеглете всички документи във вашето устройство. Документите ще бъдат изтеглени във формати, които могат да се четат от други приложения, когато такъв формат е наличен. Когато такъв формат не е наличен, документите ще бъдат изтеглени във формат, който може да се чете от CryptPad.",
"fm_padIsOwned": "Вие сте собственик на този документ",
"fm_padIsOwnedOther": "Този документ е собственост на друг потребител",
"fm_tags_name": "Име на етикет",
"fm_burnThisDrive": "Сигурни ли сте, че искате да премахнете всичко, съхранено от CryptPad във вашия браузър?<br>Това ще премахне вашия CryptDrive и неговата история от вашия браузър, но вашите документи ще продължат да съществуват (криптирани) на нашия сървър.",
"fm_deletedPads": "Тези документи вече не съществуват на сървъра, те са премахнати от вашия CryptDrive: {0}"
}

View file

@ -381,7 +381,7 @@
"settings_import": "Importieren",
"settings_importConfirm": "Bist du sicher, dass du die kürzlich besuchten Dokumente in das CryptDrive deines Accounts importieren möchtest?",
"settings_importDone": "Import abgeschlossen",
"settings_autostoreTitle": "Speichern von Pads im CryptDrive",
"settings_autostoreTitle": "Speicherung von Dokumenten im CryptDrive",
"settings_autostoreHint": "<b>Automatisch:</b> Alle Dokumente werden in deinem CryptDrive gespeichert.<br><b>Manuell (immer nachfragen):</b> Wenn du ein Dokument noch nicht gespeichert hast, wirst du gefragt, ob du es im CryptDrive speichern willst.<br><b>Manuell (nie nachfragen):</b> Dokumente werden nicht automatisch im CryptDrive gespeichert. Die Option zum Speichern wird nicht mehr angezeigt.",
"settings_autostoreYes": "Automatisch",
"settings_autostoreNo": "Manuell (nie nachfragen)",
@ -1681,5 +1681,24 @@
"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"
"admin_forcemfaTitle": "Verpflichtende Zwei-Faktor-Authentifizierung",
"admin_invitationEmail": "E-Mail-Adresse",
"admin_cat_users": "Benutzerverzeichnis",
"admin_invitationCreate": "Einladungslink erstellen",
"admin_registrationSsoTitle": "SSO-Registrierung schließen",
"admin_invitationTitle": "Einladungslinks",
"admin_invitationLink": "Einladungslink",
"admin_invitationDeleteConfirm": "Bist du sicher, dass du diese Einladung löschen möchtest?",
"admin_invitationHint": "Mit Einladungslinks kann jeweils ein Account erstellt werden, auch wenn die Registrierung geschlossen ist. Benutzername und E-Mail-Adresse helfen dir bei der Zuordnung. CryptPad versendet den Einladungslink (oder irgendetwas anderes) nicht per E-Mail. Bitte kopiere den Link und sende ihn über einen sicheren Kanal deiner Wahl.",
"admin_invitationAlias": "Benutzername",
"admin_usersHint": "Liste der bekannten Accounts auf dieser Instanz. Wähle unten aus, dass Accounts automatisch hinzugefügt werden sollen, oder gib die Informationen manuell über das Formular ein.",
"admin_usersRemove": "Entfernen",
"admin_usersAdd": "Bekannten Benutzer hinzufügen",
"admin_usersRemoveConfirm": "Bist du sicher, dass du diesen Benutzer aus dem Verzeichnis entfernen möchtest? Er kann weiterhin auf sein Konto zugreifen und es nutzen.",
"admin_usersTitle": "Benutzerverzeichnis",
"admin_storeInvitedLabel": "Eingeladene Benutzer automatisch speichern",
"admin_storeSsoLabel": "SSO-Benutzer automatisch speichern",
"admin_invitationCopy": "Link kopieren",
"register_invalidToken": "Der Einladungslink ist ungültig",
"admin_usersBlock": "Login-Block-URL des Benutzers (optional)"
}

View file

@ -1663,5 +1663,22 @@
"dph_account_destroyed": "Kontu hau bere jabeak ezabatu du",
"dph_pad_destroyed": "Dokumentu hau jabe batek suntsitu du",
"dph_pad_inactive": "Dokumentu hau ezabatu egin da jarduerarik ez zegoelako",
"dph_tmp_moderated_account": "Txantiloi hau etendako kontu batena izateagatik ezabatu da"
"dph_tmp_moderated_account": "Txantiloi hau etendako kontu batena izateagatik ezabatu da",
"admin_diskUsageWarning": "Erabili kontuz! Instantzian gordetako datuen tamainaren arabera, txosten hau sortzeak zerbitzarian dagoen memoria guztia kontsumitu eta hutsegite bat eragin dezake.",
"calendar_rec_change": "Errepikapen bat beste egutegi batera ere mugitzea. Aldaketa hau gertaera honetan edo errepikatzen diren gertaera guztietan soilik aplika dezakezu.",
"access_passwordUsed": "Pasahitz hau dagoeneko erabili da dokumentu honetarako. Ezin da berriro erabili.",
"calendar_rec_change_first": "Lehenengo errepikapena beste egutegi batera ere mugitzea. Errepikatzen diren gertaera guztiak ere mugituko dira.",
"admin_forcemfaHint": "Instantzia honetako erabiltzaile guztiei bi faktoreko autentifikazioa konfiguratzeko eskatuko zaie beren kontuan saioa hasteko.",
"ssoauth_form_hint_login": "Sartu zure CryptPad pasahitza",
"status": "Egoera orria",
"calendar_desc": "Deskribapena",
"calendar_description": "Deskribapena:{0}{1}",
"sso_login_description": "Hasi saioa honekin",
"sso_register_description": "Izena eman honekin",
"ssoauth_header": "CryptPad pasahitza",
"ssoauth_form_hint_register": "Gehitu CryptPad pasahitz bat segurtasun gehigarrirako edo utzi hutsik eta jarraitu. Pasahitzik gehitzen ez baduzu, zure datuak babesten dituzten gakoak instantzia-administratzaileen eskura egongo dira.",
"duplicate": "Bikoiztu",
"kanban_showTags": "Ikusi etiketa guztiak",
"kanban_hideTags": "Ikusi etiketa gutxiago",
"admin_forcemfaTitle": "Derrigorrezko bi faktoreko autentifikazioa"
}

View file

@ -388,7 +388,7 @@
"settings_import": "Importer",
"settings_importConfirm": "Êtes-vous sûr·e de vouloir importer les documents récents de ce navigateur dans le CryptDrive de votre compte utilisateur·e ?",
"settings_importDone": "Importation terminée",
"settings_autostoreTitle": "Stockage des pads dans CryptDrive",
"settings_autostoreTitle": "Stockage des documents dans CryptDrive",
"settings_autostoreHint": "Le stockage <b>Automatique</b> des pads permet de sauver tous les documents que vous visitez dans votre CryptDrive, sans action de votre part.<br>Le stockage <b>Manuel (toujours demander)</b> permet de ne pas stocker automatiquement les documents, mais d'afficher un message vous demandant s'il faut le faire ou non.<br>Le stockage <b>Manuel (ne pas demander)</b> permet de ne pas stocker les documents ni d'afficher le message. Une option permettant de les stocker sera toujours disponible, mais cachée.",
"settings_autostoreYes": "Automatique",
"settings_autostoreNo": "Manuel (ne pas demander)",
@ -1681,5 +1681,24 @@
"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."
"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.",
"admin_cat_users": "Registre",
"admin_invitationCreate": "Créer un lien d'invitation",
"admin_invitationTitle": "Liens d'invitation",
"admin_invitationLink": "Lien d'invitation",
"admin_invitationDeleteConfirm": "Êtes-vous sûr de vouloir supprimer cette invitation ?",
"admin_invitationAlias": "Nom d'utilisateur·ice",
"admin_usersRemove": "Supprimer",
"admin_usersAdd": "Ajouter un·e utilisateur·ice connu·e",
"admin_usersTitle": "Registre utilisateur·ices",
"admin_usersHint": "Liste des comptes connus sur cette instance. Sélectionnez les options ci-dessous pour ajouter des comptes automatiquement, ou entrez les informations manuellement dans le formulaire.",
"admin_storeInvitedLabel": "Stocker automatiquement les utilisateur·ices invité·es",
"admin_storeSsoLabel": "Stocker automatiquement les utilisateur·ices SSO",
"admin_usersBlock": "Lien du block utilisateur·ice (optionnel)",
"admin_usersRemoveConfirm": "Êtes-vous sûr de vouloir supprimer cet·te utilisateur·ice de la liste ? Iels seront toujours à même d'accéder et d'utiliser leur compte.",
"register_invalidToken": "Le lien d'invitation est invalide",
"admin_invitationHint": "Chaque lien d'invitation peut être utilisé pour créer un compte, même si les inscriptions sont fermées. Les champs utilisateur·ice et e-mail servent uniquement à vous permettre d'identifier les comptes. CryptPad n'enverra pas le lien d'invitation (ou quoique ce soit d'autre) par e-mail, veuillez copier le lien et l'envoyer en utilisant le mode de communication sécurisé de votre choix.",
"admin_invitationCopy": "Copier le lien",
"admin_registrationSsoTitle": "Fermer l'inscription avec SSO",
"admin_invitationEmail": "E-mail utilisateur·ice"
}

View file

@ -491,7 +491,7 @@
"settings_thumbnails": "Gambar kecil",
"settings_resetThumbnailsAction": "Bersihkan",
"settings_importDone": "Pengimporan selesai",
"settings_autostoreTitle": "Penyimpanan catatan di CryptDrive",
"settings_autostoreTitle": "Penyimpanan dokumen di CryptDrive",
"settings_userFeedbackTitle": "Masukan",
"settings_logoutEverywhereTitle": "Tutup sesi jarak jauh",
"settings_driveDuplicateTitle": "Duplikat dokumen yang dimiliki",
@ -1601,7 +1601,7 @@
"dph_pad_inactive": "Dokumen ini dihapus karena ketidakaktifan",
"dph_pad_moderated": "Dokumen ini dihapus oleh tim moderasi",
"dph_pad_moderated_account": "Dokumen ini dihapus karena dimiliki oleh akun yang dihentikan",
"dph_pad_pw": "Dokumen ini dilindungi oleh sebuah kata sandi baru. Masukkan kata sandinya untuk.mengakses konten",
"dph_pad_pw": "Dokumen ini dilindungi oleh sebuah kata sandi baru",
"dph_tmp_destroyed": "Templat ini dihancurkan oleh seorang pemilik",
"dph_tmp_moderated": "Templat ini dihapus oleh tim moderasi",
"dph_tmp_moderated_account": "Templat ini dihapus karena dimiliki oleh akun yang dihentikan",
@ -1666,5 +1666,39 @@
"drive_sfPassword": "Folder {0} Anda yang terbagi tidak lagi tersedia. Mungkin sudah dihapus oleh pemiliknya atau sekarang dilindungi dengan kata sandi baru. Anda dapat menghapus folder ini dari CryptDrive Anda, atau memulihkan akses menggunakan kata sandi yang baru.",
"admin_broadcastHint": "Kirimkan pesan ke semua pengguna di server ini. Semua pengguna yang sudah ada dan pengguna baru akan menerimanya sebagai notifikasi. Tampilkan pesan sebelum dikirimkan dengan \"Tampilkan notifikasi\". Notifikasi yang ditampilkan memiliki ikon merah dan hanya tersedia untuk Anda.",
"support_warning_abuse": "Mohon laporkan konten yang melanggar <a>Ketentuan Layanan</a>. Mohon sediakan tautan ke dokumen atau profil pengguna yang melanggar dan jelaskan bagaimana mereka melanggar ketentuannya. Informasi tambahan dalam konteks yang Anda mengetahui konten atau perilaku dapat membantu administrator mencegah pelanggaran di masa depan",
"safeLinks_error": "Tautan ini disalin dari bilah alamat peramban dan tidak menyediakan akses ke dokumen. Silakan gunakan menu <i></i> <b>Bagikan</b> untuk membagikan ke kontak secara langsung atau salin tautannya. <a>Baca lebih lanjut tentang fitur Tautan Aman</a>."
"safeLinks_error": "Tautan ini disalin dari bilah alamat peramban dan tidak menyediakan akses ke dokumen. Silakan gunakan menu <i></i> <b>Bagikan</b> untuk membagikan ke kontak secara langsung atau salin tautannya. <a>Baca lebih lanjut tentang fitur Tautan Aman</a>.",
"admin_cat_users": "Direktori Pengguna",
"admin_invitationCreate": "Buat tautan undangan",
"admin_registrationSsoTitle": "Tutup pendaftaran SSO",
"admin_invitationTitle": "Tautan undangan",
"admin_invitationLink": "Tautan undangan",
"admin_invitationDeleteConfirm": "Apakah Anda yakin ingin menghapus undangan ini?",
"admin_usersAdd": "Tambahkan pengguna yang diketahui",
"admin_usersTitle": "Direktori Pengguna",
"admin_usersHint": "Daftar akun yang diketahui di server ini. Pilih di bawah untuk menambahkan akun secara otomatis, atau masukkan informasi secara manual menggunakan formulir.",
"admin_storeSsoLabel": "Simpan pengguna SSO secara otomatis",
"admin_usersRemove": "Hapus",
"admin_invitationEmail": "Surel pengguna",
"admin_storeInvitedLabel": "Simpan pengguna yang diundang secara otomatis",
"admin_forcemfaHint": "Semua pengguna di server ini akan diminta untuk menyiapkan autentikasi dua faktor untuk masuk ke akun mereka.",
"admin_invitationHint": "Setiap tautan undangan membuat satu akun, bahkan jika pendaftaran ditutup. Nama pengguna dan surel hanya untuk keperluan identifikasi Anda. CryptPad tidak akan mengirim surel berisi tautan undangan (atau yang lain), mohon salin tautan dan kirimkan melalui saluran aman pilihan Anda.",
"calendar_rec_change_first": "Memindahkan pengulangan pertama ke kalender lain. Semua kejadian yang berulang juga akan dipindahkan.",
"admin_invitationCopy": "Salin tautan",
"register_invalidToken": "Tautan undangan ini tidak valid",
"ssoauth_form_hint_register": "Tambahkan kata sandi CryptPad untuk keamanan tambahan atau tinggalkan kosong dan lanjutkan. Jika Anda tidak menambahkan kata sandi, kunci yang melindungi data Anda akan tersedia bagi administrator server.",
"admin_invitationAlias": "Nama pengguna",
"admin_usersBlock": "Blok URL masuk pengguna (opsional)",
"admin_usersRemoveConfirm": "Apakah Anda yakin ingin menghapus pengguna ini dari direktori? Mereka masih akan dapat mengakses dan menggunakan akunnya.",
"ssoauth_form_hint_login": "Silakan masukkan kata sandi CryptPad Anda",
"calendar_desc": "Deskripsi",
"calendar_description": "Deskripsi:{0}{1}",
"duplicate": "Duplikat",
"sso_login_description": "Masuk dengan",
"sso_register_description": "Daftar dengan",
"ssoauth_header": "Kata Sandi CryptPad",
"kanban_showTags": "Lihat semua tag",
"kanban_hideTags": "Lihat sedikit tag",
"admin_forcemfaTitle": "Autentikasi Dua Faktor Wajib",
"loading_mfa_required": "Autentikasi dua faktor diperlukan di server ini. Silakan perbarui akun Anda menggunakan aplikasi autentikator dan formulir di bawah.",
"calendar_rec_change": "Memindahkan genap berulang ke kalender lain. Anda hanya dapat menerapkan perubahan ini pada peristiwa ini atau semua peristiwa yang berulang."
}

View file

@ -396,7 +396,7 @@
"settings_import": "Import",
"settings_importConfirm": "Are you sure you want to import recent documents from this browser to your user account's CryptDrive?",
"settings_importDone": "Import completed",
"settings_autostoreTitle": "Pad storage in CryptDrive",
"settings_autostoreTitle": "Document storage in CryptDrive",
"settings_autostoreHint": "<b>Automatic</b> All the documents you visit are stored in your CryptDrive.<br><b>Manual (always ask)</b> If you have not stored a document yet, you will be asked if you want to store them in your CryptDrive.<br><b>Manual (never ask)</b> Documents are not stored automatically in your CryptDrive. The option to store them will be hidden.",
"settings_autostoreYes": "Automatic",
"settings_autostoreNo": "Manual (never ask)",
@ -1681,5 +1681,24 @@
"kanban_hideTags": "See less tags",
"admin_forcemfaTitle": "Mandatory Two-Factor Authentication",
"admin_forcemfaHint": "All users on this instance will be asked to set up two-factor authentication to log in to their account.",
"loading_mfa_required": "Two-factor authentication is required on this instance. Please update your account using an authenticator app and the form below."
"loading_mfa_required": "Two-factor authentication is required on this instance. Please update your account using an authenticator app and the form below.",
"admin_cat_users": "User Directory",
"admin_registrationSsoTitle": "Close SSO registration",
"admin_invitationTitle": "Invitation links",
"admin_invitationCreate": "Create invitation link",
"admin_invitationHint": "Invitation links create one account each, even if registration is closed. User name and email are for your identification purposes only. CryptPad will not email the invitation link (or anything else), please copy the link and send it using the secure channel of your choice.",
"admin_invitationLink": "Invitation link",
"admin_invitationCopy": "Copy link",
"admin_invitationAlias": "User name",
"admin_invitationEmail": "User email",
"admin_invitationDeleteConfirm": "Are you sure you want to delete this invitation?",
"admin_usersTitle": "User Directory",
"admin_usersHint": "List of known accounts on this instance. Select below to add accounts automatically, or enter information manually using the form.",
"admin_usersAdd": "Add known user",
"admin_storeInvitedLabel": "Automatically store invited users",
"admin_storeSsoLabel": "Automatically store SSO users",
"admin_usersBlock": "User's login block URL (optional)",
"admin_usersRemove": "Remove",
"admin_usersRemoveConfirm": "Are you sure you want to remove this user from the directory? They will still be able to access and use their account.",
"register_invalidToken": "The invitation link is invalid"
}

View file

@ -16,11 +16,12 @@
"todo": "Do wykonania",
"media": "Media",
"whiteboard": "Tablica",
"drive": "CryptDrive"
"drive": "CryptDrive",
"diagram": "Diagram"
},
"disconnected": "Rozłączony",
"synchronizing": "Synchronizacja",
"reconnecting": "Wznawianie połączenia...",
"reconnecting": "Wznawianie połączenia",
"readonly": "Tylko do odczytu",
"anonymous": "Gość",
"users": "Użytkownicy",
@ -48,7 +49,7 @@
"poll_userPlaceholder": "Twoje imię",
"poll_removeOption": "Jesteś pewien, że chcesz usunąć tę opcję?",
"poll_removeUser": "Jesteś pewien, że chcesz usunąć tego użytkownika?",
"poll_descriptionHint": "Opis",
"poll_descriptionHint": "Opisz swoją ankietę i użyj przycisku ✓ (opublikuj), gdy skończysz.\nOpis może być napisany przy użyciu składni markdown i można w nim osadzać elementy multimedialne z CryptDrive.\nKażdy, kto ma link, może zmienić opis, ale jest to odradzane.",
"header_logoTitle": "Przejdź na stronę główną",
"storageStatus": "Pamięć:<br>Wykorzystałeś <b>{0}</b> z <b>{1}</b>",
"upgradeAccount": "Ulepsz swoje konto",
@ -74,7 +75,7 @@
"chainpadError": "Podczas ładowania zawartości, wystąpił krytyczny błąd. Dostępny jest wyłącznie odczyt, aby dać szansę na odzyskanie dokumentu.<br>Wciśnij klawisz Esc aby przejrzeć dokument lub odśwież stronę aby spróbować włączyć tryb edycji.",
"inactiveError": "Ten dokument został usunięty z powodu braku aktywności. Wciśnij klawisz Esc, aby stworzyć nowy dokument.",
"deletedError": "Ten dokument został usunięty.",
"expiredError": "Ten dokument wygasł i nie jest już dostępny.",
"expiredError": "Ten dokument został zniszczony i nie jest już dostępny.",
"anonymousStoreDisabled": "Administrator wyłączył zapisywanie danych dla niezalogowanych użytkowników. Musisz się zalogować aby otrzymać dostęp do aplikacji.",
"padNotPinnedVariable": "Dokument zostanie usunięty po {4} dniach braku aktywności, {0}zaloguj się{1} lub {2}zarejestruj{3} aby go zachować.",
"padNotPinned": "Ten dokument zostanie usunięty po 3 miesiącach braku aktywności, {0}zaloguj się{1} lub {2}zarejestruj{3} aby go zachować.",
@ -110,10 +111,10 @@
"pinLimitReachedAlertNoAccounts": "Osiągnięto limit przestrzeni dyskowej",
"pinLimitReachedAlert": "Osiągnięto limit przestrzeni dyskowej. Nowe dokumenty nie będą przechowywane w Twoim CryptDrive.<br>Możesz usunąć dokumenty z Twojego CryptDrive'a lub <a>skorzystać z oferty premium</a>.",
"pinLimitReached": "Osiągnięto limit przestrzeni dyskowej",
"formattedKB": "{0} KB",
"formattedKB": "{0} kB",
"formattedGB": "{0} GB",
"formattedMB": "{0} MB",
"KB": "KB",
"KB": "kB",
"GB": "GB",
"MB": "MB",
"fm_viewListButton": "Widok listy",
@ -376,7 +377,7 @@
"login_unhandledError": "Wystąpił nieoczekiwany błąd :(",
"login_invalPass": "Wymagane hasło",
"login_invalUser": "Wymagana nazwa użytkownika",
"login_noSuchUser": "Nieprawidłowa nazwa użytkownika lub hasło. Spróbuj ponownie lub zarejestruj się",
"login_noSuchUser": "Nieprawidłowa nazwa użytkownika lub hasło",
"login_hashing": "Hashowanie hasła, to może zająć trochę czasu.",
"login_confirm": "Potwierdź swoje hasło",
"login_password": "Hasło",
@ -576,7 +577,7 @@
"password_info": "Dokument, który próbujesz otworzyć, już nie istnieje lub jest chroniony nowym hasłem. Wprowadź prawidłowe hasło, aby uzyskać dostęp do zawartości.",
"creation_newPadModalDescription": "Kliknij na aplikację, aby utworzyć nowy dokument. Możesz również nacisnąć<b>Tab</b>, aby wybrać aplikację i nacisnąć <b>Enter</b>, aby potwierdzić.",
"creation_passwordValue": "Hasło",
"creation_expiration": "Data wygaśnięcia",
"creation_expiration": "Data zniszczenia",
"creation_noOwner": "Brak właściciela",
"creation_owners": "Właściciele",
"creation_create": "Utwórz",
@ -596,7 +597,7 @@
"feedback_about": "Jeśli to czytasz, prawdopodobnie byłeś ciekaw, dlaczego CryptPad żąda stron internetowych, gdy wykonujesz pewne czynności.",
"view": "pokaż",
"edit": "edytuj",
"help_genericMore": "Dowiedz się więcej o tym, co CryptPad może zrobić dla Ciebie, czytając naszą <a>Dokumentację</a>.",
"help_genericMore": "Dowiedz się więcej o tym, co CryptPad może zrobić dla Ciebie, czytając naszą <a>Dokumentację</a>",
"header_homeTitle": "Przejdź do strony głównej CryptPad",
"four04_pageNotFound": "Nie mogliśmy znaleźć strony, której szukałeś.",
"features_f_subscribe_note": "Zarejestrowane konto jest wymagane do subskrypcji",
@ -746,7 +747,7 @@
"form_responseMsg": "Ta wiadomość zostanie wyświetlona po wysłaniu formularza przez uczestników.",
"form_addMsg": "Dodaj wiadomość o wysłaniu",
"toolbar_preview": "Podgląd",
"form_geturl": "Skopiuj link",
"form_geturl": "Skopiuj publiczny link",
"form_changeTypeConfirm": "Wybierz rodzaj nowego pytania.",
"form_corruptAnswers": "Ten formularz zawiera już odpowiedzi. Zmiana tego rodzaju pytania może unieważnić poprzednie dane odpowiedzi.",
"form_preview": "Zobacz ten formularz",
@ -983,12 +984,12 @@
"settings_cacheTitle": "Pamięć podręczna",
"docs_link": "Dokumentacja",
"creation_helperText": "Otwórz w dokumentacji",
"creation_expiresIn": "Traci ważność w",
"creation_expiresIn": "Zniszcz w",
"register_warning_note": "Ze względu na szyfrowany charakter CryptPada, administratorzy serwisu nie będą w stanie odzyskać danych w przypadku, gdy zapomnisz swoją nazwę użytkownika i/lub hasło. Prosimy o zapisanie ich w bezpiecznym miejscu.",
"register_notes": "<ul class=\"cp-notes-list\"><li>Twoje hasło jest prywatnym kluczem, który szyfruje wszystkie Twoje dokumenty. <span class=\"red\">W przypadku jego utraty nie ma możliwości odzyskania danych.</span></li><li>Jeśli korzystasz z udostępnionego komputera, <span class=\"red\">pamiętaj o wylogowaniu</span> po zakończeniu pracy. Tylko zamknięcie okna przeglądarki powoduje, że Twoje konto nadal pozostaje narażone. </li><li>Aby zachować dokumenty, które utworzyłeś i/lub zapisałeś bez logowania, zaznacz \"Importuj dokumenty z sesji gościa\". </li></ul>",
"register_notes_title": "Ważne uwagi",
"admin_getlimitsHint": "Lista wszystkich własnych limitów przestrzeni do przechowywania danych zastosowanych do Twojej instalacji.",
"info_imprintFlavour": "<a>Informacje prawne o administratorach tej instalacji</a>.",
"info_imprintFlavour": "<a>Informacje prawne o administratorach tej instancji</a>",
"offlineError": "Nie można zsynchronizować najnowszych danych, ta strona nie może zostać wyświetlona w tej chwili. Ładowanie będzie kontynuowane po przywróceniu połączenia z serwisem.",
"share_noContactsOffline": "Jesteś obecnie w trybie offline. Kontakty nie są dostępne.",
"access_offline": "Jesteś obecnie w trybie offline. Zarządzanie dostępem nie jest dostępne.",
@ -1236,7 +1237,7 @@
"safeLinks_error": "Ten link został skopiowany z paska adresu przeglądarki i nie zapewnia dostępu do dokumentu. Proszę skorzystać z menu <i></i> <b>Udostępnij</b> , aby udostępniać bezpośrednio kontaktom lub skopiować link. <a> Przeczytaj więcej o funkcji bezpiecznych linków</a>.",
"settings_safeLinksCheckbox": "Włącz bezpieczne linki",
"settings_safeLinksTitle": "Bezpieczne linki",
"settings_cat_security": "Poufność",
"settings_cat_security": "Bezpieczeństwo & Ochrona prywatności",
"imprint": "Informacje prawne",
"oo_sheetMigration_anonymousEditor": "Ten dokument wymaga aktualizacji. Edycja jest zablokowana dla gości do momentu otwarcia go przez zarejestrowanego użytkownika.",
"oo_sheetMigration_complete": "Dostępna jest zaktualizowana wersja, naciśnij OK, aby załadować ponownie.",
@ -1265,7 +1266,7 @@
"team_inviteLinkCopy": "Skopiuj link",
"team_inviteLinkCreate": "Utwórz link",
"team_inviteLinkErrorName": "Proszę dodać nazwę dla osoby, którą zapraszasz. Osoba ta może je później zmienić. ",
"team_inviteLinkWarning": "Pierwsza osoba, która wejdzie na ten link, będzie mogła dołączyć do zespołu i zobaczyć jego zawartość. Udostępniaj go ostrożnie.",
"team_inviteLinkWarning": "Osoby, które uzyskają dostęp do tego linku, będą mogły dołączyć do tego zespołu i przeglądać jego zawartość. Udostępniaj go ostrożnie.",
"team_inviteLinkLoading": "Generowanie linku",
"team_inviteLinkNoteMsg": "Ta wiadomość zostanie wyświetlona zanim odbiorca zdecyduje, czy chce dołączyć do zespołu.",
"team_inviteLinkNote": "Dodaj prywatną wiadomość",
@ -1326,7 +1327,7 @@
"team_listTitle": "Twoje zespoły",
"team_maxTeams": "Każde konto użytkownika może być członkiem tylko {0} zespołów.",
"team_infoContent": "Każdy zespół ma swój własny CryptDrive, limit pamięci, czat i listę członków. Właściciele zespołu mogą usunąć cały zespół, administratorzy mogą zapraszać lub wyrzucać członków, członkowie mogą opuścić zespół.",
"team_avatarHint": "Maksymalny rozmiar 500KB (png, jpg, jpeg, gif)",
"team_avatarHint": "Maksymalny rozmiar 500kB (png, jpg, jpeg, gif)",
"team_avatarTitle": "Awatar zespołu",
"team_nameHint": "Podaj nazwę zespołu",
"team_nameTitle": "Nazwa zespołu",
@ -1385,16 +1386,50 @@
"ui_archive": "Archiwizuj",
"ui_undefined": "nieznany",
"admin_documentType": "Typ",
"support_warning_prompt": "Proszę wybrać kategorię najbardziej pasującą do twojego problemu. To pomoże administratorom w selekcji oraz zapewni dalsze wskazówki, jakiego typu informacje są potrzebne",
"support_warning_prompt": "Wybierz najbardziej odpowiednią kategorię dla swojego zgłoszenia. Pomaga to administratorom w selekcji zgłoszeń i zapewnia dalsze sugestie dotyczące informacji, które należy podać",
"info_sourceFlavour": "<a>Kod źródłowy</a> CryptPad",
"info_termsFlavour": "<a>Warunki korzystania z usługi</a>dla tego urządzenia",
"footer_source": "Kod źródłowy",
"admin_jurisdictionHint": "Kraj, w którym są udostępniane zaszyfrowane dane tego urządzenia",
"admin_jurisdictionHint": "Kraj, w którym przechowywane są zaszyfrowane dane tego urządzenia",
"admin_jurisdictionTitle": "Lokalizacja hostingu",
"admin_descriptionHint": "Opisowy tekst wyświetlany dla tego urządzenia w postaci listy publicznych instancji na stronie cryptpad.org",
"admin_descriptionTitle": "Opis istancji",
"admin_descriptionTitle": "Opis instancji",
"ui_saved": "{0} zapisano",
"admin_nameHint": "Wyświetlana nazwa dla tego urządzenia z listy publicznych instacji na stronie cryptpad.org",
"admin_archiveNote": "Notatka",
"common_connectionLost": "<b>Utracono połączenie z serwerem</b><br>Dopóki połączenie nie wróci, włączony będzie tryb tylko do odczytu."
"common_connectionLost": "<b>Utracono połączenie z serwerem</b><br>Dopóki połączenie nie wróci, włączony będzie tryb tylko do odczytu.",
"admin_infoNotice2": "Więcej informacji można znaleźć w zakładce \"Sieć\".",
"admin_nameTitle": "Nazwa instancji",
"support_warning_abuse": "Prosimy o zgłaszanie treści, które naruszają <a>Warunki korzystania z usługi</a>. Prosimy o podanie linków do obraźliwych dokumentów lub profili użytkowników i opisanie, w jaki sposób naruszają one warunki. Wszelkie dodatkowe informacje na temat kontekstu, w którym odkryłeś treść lub zachowanie, mogą pomóc administratorom w zapobieganiu przyszłym naruszeniom",
"support_warning_bug": "Określ, w której przeglądarce występuje problem i czy zainstalowane są jakieś rozszerzenia. Podaj jak najwięcej szczegółów dotyczących problemu i kroków niezbędnych do jego odtworzenia",
"admin_reviewCheckupNotice": "Zaleca się przejrzenie <a>strony kontrolnej</a>, aby potwierdzić, że ta instancja jest poprawnie skonfigurowana.",
"admin_infoNotice1": "Użyj poniższych pól, aby opisać swoją instancję. Informacje te są wykorzystywane na stronie głównej instancji. Są one również wysyłane jako część telemetrii serwera, jeśli zdecydujesz się dołączyć do listy publicznych instancji CryptPad.",
"admin_enableDiskMeasurementsHint": "Jeśli opcja ta jest włączona, punkt końcowy interfejsu API JSON będzie dostępny pod adresem <code>/api/profiling</code>. Utrzymuje to bieżący pomiar wejścia/wyjścia dysku w oknie czasowym ustawionym poniżej. To ustawienie może mieć wpływ na wydajność serwera i może ujawnić poufne dane. Zaleca się pozostawienie tego ustawienia wyłączonego, chyba że wiesz, co robisz.",
"support_warning_account": "Pamiętaj, że administratorzy nie mogą resetować haseł. Jeśli utraciłeś poświadczenia do swojego konta, ale nadal jesteś zalogowany, możesz <a>przenieść swoje dane na nowe konto</a>",
"support_warning_drives": "Pamiętaj, że administratorzy nie są w stanie identyfikować folderów i dokumentów po nazwie. W przypadku folderów udostępnionych należy podać <a>identyfikator dokumentu</a>",
"support_warning_other": "Jaki jest charakter Twojego zapytania? Podaj jak najwięcej istotnych informacji, aby ułatwić nam szybkie rozwiązanie problemu",
"support_cat_drives": "Dysk lub zespół",
"support_cat_document": "Dokument",
"support_cat_abuse": "Zgłoś nadużycie",
"ui_openDirectly": "Ta funkcja nie jest dostępna, gdy CryptPad jest osadzony w innej witrynie. Otworzyć ten dokument w nowej karcie?",
"support_cat_debugging": "Debuguj dane",
"support_debuggingDataTitle": "Informacje dotyczące debugowania konta",
"support_debuggingDataHint": "Poniższe informacje są zawarte w przesyłanych zgłoszeniach do pomocy technicznej. Żadna z nich nie umożliwia administratorom dostępu do dokumentów użytkownika ani ich odszyfrowania. Informacje te są zaszyfrowane w taki sposób, że tylko administratorzy mogą je odczytać.",
"fivehundred_internalServerError": "Wewnętrzny błąd serwera",
"admin_cacheEvictionRequired": "Serwer został zaktualizowany o nowe ustawienia. Użyj przycisku <b>Wyczyść pamięć podręczną</b>, aby upewnić się, że ta zmiana będzie widoczna dla wszystkich użytkowników.",
"support_warning_document": "Określ typ dokumentu, który powoduje problem i podaj jego <a>identyfikator</a> lub link",
"admin_enableDiskMeasurementsTitle": "Pomiar wydajności dysku",
"admin_bytesWrittenTitle": "Okno pomiaru wydajności dysku",
"error_evalPermitted": "Przerwanie, ponieważ eval nie powinien być dozwolony.\n\nTen błąd jest związany z nagłówkami Content-Security-Policy, może być spowodowany: przestarzałą przeglądarką, która ich nie obsługuje, rozszerzeniami przeglądarki, które zakłócają ich prawidłowe działanie lub nieprawidłową konfiguracją tej instancji CryptPad.",
"admin_enableembedsHint": "Zezwól na osadzanie dokumentów i multimediów z tej instancji na innych stronach internetowych. Spowoduje to dodanie opcji \"Osadź\" do menu Udostępnij. Ze względów bezpieczeństwa aplikacje korzystające z OnlyOffice (Arkusze, Dokument, Prezentacja) nie mogą być osadzane, nawet jeśli to ustawienie jest aktywne.",
"admin_bytesWrittenHint": "Jeśli włączono pomiary wydajności dysku, czas trwania okna można skonfigurować poniżej.",
"admin_setDuration": "Ustaw czas trwania",
"ui_ms": "milisekundy",
"admin_enableembedsTitle": "Włącz osadzanie zdalne",
"error_embeddingDisabled": "Osadzanie jest wyłączone dla tej instancji CryptPad",
"error_embeddingDisabledSpecific": "Osadzanie jest wyłączone dla tej instancji CryptPad.",
"error_incorrectAccess": "Dostęp do tej strony można uzyskać tylko przez {0}.",
"ui_experimental": "Ta funkcja jest uznawana za eksperymentalną.",
"admin_noticeTitle": "Ogłoszenie na stronie głównej",
"admin_noticeHint": "Opcjonalny komunikat wyświetlany na stronie głównej"
}

View file

@ -16,11 +16,12 @@
"teams": "Команды",
"form": "Формы",
"presentation": "Презентация",
"doc": "Документ"
"doc": "Документ",
"diagram": "Диаграмма"
},
"common_connectionLost": "<b>Нет соединения с сервером</b><br>Пока оно не восстановится, вы можете только читать.",
"typeError": "Этот документ несовместим с выбранным приложением",
"onLogout": "Вы вышли из учётной записи, {0}нажмите сюда{1}, чтобы войти<br>или нажмите клавишу Esc, чтобы просто читать документ.",
"onLogout": "Вы вышли из учётной записи, {0}нажмите сюда{1}, чтобы войти<br>или нажмите клавишу Esc, чтобы просто читать Ваш документ.",
"padNotPinned": "Этот документ исчезнет через 3 месяца неактивности, {0}войдите{1} или {2}зарегистируйтесь{3} чтобы сохранить его.",
"anonymousStoreDisabled": "Администратор этого сервера CryptPad отключил хранилище для анонимных пользователей. Войдите, чтобы использовать личный CryptDrive.",
"expiredError": "Срок уничтожения этого документа наступил, так что документ больше недоступен.",
@ -58,10 +59,10 @@
"upgradeAccount": "Изменить тариф",
"MB": "МБ",
"GB": "ГБ",
"KB": "КБ",
"KB": "кБ",
"formattedMB": "{0} МБ",
"formattedGB": "{0} ГБ",
"formattedKB": "{0} КБ",
"formattedKB": "{0} кБ",
"pinLimitReached": "У вас закончилось свободное место",
"pinLimitReachedAlert": "У вас закончилось свободное место. Новые документы не будут сохраняться в вашем CryptDrive.<br>Вы можете удалить ненужные документы из вашего CryptDrive или <a>оформить премиум-подписку</a> и получить больше свободного места.",
"pinLimitReachedAlertNoAccounts": "У вас закончилось свободное место",
@ -328,7 +329,7 @@
"fm_info_sharedFolder": "Это общая папка. Вы не вошли, поэтому можете получить к ней доступ только в режиме «Только для чтения».<br><a href=\"/register/\">Зарегистрируйтесь</a> или <a href=\"/login/\">Войдите</a>, чтобы импортировать ее в ваш CryptDrive и внести изменения.",
"fo_moveFolderToChildError": "Вы не можете переместить папку в одну из нее следующую",
"fo_unavailableName": "Файл или папка с таким же именем уже существуют в новом месте. Переименуйте элемент и повторите попытку.",
"login_noSuchUser": "Неверный логин или пароль. Попробуйте еще раз или зарегистрируйтесь",
"login_noSuchUser": "Неверный логин или пароль",
"login_invalUser": "Необходимо имя пользователя",
"login_invalPass": "Необходим пароль",
"login_unhandledError": "Произошла неожиданная ошибка :(",
@ -388,7 +389,7 @@
"settings_resetThumbnailsDone": "Все миниатюры были удалены.",
"settings_import": "Импортировать",
"settings_importDone": "Импортирование завершено",
"settings_autostoreTitle": "Хранение текста в CryptDrive",
"settings_autostoreTitle": "Сохранение документов в CryptDrive",
"settings_autostoreYes": "Автоматически",
"settings_autostoreNo": "Вручную (никогда не спрашивать)",
"settings_autostoreMaybe": "Вручную (всегда спрашивать)",
@ -571,7 +572,7 @@
"settings_cacheCheckbox": "Включить кэш на этом устройстве",
"docs_link": "Документация",
"creation_helperText": "Открыть в документации",
"creation_expiresIn": "Уничтожить в",
"creation_expiresIn": "Уничтожить через",
"settings_cacheTitle": "Кэш",
"settings_cacheButton": "Очистить кэш",
"header_homeTitle": "Домашняя страница CryptPad",
@ -672,7 +673,7 @@
"broadcast_newCustom": "Сообщение от администраторов",
"broadcast_translations": "Переводы",
"imprint": "Правовая информация",
"settings_cat_security": "Конфиденциальность",
"settings_cat_security": "Безопасность и Конфиденциальность",
"support_languagesPreamble": "Служба поддержки говорит на следующих языках:",
"form_totalResponses": "Всего ответов: {0}",
"ui_expand": "Развернуть",
@ -817,7 +818,7 @@
"team_listTitle": "Ваши команды",
"team_maxTeams": "Каждая учетная запись пользователя может быть членом только {0} команд(ы).",
"team_infoContent": "У каждой команды есть собственный CryptDrive, квота хранилища, чат и список участников. Владельцы команды могут удалить всю команду, администраторы могут приглашать или выгонять участников, участники могут покинуть команду.",
"team_avatarHint": "Максимальный размер 500 КБ (png, jpg, jpeg, gif)",
"team_avatarHint": "Максимальный размер 500 кБ (png, jpg, jpeg, gif)",
"team_avatarTitle": "Аватар команды",
"team_nameHint": "Установите название команды",
"team_nameTitle": "Название команды",
@ -1582,5 +1583,122 @@
"form_showCondorcetMethod": "Метод Кондорсе",
"form_condorcetSchulze": "Schulze",
"form_condorcetRanked": "Ranked Pairs",
"form_showCondorcetWinner": "победитель: "
"form_showCondorcetWinner": "победитель: ",
"mfa_status_off": "2FA не активна на этом аккаунте",
"mfa_enable": "Включить 2FA",
"mfa_disable": "Отключить 2FA",
"settings_otp_code": "Проверочный код",
"mfa_setup_label": "Чтобы включить 2FA, пожалуйста, начните с ввода пароля вашей учетной записи",
"mfa_revoke_code": "Пожалуйста, введите свой проверочный код",
"mfa_recovery_hint": "Если вы потеряете доступ к приложению для аутентификации, ваша учетная запись CryptPad может быть заблокирована. Этот код восстановления можно использовать для отключения 2FA и возвращения вас в систему.",
"settings_removeOwnedTitle": "Уничтожение всех принадлежащих документов",
"done": "Готово",
"mfa_recovery_warning": "Этот код больше не будет отображаться. Сохраните его в безопасном месте и никому не сообщайте.",
"settings_otp_invalid": "Неверный проверочный код",
"settings_removeOwnedButton": "Уничтожить принадлежащие документы",
"settings_mfaTitle": "Двухфакторная аутентификация (2FA)",
"mfa_setup_button": "Начать настройку 2FA",
"mfa_revoke_label": "Чтобы отключить 2FA, пожалуйста, начните с ввода пароля вашей учетной записи",
"mfa_recovery_title": "Сохраните этот код восстановления прямо сейчас",
"settings_mfaHint": "Защитите свою учетную запись с помощью дополнительного кода подтверждения, предоставленного приложением-аутентификатором по вашему выбору",
"mfa_revoke_button": "Подтвердите отключение 2FA",
"continue": "Продолжить",
"mfa_status_on": "2FA активна на этом аккаунте",
"settings_otp_tuto": "Отсканируйте этот QR-код с помощью приложения для аутентификации и введите код подтверждения для подтверждения.",
"label_viewMode": "Переключить режим просмотра",
"dph_pad_destroyed": "Этот документ был уничтожен владельцем",
"dph_tmp_destroyed": "Этот шаблон был уничтожен владельцем",
"dph_tmp_moderated": "Этот шаблон был удален командой модераторов",
"dph_tmp_moderated_account": "Этот шаблон был удален по причине принадлежности к заблокированной учетной записи",
"dph_tmp_pw": "Этот шаблон защищен новым паролем. Откройте его со своего диска, чтобы ввести новый пароль.",
"sso_login_description": "Войти применив",
"settings_removeOwnedText": "Пожалуйста, подождите, пока Ваш документ уничтожается...",
"team_nameTooLong": "Название команды слишком длинное (макс. 50 символов)",
"dph_pad_moderated_account": "Этот документ был удален из-за принадлежности к заблокированной учетной записи",
"admin_diskUsageWarning": "Используйте с осторожностью! В зависимости от размера данных, хранящихся на экземпляре, создание этого отчета может занять всю доступную память на сервере и привести к сбою.",
"dph_sf_pw": "Общая папка {0} защищена новым паролем. Введите новый пароль, чтобы получить доступ к этой папке или удалить ее со своего диска.",
"admin_archiveAccountConfirm": "Пожалуйста, укажите причину архивации, это будет показано пользователю.",
"dph_account_inactive": "Этот аккаунт был удален из-за неактивности",
"ssoauth_form_hint_register": "Добавьте пароль CryptPad для дополнительной безопасности или оставьте пустым и продолжайте. Если вы не добавите пароль, ключи, защищающие ваши данные, будут доступны администраторам экземпляра.",
"settings_removeOwnedHint": "Все документы, в которых Вы являетесь единственным владельцем, будут безвозвратно уничтожены",
"admin_channelPlaceholder": "Заглушка на месте уничтоженного документа",
"admin_forcemfaHint": "Всем пользователям этого экземпляра будет предложено настроить двухфакторную аутентификацию для входа в свою учетную запись.",
"admin_totpDisable": "Отключение 2FA для этой учетной записи",
"loading_enter_otp": "Эта учетная запись защищена двухфакторной аутентификацией. Пожалуйста, введите свой проверочный код",
"calendar_rec_change": "Перенос повторяющегося события в другой календарь. Вы можете применить это изменение только к этому событию или ко всем повторам этого события.",
"recovery_forgot": "Забыли код восстановления",
"recovery_forgot_text": "Пожалуйста, скопируйте следующую информацию и отправьте её на <a href=\"mailto:{0}\">электронную почту</a> администраторам Вашего экземпляра",
"recovery_mfa_error": "Неизвестная ошибка. Пожалуйста, перезагрузите (элемент или всю страницу) и попробуйте снова.",
"recovery_mfa_disabled": "Многофакторная аутентификация уже отключена для этой учетной записи.",
"admin_totpRecoveryTitle": "Восстановление 2FA",
"admin_totpRecoveryHint": "Пользователи могут скопировать данные восстановления на странице восстановления 2FA /recovery/ и отправить их по электронной почте администраторам экземпляра. Вставьте данные для восстановления ниже, чтобы отключить 2FA для учетной записи",
"admin_totpEnabled": "2FA включена",
"admin_totpRecoveryMethod": "Метод восстановления 2FA",
"admin_totpFailed": "Ошибка проверки подписи",
"admin_totpCheck": "Проверка подписи прошла успешно",
"goLeft": "Налево",
"label_logo": "Логотип CryptPad",
"dph_pad_moderated": "Этот документ был удален командой модераторов",
"context_menu": "Действия с папками",
"team_nameAlreadySet": "Название команды уже задано как {0}",
"selectLanguage": "Выберите язык",
"dph_reason": "Причина: {0}",
"dph_sf_destroyed": "Общая папка <b>{0}</b> была уничтожена владельцем",
"dph_sf_destroyed_team": "Общая папка <b>{0}</b> на общем диске <b>{1}</b> была уничтожена владельцем",
"admin_archiveAccount": "Архивировать этот аккаунт",
"admin_archiveAccountInfo": "Включая принадлежащие ему документы",
"admin_restoreAccount": "Восстановить эту учетную запись",
"admin_accountSuspended": "Учетная запись заархивирована администратором",
"admin_accountReport": "Отчет об архиве учетной записи",
"admin_accountReportFull": "Получить подробный отчет",
"dph_default": "Это содержимое более не доступно",
"dph_account_destroyed": "Этот аккаунт был удален его владельцем",
"dph_account_moderated": "Этот аккаунт заблокирован командой модераторов",
"dph_account_pw": "Пароль для этой учетной записи был изменен",
"dph_pad_inactive": "Этот документ был удален из-за неактивности",
"recovery_mfa_secret_ph": "Код восстановления",
"login_notFilledUser": "Пожалуйста, введите имя пользователя",
"login_notFilledPass": "Пожалуйста, введите пароль",
"calendar_settings": "Настройки календаря",
"date": "Дата",
"access_passwordUsed": "Этот пароль уже использовался для этого документа. Его нельзя использовать повторно.",
"status": "Страница статуса",
"recovery_mfa_description": "Если Вы потеряли доступ к своему методу двухфакторной аутентификации, Вы можете отключить 2FA для своей учетной записи, используя код восстановления. Пожалуйста, начните с ввода своего имени пользователя и пароля.:",
"dph_pad_pw": "Этот документ защищен новым паролем",
"calendar_rec_change_first": "Перемещение первого повторяющегося события в другой календарь. Все повторы этого события также будут перемещены.",
"calendar_desc": "Описание",
"calendar_description": "Описание:{0}{1}",
"sso_register_description": "Зарегистрироваться используя",
"ssoauth_header": "Пароль CryptPad",
"ssoauth_form_hint_login": "Пожалуйста, введите свой пароль CryptPad",
"duplicate": "Дублировать",
"kanban_showTags": "Посмотреть все теги",
"kanban_hideTags": "Посмотреть меньше тегов",
"admin_forcemfaTitle": "Обязательная двухфакторная аутентификация",
"loading_mfa_required": "В этом экземпляре требуется двухфакторная аутентификация. Пожалуйста, обновите свою учетную запись с помощью приложения аутентификации и формы ниже.",
"recovery_header": "Восстановление 2FA",
"recovery_mfa_secret": "Пожалуйста, введите свой код восстановления, чтобы отключить 2FA для Вашей учетной записи:",
"admin_totpDisableButton": "Отключить",
"register_nameTooLong": "Имена пользователей должны быть короче {0} символов",
"loading_recover": "Не можете получить код? <a href=\"/recovery/\">Восстановить свою учетную запись</a>",
"goRight": "Направо",
"admin_cat_users": "Каталог Пользователей",
"admin_registrationSsoTitle": "Закрыть регистрацию по SSO",
"admin_invitationTitle": "Пригласительные ссылки",
"admin_invitationCreate": "Создать пригласительную ссылку",
"admin_invitationLink": "Пригласительная ссылка",
"admin_invitationCopy": "Копировать ссылку",
"admin_usersAdd": "Добавить известного пользователя",
"admin_usersTitle": "Каталог Пользователей",
"admin_usersHint": "Список известных учётных записей на этом экземпляре. Выберите ниже, чтобы добавить учётные записи автоматически, или введите информацию вручную, используя форму.",
"admin_storeInvitedLabel": "Автоматически записывать приглашенных пользователей",
"admin_storeSsoLabel": "Автоматически записывать пользователей, зарегестрировавшихся по SSO",
"register_invalidToken": "Пригласительная ссылка недействительна",
"admin_usersBlock": "URL-адрес блокировки входа пользователя (необязательно)",
"admin_invitationEmail": "Эл. почта пользователя",
"admin_invitationDeleteConfirm": "Вы уверены, что хотите удалить это приглашение?",
"admin_invitationHint": "Пригласительные ссылки создают по одному аккаунту каждая, даже если регистрация закрыта. Имя пользователя и адрес электронной почты используются только для Вашей идентификации. CryptPad не отправит по электронной почте ссылку-приглашение (или что-либо ещё) - пожалуйста, скопируйте ссылку и отправьте её по выбранному Вами защищённому каналу.",
"admin_invitationAlias": "Имя пользователя",
"admin_usersRemove": "Удалить",
"admin_usersRemoveConfirm": "Вы уверены, что хотите удалить этого пользователя из каталога? Он не потеряет доступ к своей учетной записи и продолжит использовать её."
}

View file

@ -13,7 +13,7 @@
"todo": "待办事项",
"teams": "团队",
"sheet": "工作表",
"contacts": "联系我们",
"contacts": "联系",
"form": "表单",
"presentation": "演示文档",
"doc": "文档"

View file

@ -1 +1,21 @@
{}
{
"type": {
"pad": "Rich text",
"kanban": "看板",
"slide": "Markdown 投影片",
"drive": "CryptDrive",
"whiteboard": "白板",
"file": "檔案",
"media": "多媒體",
"todo": "Todo",
"contacts": "聯絡我們",
"sheet": "試算表",
"teams": "團隊",
"form": "表單",
"doc": "文件",
"diagram": "圖表"
},
"deletedError": "這份文件已被永久刪除。",
"common_connectionLost": "<b>伺服器連線中斷</b><br>在恢復連線前都會是唯讀模式。",
"inactiveError": "這份文件已因太久未被編輯而刪除,請按 Esc 鍵來創建一份新文件。"
}

View file

@ -166,6 +166,9 @@
config.events.onHasUnsavedChanges(unsavedChanges);
cb();
});
chan.on('ON_INSERT_IMAGE', function(data, cb) {
config.events.onInsertImage(data, cb);
});
});
});
@ -184,6 +187,7 @@
* @param {object} config.events Event handlers.
* @param {function} events.onSave (blob, callback) The save function to store the document when edited.
* @param {function} events.onNewKey (data, callback) The function called when a new key is used.
* @param {function} events.onInsertImage (data, callback) The function called the user wants to add an image.
* @param {string} config.documentType The editor to load in CryptPad.
* @return {promise}
*/
@ -206,7 +210,7 @@
if (!config) { return reject('Missing args: no data provided'); }
if(['document.url', 'document.fileType', 'documentType',
'events.onSave', 'events.onHasUnsavedChanges',
'events.onNewKey'].some(function (k) {
'events.onNewKey', 'events.onInsertImage'].some(function (k) {
var s = k.split('.');
var c = config;
return s.some(function (key) {

View file

@ -4,15 +4,92 @@
define([
'/components/x2js/x2js.js',
'/diagram/util.js',
], function (
X2JS) {
X2JS,
DiagramUtil
) {
const x2js = new X2JS();
const jsonContentAsXML = (content) => x2js.js2xml(content);
const parseDrawioStyle = (styleAttrValue) => {
if (!styleAttrValue) {
return;
}
const result = {};
for (const part of styleAttrValue.split(';')) {
const s = part.split(/=(.*)/);
result[s[0]] = s[1];
}
return result;
};
const stringifyDrawioStyle = (styleAttrValue) => {
const parts = [];
for (const [key, value] of Object.entries(styleAttrValue)) {
parts.push(`${key}=${value}`);
}
return parts.join(';');
};
const blobToImage = (blob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = function() {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
};
const loadImage = (url) => {
return DiagramUtil.loadImage(url).then((blob) => blobToImage(blob));
};
const loadCryptPadImages = (doc) => {
return Array.from(doc .querySelectorAll('mxCell'))
.map((element) => [element, parseDrawioStyle(element.getAttribute('style'))])
.filter(([element, style]) => style && style.image && style.image.startsWith('cryptpad://'))
.map(([element, style]) => {
return loadImage(style.image)
.then((dataUrl) => {
style.image = dataUrl.replace(';base64', ''); // ';' breaks draw.ios style format
element.setAttribute('style', stringifyDrawioStyle(style));
});
});
};
const parseXML = (xmlStr) => {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlStr, "application/xml");
const errorNode = doc.querySelector("parsererror");
if (errorNode) {
throw Error("error while parsing " + errorNode);
}
return doc;
};
return {
main: function(userDoc, cb) {
delete userDoc.metadata;
cb(jsonContentAsXML(userDoc), '.drawio');
const xml = jsonContentAsXML(userDoc);
let doc;
try {
doc = parseXML(xml);
} catch(e) {
console.error(e);
return;
}
const promises = loadCryptPadImages(doc);
Promise.all(promises).then(() => {
cb(new XMLSerializer().serializeToString(doc), '.drawio');
});
}
};
});

View file

@ -8,6 +8,7 @@ define([
'/customize/messages.js', // translation keys
'/components/pako/dist/pako.min.js',
'/components/x2js/x2js.js',
'/diagram/util.js',
'/components/tweetnacl/nacl-fast.min.js',
'less!/diagram/app-diagram.less',
'css!/diagram/drawio.css',
@ -15,8 +16,11 @@ define([
Framework,
Messages,
pako,
X2JS) {
X2JS,
DiagramUtil
) {
const Nacl = window.nacl;
const APP = window.APP = {};
// As described here: https://drawio-app.com/extracting-the-xml-from-mxfiles/
const decompressDrawioXml = function(xmlDocStr) {
@ -125,6 +129,21 @@ define([
autosave: onDrawioAutosave,
};
APP.loadImage = DiagramUtil.loadImage;
APP.addImage = function() {
return new Promise((resolve) => {
framework.insertImage({}, (imageData) => {
if (imageData.blob) {
resolve(imageData.blob);
} else if (imageData.url) {
resolve(imageData.url);
} else {
resolve(DiagramUtil.getCryptPadUrl(imageData.src, imageData.key, imageData.fileType));
}
});
});
};
// This is the function from which you will receive updates from CryptPad
framework.onContentUpdate(function (newContent) {
lastContent = newContent;
@ -151,9 +170,13 @@ define([
framework.setFileExporter(
'.drawio',
() => {
return new Blob([jsonContentAsXML(lastContent)], {type: 'application/x-drawio'});
}
(cb) => {
require(['/diagram/export.js'], (exporter) => {
exporter.main(lastContent, (xml) => {
cb(new Blob([xml], {type: 'application/x-drawio'}));
});
});
}, true
);
framework.onEditableChange(function () {
@ -170,13 +193,12 @@ define([
drawioFrame.src = '/components/drawio/src/main/webapp/index.html?'
+ new URLSearchParams({
// pages: 0,
// dev: 1,
test: 1,
stealth: 1,
embed: 1,
drafts: 0,
plugins: 0,
p: 'cryptpad',
integrated: framework.isIntegrated() ? 'true' : 'false',
chrome: framework.isReadOnly() ? 0 : 1,
dark: window.CryptPad_theme === "dark" ? 1 : 0,
@ -185,6 +207,10 @@ define([
noSaveBtn: 1,
saveAndExit: 0,
noExitBtn: 1,
browser: 0,
noDevice: 1,
filesupport: 0,
modified: 'unsavedChanges',
proto: 'json',

70
www/diagram/util.js Normal file
View file

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/common/common-util.js',
'/file/file-crypto.js',
'/common/outer/cache-store.js',
], function (
Util,
FileCrypto,
Cache
) {
const Nacl = window.nacl;
const parseCryptPadUrl = function(href) {
const url = new URL(href);
const protocol = url.searchParams.get('protocol');
const key = url.hash.substring(1); // remove leading '#'
const type = url.searchParams.get('type');
url.search = '';
url.hash = '';
return { src: url.href.replace(/cryptpad?:\/\//, `${protocol}//`), key, type };
};
const getCryptPadUrl = function(src, key, type) {
const url = new URL(src);
const params = new URLSearchParams();
params.set('type', type);
params.set('protocol', url.protocol);
url.search = params.toString();
url.hash = key;
return url.href.replace(/https?:\/\//, 'cryptpad://');
};
const setBlobType = (blob, mimeType) => {
const fixedBlob = new Blob([blob], {type: mimeType});
return fixedBlob;
};
return {
parseCryptPadUrl,
getCryptPadUrl,
loadImage: function(href) {
return new Promise((resolve, reject) => {
const { src, key, type } = parseCryptPadUrl(href);
Util.fetch(src, function (err, u8) {
if (err) {
console.error(err);
return void reject(err);
}
try {
FileCrypto.decrypt(u8, Nacl.util.decodeBase64(key), (err, res) => {
if (err || !res.content) {
console.error("Decrypting failed");
return void reject(err);
}
resolve(setBlobType(res.content, type));
});
} catch (e) {
console.error(e);
reject(err);
}
}, void 0, Cache);
});
}
};
});

View file

@ -365,7 +365,7 @@ define([
});
if (!v.type || v.type === "text") {
$input.keyup(function (e) {
$input.keydown(function (e) {
try {
if (e.which === 13) {
var $line = $input.closest('.cp-form-edit-block-input');
@ -572,7 +572,8 @@ define([
}
// "Add option" button handler
$add = $(add).click(function () {
$add = $(add);
Util.onClickEnter($add, function () {
var txt = v.type ? '' : Messages.form_newOption;
var el = getOption(txt, true, false, Util.uid());
$add.before(el);
@ -2065,6 +2066,7 @@ define([
var tag = h('input');
var picker = Flatpickr(tag, {
disableMobile: true,
enableTime: true,
time_24hr: is24h,
dateFormat: dateFormat,
@ -3295,7 +3297,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 +4111,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

@ -130,25 +130,31 @@ define([
function (yes) {
if (!yes) { return; }
Login.loginOrRegisterUI(uname, passwd, true, shouldImport,
UI.getOTPScreen, false, function (data) {
var proxy = data.proxy;
if (!proxy || !proxy.edPublic) { UI.alert(Messages.error); return true; }
Login.loginOrRegisterUI({
uname,
passwd,
isRegister: true,
onOTP: UI.getOTPScreen,
shouldImport,
cb: function (data) {
var proxy = data.proxy;
if (!proxy || !proxy.edPublic) { UI.alert(Messages.error); return true; }
Rpc.createAnonymous(data.network, function (e, call) {
if (e) { UI.alert(Messages.error); return console.error(e); }
var anon_rpc = call;
anon_rpc.send('ADD_FIRST_ADMIN', {
token: token,
edPublic: proxy.edPublic
}, function (e) {
Rpc.createAnonymous(data.network, function (e, call) {
if (e) { UI.alert(Messages.error); return console.error(e); }
window.location.href = '/drive/';
});
});
var anon_rpc = call;
return true;
anon_rpc.send('ADD_FIRST_ADMIN', {
token: token,
edPublic: proxy.edPublic
}, function (e) {
if (e) { UI.alert(Messages.error); return console.error(e); }
window.location.href = '/drive/';
});
});
return true;
}
});
registering = true;
}, {

View file

@ -14,7 +14,7 @@ define([
console.warn('INIT');
var p = window.parent;
var txid = getTxid();
p.postMessage(JSON.stringify({ q: 'INTEGRATION_READY', txid: txid }), '*');
p.postMessage({ q: 'INTEGRATION_READY', txid: txid }, '*');
var makeChan = function () {
var handlers = {};
@ -38,6 +38,7 @@ define([
// On new command
var msg = data.msg;
if (!msg) { return; }
var txid = data.txid;
if (commands[msg.q]) {
commands[msg.q](msg.data, function (args) {
@ -122,6 +123,9 @@ define([
var onHasUnsavedChanges = function (unsavedChanges, cb) {
chan.send('HAS_UNSAVED_CHANGES', unsavedChanges, cb);
};
var onInsertImage = function (data, cb) {
chan.send('ON_INSERT_IMAGE', data, cb);
};
chan.on('START', function (data) {
console.warn('INNER START', data);
@ -139,7 +143,8 @@ define([
utils: {
save: save,
reload: reload,
onHasUnsavedChanges: onHasUnsavedChanges
onHasUnsavedChanges: onHasUnsavedChanges,
onInsertImage: onInsertImage
}
};
require(['/common/sframe-app-outer.js'], function () {

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();

View file

@ -30,17 +30,22 @@ define([
enableTime: true,
time_24hr: is24h,
dateFormat: dateFormat,
minDate: start.date
minDate: start.date,
onChange: function () {
duration = parseDate(e.value) - parseDate(s.value);
}
});
endPickr.setDate(end.date);
var s = $(start.input)[0];
var duration = end.date - start.date;
var startPickr = Flatpickr(s, {
enableTime: true,
time_24hr: is24h,
dateFormat: dateFormat,
onChange: function () {
endPickr.set('minDate', parseDate(s.value));
endPickr.setDate(parseDate(s.value).getTime() + duration);
}
});
startPickr.setDate(start.date);

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Some files were not shown because too many files have changed in this diff Show more