mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'staging' into http-workers
This commit is contained in:
commit
b003d4d825
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,3 +1,37 @@
|
|||
# 5.2.1
|
||||
|
||||
## Goals
|
||||
|
||||
This minor releases fixes a bug with one of the Form features introduced in 5.2.0.
|
||||
|
||||
We took the opportunity to include two other fixes for older issues.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- The option to delete all responses to a form was not available to form authors when the form had been created in a drive (user or team) using the **+ NEW** button
|
||||
|
||||
- Drag & drop from a shared folder into the Templates folder made documents "disappear". They would reappear in the root of the drive when using a new worker (after all CryptPad tabs had been closed)
|
||||
|
||||
- Clicking a link in a Calendar event location field failed to open
|
||||
|
||||
## Update notes
|
||||
|
||||
Our `5.2.0` release introduced some changes to the Nginx configuration. If you are not already running `5.2.0` we recommend following the upgrade notes for that version first, and then updating to `5.2.1`
|
||||
|
||||
To do so:
|
||||
|
||||
1. Stop your server
|
||||
2. Get the latest code with git
|
||||
|
||||
```bash
|
||||
git fetch origin --tags
|
||||
git checkout 5.2.1
|
||||
```
|
||||
|
||||
1. Install the latest dependencies with `bower update`
|
||||
2. Restart your server
|
||||
3. Review your instance's checkup page to ensure that you are passing all tests
|
||||
|
||||
# 5.2.0
|
||||
|
||||
## Goals
|
||||
|
|
|
@ -52,6 +52,12 @@ body > .non-realtime:first-child + * {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.cke_editable
|
||||
{
|
||||
font-size: 16px;
|
||||
|
|
|
@ -221,21 +221,21 @@ define([
|
|||
// If they are trying to register,
|
||||
// and the proxy is empty, then there is no 'legacy user' either
|
||||
// so we should just shut down this session and disconnect.
|
||||
rt.network.disconnect();
|
||||
//rt.network.disconnect();
|
||||
return; // proceed to the next async block
|
||||
}
|
||||
|
||||
// they tried to just log in but there's no such user
|
||||
// and since we're here at all there is no modern-block
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
rt.network.disconnect(); // clean up after yourself
|
||||
//rt.network.disconnect(); // clean up after yourself
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
rt.network.disconnect();
|
||||
//rt.network.disconnect();
|
||||
waitFor.abort();
|
||||
Feedback.send('LOGIN', true);
|
||||
return void cb('ALREADY_REGISTERED', res);
|
||||
|
@ -247,6 +247,7 @@ define([
|
|||
// so setting them is just a precaution to keep things in good shape
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = opt.userHash;
|
||||
|
@ -317,6 +318,7 @@ define([
|
|||
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
res.network = rt.network;
|
||||
|
||||
// they're registering...
|
||||
res.userHash = userHash;
|
||||
|
@ -328,14 +330,14 @@ define([
|
|||
// this really shouldn't happen, but let's handle it anyway
|
||||
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
|
||||
|
||||
rt.network.disconnect(); // clean up after yourself
|
||||
//rt.network.disconnect(); // clean up after yourself
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
rt.network.disconnect();
|
||||
//rt.network.disconnect();
|
||||
waitFor.abort();
|
||||
res.blockHash = blockHash;
|
||||
if (shouldImport) {
|
||||
|
@ -462,7 +464,8 @@ define([
|
|||
|
||||
var proceed = function (result) {
|
||||
hashing = false;
|
||||
if (test && typeof test === "function" && test()) { return; }
|
||||
// NOTE: test is also use as a cb for the install page
|
||||
if (test && typeof test === "function" && test(result)) { return; }
|
||||
LocalStore.clearLoginToken();
|
||||
Realtime.whenRealtimeSyncs(result.realtime, function () {
|
||||
Exports.redirect();
|
||||
|
|
|
@ -95,7 +95,7 @@ define([
|
|||
return h('a', attrs, [icon, text]);
|
||||
};
|
||||
|
||||
Pages.versionString = "5.2.0";
|
||||
Pages.versionString = "5.2.1";
|
||||
|
||||
var customURLs = Pages.customURLs = {};
|
||||
(function () {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
define([
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-interface.js',
|
||||
'/customize/messages.js',
|
||||
'/customize/pages.js'
|
||||
], function (Config, $, h, UI, Msg, Pages) {
|
||||
Config.adminKeys = [];
|
||||
return function () {
|
||||
// Redirect to drive if this instance already has admins
|
||||
if (Array.isArray(Config.adminKeys) && Config.adminKeys.length) {
|
||||
document.location.href = '/drive/';
|
||||
return;
|
||||
}
|
||||
|
||||
Msg.install_token = "Install token";
|
||||
|
||||
document.title = Msg.install_header;
|
||||
|
||||
var frame = function (content) {
|
||||
return [
|
||||
h('div#cp-main', [
|
||||
//Pages.infopageTopbar(),
|
||||
h('div.container.cp-container', [
|
||||
//h('div.row.cp-page-title', h('h1', Msg.install_header)),
|
||||
h('div.row.cp-page-title', h('h1', Msg.register_header)),
|
||||
].concat(content)),
|
||||
Pages.infopageFooter(),
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
return frame([
|
||||
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.install_notes)
|
||||
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
|
||||
]),
|
||||
h('div.cp-reg-form.col-md-6', [
|
||||
h('div#userForm.form-group.hidden', [
|
||||
h('div.cp-register-instance', [
|
||||
Msg._getKey('register_instance', [ Pages.Instance.name ]),
|
||||
/*h('br'),
|
||||
h('a', {
|
||||
href: '/features.html'
|
||||
}, Msg.register_whyRegister)*/
|
||||
]),
|
||||
h('input.form-control#installtoken', {
|
||||
type: 'text',
|
||||
placeholder: Msg.install_token
|
||||
}),
|
||||
h('input.form-control#username', {
|
||||
type: 'text',
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: false,
|
||||
placeholder: Msg.login_username,
|
||||
autofocus: true,
|
||||
}),
|
||||
h('input.form-control#password', {
|
||||
type: 'password',
|
||||
placeholder: Msg.login_password,
|
||||
}),
|
||||
h('input.form-control#password-confirm', {
|
||||
type: 'password',
|
||||
placeholder: Msg.login_confirm,
|
||||
}),
|
||||
/*h('div.checkbox-container', [
|
||||
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
|
||||
]),*/
|
||||
h('button#register', Msg.login_register)
|
||||
])
|
||||
]),
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
@import (reference) "../include/infopages.less";
|
||||
@import (reference) "../include/colortheme-all.less";
|
||||
@import (reference) "../include/alertify.less";
|
||||
@import (reference) "../include/checkmark.less";
|
||||
@import (reference) "../include/forms.less";
|
||||
|
||||
&.cp-page-install {
|
||||
.infopages_main();
|
||||
.forms_main();
|
||||
|
||||
.alertify_main();
|
||||
.checkmark_main(20px);
|
||||
|
||||
.cp-container {
|
||||
.form-group {
|
||||
.cp-register-instance {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#register {
|
||||
&.btn {
|
||||
padding: .5rem .5rem;
|
||||
}
|
||||
margin-top: 16px;
|
||||
font-size: 1.25em;
|
||||
min-width: 30%;
|
||||
}
|
||||
}
|
||||
padding-bottom: 3em;
|
||||
min-height: 5vh;
|
||||
}
|
||||
.alertify {
|
||||
// workaround for alertify making empty p
|
||||
p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
nav .btn-danger {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.cp-restricted-registration {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.cp-register-det {
|
||||
#data {
|
||||
|
||||
p {
|
||||
li {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.fa {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-weight: 700;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
.cp-reg-form {
|
||||
img {
|
||||
margin-top: 0px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
#userForm {
|
||||
padding: 15px;
|
||||
background-color: @cp_static-card-bg;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 100px;
|
||||
border-radius: @infopages-radius-L;
|
||||
.cp-shadow();
|
||||
.form-control {
|
||||
border-radius: @infopages-radius;
|
||||
color: @cryptpad_text_col;
|
||||
background-color: @cp_forms-bg;
|
||||
margin-bottom: 10px;
|
||||
&:focus {
|
||||
border-color: @cryptpad_color_brand;
|
||||
}
|
||||
.tools_placeholder-color();
|
||||
}
|
||||
.checkbox-container {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
button#register {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-register-notes {
|
||||
ul.cp-notes-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 30px;
|
||||
position: relative;
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-family: "FontAwesome";
|
||||
content: "\f071";
|
||||
}
|
||||
.red {
|
||||
background-color: @cp_static-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,8 @@ $(function () {
|
|||
|
||||
if (/^\/register\//.test(pathname)) {
|
||||
require([ '/register/main.js' ], function () {});
|
||||
} else if (/^\/install\//.test(pathname)) {
|
||||
require([ '/install/main.js' ], function () {});
|
||||
} else if (/^\/login\//.test(pathname)) {
|
||||
require([ '/login/main.js' ], function () {});
|
||||
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
|
||||
|
|
|
@ -777,6 +777,27 @@ var commands = {
|
|||
REMOVE_DOCUMENT: removeDocument,
|
||||
};
|
||||
|
||||
// addFirstAdmin is an anon_rpc command
|
||||
Admin.addFirstAdmin = function (Env, data, cb) {
|
||||
if (!Env.installToken) { return void cb('EINVAL'); }
|
||||
var token = data.token;
|
||||
if (!token || !data.edPublic) { return void cb('MISSING_ARGS'); }
|
||||
if (token.length !== 64 || data.edPublic.length !== 44) { return void cb('INVALID_ARGS'); }
|
||||
if (token !== Env.installToken) { return void cb('FORBIDDEN'); }
|
||||
if (Array.isArray(Env.admins) && Env.admins.length) { return void cb('EEXISTS'); }
|
||||
|
||||
var key = data.edPublic;
|
||||
|
||||
adminDecree(Env, null, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
Env.flushCache();
|
||||
cb();
|
||||
}, ['ADD_FIRST_ADMIN', [
|
||||
'ADD_ADMIN_KEY',
|
||||
[key]
|
||||
]], "");
|
||||
};
|
||||
|
||||
Admin.command = function (Env, safeKey, data, _cb, Server) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
|
||||
|
|
|
@ -322,6 +322,32 @@ commands.RM_QUOTA = function (Env, args) {
|
|||
return true;
|
||||
};
|
||||
|
||||
commands.ADD_INSTALL_TOKEN = function (Env, args) {
|
||||
if (!Array.isArray(args) || args.length !== 1 || !args[0]) {
|
||||
throw new Error("INVALID_ARGS");
|
||||
}
|
||||
|
||||
var token = args[0];
|
||||
|
||||
Env.installToken = token;
|
||||
|
||||
return true;
|
||||
};
|
||||
commands.ADD_ADMIN_KEY = function (Env, args) {
|
||||
if (!Array.isArray(args) || args.length !== 1 || !args[0]) {
|
||||
throw new Error("INVALID_ARGS");
|
||||
}
|
||||
|
||||
Env.admins = Env.admins || [];
|
||||
|
||||
|
||||
var key = Keys.canonicalize(args[0]);
|
||||
if (!key) { throw new Error("INVALID_KEY"); }
|
||||
Env.admins.push(key);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// [<command>, <args>, <author>, <time>]
|
||||
var handleCommand = Decrees.handleCommand = function (Env, line) {
|
||||
var command = line[0];
|
||||
|
|
|
@ -172,6 +172,7 @@ module.exports.create = function (config) {
|
|||
|
||||
limits: {},
|
||||
admins: [],
|
||||
installToken: undefined,
|
||||
WARN: function (e, output) { // TODO deprecate this
|
||||
if (!Env.Log) { return; }
|
||||
if (e && output) {
|
||||
|
|
160
lib/eviction.js
160
lib/eviction.js
|
@ -3,6 +3,10 @@ var Bloom = require("@mcrowe/minibloom");
|
|||
var Util = require("../lib/common-util");
|
||||
var Pins = require("../lib/pins");
|
||||
var Keys = require("./keys");
|
||||
var Path = require('node:path');
|
||||
var config = require("./load-config");
|
||||
var Fs = require("node:fs");
|
||||
var Fse = require("fs-extra");
|
||||
|
||||
var getNewestTime = function (stats) {
|
||||
return stats[['atime', 'ctime', 'mtime'].reduce(function (a, b) {
|
||||
|
@ -32,6 +36,7 @@ Env = {
|
|||
|
||||
// the number of ms artificially introduced between CPU-intensive operations
|
||||
var THROTTLE_FACTOR = 10;
|
||||
var PROGRESS_FACTOR = 1000;
|
||||
|
||||
var evictArchived = function (Env, cb) {
|
||||
var Log;
|
||||
|
@ -70,6 +75,103 @@ var evictArchived = function (Env, cb) {
|
|||
blobs = Env.blobStore;
|
||||
};
|
||||
|
||||
var migrateBlobRoot = function (from, to) {
|
||||
// only migrate subpaths, leave everything else alone
|
||||
if (!Path.dirname(from).startsWith(Path.dirname(to))) { return; }
|
||||
|
||||
// expects a directory
|
||||
var recurse = function (relativePath) {
|
||||
var src = Path.join(from, relativePath);
|
||||
var children;
|
||||
try {
|
||||
children = Fs.readdirSync(src);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') { return; }
|
||||
// if you can't read a directory's contents
|
||||
// then nothing else will work, so just abort
|
||||
Log.verbose("EVICT_ARCHIVED_NOT_DIRECTORY", {
|
||||
error: err,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var dest;
|
||||
if (children.length === 0) {
|
||||
try {
|
||||
Fse.removeSync(src);
|
||||
} catch (err2) {
|
||||
Log.error('EVICT_ARCHIVED_EMPTY_DIR_REMOVAL', {
|
||||
error: err2,
|
||||
});
|
||||
// removal is non-essential, so we can continue
|
||||
}
|
||||
} else {
|
||||
// make an equivalent path in the target directory
|
||||
dest = Path.join(to, relativePath);
|
||||
|
||||
try {
|
||||
Fse.mkdirpSync(dest);
|
||||
} catch (err3) {
|
||||
Log.error("EVICT_ARCHIVED_BLOB_MIGRATION", {
|
||||
error: err3,
|
||||
});
|
||||
|
||||
// failure to create the host directory
|
||||
// will cause problems when we try to move
|
||||
// so bail out here
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
children.forEach(function (child) {
|
||||
var childSrcPath = Path.join(src, child);
|
||||
var stat = Fs.statSync(childSrcPath);
|
||||
if (stat.isDirectory()) {
|
||||
return void recurse(Path.join(relativePath, child));
|
||||
}
|
||||
|
||||
var childDestPath = Path.join(dest, child);
|
||||
|
||||
try {
|
||||
Log.verbose("EVICT_ARCHIVED_MOVE_FROM_DEPRECATED_PATH", {
|
||||
from: childSrcPath,
|
||||
to: childDestPath,
|
||||
});
|
||||
Fse.moveSync(childSrcPath, childDestPath, {
|
||||
overwrite: false,
|
||||
});
|
||||
} catch (err4) {
|
||||
Log.error('EVICT_ARCHIVED_MOVE_FAILURE', {
|
||||
error: err4,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
recurse('');
|
||||
};
|
||||
|
||||
/* In CryptPad 5.2.0 we merged a patch which converted
|
||||
all of CryptPad's root filepaths to their absolute form,
|
||||
rather than the relative paths we'd been using until then.
|
||||
Unfortunately, we overlooked a case where two absolute
|
||||
paths were concatenated together, resulting in blobs being
|
||||
archived to an incorrect path.
|
||||
|
||||
This migration detects evidence of incorrect archivals
|
||||
and moves such archived files to their intended location
|
||||
before continuing with the normal eviction procedure.
|
||||
*/
|
||||
var migrateIncorrectBlobs = function () {
|
||||
var incorrectPaths = [
|
||||
Path.join(Env.paths.archive, config.blobPath),
|
||||
Path.join(Env.paths.archive, Path.resolve(config.blobPath))
|
||||
];
|
||||
var correctPath = Path.join(Env.paths.archive, 'blob');
|
||||
incorrectPaths.forEach(root => {
|
||||
migrateBlobRoot(root, correctPath);
|
||||
});
|
||||
};
|
||||
|
||||
var removeArchivedChannels = function (w) {
|
||||
// this block will iterate over archived channels and removes them
|
||||
// if they've been in cold storage for longer than your configured archive time
|
||||
|
@ -186,6 +288,7 @@ var evictArchived = function (Env, cb) {
|
|||
};
|
||||
|
||||
nThen(loadStorage)
|
||||
.nThen(migrateIncorrectBlobs)
|
||||
.nThen(removeArchivedChannels)
|
||||
.nThen(removeArchivedBlobProofs)
|
||||
.nThen(removeArchivedBlobs)
|
||||
|
@ -289,6 +392,12 @@ module.exports = function (Env, cb) {
|
|||
var active = 0;
|
||||
var handler = function (err, item, cb) {
|
||||
channels++;
|
||||
if (channels % PROGRESS_FACTOR === 0) {
|
||||
Log.info('EVICT_CHANNEL_CATEGORIZATION_PROGRESS', {
|
||||
channels: channels,
|
||||
});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
Log.error('EVICT_CHANNEL_CATEGORIZATION', err);
|
||||
return void cb();
|
||||
|
@ -315,6 +424,7 @@ module.exports = function (Env, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Log.info('EVICT_CHANNEL_ACTIVITY_START', 'Assessing channel activity');
|
||||
store.listChannels(handler, w(done));
|
||||
};
|
||||
|
||||
|
@ -322,9 +432,16 @@ module.exports = function (Env, cb) {
|
|||
var n_blobs = 0;
|
||||
var active = 0;
|
||||
|
||||
Log.info('EVICT_BLOBS_ACTIVITY_START', 'Assessing blob activity');
|
||||
blobs.list.blobs(function (err, item, next) {
|
||||
next = Util.mkAsync(next, THROTTLE_FACTOR);
|
||||
n_blobs++;
|
||||
if (n_blobs % PROGRESS_FACTOR === 0) {
|
||||
Log.info('EVICT_BLOB_CATEGORIZATION_PROGRESS', {
|
||||
blobs: n_blobs,
|
||||
});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
Log.error("EVICT_BLOB_CATEGORIZATION", err);
|
||||
return void next();
|
||||
|
@ -393,6 +510,11 @@ module.exports = function (Env, cb) {
|
|||
var handler = function (content, id, next) {
|
||||
next = Util.mkAsync(next, THROTTLE_FACTOR);
|
||||
accounts++;
|
||||
if (accounts % PROGRESS_FACTOR === 0) {
|
||||
Log.info('EVICT_ACCOUNT_CATEGORIZATION_PROGRESS', {
|
||||
accounts: accounts,
|
||||
});
|
||||
}
|
||||
|
||||
var mtime = content.latest;
|
||||
var pinList = Object.keys(content.pins);
|
||||
|
@ -449,6 +571,7 @@ module.exports = function (Env, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Log.info('EVICT_ACCOUNTS_ACTIVITY_START', 'Assessing account activity');
|
||||
Pins.load(w(done), {
|
||||
pinPath: Env.paths.pin,
|
||||
handler: handler,
|
||||
|
@ -460,6 +583,8 @@ module.exports = function (Env, cb) {
|
|||
// if they have not been accessed within the specified retention time
|
||||
var removed = 0;
|
||||
var total = 0;
|
||||
|
||||
Log.info('EVICT_BLOB_START', {});
|
||||
blobs.list.blobs(function (err, item, next) {
|
||||
next = Util.mkAsync(next, THROTTLE_FACTOR);
|
||||
if (err) {
|
||||
|
@ -471,6 +596,12 @@ module.exports = function (Env, cb) {
|
|||
return void Log.error('EVICT_BLOB_LIST_BLOBS_NO_ITEM', item);
|
||||
}
|
||||
total++;
|
||||
if (total % PROGRESS_FACTOR === 0) {
|
||||
Log.info("EVICT_BLOB_PROGRESS", {
|
||||
blobs: total,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedDocs.test(item.blobId)) { return void next(); }
|
||||
if (activeDocs.test(item.blobId)) { return void next(); }
|
||||
|
||||
|
@ -504,6 +635,9 @@ module.exports = function (Env, cb) {
|
|||
// iterate over blob proofs and remove them
|
||||
// if they don't correspond to a pinned or active file
|
||||
var removed = 0;
|
||||
var total = 0;
|
||||
|
||||
Log.info("EVICT_ARCHIVE_INACTIVE_BLOB_PROOFS_START", {});
|
||||
blobs.list.proofs(function (err, item, next) {
|
||||
next = Util.mkAsync(next, THROTTLE_FACTOR);
|
||||
if (err) {
|
||||
|
@ -514,6 +648,14 @@ module.exports = function (Env, cb) {
|
|||
next();
|
||||
return void Log.error('EVICT_BLOB_LIST_PROOFS_NO_ITEM', item);
|
||||
}
|
||||
total++;
|
||||
|
||||
if (total % PROGRESS_FACTOR === 0) {
|
||||
Log.info('EVICT_BLOB_PROOF_PROGRESS', {
|
||||
proofs: total,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinnedDocs.test(item.blobId)) { return void next(); }
|
||||
if (getNewestTime(item) > inactiveTime) { return void next(); }
|
||||
nThen(function (w) {
|
||||
|
@ -539,7 +681,10 @@ module.exports = function (Env, cb) {
|
|||
});
|
||||
});
|
||||
}, w(function () {
|
||||
Log.info("EVICT_BLOB_PROOFS_REMOVED", removed);
|
||||
Log.info("EVICT_BLOB_PROOFS_REMOVED", {
|
||||
removed,
|
||||
total,
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -550,6 +695,13 @@ module.exports = function (Env, cb) {
|
|||
var handler = function (err, item, cb) {
|
||||
cb = Util.mkAsync(cb, THROTTLE_FACTOR);
|
||||
channels++;
|
||||
if (channels % PROGRESS_FACTOR === 0) {
|
||||
Log.info('EVICT_INACTIVE_CHANNELS_PROGRESS', {
|
||||
channels,
|
||||
archived,
|
||||
});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
Log.error('EVICT_CHANNEL_ITERATION', err);
|
||||
return void cb();
|
||||
|
@ -606,9 +758,13 @@ module.exports = function (Env, cb) {
|
|||
|
||||
var done = function () {
|
||||
report.channelsArchived = archived;
|
||||
return void Log.info('EVICT_CHANNELS_ARCHIVED', archived);
|
||||
return void Log.info('EVICT_CHANNELS_ARCHIVED', {
|
||||
channels,
|
||||
archived,
|
||||
});
|
||||
};
|
||||
|
||||
Log.info('EVICT_INACTIVE_CHANNELS_START', {});
|
||||
store.listChannels(handler, w(done), true); // using a hacky "fast mode" since we only need the channel id
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ const UNAUTHENTICATED_CALLS = {
|
|||
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
|
||||
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
|
||||
GET_METADATA: Metadata.getMetadata,
|
||||
ADD_FIRST_ADMIN: Admin.addFirstAdmin
|
||||
};
|
||||
|
||||
var isUnauthenticateMessage = function (msg) {
|
||||
|
|
|
@ -74,7 +74,7 @@ Stats.instanceData = function (Env) {
|
|||
}
|
||||
|
||||
// Admins can opt-in to providing more detailed information about the extent of the instance's usage
|
||||
if (!Env.provideAggregateStatistics) {
|
||||
if (Env.provideAggregateStatistics) {
|
||||
// check how many instances provide stats before we put more work into it
|
||||
data.providesAggregateStatistics = true;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,11 @@ var isValidId = function (id) {
|
|||
// helpers
|
||||
|
||||
var prependArchive = function (Env, path) {
|
||||
return Path.join(Env.archivePath, path);
|
||||
// Env has an absolute path to the blob storage
|
||||
// we want the path to the blob relative to that
|
||||
var relativePathToBlob = Path.relative(Env.blobPath, path);
|
||||
// the new path structure is the same, but relative to the blob archive root
|
||||
return Path.join(Env.archivePath, 'blob', relativePathToBlob);
|
||||
};
|
||||
|
||||
// /blob/<safeKeyPrefix>/<safeKey>/<blobPrefix>/<blobId>
|
||||
|
@ -492,7 +496,7 @@ BlobStore.create = function (config, _cb) {
|
|||
if (e) { CB(e); }
|
||||
}));
|
||||
|
||||
Fse.mkdirp(Path.join(Env.archivePath, Env.blobPath), w(function (e) {
|
||||
Fse.mkdirp(Path.join(Env.archivePath, './blob'), w(function (e) {
|
||||
if (e) { CB(e); }
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cryptpad",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cryptpad",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1",
|
||||
"license": "AGPL-3.0+",
|
||||
"dependencies": {
|
||||
"@mcrowe/minibloom": "^0.2.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cryptpad",
|
||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1",
|
||||
"license": "AGPL-3.0+",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -21,6 +21,7 @@
|
|||
"http-proxy-middleware": "^2.0.6",
|
||||
"netflux-websocket": "^0.1.20",
|
||||
"nthen": "0.1.8",
|
||||
"prompt-confirm": "^2.0.4",
|
||||
"pull-stream": "^3.6.1",
|
||||
"saferphore": "0.0.1",
|
||||
"sortify": "^1.0.4",
|
||||
|
@ -51,6 +52,8 @@
|
|||
"test": "node scripts/TestSelenium.js",
|
||||
"test-rpc": "cd scripts/tests && node test-rpc",
|
||||
"evict-inactive": "node scripts/evict-inactive.js",
|
||||
"build": "node scripts/build.js"
|
||||
"build": "node scripts/build.js",
|
||||
"clear": "node scripts/clear.js",
|
||||
"installtoken": "node scripts/install.js"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,7 +229,7 @@ appIndexesToBuild.forEach(function (app) {
|
|||
|
||||
write(built, `./www/${app}/index.html`);
|
||||
|
||||
// XXX preloading version for inner.html
|
||||
// TODO preloading version for inner.html
|
||||
});
|
||||
|
||||
var instance;
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
var prompt = require('prompt-confirm');
|
||||
const p = new prompt('Are you sure? This will permanently delete all existing data on your instance.');
|
||||
|
||||
const Fs = require("fs");
|
||||
|
||||
var config = require("../lib/load-config");
|
||||
var Env = require("../lib/env").create(config);
|
||||
Env.Log = { error: console.log };
|
||||
|
||||
var paths = Env.paths;
|
||||
p.ask(function (answer) {
|
||||
if (!answer) {
|
||||
console.log('Abort');
|
||||
return;
|
||||
}
|
||||
console.log('Deleting all data...');
|
||||
Object.values(paths).forEach(function (path) {
|
||||
console.log(`Deleting ${path}`);
|
||||
Fs.rmSync(path, { recursive: true, force: true });
|
||||
console.log('Deleted');
|
||||
});
|
||||
console.log('Success');
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
const nThen = require("nthen");
|
||||
const Fs = require("fs");
|
||||
const Path = require("path");
|
||||
const Decrees = require("../lib/decrees");
|
||||
|
||||
var config = require("../lib/load-config");
|
||||
var Hash = require('../www/common/common-hash');
|
||||
var Env = require("../lib/env").create(config);
|
||||
|
||||
Env.Log = { error: console.log };
|
||||
|
||||
var path = Path.join(Env.paths.decree, 'decree.ndjson');
|
||||
var token;
|
||||
nThen(function (w) {
|
||||
Decrees.load(Env, w(function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
w.abort();
|
||||
return;
|
||||
}
|
||||
if (Env.installToken) {
|
||||
console.log('Existing token');
|
||||
token = Env.installToken;
|
||||
}
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
if (Env.installToken) { return; }
|
||||
console.log(Env.paths.decree);
|
||||
token = Hash.createChannelId() + Hash.createChannelId();
|
||||
var decree = ["ADD_INSTALL_TOKEN",[token],"",+new Date()];
|
||||
Fs.appendFile(path, JSON.stringify(decree) + '\n', w(function (err) {
|
||||
if (err) { console.log(err); return; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
console.log('Install token:');
|
||||
console.log(token);
|
||||
var url = config.httpUnsafeOrigin + '/install/';
|
||||
console.log(`Please visit ${url} to create your first admin user`);
|
||||
|
||||
});
|
|
@ -1397,8 +1397,8 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
|||
if (updatedOn) { delete APP.recurrenceRule._next; }
|
||||
APP.wasRecurrent = Boolean(APP.recurrenceRule);
|
||||
|
||||
// XXX TEST
|
||||
/*
|
||||
// Test data:
|
||||
APP.recurrenceRule = {
|
||||
freq: 'yearly',
|
||||
interval: 2,
|
||||
|
@ -2085,12 +2085,6 @@ APP.recurrenceRule = {
|
|||
}, function () {
|
||||
$del.click();
|
||||
});
|
||||
var $section = $el.find('.tui-full-calendar-section-button');
|
||||
var ev = APP.editModalData;
|
||||
var data = ev.schedule || {};
|
||||
var id = data.id;
|
||||
if (!id) { return; }
|
||||
if (id.indexOf('|') === -1) { return; } // Original event ID doesn't contain |
|
||||
|
||||
if (APP.nextLocationUid) {
|
||||
var uid = APP.nextLocationUid;
|
||||
|
@ -2102,6 +2096,15 @@ APP.recurrenceRule = {
|
|||
common.openUnsafeURL($a.attr('href'));
|
||||
});
|
||||
}
|
||||
|
||||
var $section = $el.find('.tui-full-calendar-section-button');
|
||||
var ev = APP.editModalData;
|
||||
var data = ev.schedule || {};
|
||||
var id = data.id;
|
||||
|
||||
if (!id) { return; }
|
||||
if (id.indexOf('|') === -1) { return; } // Original event ID doesn't contain |
|
||||
|
||||
// This is a recurring event, add button to stop recurrence now
|
||||
var $b = $(h('button.btn.btn-default', [
|
||||
h('i.fa.fa-times'),
|
||||
|
|
|
@ -1530,6 +1530,7 @@ define([
|
|||
[
|
||||
'application_config.js',
|
||||
'pages.js',
|
||||
'pages/index.js',
|
||||
].forEach(resource => {
|
||||
// sort this above errors and warnings and style in a neutral color.
|
||||
var A = `/customize.dist/${resource}`;
|
||||
|
@ -1579,8 +1580,7 @@ define([
|
|||
var HSTS = H['strict-transport-security'];
|
||||
|
||||
// check for a numerical value of max-age
|
||||
// and the use of includeSubDomains
|
||||
if (/max\-age=\d+/.test(HSTS) && /includeSubDomains/.test(HSTS)) {
|
||||
if (/max\-age=\d+/.test(HSTS)) {
|
||||
return void cb(true);
|
||||
}
|
||||
|
||||
|
@ -1713,7 +1713,7 @@ define([
|
|||
var href = `/customize/${asset}`;
|
||||
return h('li', [
|
||||
h('a', {
|
||||
href: `href?${+new Date()}`,
|
||||
href: `${href}?${+new Date()}`,
|
||||
target: '_blank',
|
||||
}, href),
|
||||
]);
|
||||
|
|
|
@ -146,6 +146,9 @@
|
|||
flex: 1;
|
||||
max-width: 100%;
|
||||
resize: none;
|
||||
.CodeMirror-sizer > div {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
#cp-app-code-preview {
|
||||
display: none !important;
|
||||
|
|
|
@ -571,8 +571,8 @@ define([
|
|||
}, todo);
|
||||
};
|
||||
|
||||
common.clearOwnedChannel = function (channel, cb) {
|
||||
postMessage("CLEAR_OWNED_CHANNEL", channel, cb);
|
||||
common.clearOwnedChannel = function (data, cb) {
|
||||
postMessage("CLEAR_OWNED_CHANNEL", data, cb);
|
||||
};
|
||||
// "force" allows you to delete your drive ID
|
||||
common.removeOwnedChannel = function (data, cb) {
|
||||
|
@ -2320,7 +2320,6 @@ define([
|
|||
localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]);
|
||||
}
|
||||
}
|
||||
|
||||
initFeedback(data.feedback);
|
||||
};
|
||||
|
||||
|
@ -2729,6 +2728,7 @@ define([
|
|||
if (data.error) { throw new Error(data.error); }
|
||||
if (data.state === 'ALREADY_INIT') {
|
||||
data = data.returned;
|
||||
initFeedback(data.feedback);
|
||||
}
|
||||
|
||||
if (data.loggedIn) {
|
||||
|
|
|
@ -332,6 +332,12 @@ define([
|
|||
: Messages.error;
|
||||
return void UI.warn(text);
|
||||
}
|
||||
sframeChan.query('Q_ACCEPT_OWNERSHIP', data, function (err, res) {
|
||||
if (err || (res && res.error)) {
|
||||
return void console.error(err || res.error);
|
||||
}
|
||||
UI.log(Messages.saved);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
|
@ -867,7 +873,7 @@ define([
|
|||
// In the properties, we should have the edit href if we know it.
|
||||
// We should know it because the pad is stored, but it's better to check...
|
||||
//if (!data.noEditPassword && !opts.noEditPassword && owned && data.href) {
|
||||
if (!data.noEditPassword && !opts.noEditPassword && owned && data.href && parsed.type !== "form") { // XXX password change in forms block responses (validation & decryption)
|
||||
if (!data.noEditPassword && !opts.noEditPassword && owned && data.href && parsed.type !== "form") { // TODO password change in forms block responses (validation & decryption)
|
||||
var isOO = parsed.type === 'sheet';
|
||||
var isFile = parsed.hashData.type === 'file';
|
||||
var isSharedFolder = parsed.type === 'drive';
|
||||
|
|
|
@ -2134,9 +2134,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
|||
var exportXLSXFile = function() {
|
||||
var text = getContent();
|
||||
var suggestion = Title.suggestTitle(Title.defaultTitle);
|
||||
var ext = ['.xlsx', '.ods', '.bin',
|
||||
//'.csv', // XXX 4.11.0
|
||||
'.pdf'];
|
||||
var ext = ['.xlsx', '.ods', '.bin', '.pdf'];
|
||||
var type = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var warning = '';
|
||||
if (type==="presentation") {
|
||||
|
@ -2993,7 +2991,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
|
|||
var m = metadataMgr.getChannelMembers().filter(function (str) {
|
||||
return str.length === 32;
|
||||
}).length;
|
||||
if ((m - v) === 1 && !readOnly) {
|
||||
if ((m - v) === 1 && !readOnly && common.isLoggedIn()) {
|
||||
var needCp = ooChannel.queue.length > CHECKPOINT_INTERVAL;
|
||||
APP.initCheckpoint = needCp;
|
||||
}
|
||||
|
|
|
@ -342,10 +342,11 @@ define([
|
|||
cb(account);
|
||||
};
|
||||
|
||||
// clearOwnedChannel is only used for private chat at the moment
|
||||
// clearOwnedChannel is only used for private chat and forms
|
||||
Store.clearOwnedChannel = function (clientId, data, cb) {
|
||||
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
store.rpc.clearOwnedChannel(data, function (err) {
|
||||
var s = getStore(data && data.teamId);
|
||||
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
s.rpc.clearOwnedChannel(data.channel, function (err) {
|
||||
cb({error:err});
|
||||
});
|
||||
};
|
||||
|
@ -1693,7 +1694,7 @@ define([
|
|||
var ed = Util.find(store, ['proxy', 'teams', teamId, 'keys', 'drive', 'edPublic']);
|
||||
var edPrivate = Util.find(store, ['proxy', 'teams', teamId, 'keys', 'drive', 'edPrivate']);
|
||||
if (allowed.indexOf(ed) === -1) { return false; }
|
||||
if (!edPrivate) { return false; } // XXX: Only editors can authenticate...
|
||||
if (!edPrivate) { return false; } // FIXME: Only editors can authenticate...
|
||||
// This team is allowed: use its rpc
|
||||
var t = teamModule.getTeam(teamId);
|
||||
_store = t;
|
||||
|
@ -1951,11 +1952,15 @@ define([
|
|||
// contactPadOwner is used to send "REQUEST_ACCESS" messages
|
||||
// and to notify form owners when sending a response
|
||||
Store.contactPadOwner = function (clientId, data, cb) {
|
||||
var owner = data.owner;
|
||||
var owners = data.owners;
|
||||
|
||||
// If send is true, send the request to the owner.
|
||||
if (owner) {
|
||||
if (data.send) {
|
||||
if (!Array.isArray(owners) || !owners.length) { return cb({state: false}); }
|
||||
|
||||
if (!data.send) { return void cb({state: true}); }
|
||||
|
||||
nThen(function (waitFor) {
|
||||
owners.forEach(function (owner) {
|
||||
var sendTo = function (query, msg, user, _cb) {
|
||||
if (store.mailbox && !data.anon) {
|
||||
return store.mailbox.sendTo(query, msg, user, _cb);
|
||||
|
@ -1968,14 +1973,11 @@ define([
|
|||
}, {
|
||||
channel: owner.notifications,
|
||||
curvePublic: owner.curvePublic
|
||||
}, function () {
|
||||
cb({state: true});
|
||||
});
|
||||
return;
|
||||
}
|
||||
return void cb({state: true});
|
||||
}
|
||||
cb({state: false});
|
||||
}, waitFor());
|
||||
});
|
||||
}).nThen(function () {
|
||||
cb({state: true});
|
||||
});
|
||||
};
|
||||
Store.givePadAccess = function (clientId, data, cb) {
|
||||
var edPublic = store.proxy.edPublic;
|
||||
|
@ -2439,6 +2441,27 @@ define([
|
|||
// Clients management
|
||||
var driveEventClients = [];
|
||||
|
||||
// Check if this is a channel that we shouldn't leave when closing the debug app
|
||||
var alwaysOnline = function (chanId) {
|
||||
if (!store) { return; }
|
||||
// Drive
|
||||
if (store.driveChannel === chanId) { return true; }
|
||||
// Shared folders
|
||||
if (SF.isSharedFolderChannel(chanId)) { return true; }
|
||||
// Teams
|
||||
if (Util.find(store, ['proxy', 'teams'])) {
|
||||
var t = Util.find(store, ['proxy', 'teams']) || {};
|
||||
return Object.keys(t).some(function (id) {
|
||||
return t[id].channel === chanId;
|
||||
});
|
||||
}
|
||||
// Profile
|
||||
if (Util.find(store, ['proxy', 'profile', 'href'])) {
|
||||
return Hash.hrefToHexChannelId(Util.find(store, ['proxy', 'profile', 'href']))
|
||||
=== chanId;
|
||||
}
|
||||
};
|
||||
|
||||
var dropChannel = Store.dropChannel = function (chanId) {
|
||||
console.error('Drop channel', chanId);
|
||||
|
||||
|
@ -2451,6 +2474,14 @@ define([
|
|||
try {
|
||||
store.onlyoffice.leavePad(chanId);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
try {
|
||||
if (alwaysOnline(chanId)) {
|
||||
delete Store.channels[chanId];
|
||||
return;
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
try {
|
||||
Cache.leaveChannel(chanId);
|
||||
} catch (e) { console.error(e); }
|
||||
|
|
|
@ -8,9 +8,10 @@ define([
|
|||
'/customize/messages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'chainpad-listmap',
|
||||
'/lib/datepicker/flatpickr.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, FP, Crypto, ChainPad) {
|
||||
var Calendar = {};
|
||||
|
||||
var getStore = function (ctx, id) {
|
||||
|
@ -131,9 +132,9 @@ define([
|
|||
var last = ctx.store.data.lastVisit;
|
||||
|
||||
if (ev.isAllDay) {
|
||||
if (ev.startDay) { ev.start = +new Date(ev.startDay); }
|
||||
if (ev.startDay) { ev.start = +FP.parseDate(ev.startDay); }
|
||||
if (ev.endDay) {
|
||||
var endDate = new Date(ev.endDay);
|
||||
var endDate = FP.parseDate(ev.endDay);
|
||||
endDate.setHours(23);
|
||||
endDate.setMinutes(59);
|
||||
endDate.setSeconds(59);
|
||||
|
@ -223,7 +224,7 @@ define([
|
|||
};
|
||||
var addReminders = function (ctx, id, ev) {
|
||||
var calendar = ctx.calendars[id];
|
||||
if (!ev) { return; } // XXX deleted event remote: delete reminders
|
||||
if (!ev) { return; }
|
||||
if (!calendar || !calendar.reminders) { return; }
|
||||
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
|
||||
|
||||
|
@ -1063,7 +1064,6 @@ define([
|
|||
Calendar.init = function (cfg, waitFor, emit) {
|
||||
var calendar = {};
|
||||
var store = cfg.store;
|
||||
//if (!store.loggedIn || !store.proxy.edPublic) { return; } // XXX logged in only? we should al least allow read-only for URL calendars
|
||||
var ctx = {
|
||||
loggedIn: store.loggedIn && store.proxy.edPublic,
|
||||
store: store,
|
||||
|
|
|
@ -470,7 +470,6 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
|
|||
delete clone.previewChannel;
|
||||
members[curve] = clone;
|
||||
|
||||
// XXX
|
||||
var remaining = members[author].remaining || 1;
|
||||
if (remaining === -1) { return true; } // Infinite uses, keep the link
|
||||
if (remaining > 1) { // Remove 1 use
|
||||
|
|
|
@ -385,5 +385,9 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
SF.isSharedFolderChannel = function (chanId) {
|
||||
return Object.keys(allSharedFolders).includes(chanId);
|
||||
};
|
||||
|
||||
return SF;
|
||||
});
|
||||
|
|
|
@ -111,7 +111,7 @@ var init = function (client, cb) {
|
|||
if (data && data.state === "ALREADY_INIT") {
|
||||
debug('Store already exists!');
|
||||
self.store = data.returned;
|
||||
return void cb(data.returned);
|
||||
return void cb(data);
|
||||
}
|
||||
self.store = data;
|
||||
cb(data);
|
||||
|
|
|
@ -304,6 +304,10 @@ define([
|
|||
var newParent = exp.find(path);
|
||||
var tempName = exp.isFile(element) ? Hash.createChannelId() : key;
|
||||
var newName = exp.getAvailableName(newParent, tempName);
|
||||
if (Array.isArray(newParent)) {
|
||||
newParent.push(element);
|
||||
return;
|
||||
}
|
||||
newParent[newName] = element;
|
||||
};
|
||||
|
||||
|
@ -852,7 +856,7 @@ define([
|
|||
}
|
||||
}
|
||||
if (!Hash.isValidChannel(el.channel)) {
|
||||
// XXX delete channel? replace with parsed.channel?
|
||||
// FIXME delete channel? replace with parsed.channel?
|
||||
console.error('Remove invalid channel', el.channel, el);
|
||||
// toClean.push(id);
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ define([
|
|||
var obj = Util.clone(proxy.metadata || {});
|
||||
|
||||
for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) {
|
||||
if (typeof(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]) === "undefined") { // XXX "deleted folder" for restricted shared folders when viewer in a team
|
||||
if (typeof(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]) === "undefined") { // TODO "deleted folder" for restricted shared folders when viewer in a team
|
||||
continue;
|
||||
}
|
||||
var data = Util.clone(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]);
|
||||
|
|
|
@ -140,10 +140,15 @@ define([
|
|||
return text.trim();
|
||||
};
|
||||
|
||||
var isMobile = /Android|iPhone/i.test(navigator.userAgent);
|
||||
|
||||
module.mkIndentSettings = function (editor, metadataMgr) {
|
||||
var setIndentation = function (units, useTabs, fontSize, spellcheck, brackets) {
|
||||
if (typeof(units) !== 'number') { return; }
|
||||
var doc = editor.getDoc();
|
||||
if (isMobile && fontSize < 16) {
|
||||
fontSize = 16;
|
||||
}
|
||||
editor.setOption('indentUnit', units);
|
||||
editor.setOption('tabSize', units);
|
||||
editor.setOption('indentWithTabs', useTabs);
|
||||
|
|
|
@ -943,6 +943,50 @@ define([
|
|||
}, href);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) {
|
||||
var parsed = Utils.Hash.parsePadUrl(data.href);
|
||||
if (parsed.type === 'drive') {
|
||||
// Shared folder
|
||||
var secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, data.password);
|
||||
Cryptpad.addSharedFolder(null, secret, cb);
|
||||
} else {
|
||||
var _data = {
|
||||
password: data.password,
|
||||
href: data.href,
|
||||
channel: data.channel,
|
||||
title: data.title,
|
||||
owners: data.metadata ? data.metadata.owners : data.owners,
|
||||
expire: data.metadata ? data.metadata.expire : data.expire,
|
||||
forceSave: true
|
||||
};
|
||||
Cryptpad.setPadTitle(_data, function (err) {
|
||||
cb({error: err});
|
||||
});
|
||||
}
|
||||
|
||||
// Also add your mailbox to the metadata object
|
||||
var padParsed = Utils.Hash.parsePadUrl(data.href);
|
||||
var padSecret = Utils.Hash.getSecrets(padParsed.type, padParsed.hash, data.password);
|
||||
var padCrypto = Utils.Crypto.createEncryptor(padSecret.keys);
|
||||
try {
|
||||
var value = {};
|
||||
value[edPublic] = padCrypto.encrypt(JSON.stringify({
|
||||
notifications: notifications,
|
||||
curvePublic: curvePublic
|
||||
}));
|
||||
var msg = {
|
||||
channel: data.channel,
|
||||
command: 'ADD_MAILBOX',
|
||||
value: value
|
||||
};
|
||||
Cryptpad.setPadMetadata(msg, function (res) {
|
||||
if (res.error) { console.error(res.error); }
|
||||
});
|
||||
} catch (err) {
|
||||
return void console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Add or remove our mailbox from the list if we're an owner
|
||||
sframeChan.on('Q_UPDATE_MAILBOX', function (data, cb) {
|
||||
var metadata = data.metadata;
|
||||
|
@ -1024,7 +1068,7 @@ define([
|
|||
}
|
||||
var send = data.send;
|
||||
var metadata = data.metadata;
|
||||
var owner, owners;
|
||||
var owners = [];
|
||||
var _secret = secret;
|
||||
if (metadata && metadata.roHref) {
|
||||
var _parsed = Utils.Hash.parsePadUrl(metadata.roHref);
|
||||
|
@ -1037,24 +1081,24 @@ define([
|
|||
nThen(function (waitFor) {
|
||||
// Try to get the owner's mailbox from the pad metadata first.
|
||||
var todo = function (obj) {
|
||||
owners = obj.owners;
|
||||
|
||||
var mailbox;
|
||||
// Get the first available mailbox (the field can be an string or an object)
|
||||
// TODO maybe we should send the request to all the owners?
|
||||
if (typeof (obj.mailbox) === "string") {
|
||||
mailbox = obj.mailbox;
|
||||
} else if (obj.mailbox && obj.owners && obj.owners.length) {
|
||||
mailbox = obj.mailbox[obj.owners[0]];
|
||||
}
|
||||
if (mailbox) {
|
||||
var decrypt = function (mailbox) {
|
||||
try {
|
||||
var dataStr = crypto.decrypt(mailbox, true, true);
|
||||
var data = JSON.parse(dataStr);
|
||||
if (!data.notifications || !data.curvePublic) { return; }
|
||||
owner = data;
|
||||
return data;
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
if (typeof (obj.mailbox) === "string") {
|
||||
owners = [decrypt(obj.mailbox)];
|
||||
return;
|
||||
}
|
||||
if (!obj.mailbox || !obj.owners || !obj.owners.length) { return; }
|
||||
owners = obj.owners.map(function (edPublic) {
|
||||
var mailbox = obj.mailbox[edPublic];
|
||||
if (typeof(mailbox) !== "string") { return; }
|
||||
return decrypt(mailbox);
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
// If we already have metadata, use it, otherwise, try to get it
|
||||
|
@ -1069,7 +1113,7 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
// If we are just checking (send === false) and there is a mailbox field, cb state true
|
||||
if (!send) { return void cb({state: Boolean(owner)}); }
|
||||
if (!send) { return void cb({state: Boolean(owners.length)}); }
|
||||
|
||||
Cryptpad.padRpc.contactOwner({
|
||||
send: send,
|
||||
|
@ -1077,7 +1121,6 @@ define([
|
|||
query: data.query,
|
||||
msgData: data.msgData,
|
||||
channel: _secret.channel,
|
||||
owner: owner,
|
||||
owners: owners
|
||||
}, cb);
|
||||
});
|
||||
|
@ -1238,50 +1281,6 @@ define([
|
|||
});
|
||||
});
|
||||
|
||||
sframeChan.on('Q_ACCEPT_OWNERSHIP', function (data, cb) {
|
||||
var parsed = Utils.Hash.parsePadUrl(data.href);
|
||||
if (parsed.type === 'drive') {
|
||||
// Shared folder
|
||||
var secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, data.password);
|
||||
Cryptpad.addSharedFolder(null, secret, cb);
|
||||
} else {
|
||||
var _data = {
|
||||
password: data.password,
|
||||
href: data.href,
|
||||
channel: data.channel,
|
||||
title: data.title,
|
||||
owners: data.metadata.owners,
|
||||
expire: data.metadata.expire,
|
||||
forceSave: true
|
||||
};
|
||||
Cryptpad.setPadTitle(_data, function (err) {
|
||||
cb({error: err});
|
||||
});
|
||||
}
|
||||
|
||||
// Also add your mailbox to the metadata object
|
||||
var padParsed = Utils.Hash.parsePadUrl(data.href);
|
||||
var padSecret = Utils.Hash.getSecrets(padParsed.type, padParsed.hash, data.password);
|
||||
var padCrypto = Utils.Crypto.createEncryptor(padSecret.keys);
|
||||
try {
|
||||
var value = {};
|
||||
value[edPublic] = padCrypto.encrypt(JSON.stringify({
|
||||
notifications: notifications,
|
||||
curvePublic: curvePublic
|
||||
}));
|
||||
var msg = {
|
||||
channel: data.channel,
|
||||
command: 'ADD_MAILBOX',
|
||||
value: value
|
||||
};
|
||||
Cryptpad.setPadMetadata(msg, function (res) {
|
||||
if (res.error) { console.error(res.error); }
|
||||
});
|
||||
} catch (err) {
|
||||
return void console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
sframeChan.on('Q_IMPORT_MEDIATAG', function (obj, cb) {
|
||||
var key = obj.key;
|
||||
var channel = obj.channel;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
define([], function () {
|
||||
if (window.__CRYPTPAD_TEST_OBJ_) { return window.__CRYPTPAD_TEST_OBJ_; }
|
||||
/*
|
||||
// XXX localhost secureiframe fix
|
||||
var out = function () {};
|
||||
out.options = {};
|
||||
out.testing = false;
|
||||
|
|
|
@ -1577,5 +1577,6 @@
|
|||
"team_linkUsesInfinite": "(unbegrenzte Verwendungen)",
|
||||
"team_inviteUses": "Erlaubte Verwendung(en) dieses Links (0 = keine Begrenzung)",
|
||||
"team_linkUses": "({0}/{1} verbleibend)",
|
||||
"form_settingsButton": "Formulareinstellungen"
|
||||
"form_settingsButton": "Formulareinstellungen",
|
||||
"form_editable_on": "Einmalig und Bearbeiten"
|
||||
}
|
||||
|
|
|
@ -1577,5 +1577,6 @@
|
|||
"team_linkUses": "({0}/{1} restant)",
|
||||
"team_linkUsesInfinite": "(usages illimités)",
|
||||
"form_anonymized": "Réponses anonymisées",
|
||||
"form_settingsButton": "Réglages Formulaire"
|
||||
"form_settingsButton": "Réglages Formulaire",
|
||||
"form_editable_on": "Une seule fois et éditer"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
"pad": "रिच टेक्स्ट",
|
||||
"code": "कोड",
|
||||
"poll": "मतदान",
|
||||
"kanban": "कानबन"
|
||||
"kanban": "कानबन",
|
||||
"todo": "टुडू",
|
||||
"media": "मीडिया",
|
||||
"file": "फ़ाइल",
|
||||
"whiteboard": "व्हाइटबोर्ड",
|
||||
"drive": "क्रिप्टड्राइव",
|
||||
"slide": "मार्कडाउन स्लाइड्स"
|
||||
},
|
||||
"main_title": "क्रिप्टपैड: शून्य ज्ञान, सहयोगात्मक रीयल टाइम संपादन"
|
||||
}
|
||||
|
|
|
@ -1577,5 +1577,6 @@
|
|||
"team_linkUsesInfinite": "(unlimited uses)",
|
||||
"team_linkUses": "({0}/{1} remaining)",
|
||||
"form_anonymized": "Responses are anonymized",
|
||||
"form_settingsButton": "Form settings"
|
||||
"form_settingsButton": "Form settings",
|
||||
"form_editable_on": "One time and edit"
|
||||
}
|
||||
|
|
|
@ -88,14 +88,14 @@
|
|||
"newButton": "Создать",
|
||||
"uploadButton": "Загрузить файлы",
|
||||
"uploadButtonTitle": "Загрузить новый файл в ваш CryptDrive",
|
||||
"saveTemplateButton": "Сохранить как образец",
|
||||
"saveTemplatePrompt": "Выбрать название для образца",
|
||||
"templateSaved": "Образец сохранен!",
|
||||
"selectTemplate": "Выберите образец или нажмите Esc",
|
||||
"useTemplate": "Начать с образца?",
|
||||
"useTemplateOK": "Выбрать образец (Enter)",
|
||||
"template_import": "Импортировать образец",
|
||||
"template_empty": "Образцы отсутствуют",
|
||||
"saveTemplateButton": "Сохранить как шаблон",
|
||||
"saveTemplatePrompt": "Выберите название для шаблона",
|
||||
"templateSaved": "Шаблон сохранен!",
|
||||
"selectTemplate": "Выберите шаблон или нажмите Esc",
|
||||
"useTemplate": "Использовать шаблон?",
|
||||
"useTemplateOK": "Выбрать шаблон (Enter)",
|
||||
"template_import": "Импортировать шаблон",
|
||||
"template_empty": "Шаблоны отсутствуют",
|
||||
"presentButtonTitle": "Начать режим презентации",
|
||||
"backgroundButtonTitle": "Изменить фоновый цвет в презентации",
|
||||
"colorButtonTitle": "Изменить цвет шрифта в презентации",
|
||||
|
@ -357,7 +357,7 @@
|
|||
"settings_exportCancel": "Вы уверены, что хотите отменить экспорт? В следующий раз вам придется начинать все сначала.",
|
||||
"settings_export_reading": "Читаем ваше хранилище...",
|
||||
"settings_export_download": "Скачиваем и расшифровываем ваши документы...",
|
||||
"contacts_request": "<em>{0}</em> хотел бы добавить вас в список контактов. <b>Принять </b>?",
|
||||
"contacts_request": "<em>{0}</em> хотел(а) бы добавить Вас в свой список контактов. <b>Согласны</b>?",
|
||||
"contacts_confirmRemove": "Вы уверены, что хотите удалить <em>1{0}</em>2 из ваших контактов?",
|
||||
"register_acceptTerms": "Я принимаю <a>условия использования</a>",
|
||||
"register_warning": "Внимание",
|
||||
|
@ -412,7 +412,7 @@
|
|||
"burnAfterReading_generateLink": "Нажмите на кнопку ниже, чтобы создать ссылку.",
|
||||
"upload_size": "Размер",
|
||||
"upload_pending": "Ожидайте",
|
||||
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ",
|
||||
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ установленный на этом Диске",
|
||||
"upload_notEnoughSpaceBrief": "Недостаточно места",
|
||||
"upload_notEnoughSpace": "Недостаточно места для этого файла на вашем CryptDrive.",
|
||||
"settings_cursorShareTitle": "Делиться позицией моего курсора",
|
||||
|
@ -745,7 +745,7 @@
|
|||
"feedback_optout": "Если вы хотите отказаться, посетите <a>страницу настроек пользователя</a>, где вы найдете флажок для включения или отключения обратной связи с пользователем.",
|
||||
"feedback_privacy": "Мы заботимся о Вашей конфиденциальности и в то же время хотим, чтобы CryptPad был очень простым в использовании. Мы используем этот файл, чтобы выяснить, какие функции пользовательского интерфейса важны для наших пользователей, запрашивая его вместе с параметром, указывающим, какое действие было предпринято.",
|
||||
"feedback_about": "Если Вы читаете это, Вам, вероятно, было любопытно, почему CryptPad запрашивает веб-страницы при выполнении определенных действий.",
|
||||
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>.",
|
||||
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>",
|
||||
"header_logoTitle": "Перейти в Ваш CryptDrive",
|
||||
"features_f_cryptdrive0_note": "Возможность сохранять в вашем браузере посещенные документы, чтобы иметь возможность открывать их позже",
|
||||
"settings_padOpenLinkLabel": "Разрешить открытие прямой ссылки",
|
||||
|
@ -760,7 +760,7 @@
|
|||
"team_inviteLinkCopy": "Копировать ссылку",
|
||||
"team_inviteLinkCreate": "Создать ссылку",
|
||||
"team_inviteLinkErrorName": "Добавьте имя человека, которого Вы приглашаете. Они могут изменить это позже. ",
|
||||
"team_inviteLinkWarning": "Первый, кто получит доступ к этой ссылке, сможет присоединиться к этой команде и просмотреть её содержимое. Делитесь ею с осторожностью.",
|
||||
"team_inviteLinkWarning": "Те, кто получат к эту ссылку, смогут присоединиться к этой команде и просмотреть её содержимое. Делитесь ссылкой с осторожностью.",
|
||||
"team_inviteLinkLoading": "Создание Вашей ссылки",
|
||||
"team_inviteLinkNoteMsg": "Это сообщение будет показано до того, как получатель решит, присоединиться ли к этой команде.",
|
||||
"team_inviteLinkNote": "Добавьте личное сообщение",
|
||||
|
@ -939,7 +939,7 @@
|
|||
"admin_authError": "Только администраторы могут получить доступ к этой странице",
|
||||
"survey": "Опрос CryptPad",
|
||||
"crowdfunding_popup_text": "<h3>Нам нужна ваша помощь!</h3>Чтобы быть уверенными, что CryptPad активно развивается - рассмотрите возможность поддержки проекта через страницу OpenCollective, где Вы можете увидеть нашу <b>Дорожную карту</b> и<b>Цели финансирования</b>.",
|
||||
"crowdfunding_button2": "Помочь CryptPad",
|
||||
"crowdfunding_button2": "Помочь деньгами",
|
||||
"autostore_notAvailable": "Вы должны сохранить этот документ на Вашем CryptDrive, прежде чем сможете использовать эту функцию.",
|
||||
"autostore_forceSave": "Сохраните файл в Вашем CryptDrive",
|
||||
"autostore_saved": "Документ успешно сохранен на вашем CryptDrive!",
|
||||
|
@ -1166,9 +1166,9 @@
|
|||
"support_cat_bug": "Сообщить об ошибке",
|
||||
"support_cat_data": "Пропажа содержимого",
|
||||
"support_cat_account": "Учетная запись пользователя",
|
||||
"info_privacyFlavour": "Наша <a>политика конфиденциальности</a> описывает, как мы обрабатываем Ваши данные.",
|
||||
"info_privacyFlavour": "<a>Политика конфиденциальности</a> этого экземпляра CryptPad",
|
||||
"user_about": "О CryptPad",
|
||||
"info_imprintFlavour": "<a>Правовая информация об администраторах данного экземпляра</a>.",
|
||||
"info_imprintFlavour": "<a>Правовая информация</a> об администраторах данного экземпляра",
|
||||
"settings_safeLinkDefault": "Безопасные ссылки теперь включены по умолчанию. Для копирования ссылок используйте меню <i></i><b>Поделиться</b>, а не адресную строку браузера.",
|
||||
"slide_textCol": "Цвет текста",
|
||||
"slide_backCol": "Цвет фона",
|
||||
|
@ -1386,6 +1386,196 @@
|
|||
"bounce_confirm": "Вы покинете: {0}\n\nВы точно хотите перейти к \"{1}\"?",
|
||||
"ui_restore": "Восстановить",
|
||||
"ui_archive": "Архивировать",
|
||||
"ui_undefined": "неизвестный",
|
||||
"admin_documentType": "Тип"
|
||||
"ui_undefined": "неизвестно",
|
||||
"admin_documentType": "Тип документа",
|
||||
"form_settingsButton": "Настройки формы",
|
||||
"form_anonymized": "Ответы анонимизированы",
|
||||
"team_linkUses": "({0}/{1} осталось)",
|
||||
"team_linkUsesInfinite": "(неограниченное использование)",
|
||||
"form_editable_str": "Подача",
|
||||
"form_multiple_edit": "Многократно и редактировать/удалять",
|
||||
"form_multiple": "Многократно",
|
||||
"form_editable_on_del": "Один раз и отредактировать/удалить",
|
||||
"form_editable_off": "Только один раз",
|
||||
"form_allowNotifications": "Уведомления о новых ответах",
|
||||
"form_answer_new": "Отправить снова",
|
||||
"form_deleteAll": "Удалить все",
|
||||
"form_responseNotification": "Новые ответы на форму: <b>{0}</b>",
|
||||
"form_alreadyAnsweredMult": "Вы ответили на эту форму:",
|
||||
"form_exportJSON": "Экспорт в JSON",
|
||||
"team_inviteUses": "Сколько раз можно использовать эту ссылку (0 = без ограничений)",
|
||||
"team_inviteRole": "Начальная роль",
|
||||
"calendar_removeNotification": "Удалить напоминание",
|
||||
"calendar_rec_warn_updateall": "Правило для повторения этого события было изменено. Первое событие на {0} будет сохранено, все остальные будут заменены.",
|
||||
"calendar_rec_warn_update": "Правило для повторения этого события было изменено. Будущие события будут заменены.",
|
||||
"calendar_rec_warn_delall": "Это событие больше не повторится. Первое событие на {0} будет сохранено, все остальные будут удалены.",
|
||||
"calendar_rec_warn_del": "Это событие больше не повторится. Будущие события будут удалены.",
|
||||
"calendar_nth_5": "пятый",
|
||||
"calendar_nth_last": "последний",
|
||||
"calendar_rec_monthly_nth": "Каждый {0} {1} месяца",
|
||||
"calendar_rec_yearly_nth": "Каждый {0} {1} {2}",
|
||||
"calendar_rec_every_date": "Каждый {0}",
|
||||
"calendar_nth_4": "четвёртый",
|
||||
"calendar_month_last": "последний день",
|
||||
"calendar_nth_3": "третий",
|
||||
"calendar_list": "{0}, {1}",
|
||||
"calendar_nth_2": "второй",
|
||||
"calendar_list_end": "{0} или {1}",
|
||||
"calendar_nth_1": "первый",
|
||||
"calendar_str_yearly": "{0} год(ы)",
|
||||
"calendar_str_monthly": "{0} месяц(ы)",
|
||||
"calendar_rec_monthly_pick": "По дням",
|
||||
"calendar_str_weekly": "{0} недель",
|
||||
"calendar_rec_until_count2": "раз",
|
||||
"calendar_rec_until_count": "После",
|
||||
"calendar_str_daily": "{0} день",
|
||||
"calendar_rec_until_date": "На",
|
||||
"calendar_str_day": "на {0}",
|
||||
"calendar_rec_until_no": "Никогда",
|
||||
"calendar_rec_until": "Прекратить повторение",
|
||||
"calendar_str_monthday": "на {0}",
|
||||
"calendar_rec_freq_yearly": "годы",
|
||||
"calendar_str_nthdayofmonth": "на {0} день {1}",
|
||||
"calendar_str_for": "{0} раз",
|
||||
"calendar_rec_freq_monthly": "месяцы",
|
||||
"calendar_str_until": "до {0}",
|
||||
"calendar_rec_freq_weekly": "недели",
|
||||
"calendar_rec_freq_daily": "дни",
|
||||
"calendar_str_filter": "Фильтры:",
|
||||
"calendar_rec_txt": "Повторять каждый",
|
||||
"calendar_str_filter_month": "Месяцы: {0}",
|
||||
"calendar_str_filter_weekno": "Недели: {0}",
|
||||
"calendar_str_filter_yearday": "Дни года: {0}",
|
||||
"calendar_rec_custom": "Пользовательское",
|
||||
"calendar_str_filter_monthday": "Дни месяца: {0}",
|
||||
"calendar_rec_weekdays": "Ежедневно по будням",
|
||||
"calendar_str_filter_day": "Дни: {0}",
|
||||
"calendar_rec_edit": "Это повторяющееся событие",
|
||||
"calendar_rec_weekend": "Ежедневно по выходным",
|
||||
"calendar_rec_edit_one": "Редактировать только это событие",
|
||||
"calendar_rec_yearly": "Ежегодно по {2}",
|
||||
"calendar_rec_edit_from": "Редактировать будущие события",
|
||||
"calendar_rec_monthly": "Ежемесячно, день {1}",
|
||||
"calendar_rec_edit_all": "Редактировать все события",
|
||||
"calendar_rec_weekly": "Еженедельно по {0}",
|
||||
"calendar_rec_stop": "Прекратить повторение",
|
||||
"calendar_rec_daily": "Ежедневно",
|
||||
"calendar_rec_updated": "Правило обновлено {0}",
|
||||
"calendar_rec_no": "Один раз",
|
||||
"calendar_rec": "Повтор",
|
||||
"fm_rmFilter": "Удалить фильтр",
|
||||
"fm_filterBy": "Фильтр",
|
||||
"admin_conflictExplanation": "Существуют две версии этого документа. Восстановление архивной версии приведет к перезаписи текущей версии. Архивирование текущей версии приведет к перезаписи архивной версии. Ни одно из действий не может быть отменено.",
|
||||
"admin_documentConflict": "Архивировать/восстановить",
|
||||
"og_encryptedAppType": "Зашифровано {0}",
|
||||
"ui_jsRequired": "Для выполнения шифрования в вашем браузере должен быть включен JavaScript",
|
||||
"og_features": "{0} Возможности",
|
||||
"og_pricing": "{0} Цены",
|
||||
"og_contact": "{0} Контакт",
|
||||
"og_register": "Создать учетную запись на {0}",
|
||||
"og_login": "Войти в {0}",
|
||||
"og_default": "CryptPad: пакет для совместной работы со сквозным шифрованием",
|
||||
"og_teamDrive": "Диск команды",
|
||||
"admin_getRawMetadata": "История метаданных",
|
||||
"admin_planlimit": "Лимит хранилища",
|
||||
"admin_restoreDocument": "Восстановить документ из архива",
|
||||
"admin_archiveDocument": "Поместить документ в архив",
|
||||
"admin_restoreArchivedPins": "Восстановить журнал пин-кодов из архива",
|
||||
"admin_archivePinLog": "Архивировать лог пинов этой учетной записи",
|
||||
"admin_getPinList": "Текущий список пинов",
|
||||
"admin_restoreBlock": "Восстановить блок из архива",
|
||||
"admin_archiveBlock": "Архивировать блок",
|
||||
"admin_blockArchived": "Блок помещён в архив",
|
||||
"admin_blockAvailable": "Блок доступен",
|
||||
"admin_blockKey": "Публичный ключ блока",
|
||||
"admin_pinLogArchived": "Журнал пинов находится в архиве",
|
||||
"admin_pinLogAvailable": "Журнал пин-кодов доступен",
|
||||
"admin_fileCount": "Количество файлов",
|
||||
"admin_channelCount": "Количество документов",
|
||||
"admin_storageUsage": "Размер данных",
|
||||
"admin_note": "Примечание к тарифному плану",
|
||||
"admin_planName": "Название тарифного плана",
|
||||
"admin_currentlyOnline": "В настоящее время онлайн",
|
||||
"admin_lastPinTime": "Время активности второго PIN-а",
|
||||
"admin_firstPinTime": "Время активности первого PIN-а",
|
||||
"admin_accountMetadataPlaceholder": "Идентификатор пользователя (открытый ключ подписи)",
|
||||
"admin_blockMetadataPlaceholder": "Абсолютный или относительный URL-адрес блока",
|
||||
"admin_blockMetadataHint": "Блок информации о логине — это то, что позволяет войти в CryptPad под своей учётной записью с помощью комбинации имени пользователя и пароля",
|
||||
"admin_blockMetadataTitle": "Блок информации о логине",
|
||||
"admin_documentMetadataPlaceholder": "URL-адрес или идентификатор документа",
|
||||
"admin_channelArchived": "Архивировано",
|
||||
"admin_channelAvailable": "Доступно",
|
||||
"admin_currentlyOpen": "Сейчас открыто",
|
||||
"admin_documentModifiedTime": "Последнее изменение",
|
||||
"admin_documentCreationTime": "Создано",
|
||||
"admin_documentMetadata": "Метаданные на данный момент",
|
||||
"admin_documentSize": "Размер документа",
|
||||
"admin_documentMetadataHint": "Запросить документ или файл по его идентификатору или URL-адресу",
|
||||
"admin_documentMetadataTitle": "Информация о документе",
|
||||
"admin_accountMetadataHint": "Введите открытый ключ пользователя, чтобы получить данные об его учетной записи.",
|
||||
"admin_accountMetadataTitle": "Информация об учетной записи",
|
||||
"admin_restoreReason": "Пожалуйста, укажите причину восстановления и подтвердите, что Вы хотели бы продолжить",
|
||||
"admin_archiveReason": "Пожалуйста, укажите причину архивации и подтвердите, что Вы хотели бы продолжить",
|
||||
"ui_confirm": "Подтвердить",
|
||||
"ui_fetch": "Загрузить (fetch)",
|
||||
"ui_success": "Успешно",
|
||||
"ui_generateReport": "Создание отчета",
|
||||
"ui_none": "нет значения",
|
||||
"ui_false": "нет",
|
||||
"ui_true": "да",
|
||||
"admin_generatedAt": "Дата и время отчёта",
|
||||
"admin_cat_database": "База данных",
|
||||
"admin_uptimeHint": "Дата и время, в которое был запущен сервер",
|
||||
"admin_uptimeTitle": "Время запуска",
|
||||
"register_instance": "Создание новой учетной записи на {0}",
|
||||
"login_instance": "Подключитесь к своей учетной записи на {0}",
|
||||
"home_morestorage": "Чтобы получить больше места:",
|
||||
"home_location": "Зашифрованные данные размещены в {0}",
|
||||
"footer_website": "Веб-сайт проекта",
|
||||
"admin_noticeHint": "Необязательное сообщение для отображения на главной странице",
|
||||
"admin_noticeTitle": "Уведомление на домашней странице",
|
||||
"ui_experimental": "Эта функция считается экспериментальной.",
|
||||
"error_evalPermitted": "Прервано, потому что eval не должно быть разрешено.\n\nЭта ошибка связана с заголовками политики безопасности содержимого (Content-Security-Policy headers), это может быть связано с: устаревшим браузером, который их не поддерживает, расширениями браузера, которые мешают их правильному поведению, или неправильной конфигурацией этого экземпляра CryptPad.",
|
||||
"error_incorrectAccess": "Доступ к этой странице возможен только через {0}.",
|
||||
"error_embeddingDisabledSpecific": "Встраивание отключено для этого приложения CryptPad.",
|
||||
"error_embeddingDisabled": "Встраивание отключено для этого экземпляра CryptPad",
|
||||
"admin_enableembedsHint": "Разрешить встраивать документы и носители из этого экземпляра на другие веб-сайты. Это добавит опцию 'Встроить' в меню 'Поделиться'. По соображениям безопасности приложения, использующие OnlyOffice (Листы, Документ, Презентация), не могут быть встроены, даже если этот параметр активен.",
|
||||
"admin_enableembedsTitle": "Включить удаленное встраивание",
|
||||
"ui_ms": "миллисекунд",
|
||||
"admin_setDuration": "Установить продолжительность",
|
||||
"admin_bytesWrittenHint": "Если Вы включили измерение производительности диска, то продолжительность окна можно настроить ниже.",
|
||||
"admin_bytesWrittenTitle": "Окно измерения производительности диска",
|
||||
"admin_enableDiskMeasurementsHint": "Если включено, эндпойнт JSON API будет доступен в разделе <code>/api/profiling</code>. Это позволяет поддерживать текущее измерение дискового ввода-вывода в пределах временного окна, установленного ниже. Этот параметр может повлиять на производительность сервера и может привести к раскрытию конфиденциальных данных. Рекомендуется оставить этот параметр отключенным, если только Вы не знаете, что делаете.",
|
||||
"admin_enableDiskMeasurementsTitle": "Измерение производительности диска",
|
||||
"admin_infoNotice2": "Подробности смотрите на вкладке 'Сеть'.",
|
||||
"admin_infoNotice1": "Используйте следующие поля для описания Вашего экземпляра. Эта информация используется на главной странице экземпляра. Она также отправляется как часть телеметрии сервера, если Вы хотите быть включенным в список общедоступных экземпляров CryptPad.",
|
||||
"admin_reviewCheckupNotice": "Рекомендуется просмотреть страницу <a>проверки</a>, чтобы убедиться, что этот экземпляр настроен правильно.",
|
||||
"admin_cacheEvictionRequired": "Сервер был обновлен с учетом новых настроек. Пожалуйста, используйте кнопку <b>Очистить кэш</b>, чтобы убедиться, что это изменение станет видимым для всех пользователей.",
|
||||
"fivehundred_internalServerError": "Внутренняя ошибка сервера",
|
||||
"support_debuggingDataHint": "Следующая информация будет включена в отправленные Вами обращения в службу поддержки. Ничто из этого не позволяет администраторам получать доступ к Вашим документам или расшифровывать их. Эта информация зашифрована таким образом, что только администраторы могут ее прочитать.",
|
||||
"support_debuggingDataTitle": "Отладочные данные учётной записи",
|
||||
"support_cat_debugging": "Отладочные данные",
|
||||
"ui_openDirectly": "Эта функция недоступна когда (документ) CryptPad встроен в другой сайт. Открыть этот документ на отдельной вкладке браузера?",
|
||||
"support_cat_abuse": "Нарушены Условия Обслуживания",
|
||||
"support_cat_document": "Документ",
|
||||
"support_cat_drives": "Хранилище (Drive) или Команда",
|
||||
"support_warning_other": "О чём Ваш запрос? Пожалуйста, предоставьте как можно больше актуальной информации, чтобы нам было легче решить вашу проблему быстро",
|
||||
"support_warning_abuse": "Пожалуйста, сообщайте о контенте, который нарушает <a>Условия Обслуживания</a>. Пожалуйста, предоставьте ссылки на оскорбительные документы или профили пользователей и опишите, как они нарушают условия. Любая дополнительная информация о контексте, в котором Вы обнаружили контент или поведение, может помочь администраторам предотвратить будущие нарушения",
|
||||
"support_warning_bug": "Пожалуйста, укажите, в каком браузере возникает проблема и установлены ли какие-либо расширения. Пожалуйста, предоставьте как можно больше подробностей о проблеме и шагах, необходимых для ее воспроизведения",
|
||||
"support_warning_document": "Пожалуйста, укажите, какой тип документа вызывает проблему, и укажите <a>идентификатор документа</a> или ссылку на документ",
|
||||
"support_warning_drives": "Обратите внимание, что у администраторов нет возможности находить папки и документы по имени. Для общих папок, пожалуйста, укажите <a>идентификатор документа</a>",
|
||||
"support_warning_account": "Пожалуйста, обратите внимание, что администраторы не могут сменить Ваш пароль. Если Вы потеряли учетные данные для своей учетной записи, но всё ещё авторизованы в системе, Вы можете <a>перенести свои данные в новую учетную запись</a>",
|
||||
"support_warning_prompt": "Пожалуйста, выберите наиболее подходящую категорию для вашего вопроса. Это помогает администраторам определять срочность и сложность проблемы, и дает дополнительные рекомендации относительно того, какую информацию следует предоставлять",
|
||||
"info_sourceFlavour": "<a>Исходный код</a> CryptPad",
|
||||
"info_termsFlavour": "<a>Условия обслуживания</a> в этом экземпляре CryptPad",
|
||||
"footer_source": "Исходный код",
|
||||
"admin_jurisdictionHint": "Страна, в которой размещены зашифрованные данные этого экземпляра",
|
||||
"admin_jurisdictionTitle": "Местоположение хостинга",
|
||||
"admin_descriptionHint": "Текстовое описание, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
|
||||
"admin_descriptionTitle": "Описание экземпляра CryptPad",
|
||||
"ui_saved": "{0} сохранено",
|
||||
"admin_nameHint": "Имя, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
|
||||
"admin_nameTitle": "Имя экземпляря CryptPad",
|
||||
"admin_archiveNote": "Заметка",
|
||||
"form_editable_on": "Один раз и отредактировать"
|
||||
}
|
||||
|
|
|
@ -294,6 +294,7 @@ define([
|
|||
|
||||
Messages.convertPage = "Convert"; // XXX 4.11.0
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterwards."; // XXX 4.11.0
|
||||
Messages.convert_unsupported = "UNSUPPORTED FILE TYPE :("; // XXX
|
||||
|
||||
var createToolbar = function () {
|
||||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||||
|
@ -328,7 +329,6 @@ define([
|
|||
type: 'file'
|
||||
});
|
||||
APP.$rightside.append([hint, picker]);
|
||||
Messages.convert_unsupported = "UNSUPPORTED FILE TYPE :("; // XXX
|
||||
|
||||
$(picker).on('change', function () {
|
||||
APP.$rightside.find('button, div.notice').remove();
|
||||
|
|
|
@ -148,7 +148,7 @@ define([
|
|||
// if metadata is too large, drop the thumbnail.
|
||||
if (plaintext.length > 65535) {
|
||||
var temp = JSON.parse(JSON.stringify(metadata));
|
||||
delete metadata.thumbnail;
|
||||
delete temp.thumbnail;
|
||||
plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp));
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.flatpickr-calendar.open {
|
||||
z-index: 100001 !important; // Alertify is 100000
|
||||
}
|
||||
|
||||
@palette0: @cp_kanban-color0; // Default bg color for header
|
||||
@form-colors: @cp_form-palette;
|
||||
.form-colors(@form-colors; @index) when (@index > 0){
|
||||
|
|
|
@ -2752,7 +2752,8 @@ define([
|
|||
UI.confirmButton(button, {classes:'danger'}, function () {
|
||||
var sframeChan = framework._.sfCommon.getSframeChannel();
|
||||
sframeChan.query('Q_FORM_DELETE_ALL_ANSWERS', {
|
||||
channel: content.answers.channel
|
||||
channel: content.answers.channel,
|
||||
teamId: typeof(owned) === "number" ? owned : undefined
|
||||
}, function (err, obj) {
|
||||
if (err || (obj && obj.error)) { return void UI.warn(Messages.error); }
|
||||
APP.getResults();
|
||||
|
@ -4004,7 +4005,7 @@ define([
|
|||
$container.empty().append(_content);
|
||||
|
||||
|
||||
// XXX Delete key form_updateMsg
|
||||
// XXX Delete translation key form_updateMsg
|
||||
if (editable) {
|
||||
var responseMsg = h('div.cp-form-response-msg-container');
|
||||
var $responseMsg = $(responseMsg).appendTo($container);
|
||||
|
|
|
@ -141,12 +141,13 @@ define([
|
|||
CPNetflux = _CPNetflux;
|
||||
Pinpad = _Pinpad;
|
||||
}));
|
||||
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
|
||||
Cryptpad.getAccessKeys(w(function (_keys) {
|
||||
if (!Array.isArray(_keys)) { return; }
|
||||
accessKeys = _keys;
|
||||
|
||||
_keys.some(function (_k) {
|
||||
if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) {
|
||||
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
|
||||
myKeys = _k;
|
||||
return true;
|
||||
}
|
||||
|
@ -313,7 +314,7 @@ define([
|
|||
if (obj && obj.error) { err = obj.error; return; }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) {
|
||||
// XXX TODO delete from drive.forms
|
||||
// TODO delete from drive.forms?
|
||||
return;
|
||||
}
|
||||
if (obj.lastKnownHash !== answer.hash) { return; }
|
||||
|
@ -411,7 +412,7 @@ define([
|
|||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
|
||||
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.clearOwnedChannel(data.channel, cb);
|
||||
Cryptpad.clearOwnedChannel(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
|
||||
if (!deleteLines) {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
|
||||
<head>
|
||||
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript></noscript>
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
define([
|
||||
'jquery',
|
||||
'/customize/login.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/common-credential.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/common-constants.js',
|
||||
'/common/common-feedback.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/pages.js',
|
||||
'/common/rpc.js',
|
||||
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages, Rpc) {
|
||||
if (window.top !== window) { return; }
|
||||
var Messages = Cryptpad.Messages;
|
||||
$(function () {
|
||||
if (LocalStore.isLoggedIn()) {
|
||||
// already logged in, redirect to drive
|
||||
document.location.href = '/drive/';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// text and password input fields
|
||||
var $token = $('#installtoken');
|
||||
var $uname = $('#username');
|
||||
var $passwd = $('#password');
|
||||
var $confirm = $('#password-confirm');
|
||||
|
||||
[ $token, $uname, $passwd, $confirm]
|
||||
.some(function ($el) { if (!$el.val()) { $el.focus(); return true; } });
|
||||
|
||||
// checkboxes
|
||||
var $register = $('button#register');
|
||||
|
||||
var registering = false;
|
||||
|
||||
var I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = false;
|
||||
var br = function () { return h('br'); };
|
||||
|
||||
// If the token is provided in the URL, hide the field
|
||||
var token;
|
||||
if (window.location.hash) {
|
||||
var hash = window.location.hash.slice(1);
|
||||
if (hash.length === 64) {
|
||||
token = hash;
|
||||
$token.hide();
|
||||
console.log(`Install token: ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
var registerClick = function () {
|
||||
var uname = $uname.val().trim();
|
||||
// trim whitespace surrounding the username since it is otherwise included in key derivation
|
||||
// most people won't realize that its presence is significant
|
||||
$uname.val(uname);
|
||||
|
||||
var passwd = $passwd.val();
|
||||
var confirmPassword = $confirm.val();
|
||||
|
||||
if (!token) { token = $token.val().trim(); }
|
||||
|
||||
var shouldImport = false;
|
||||
var doesAccept;
|
||||
try {
|
||||
// if this throws there's either a horrible bug (which someone will report)
|
||||
// or the instance admins did not configure a terms page.
|
||||
doesAccept = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (Cred.isEmail(uname) && !I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME) {
|
||||
var emailWarning = [
|
||||
Messages.register_emailWarning0,
|
||||
br(), br(),
|
||||
Messages.register_emailWarning1,
|
||||
br(), br(),
|
||||
Messages.register_emailWarning2,
|
||||
br(), br(),
|
||||
Messages.register_emailWarning3,
|
||||
];
|
||||
|
||||
Feedback.send("EMAIL_USERNAME_WARNING", true);
|
||||
|
||||
return void UI.confirm(emailWarning, function (yes) {
|
||||
if (!yes) { return; }
|
||||
I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = true;
|
||||
registerClick();
|
||||
});
|
||||
}
|
||||
|
||||
/* basic validation */
|
||||
if (!Cred.isLongEnoughPassword(passwd)) {
|
||||
var warning = Messages._getKey('register_passwordTooShort', [
|
||||
Cred.MINIMUM_PASSWORD_LENGTH
|
||||
]);
|
||||
return void UI.alert(warning, function () {
|
||||
registering = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (passwd !== confirmPassword) { // do their passwords match?
|
||||
return void UI.alert(Messages.register_passwordsDontMatch);
|
||||
}
|
||||
|
||||
if (Pages.customURLs.terms && !doesAccept) { // do they accept the terms of service? (if they exist)
|
||||
return void UI.alert(Messages.register_mustAcceptTerms);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
var span = h('span', [
|
||||
h('h2', [
|
||||
h('i.fa.fa-warning'),
|
||||
' ',
|
||||
Messages.register_warning,
|
||||
]),
|
||||
Messages.register_warning_note
|
||||
]);
|
||||
|
||||
UI.confirm(span,
|
||||
function (yes) {
|
||||
if (!yes) { return; }
|
||||
|
||||
Login.loginOrRegisterUI(uname, passwd, true, shouldImport, false, 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) {
|
||||
if (e) { UI.alert(Messages.error); return console.error(e); }
|
||||
window.location.href = '/drive/';
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
registering = true;
|
||||
}, {
|
||||
ok: Messages.register_writtenPassword,
|
||||
cancel: Messages.register_cancel,
|
||||
/* If we're certain that we aren't using these "*Class" APIs
|
||||
anywhere else then we can deprecate them and make this a
|
||||
custom modal in common-interface (or here). */
|
||||
cancelClass: 'btn.btn-cancel.btn-register',
|
||||
okClass: 'btn.btn-danger.btn-register',
|
||||
reverseOrder: true,
|
||||
done: function ($dialog) {
|
||||
$dialog.find('> div').addClass('half');
|
||||
},
|
||||
});
|
||||
}, 150);
|
||||
};
|
||||
|
||||
$register.click(registerClick);
|
||||
|
||||
var clickRegister = Util.notAgainForAnother(function () {
|
||||
$register.click();
|
||||
}, 500);
|
||||
|
||||
$register.on('keypress', function (e) {
|
||||
if (e.which === 13) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return clickRegister();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -79,7 +79,7 @@ define([
|
|||
Env.metadataMgr.updateMetadata(md);
|
||||
};
|
||||
|
||||
var sendReplyNotification = function(Env, uid) {
|
||||
var sendReplyNotification = function(Env, uid, mentionedCurve) {
|
||||
if (!Env.comments || !Env.comments.data || !Env.comments.authors) { return; }
|
||||
if (!Env.common.isLoggedIn()) { return; }
|
||||
var thread = Env.comments.data[uid];
|
||||
|
@ -88,8 +88,6 @@ define([
|
|||
var privateData = Env.metadataMgr.getPrivateData();
|
||||
var others = {};
|
||||
|
||||
|
||||
// XXX mentioned users should be excluded from the list of notified recipients to avoid notifying them twice
|
||||
// Get all the other registered users with a mailbox
|
||||
thread.m.forEach(function(obj) {
|
||||
var u = obj.u;
|
||||
|
@ -97,6 +95,9 @@ define([
|
|||
var author = Env.comments.authors[u];
|
||||
if (!author || others[u] || !author.notifications || !author.curvePublic) { return; }
|
||||
if (author.curvePublic === userData.curvePublic) { return; } // don't send to yourself
|
||||
if (Object.keys(mentionedCurve || {}).includes(author.curvePublic)) {
|
||||
return; // Don't send to mentioned users
|
||||
}
|
||||
others[u] = {
|
||||
curvePublic: author.curvePublic,
|
||||
comment: obj.m,
|
||||
|
@ -203,7 +204,7 @@ define([
|
|||
});
|
||||
|
||||
// Push the content
|
||||
cb(content);
|
||||
cb(content, notify);
|
||||
});
|
||||
$(cancel).click(function(e) {
|
||||
e.stopPropagation();
|
||||
|
@ -525,7 +526,7 @@ define([
|
|||
$(reply).click(function(e) {
|
||||
e.stopPropagation();
|
||||
$actions.hide();
|
||||
var form = getCommentForm(Env, key, function(val) {
|
||||
var form = getCommentForm(Env, key, function(val, mentioned) {
|
||||
// Show the "reply" and "resolve" buttons again
|
||||
$(form).closest('.cp-comment-container')
|
||||
.find('.cp-comment-actions').css('display', '');
|
||||
|
@ -551,7 +552,7 @@ define([
|
|||
});
|
||||
|
||||
// Notify other users
|
||||
sendReplyNotification(Env, key);
|
||||
sendReplyNotification(Env, key, mentioned);
|
||||
|
||||
// Send to chainpad
|
||||
updateMetadata(Env);
|
||||
|
|
|
@ -903,6 +903,10 @@ define([
|
|||
$toc.addClass('hidden');
|
||||
localHide = true;
|
||||
if (store) { store.put(key, '1'); }
|
||||
|
||||
if (APP.tocScroll) {
|
||||
APP.tocScroll();
|
||||
}
|
||||
});
|
||||
$(showBtn).click(function () {
|
||||
$toc.removeClass('hidden');
|
||||
|
@ -922,6 +926,23 @@ define([
|
|||
e.stopPropagation();
|
||||
if (!obj.el || UIElements.isVisible(obj.el, $contentContainer)) { return; }
|
||||
obj.el.scrollIntoView();
|
||||
var $iframe = $('iframe').contents();
|
||||
var onScroll = function () {
|
||||
APP.tocScrollOff();
|
||||
};
|
||||
APP.tocScrollOff = function () {
|
||||
delete APP.tocScroll;
|
||||
delete APP.tocScrollOff;
|
||||
$iframe.off('scroll', onScroll);
|
||||
};
|
||||
APP.tocScroll = function () {
|
||||
obj.el.scrollIntoView();
|
||||
APP.tocScrollOff();
|
||||
};
|
||||
//$(window).on('scroll', onScroll);
|
||||
setTimeout(function () {
|
||||
$iframe.on('scroll', onScroll);
|
||||
});
|
||||
});
|
||||
a.innerHTML = title;
|
||||
content.push(h('p.cp-pad-toc-'+level, a));
|
||||
|
@ -1003,6 +1024,8 @@ define([
|
|||
if (scrollMax) {
|
||||
$iframe.scrollTop($iframe.innerHeight());
|
||||
}
|
||||
|
||||
if (APP.tocScrollOff) { APP.tocScrollOff(); }
|
||||
});
|
||||
|
||||
framework.setTextContentGetter(function() {
|
||||
|
@ -1017,6 +1040,8 @@ define([
|
|||
return str;
|
||||
});
|
||||
framework.setContentGetter(function() {
|
||||
if (APP.tocScrollOff) { APP.tocScrollOff(); }
|
||||
|
||||
$inner.find('span[data-cke-display-name="media-tag"]:empty').each(function(i, el) {
|
||||
$(el).remove();
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ define([
|
|||
};
|
||||
window.rc = requireConfig;
|
||||
window.apiconf = ApiConfig;
|
||||
// XXX extra sandboxing features are temporarily disabled as I suspect this is the cause of a regression in Safari
|
||||
// FIXME extra sandboxing features are temporarily disabled as I suspect this is the cause of a regression in Safari
|
||||
$('#sbox-secure-iframe')/*.attr('sandbox', 'allow-scripts allow-popups allow-modals')*/.attr('src',
|
||||
ApiConfig.httpSafeOrigin + '/secureiframe/inner.html?' + requireConfig.urlArgs +
|
||||
'#' + encodeURIComponent(JSON.stringify(req)));
|
||||
|
|
|
@ -147,6 +147,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: @browser_media-medium-screen) {
|
||||
#cp-app-slide-editor {
|
||||
#cp-app-slide-editor-container {
|
||||
.CodeMirror-sizer > div {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Slide position (print mode) */
|
||||
@ratio:0.9;
|
||||
@media print {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// This file is used when a user tries to export the entire CryptDrive.
|
||||
// Pads from the code app will be exported using this format instead of plain text.
|
||||
define([
|
||||
'/bower_components/secure-fabric.js/dist/fabric.min.js',
|
||||
'/lib/fabric.min.js',
|
||||
], function () {
|
||||
var module = {};
|
||||
|
||||
|
|
Loading…
Reference in New Issue