Merge branch 'staging' into http-workers

This commit is contained in:
ansuz 2023-03-02 11:59:58 +05:30
commit b003d4d825
52 changed files with 1169 additions and 155 deletions

View file

@ -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

View file

@ -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;

View file

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

View file

@ -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 () {

View file

@ -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)
])
]),
])
]);
};
});

View file

@ -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;
}
}
}
}
}
}

View file

@ -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)) {

View file

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

View file

@ -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];

View file

@ -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) {

View file

@ -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
};

View file

@ -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) {

View file

@ -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;
}

View file

@ -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) {

4
package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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;

23
scripts/clear.js Normal file
View file

@ -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');
});

40
scripts/install.js Normal file
View file

@ -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`);
});

View file

@ -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'),

View file

@ -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),
]);

View file

@ -146,6 +146,9 @@
flex: 1;
max-width: 100%;
resize: none;
.CodeMirror-sizer > div {
padding-bottom: 100px;
}
}
#cp-app-code-preview {
display: none !important;

View file

@ -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) {

View file

@ -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';

View file

@ -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;
}

View file

@ -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 () {
}, waitFor());
});
}).nThen(function () {
cb({state: true});
});
return;
}
return void cb({state: true});
}
cb({state: false});
};
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); }

View file

@ -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,

View file

@ -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

View file

@ -385,5 +385,9 @@ define([
});
};
SF.isSharedFolderChannel = function (chanId) {
return Object.keys(allSharedFolders).includes(chanId);
};
return SF;
});

View file

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

View file

@ -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);
}

View file

@ -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]);

View file

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

View file

@ -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;

View file

@ -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;

View file

@ -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"
}

View file

@ -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"
}

View file

@ -3,7 +3,13 @@
"pad": "रिच टेक्स्ट",
"code": "कोड",
"poll": "मतदान",
"kanban": "कानबन"
"kanban": "कानबन",
"todo": "टुडू",
"media": "मीडिया",
"file": "फ़ाइल",
"whiteboard": "व्हाइटबोर्ड",
"drive": "क्रिप्टड्राइव",
"slide": "मार्कडाउन स्लाइड्स"
},
"main_title": "क्रिप्टपैड: शून्य ज्ञान, सहयोगात्मक रीयल टाइम संपादन"
}

View file

@ -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"
}

View file

@ -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": "Один раз и отредактировать"
}

View file

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

View file

@ -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));
}

View file

@ -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){

View file

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

View file

@ -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) {

15
www/install/index.html Normal file
View file

@ -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>

179
www/install/main.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {};