Merge branch 'soon' into staging

This commit is contained in:
yflory 2020-03-26 15:23:48 +01:00
commit cad12ab31f
22 changed files with 835 additions and 454 deletions

View file

@ -7,6 +7,7 @@ var map = {
'es': 'Español',
'fi': 'Suomalainen',
'fr': 'Français',
//'hi': 'हिन्दी',
'it': 'Italiano',
'nb': 'Norwegian Bokmål',
//'pl': 'Polski',

View file

@ -0,0 +1,14 @@
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.hi.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View file

@ -5,10 +5,6 @@ const Util = require("../common-util");
const escapeKeyCharacters = Util.escapeKeyCharacters;
//const { fork } = require('child_process');
/* Use Nacl for checking signatures of messages */
const Nacl = require("tweetnacl/nacl-fast");
Core.DEFAULT_LIMIT = 50 * 1024 * 1024;
Core.SESSION_EXPIRATION_TIME = 60 * 1000;

View file

@ -18,15 +18,7 @@ Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
}
Env.batchMetadata(channel, cb, function (done) {
var ref = {};
var lineHandler = Meta.createLineHandler(ref, Env.Log.error);
return void Env.msgStore.readChannelMetadata(channel, lineHandler, function (err) {
if (err) {
// stream errors?
return void done(err);
}
done(void 0, ref.meta);
});
Env.computeMetadata(channel, done);
});
};

View file

@ -1,14 +1,10 @@
/*jshint esversion: 6 */
const Core = require("./core");
const BatchRead = require("../batch-read");
const Pins = require("../pins");
const Pinning = module.exports;
const Nacl = require("tweetnacl/nacl-fast");
const Util = require("../common-util");
const nThen = require("nthen");
const Saferphore = require("saferphore");
//const escapeKeyCharacters = Util.escapeKeyCharacters;
const unescapeKeyCharacters = Util.unescapeKeyCharacters;
@ -37,126 +33,10 @@ var getLimit = Pinning.getLimit = function (Env, safeKey, cb) {
cb(void 0, toSend);
};
const answerDeferred = function (Env, channel, bool) {
const pending = Env.pendingPinInquiries;
const stack = pending[channel];
if (!Array.isArray(stack)) { return; }
delete pending[channel];
stack.forEach(function (cb) {
cb(void 0, bool);
});
};
var addPinned = function (
Env,
safeKey /*:string*/,
channelList /*Array<string>*/,
cb /*:()=>void*/)
{
channelList.forEach(function (channel) {
Pins.addUserPinToState(Env.pinnedPads, safeKey, channel);
answerDeferred(Env, channel, true);
});
cb();
};
const isEmpty = function (obj) {
if (!obj || typeof(obj) !== 'object') { return true; }
for (var key in obj) {
if (obj.hasOwnProperty(key)) { return true; }
}
return false;
};
const deferUserTask = function (Env, safeKey, deferred) {
const pending = Env.pendingUnpins;
(pending[safeKey] = pending[safeKey] || []).push(deferred);
};
const runUserDeferred = function (Env, safeKey) {
const pending = Env.pendingUnpins;
const stack = pending[safeKey];
if (!Array.isArray(stack)) { return; }
delete pending[safeKey];
stack.forEach(function (cb) {
cb();
});
};
const runRemainingDeferred = function (Env) {
const pending = Env.pendingUnpins;
for (var safeKey in pending) {
runUserDeferred(Env, safeKey);
}
};
const removeSelfFromPinned = function (Env, safeKey, channelList) {
channelList.forEach(function (channel) {
const channelPinStatus = Env.pinnedPads[channel];
if (!channelPinStatus) { return; }
delete channelPinStatus[safeKey];
if (isEmpty(channelPinStatus)) {
delete Env.pinnedPads[channel];
}
});
};
var removePinned = function (
Env,
safeKey /*:string*/,
channelList /*Array<string>*/,
cb /*:()=>void*/)
{
// if pins are already loaded then you can just unpin normally
if (Env.pinsLoaded) {
removeSelfFromPinned(Env, safeKey, channelList);
return void cb();
}
// otherwise defer until later...
deferUserTask(Env, safeKey, function () {
removeSelfFromPinned(Env, safeKey, channelList);
cb();
});
};
var getMultipleFileSize = function (Env, channels, cb) {
if (!Array.isArray(channels)) { return cb('INVALID_PIN_LIST'); }
if (typeof(Env.msgStore.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
var i = channels.length;
var counts = {};
var done = function () {
i--;
if (i === 0) { return cb(void 0, counts); }
};
channels.forEach(function (channel) {
Pinning.getFileSize(Env, channel, function (e, size) {
if (e) {
// most likely error here is that a file no longer exists
// but a user still has it in their drive, and wants to know
// its size. We should find a way to inform them of this in
// the future. For now we can just tell them it has no size.
//WARN('getFileSize', e);
counts[channel] = 0;
return done();
}
counts[channel] = size;
done();
});
});
Env.getMultipleFileSize(channels, cb);
};
const batchUserPins = BatchRead("LOAD_USER_PINS");
var loadUserPins = function (Env, safeKey, cb) {
var session = Core.getSession(Env.Sessions, safeKey);
@ -164,23 +44,14 @@ var loadUserPins = function (Env, safeKey, cb) {
return cb(session.channels);
}
batchUserPins(safeKey, cb, function (done) {
var ref = {};
var lineHandler = Pins.createLineHandler(ref, function (label, data) {
Env.Log.error(label, {
log: safeKey,
data: data,
});
});
// if channels aren't in memory. load them from disk
// TODO replace with readMessagesBin
Env.pinStore.getMessages(safeKey, lineHandler, function () {
// no more messages
// only put this into the cache if it completes
session.channels = ref.pins;
done(ref.pins); // FIXME no error handling?
Env.batchUserPins(safeKey, cb, function (done) {
Env.getPinState(safeKey, function (err, value) {
if (!err) {
// only put this into the cache if it completes
session.channels = value;
}
session.channels = value;
done(value);
});
});
};
@ -198,7 +69,6 @@ var getChannelList = Pinning.getChannelList = function (Env, safeKey, _cb) {
});
};
const batchTotalSize = BatchRead("GET_TOTAL_SIZE");
Pinning.getTotalSize = function (Env, safeKey, cb) {
var unsafeKey = unescapeKeyCharacters(safeKey);
var limit = Env.limits[unsafeKey];
@ -206,9 +76,14 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
// Get a common key if multiple users share the same quota, otherwise take the public key
var batchKey = (limit && Array.isArray(limit.users)) ? limit.users.join('') : safeKey;
batchTotalSize(batchKey, cb, function (done) {
Env.batchTotalSize(batchKey, cb, function (done) {
var channels = [];
var bytes = 0;
var addUnique = function (channel) {
if (channels.indexOf(channel) !== -1) { return; }
channels.push(channel);
};
nThen(function (waitFor) {
// Get the channels list for our user account
getChannelList(Env, safeKey, waitFor(function (_channels) {
@ -216,7 +91,7 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
waitFor.abort();
return done('INVALID_PIN_LIST');
}
Array.prototype.push.apply(channels, _channels);
_channels.forEach(addUnique);
}));
// Get the channels list for users sharing our quota
if (limit && Array.isArray(limit.users) && limit.users.length > 1) {
@ -224,22 +99,12 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
if (key === unsafeKey) { return; } // Don't count ourselves twice
getChannelList(Env, key, waitFor(function (_channels) {
if (!_channels) { return; } // Broken user, don't count their quota
Array.prototype.push.apply(channels, _channels);
_channels.forEach(addUnique);
}));
});
}
}).nThen(function (waitFor) {
// Get size of the channels
var list = []; // Contains the channels already counted in the quota to avoid duplicates
channels.forEach(function (channel) { // TODO semaphore?
if (list.indexOf(channel) !== -1) { return; }
list.push(channel);
Pinning.getFileSize(Env, channel, waitFor(function (e, size) {
if (!e) { bytes += size; }
}));
});
}).nThen(function () {
done(void 0, bytes);
Env.getTotalSize(channels, done);
});
});
};
@ -247,9 +112,6 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
/* Users should be able to clear their own pin log with an authenticated RPC
*/
Pinning.removePins = function (Env, safeKey, cb) {
if (typeof(Env.pinStore.removeChannel) !== 'function') {
return void cb("E_NOT_IMPLEMENTED");
}
Env.pinStore.removeChannel(safeKey, function (err) {
Env.Log.info('DELETION_PIN_BY_OWNER_RPC', {
safeKey: safeKey,
@ -280,23 +142,9 @@ var getFreeSpace = Pinning.getFreeSpace = function (Env, safeKey, cb) {
});
};
var hashChannelList = function (A) {
var uniques = [];
A.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
return hash;
};
var getHash = Pinning.getHash = function (Env, safeKey, cb) {
getChannelList(Env, safeKey, function (channels) {
cb(void 0, hashChannelList(channels));
Env.hashChannelList(channels, cb);
});
};
@ -335,7 +183,6 @@ Pinning.pinChannel = function (Env, safeKey, channels, cb) {
toStore.forEach(function (channel) {
session.channels[channel] = true;
});
addPinned(Env, safeKey, toStore, () => {});
getHash(Env, safeKey, cb);
});
});
@ -367,7 +214,6 @@ Pinning.unpinChannel = function (Env, safeKey, channels, cb) {
toStore.forEach(function (channel) {
delete session.channels[channel];
});
removePinned(Env, safeKey, toStore, () => {});
getHash(Env, safeKey, cb);
});
});
@ -418,9 +264,6 @@ Pinning.resetUserPins = function (Env, safeKey, channelList, cb) {
} else {
oldChannels = [];
}
removePinned(Env, safeKey, oldChannels, () => {
addPinned(Env, safeKey, channelList, ()=>{});
});
// update in-memory cache IFF the reset was allowed.
session.channels = pins;
@ -432,28 +275,8 @@ Pinning.resetUserPins = function (Env, safeKey, channelList, cb) {
});
};
Pinning.getFileSize = function (Env, channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
if (typeof(Env.msgStore.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
return void Env.msgStore.getChannelSize(channel, function (e, size /*:number*/) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
cb(void 0, size);
});
}
// 'channel' refers to a file, so you need another API
Env.blobStore.size(channel, function (e, size) {
if (typeof(size) === 'undefined') { return void cb(e); }
cb(void 0, size);
});
Pinning.getFileSize = function (Env, channel, cb) {
Env.getFileSize(channel, cb);
};
/* accepts a list, and returns a sublist of channel or file ids which seem
@ -463,107 +286,11 @@ Pinning.getFileSize = function (Env, channel, _cb) {
ENOENT, but for now it's simplest to just rely on getFileSize...
*/
Pinning.getDeletedPads = function (Env, channels, cb) {
if (!Array.isArray(channels)) { return cb('INVALID_LIST'); }
var L = channels.length;
var sem = Saferphore.create(10);
var absentees = [];
var job = function (channel, wait) {
return function (give) {
Pinning.getFileSize(Env, channel, wait(give(function (e, size) {
if (e) { return; }
if (size === 0) { absentees.push(channel); }
})));
};
};
nThen(function (w) {
for (var i = 0; i < L; i++) {
sem.take(job(channels[i], w));
}
}).nThen(function () {
cb(void 0, absentees);
});
Env.getDeletedPads(channels, cb);
};
const answerNoConclusively = function (Env) {
const pending = Env.pendingPinInquiries;
for (var channel in pending) {
answerDeferred(Env, channel, false);
}
};
// inform that the
Pinning.loadChannelPins = function (Env) {
const stats = {
surplus: 0,
pinned: 0,
duplicated: 0,
// in theory we could use this number for the admin panel
// but we'd have to keep updating it whenever a new pin log
// was created or deleted. In practice it's probably not worth the trouble
users: 0,
};
const handler = function (ref, safeKey, pinned) {
if (ref.surplus) {
stats.surplus += ref.surplus;
}
for (var channel in ref.pins) {
if (!pinned.hasOwnProperty(channel)) {
answerDeferred(Env, channel, true);
stats.pinned++;
} else {
stats.duplicated++;
}
}
stats.users++;
runUserDeferred(Env, safeKey);
};
Pins.list(function (err) {
if (err) {
Env.pinsLoaded = true;
Env.Log.error("LOAD_CHANNEL_PINS", err);
return;
}
Env.pinsLoaded = true;
answerNoConclusively(Env);
runRemainingDeferred(Env);
}, {
pinPath: Env.paths.pin,
handler: handler,
pinned: Env.pinnedPads,
workers: Env.pinWorkers,
});
};
/*
const deferResponse = function (Env, channel, cb) {
const pending = Env.pendingPinInquiries;
(pending[channel] = pending[channel] || []).push(cb);
};
*/
// FIXME this will be removed from the client
Pinning.isChannelPinned = function (Env, channel, cb) {
return void cb(void 0, true);
/*
// if the pins are fully loaded then you can answer yes/no definitively
if (Env.pinsLoaded) {
return void cb(void 0, !isEmpty(Env.pinnedPads[channel]));
}
// you may already know that a channel is pinned
// even if you're still loading. answer immediately if so
if (!isEmpty(Env.pinnedPads[channel])) { return cb(void 0, true); }
// if you're still loading them then can answer 'yes' as soon
// as you learn that one account has pinned a file.
// negative responses have to wait until the end
deferResponse(Env, channel, cb);
*/
};

View file

@ -42,6 +42,8 @@ module.exports.create = function (config, cb) {
batchMetadata: BatchRead('GET_METADATA'),
batchRegisteredUsers: BatchRead("GET_REGISTERED_USERS"),
batchDiskUsage: BatchRead('GET_DISK_USAGE'),
batchUserPins: BatchRead('LOAD_USER_PINS'),
batchTotalSize: BatchRead('GET_TOTAL_SIZE'),
//historyKeeper: config.historyKeeper,
intervals: config.intervals || {},
@ -242,16 +244,18 @@ module.exports.create = function (config, cb) {
}));
}).nThen(function (w) {
HK.initializeIndexWorkers(Env, {
blobPath: config.blobPath,
blobStagingPath: config.blobStagingPath,
pinPath: pinPath,
filePath: config.filePath,
archivePath: config.archivePath,
channelExpirationMs: config.channelExpirationMs,
verbose: config.verbose,
openFileLimit: config.openFileLimit,
}, w(function (err, computeIndex) {
}, w(function (err) {
if (err) {
throw new Error(err);
}
Env.computeIndex = computeIndex;
}));
}).nThen(function (w) {
// create a task store

View file

@ -339,8 +339,6 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
*/
const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
const store = Env.store;
const Log = Env.Log;
// lastKnownhash === -1 means we want the complete history
if (lastKnownHash === -1) { return void cb(null, 0); }
@ -384,7 +382,7 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
offset = lkh;
}));
}).nThen((waitFor) => {
}).nThen((w) => {
// if offset is less than zero then presumably the channel has no messages
// returning falls through to the next block and therefore returns -1
if (offset !== -1) { return; }
@ -392,18 +390,12 @@ const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
// do a lookup from the index
// FIXME maybe we don't need this anymore?
// otherwise we have a non-negative offset and we can start to read from there
store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => {
// tryParse return a parsed message or undefined
const msg = tryParse(Env, msgObj.buff.toString('utf8'));
// if it was undefined then go onto the next message
if (typeof msg === "undefined") { return readMore(); }
if (typeof(msg[4]) !== 'string' || lastKnownHash !== getHash(msg[4], Log)) {
return void readMore();
Env.getHashOffset(channelName, lastKnownHash, w(function (err, _offset) {
if (err) {
w.abort();
return void cb(err);
}
offset = msgObj.offset;
abort();
}, waitFor(function (err) {
if (err) { waitFor.abort(); return void cb(err); }
offset = _offset;
}));
}).nThen(() => {
cb(null, offset);
@ -445,48 +437,6 @@ const getHistoryAsync = (Env, channelName, lastKnownHash, beforeHash, handler, c
});
};
/* getOlderHistory
* allows clients to query for all messages until a known hash is read
* stores all messages in history as they are read
* can therefore be very expensive for memory
* should probably be converted to a streaming interface
Used by:
* GET_HISTORY_RANGE
*/
const getOlderHistory = function (Env, channelName, oldestKnownHash, cb) {
const store = Env.store;
const Log = Env.Log;
var messageBuffer = [];
var found = false;
store.getMessages(channelName, function (msgStr) {
if (found) { return; }
let parsed = tryParse(Env, msgStr);
if (typeof parsed === "undefined") { return; }
// identify classic metadata messages by their inclusion of a channel.
// and don't send metadata, since:
// 1. the user won't be interested in it
// 2. this metadata is potentially incomplete/incorrect
if (isMetadataMessage(parsed)) { return; }
var content = parsed[4];
if (typeof(content) !== 'string') { return; }
var hash = getHash(content, Log);
if (hash === oldestKnownHash) {
found = true;
}
messageBuffer.push(parsed);
}, function (err) {
if (err && err.code !== 'ENOENT') {
Log.error("HK_GET_OLDER_HISTORY", err);
}
cb(messageBuffer);
});
};
const handleRPC = function (Env, Server, seq, userId, parsed) {
const HISTORY_KEEPER_ID = Env.id;
@ -662,7 +612,13 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
}
Server.send(userId, [seq, 'ACK']);
return void getOlderHistory(Env, channelName, oldestKnownHash, function (messages) {
Env.getOlderHistory(channelName, oldestKnownHash, function (err, messages) {
if (err && err.code !== 'ENOENT') {
Env.Log.error("HK_GET_OLDER_HISTORY", err);
}
if (!Array.isArray(messages)) { messages = []; }
var toSend = [];
if (typeof (desiredMessages) === "number") {
toSend = messages.slice(-desiredMessages);
@ -833,7 +789,14 @@ HK.initializeIndexWorkers = function (Env, config, _cb) {
});
worker.on('message', function (res) {
if (!res || !res.txid) { return; }
if (!res) { return; }
if (!res.txid) {
// !report errors...
if (res.error) {
Env.Log.error(res.error, res.value);
}
return;
}
//console.log(res);
try {
response.handle(res.txid, [res.error, res.value]);
@ -860,20 +823,18 @@ HK.initializeIndexWorkers = function (Env, config, _cb) {
};
var workerIndex = 0;
var sendCommand = function (Env, channel, cb) {
var sendCommand = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
workerIndex = (workerIndex + 1) % workers.length;
if (workers.length === 0 ||
typeof(workers[workerIndex].send) !== 'function') {
return void cb("NO_WORKERS");
}
Env.store.getWeakLock(channel, function (next) {
const txid = Util.uid();
response.expect(txid, Util.both(next, cb), 45000);
workers[workerIndex].send({
txid: txid,
args: channel,
});
});
const txid = Util.uid();
msg.txid = txid;
response.expect(txid, cb, 45000);
workers[workerIndex].send(msg);
};
nThen(function (w) {
@ -885,8 +846,91 @@ HK.initializeIndexWorkers = function (Env, config, _cb) {
}));
});
}).nThen(function () {
Env.computeIndex = function (Env, channel, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({
channel: channel,
command: 'COMPUTE_INDEX',
}, function (err, index) {
next();
cb(err, index);
});
});
};
Env.computeMetadata = function (channel, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({
channel: channel,
command: 'COMPUTE_METADATA',
}, function (err, metadata) {
next();
cb(err, metadata);
});
});
};
Env.getOlderHistory = function (channel, oldestKnownHash, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({
channel: channel,
command: "GET_OLDER_HISTORY",
hash: oldestKnownHash,
}, Util.both(next, cb));
});
};
Env.getPinState = function (safeKey, cb) {
Env.pinStore.getWeakLock(safeKey, function (next) {
sendCommand({
key: safeKey,
command: 'GET_PIN_STATE',
}, Util.both(next, cb));
});
};
Env.getFileSize = function (channel, cb) {
sendCommand({
command: 'GET_FILE_SIZE',
channel: channel,
}, cb);
};
Env.getDeletedPads = function (channels, cb) {
sendCommand({
command: "GET_DELETED_PADS",
channels: channels,
}, cb);
};
Env.getTotalSize = function (channels, cb) {
// we could take out locks for all of these channels,
// but it's OK if the size is slightly off
sendCommand({
command: 'GET_TOTAL_SIZE',
channels: channels,
}, cb);
};
Env.getMultipleFileSize = function (channels, cb) {
sendCommand({
command: "GET_MULTIPLE_FILE_SIZE",
channels: channels,
}, cb);
};
Env.getHashOffset = function (channel, hash, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({
command: 'GET_HASH_OFFSET',
channel: channel,
hash: hash,
}, Util.both(next, cb));
});
};
//console.log("index workers ready");
cb(void 0, sendCommand);
cb(void 0);
});
};
@ -907,7 +951,7 @@ HK.initializeValidationWorkers = function (Env) {
worker.on('message', function (res) {
if (!res || !res.txid) { return; }
//console.log(+new Date(), "Received verification response");
response.handle(res.txid, [res.error]);
response.handle(res.txid, [res.error, res.value]);
});
// Spawn a new process in one ends
worker.on('exit', function () {
@ -925,14 +969,13 @@ HK.initializeValidationWorkers = function (Env) {
var nextWorker = 0;
const send = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// let's be paranoid about asynchrony and only calling back once..
nextWorker = (nextWorker + 1) % workers.length;
if (workers.length === 0 || typeof(workers[nextWorker].send) !== 'function') {
console.error(workers);
throw new Error("INVALID_WORKERS");
return void cb("INVALID_WORKERS");
}
var cb = Util.once(Util.mkAsync(_cb));
var txid = msg.txid = Util.uid();
// expect a response within 15s
@ -958,6 +1001,13 @@ HK.initializeValidationWorkers = function (Env) {
key: publicKey,
}, cb);
};
Env.hashChannelList = function (channels, cb) {
send({
command: 'HASH_CHANNEL_LIST',
channels: channels,
}, cb);
};
};
/* onChannelMessage

View file

@ -64,7 +64,7 @@ const destroyStream = function (stream) {
try { stream.close(); } catch (err) { console.error(err); }
setTimeout(function () {
try { stream.destroy(); } catch (err) { console.error(err); }
}, 5000);
}, 15000);
};
const ensureStreamCloses = function (stream, id, ms) {
@ -729,7 +729,7 @@ var getChannel = function (env, id, _callback) {
delete env.channels[id];
destroyStream(channel.writeStream, path);
//console.log("closing writestream");
}, 30000);
}, 120000);
channel.delayClose();
env.channels[id] = channel;
done(void 0, channel);

View file

@ -66,24 +66,41 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
COMMANDS.DETACHED = function (data, cb) {
try {
checkDetachedSignature(data.msg, data.sig, data.key)
checkDetachedSignature(data.msg, data.sig, data.key);
} catch (err) {
return void cb(err && err.message);
}
cb();
};
COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb('INVALID_CHANNEL_LIST'); }
var uniques = [];
channels.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
cb(void 0, hash);
};
process.on('message', function (data) {
if (!data || !data.key || !data.msg || !data.txid) {
if (!data || !data.txid) {
return void process.send({
error:'E_INVAL'
});
}
const cb = function (err) {
const cb = function (err, value) {
process.send({
txid: data.txid,
error: err,
value: value,
});
};

View file

@ -3,21 +3,56 @@
const HK = require("../hk-util");
const Store = require("../storage/file");
const BlobStore = require("../storage/blob");
const Util = require("../common-util");
const nThen = require("nthen");
const Meta = require("../metadata");
const Pins = require("../pins");
const Core = require("../commands/core");
const Saferphore = require("saferphore");
const Env = {};
var ready = false;
var store;
const init = function (config, cb) {
var pinStore;
var blobStore;
const init = function (config, _cb) {
const cb = Util.once(Util.mkAsync(_cb));
if (!config) {
return void cb('E_INVALID_CONFIG');
}
Store.create(config, function (err, _store) {
if (err) { return void cb(err); }
store = _store;
nThen(function (w) {
Store.create(config, w(function (err, _store) {
if (err) {
w.abort();
return void cb(err);
}
store = _store;
}));
Store.create({
filePath: config.pinPath,
}, w(function (err, _pinStore) {
if (err) {
w.abort();
return void cb(err);
}
pinStore = _pinStore;
}));
BlobStore.create({
blobPath: config.blobPath,
blobStagingPath: config.blobStagingPath,
archivePath: config.archivePath,
getSession: function () {},
}, w(function (err, blob) {
if (err) {
w.abort();
return void cb(err);
}
blobStore = blob;
}));
}).nThen(function () {
cb();
});
};
@ -46,7 +81,13 @@ const tryParse = function (Env, str) {
* including the initial metadata line, if it exists
*/
const computeIndex = function (channelName, cb) {
const computeIndex = function (data, cb) {
if (!data || !data.channel) {
return void cb('E_NO_CHANNEL');
}
const channelName = data.channel;
const cpIndex = [];
let messageBuf = [];
let i = 0;
@ -125,45 +166,226 @@ const computeIndex = function (channelName, cb) {
});
};
const computeMetadata = function (data, cb, errorHandler) {
const ref = {};
const lineHandler = Meta.createLineHandler(ref, errorHandler);
return void store.readChannelMetadata(data.channel, lineHandler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb(void 0, ref.meta);
});
};
/* getOlderHistory
* allows clients to query for all messages until a known hash is read
* stores all messages in history as they are read
* can therefore be very expensive for memory
* should probably be converted to a streaming interface
Used by:
* GET_HISTORY_RANGE
*/
const getOlderHistory = function (data, cb) {
const oldestKnownHash = data.hash;
const channelName = data.channel;
//const store = Env.store;
//const Log = Env.Log;
var messageBuffer = [];
var found = false;
store.getMessages(channelName, function (msgStr) {
if (found) { return; }
let parsed = tryParse(Env, msgStr);
if (typeof parsed === "undefined") { return; }
// identify classic metadata messages by their inclusion of a channel.
// and don't send metadata, since:
// 1. the user won't be interested in it
// 2. this metadata is potentially incomplete/incorrect
if (HK.isMetadataMessage(parsed)) { return; }
var content = parsed[4];
if (typeof(content) !== 'string') { return; }
var hash = HK.getHash(content);
if (hash === oldestKnownHash) {
found = true;
}
messageBuffer.push(parsed);
}, function (err) {
cb(err, messageBuffer);
});
};
const getPinState = function (data, cb, errorHandler) {
const safeKey = data.key;
var ref = {};
var lineHandler = Pins.createLineHandler(ref, errorHandler);
// if channels aren't in memory. load them from disk
// TODO replace with readMessagesBin
pinStore.getMessages(safeKey, lineHandler, function () {
cb(void 0, ref.pins); // FIXME no error handling?
});
};
const _getFileSize = function (channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
return void store.getChannelSize(channel, function (e, size) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
cb(void 0, size);
});
}
// 'channel' refers to a file, so you need another API
blobStore.size(channel, function (e, size) {
if (typeof(size) === 'undefined') { return void cb(e); }
cb(void 0, size);
});
};
const getFileSize = function (data, cb) {
_getFileSize(data.channel, cb);
};
const _iterateFiles = function (channels, handler, cb) {
if (!Array.isArray(channels)) { return cb('INVALID_LIST'); }
var L = channels.length;
var sem = Saferphore.create(10);
// (channel, next) => { ??? }
var job = function (channel, wait) {
return function (give) {
handler(channel, wait(give()));
};
};
nThen(function (w) {
for (var i = 0; i < L; i++) {
sem.take(job(channels[i], w));
}
}).nThen(function () {
cb();
});
};
const getTotalSize = function (data, cb) {
var bytes = 0;
_iterateFiles(data.channels, function (channel, next) {
_getFileSize(channel, function (err, size) {
if (!err) { bytes += size; }
next();
});
}, function (err) {
if (err) { return cb(err); }
cb(void 0, bytes);
});
};
const getDeletedPads = function (data, cb) {
var absentees = [];
_iterateFiles(data.channels, function (channel, next) {
_getFileSize(channel, function (err, size) {
if (err) { return next(); }
if (size === 0) { absentees.push(channel); }
next();
});
}, function (err) {
if (err) { return void cb(err); }
cb(void 0, absentees);
});
};
const getMultipleFileSize = function (data, cb) {
const counts = {};
_iterateFiles(data.channels, function (channel, next) {
_getFileSize(channel, function (err, size) {
counts[channel] = err? 0: size;
next();
});
}, function (err) {
if (err) {
return void cb(err);
}
cb(void 0, counts);
});
};
const getHashOffset = function (data, cb) {
const channelName = data.channel;
const lastKnownHash = data.lastKnownHash;
var offset = -1;
store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => {
// tryParse return a parsed message or undefined
const msg = tryParse(Env, msgObj.buff.toString('utf8'));
// if it was undefined then go onto the next message
if (typeof msg === "undefined") { return readMore(); }
if (typeof(msg[4]) !== 'string' || lastKnownHash !== HK.getHash(msg[4])) {
return void readMore();
}
offset = msgObj.offset;
abort();
}, function (err) {
if (err) { return void cb(err); }
cb(void 0, offset);
});
};
const COMMANDS = {
COMPUTE_INDEX: computeIndex,
COMPUTE_METADATA: computeMetadata,
GET_OLDER_HISTORY: getOlderHistory,
GET_PIN_STATE: getPinState,
GET_FILE_SIZE: getFileSize,
GET_TOTAL_SIZE: getTotalSize,
GET_DELETED_PADS: getDeletedPads,
GET_MULTIPLE_FILE_SIZE: getMultipleFileSize,
GET_HASH_OFFSET: getHashOffset,
};
process.on('message', function (data) {
if (!data || !data.txid) {
return void process.send({
error:'E_INVAL'
});
}
const txid = data.txid;
const cb = function (err, value) {
process.send({
error: err,
txid: data.txid,
value: value,
});
};
if (!ready) {
return void init(data.config, function (err) {
if (err) {
return void process.send({
txid: txid,
error: err,
});
}
if (err) { return void cb(err); }
ready = true;
process.send({txid: txid,});
cb();
});
}
const channel = data.args;
if (!channel) {
return void process.send({
error: 'E_NO_CHANNEL',
});
const command = COMMANDS[data.command];
if (typeof(command) !== 'function') {
return void cb("E_BAD_COMMAND");
}
// computeIndex
computeIndex(channel, function (err, index) {
if (err) {
return void process.send({
txid: txid,
error: err,
});
}
return void process.send({
txid: txid,
value: index,
command(data, cb, function (label, info) {
// for streaming errors
process.send({
error: label,
value: info,
});
});
});

6
package-lock.json generated
View file

@ -190,9 +190,9 @@
}
},
"chainpad-server": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-4.0.8.tgz",
"integrity": "sha512-QlmomAMQN4msdYnRqGEjL12FAeOPIJ5yoxIzROohWt/31SwF1UlyV+zFp1M1dhtV8PoS7JXvLyBBLCEEVN73Cg==",
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-4.0.9.tgz",
"integrity": "sha512-8h1W41ktE05TM6LuXrklpW2TUxWeNyIDiRaQygKsXaA/7pyJxF7+AmPVS+xW0c31VkHjQDPiaMzPoxhcxXnIyA==",
"requires": {
"nthen": "0.1.8",
"pull-stream": "^3.6.9",

View file

@ -13,7 +13,7 @@
},
"dependencies": {
"chainpad-crypto": "^0.2.2",
"chainpad-server": "^4.0.8",
"chainpad-server": "^4.0.9",
"express": "~4.16.0",
"fs-extra": "^7.0.0",
"get-folder-size": "^2.0.1",

View file

@ -121,7 +121,7 @@ define([
return $div;
};
create['open-files'] = function () {
var key = 'open-files'; // XXX
var key = 'open-files';
var $div = makeBlock(key);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_FILE_DESCRIPTOR_COUNT',

View file

@ -723,7 +723,7 @@ MessengerUI, Messages) {
};
var createUnpinnedWarning0 = function (toolbar, config) {
//if (true) { return; } // stub this call since it won't make it into the next release
if (true) { return; } // stub this call since it won't make it into the next release
if (Common.isLoggedIn()) { return; }
var pd = config.metadataMgr.getPrivateData();
var o = pd.origin;

View file

@ -575,7 +575,7 @@
"mdToolbar_button": "Die Markdown-Werkzeugleiste anzeigen oder verbergen",
"mdToolbar_defaultText": "Dein Text hier",
"mdToolbar_help": "Hilfe",
"mdToolbar_tutorial": "http://www.markdowntutorial.com/",
"mdToolbar_tutorial": "https://www.markdowntutorial.com/",
"mdToolbar_bold": "Fett",
"mdToolbar_italic": "Kursiv",
"mdToolbar_strikethrough": "Durchgestrichen",
@ -1337,5 +1337,10 @@
"kanban_color": "Farbe",
"kanban_body": "Inhalt",
"kanban_title": "Titel",
"oo_isLocked": "synchronisiere Änderungen, bitte warten"
"oo_isLocked": "synchronisiere Änderungen, bitte warten",
"admin_openFilesHint": "Anzahl der momentan geöffneten Dateideskriptoren auf dem Server",
"admin_openFilesTitle": "Offene Dateien",
"canvas_select": "Auswahl",
"canvas_brush": "Pinsel",
"profile_copyKey": "Öffentlichen Schlüssel kopieren"
}

View file

@ -1337,5 +1337,10 @@
"kanban_color": "Couleur",
"kanban_body": "Contenu",
"kanban_title": "Titre",
"oo_isLocked": "Synchronisation, veuillez patienter"
"oo_isLocked": "Synchronisation, veuillez patienter",
"admin_openFilesHint": "Nombre de descripteurs de fichiers actuellement ouverts sur le serveur.",
"admin_openFilesTitle": "Fichiers Ouverts",
"profile_copyKey": "Copier la clé publique",
"canvas_select": "Selection",
"canvas_brush": "Pinceau"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -1,5 +1,5 @@
{
"main_title": "CryptPad: Editor zero knowledge collaborativo in tempo reale",
"main_title": "CryptPad: Editor Zero Knowledge collaborativo in tempo reale",
"type": {
"pad": "Testo",
"code": "Codice",
@ -13,7 +13,7 @@
"todo": "Promemoria",
"contacts": "Contatti",
"sheet": "Fogli",
"teams": "Team"
"teams": "Teams"
},
"button_newpad": "Nuovo pad di Testo",
"button_newcode": "Nuovo pad di Codice",
@ -25,14 +25,14 @@
"common_connectionLost": "<b>Connessione al server persa</b><br>Rimarrai in modalità solo lettura finché la connessione non sarà ripristinata.",
"websocketError": "Impossibile connettersi al WebSocket server...",
"typeError": "Questo pad non è compatibile con l'applicazione selezionata",
"onLogout": "Sei logged out, {0}Click qui{1} per fare il log in<br>o premi <em>Esc</em> per accedere il tuo pad in modalità solo lettura.",
"wrongApp": "Impossibile mostrare il contenuto di quella sessione in tempo reale nel tuo browser. Per favore, prova a ricaricare la pagina.",
"padNotPinned": "Questo pad scadrà dopo 3 mesi di inattività, {0}login{1} o {2}registrati{3} per mantenerlo permanentemente.",
"anonymousStoreDisabled": "Il webmaster di questa istanza di CryptPad ha disabilitato il drive per gli utenti anonimi. Devi eseguire il login per poter usare CryptDrive.",
"onLogout": "Sei uscito, {0}Clicca qui{1} per entrare<br>o premi <em>Esc</em> per accedere al tuo pad in modalità solo lettura.",
"wrongApp": "Impossibile mostrare il contenuto di quella sessione in tempo reale nel tuo browser. Per favore, prova a ricaricare quella pagina.",
"padNotPinned": "Questo pad scadrà dopo 3 mesi di inattività, {0}accedi{1} o {2}registrati{3} per conservarlo.",
"anonymousStoreDisabled": "Il webmaster di questa istanza di CryptPad ha disabilitato il drive per gli utenti anonimi. Devi accedere per poter usare CryptDrive.",
"expiredError": "Questo pad ha raggiunto la sua data di scadenza e non è più disponibile.",
"deletedError": "Questo pad è stato cancellato dal suo autore e non è più disponibile.",
"inactiveError": "Questo pad è stato cancellato per inattività. Premi Esc per creare un nuovo pad.",
"chainpadError": "Si è verificato un errore critico nell'aggiornamento del tuo contenuto. Questa pagina è in modalità solo lettura per assicurarci che non perderai il tuo lavoro..<br>Premi <em>Esc</em> per continuare a visualizzare questo pad, o ricarica la pagina per provare a modificarlo di nuovo.",
"chainpadError": "Si è verificato un errore critico nell'aggiornamento del tuo contenuto. Questa pagina è in modalità solo lettura per assicurarci che non perderai il tuo lavoro.<br>Premi <em>Esc</em> per continuare a visualizzare questo pad, o ricarica la pagina per provare a modificarlo di nuovo.",
"invalidHashError": "Il documento richiesto ha un URL non valido.",
"errorCopy": " Puoi ancora accedere al contenuto premendo <em>Esc</em>.<br>Una volta chiusa questa finestra, non sarà possibile accedere di nuovo.",
"errorRedirectToHome": "Premi <em>Esc</em> per essere reindirizzato al tuo CryptDrive.",
@ -537,7 +537,7 @@
},
"security": {
"crypto": {
"a": "nome utente",
"a": "CryptPad è basato su due librerie open-source di crittografia: <a href='https://github.com/dchest/tweetnacl-js' target='_blank'>tweetnacl.js</a> and <a href='https://github.com/dchest/scrypt-async-js' target='_blank'>scrypt-async.js</a>.<br><br>Scrypt è un <em>algoritmo di derivazione delle chiavi basato su password</em>. Lo utilizziamo per trasformare i tuoi username e password in una chiave unica che assicura l'accesso al tuo CryptDrive così che solo tu possa accedere ai tuoi pad.<br><br>Utilizziamo i crypters <em>xsalsa20-poly1305</em> e <em>x25519-xsalsa20-poly1305</em> forniti da tweetnacl rispettivamente per criptare i pad e la chat.",
"q": "Quale crittografia utilizzate?"
},
"pad_password": {
@ -549,16 +549,37 @@
"why": {
"q": "Perché dovrei usare CryptPad?"
},
"title": "Sicurezza"
"title": "Sicurezza",
"proof": {
"q": "Come utilizzate le Zero Knowledge Proofs?"
}
},
"privacy": {
"register": {
"a": "nome utente"
"a": "Non richiediamo agli utenti la verifica del loro indirizzo email, e il server non registra il vostro username o la password quando vi registrate. Piuttosto, la form di accesso genera una chiave univoca basata su ciò che scrivete, ed il server registra esclusivamente la vostra firma crittografata. Utilizziamo questa chiave per tracciare dettagli su quanto spazio utilizzate che ci permettono di restringere ciascun utente ad una specifica quota.<br><br>Utilizziamo le nostre funzionalità di <em>feedback</em> per registrare sul server che qualcuno con il vostro indirizzo IP ha registrato un account. Utilizziamo questo dato per conteggiare quante persone abbiano attivato un account CryptPad, e vedere da quale area geografica provengano in modo da capire quali linguaggi richiedano maggiore impegno.<br><br>Gli utenti registrati sono associati nel server ai loro pad nel loro CryptDrive in modo tale che non vengano considerati abbandonati e rimossi dal server in seguito a mancata attività.",
"q": "Il server avrà maggiori informazioni su di me se mi registro?"
},
"policy": {
"a": "Sì! È disponibile <a href='/privacy.html' target='_blank'>qui</a>."
"a": "Sì! È disponibile <a href='/privacy.html' target='_blank'>qui</a>.",
"q": "Avete una politica di privacy dei dati?"
},
"title": "Privacy"
"title": "Privacy",
"anonymous": {
"q": "CryptPad mi rende anonimo?",
"a": "Sebbene CryptPad per raccogliere il minimo di informazione possibile, non garantisce il totale anonimato. Il server ha accesso al vostro indirizzo IP, comunque potete utilizzare strumenti per nascondere questa informazione, ad esempio usando Tor per accedere a CryptPad. Utilizzare Tor senza cambiare il vostro comportamento non garantirà l'anonimato, visto che il server riconosce l'utente anche dalla sua firma crittografata. Se utilizzate lo stesso account quando usate Tor e quando non lo usate, è possibile collegare la sessione con IP a quella senza.<br><br>Per utenti che richiedano un livello di privacy meno ferreo basti sapere che CryptPad non richiede all'utente di identificarsi con nome, numero di telefono o indirizzo mail come la maggior parte degli altri servizi."
},
"other": {
"q": "Cosa possono conoscere di me gli altri collaboratori?",
"a": "Quando lavorate su un pad insieme ad altri, comunicate attraverso il server, l'unica informazione raccolta è il vostro indirizzo IP. Gli altri possono vedere il vostro nome utente, l'avatar, un link al vostro profilo (se ne avete uno), e la vostra <em>chiave pubblica</em> (che è utilizzata per criptare le informazioni dall'uno all'altro)."
},
"me": {
"q": "Che informazioni ha il server su di me?",
"a": "L'amministratore del server è in grado di vedere l'indirizzo IP degli utenti che accedono a CryptPad. Non registriamo quale siano i pad visitati da un indirizzo IP, ma potremmo, sebbene senza la possibilità di leggere i pad criptati. Se ti preoccupa il fatto che noi si possa leggere i tuoi contenuti, è più sicuro che tu assuma che noi lo si possa fare, giacché non possiamo dimostrarti il contrario.<br><br>Raccogliamo alcune misurazioni di base su come le persone usano CryptPad, come la risoluzione dello schermo, o quali pulsanti usino più frequentemente. Questo ci aiuta a migliorare il software, ma se preferisci non inviare queste informazioni al server, puoi <strong>cessare l'invio togliendo la spunta sul check <em>Abilita il feedback utente</em></strong>.<br><br>Teniamo traccia di quali pad siano nel CryptDrive di ciascun utente così da poter definire limiti all'uso di spazio, ma non conosciamo il contenuto o il tipo di questi pad. Le occupazioni di spazio sono collegate alle chiavi pubbliche degli utenti, ma non associamo nomi o indirizzi mail a queste chiavi pubbliche.<br><br>Per maggiori informazioni puoi leggere questo <a href='https://blog.cryptpad.fr/2017/07/07/cryptpad-analytics-what-we-cant-know-what-we-must-know-what-we-want-to-know/' target='_blank'>articolo nel blog</a> che abbiamo scritto sull'argomento."
},
"different": {
"q": "In cosa è diverso CryptPad dagli altri servizi Pad?",
"a": "CryptPad cripta i cambiamenti ai tuoi documenti prima di inviare queste informazioni al server per l'archiviazione, quindi noi non possiamo leggere ciò che tu scrivi."
}
},
"other": {
"jobs": {
@ -575,7 +596,8 @@
},
"keywords": {
"tag": {
"q": "Come utilizzo i tag?"
"q": "Come utilizzo i tag?",
"a": "Puoi taggare i file creati o caricati con il tuo CryptDrive, oppure utilizzando il pulsante <em>tag</em> (<span class='fa fa-hashtag'></span>) nella barra degli strumenti di qualsiasi editor. Cerca gli appunti ed i file nel tuo CryptDrive utilizzando la barra di ricerca con una parola marcata come hashtag, ad esempio <em>#crypto</em>."
},
"pad": {
"q": "Cos'è un pad?",
@ -583,13 +605,21 @@
},
"expiring": {
"q": "Cos'è un pad effimero?",
"a": "Un <em>expiring pad</em> è un pad creato con una scadenza, raggiunta il pad verrà automaticamente cancellato dal server. Gli expiring pad possono essere configurati per durare da un minimo di un'ora ad un massimo di 100 mesi. Il pad e tutta la sua cronologia diventeranno permanentemente non disponibili anche se vengono modificati nel momento in cui scadono.<br><br>Se un pad è impostato con una scadenza, puoi controllare il suo tempo di durata visualizzando le sue <em> proprietà </em>, sia facendo clic con il tasto destro del mouse sul pad in CryptDrive, sia usando la <em> proprietà </em> -menu dalla barra degli strumenti di un'applicazione."
"a": "Un <em>pad effimero</em> è un pad creato con una scadenza, raggiunta la quale il pad verrà automaticamente cancellato dal server. I pad effimeri possono essere configurati per durare da un minimo di un'ora ad un massimo di 100 mesi. Il pad e tutta la sua cronologia diventeranno permanentemente non disponibili anche se vengono modificati nel momento in cui scadono.<br><br>Se un pad è impostato con una scadenza, puoi controllare il suo tempo di durata visualizzando le sue <em> proprietà </em>, sia facendo clic con il tasto destro del mouse sul pad nel tuo CryptDrive, sia usando le <em> proprietà </em> sub-menu dalla barra degli strumenti di un'applicazione."
},
"owned": {
"a": "Un <em>pad di proprietà</em> è un pad creato da un esplicito <em>proprietario</em>, identificato dal server dalla sua <em>chiave di crittografia pubblica</em>. Il proprietario di un pad può scegliere di cancellare i suoi pad dal server, rendendoli invalidi per gli altri collaboratori nel futuro, sia che essi li avessero oppure no nei loro Cryptdrive.",
"q": "Cos'è un Pad di proprietà?"
},
"title": "Parole chiave"
"title": "Parole chiave",
"abandoned": {
"q": "Cos'è un pad abbandonato?",
"a": "Un <em>pad abbandonato</em> è un pad che non è collegato al CryptDrive di nessun utente registrato e non è stato modificato da sei mesi. I documenti abbandonati vengono rimossi automaticamente dal server."
},
"template": {
"q": "Cos'è un modello?",
"a": "Un template (modello) è un file che può essere usato per definire il contenuto iniziale per altri dello stesso tipo quando li crei. Qualsiasi salvataggio esistente può essere trasformato in un template spostandolo nella sezione <em>Template</em> del tuo CryptDrive. Puoi anche creare una copia di un pad da utilizzare come template cliccando il pulsante template (<span class='fa fa-bookmark'></span>) nella barra degli strumenti dell'editor."
}
}
},
"whatis_zeroknowledge_p2": "Quando ti registri e accedi, il tuo nome utente e la tua password vengono computati in una chiave segreta utilizzando la <a href=\"https://en.wikipedia.org/wiki/Scrypt\">funzione di derivazione scrypt</a>. Né questa chiave, né il tuo nome utente o la tua password vengono inviati al server. Infatti sono usati soltanto dal lato client per decriptare il contenuto del tuo CryptDrive, che contiene le chiavi per tutti i pad a cui hai accesso.",
@ -635,7 +665,7 @@
"mdToolbar_link": "Link",
"mdToolbar_italic": "Corsivo",
"mdToolbar_bold": "Grassetto",
"mdToolbar_tutorial": "http://www.markdowntutorial.com/",
"mdToolbar_tutorial": "https://www.markdowntutorial.com/",
"mdToolbar_help": "Aiuto",
"pad_hideToolbar": "Nascondi barra degli strumenti",
"pad_showToolbar": "Mostra barra degli strumenti",

View file

@ -2,7 +2,7 @@
"websocketError": "ウェブソケットサーバーとの接続ができません。",
"common_connectionLost": "サーバーとの接続が切断しました。\nサーバーと再接続するまで閲覧モードになります。",
"button_newsheet": "新規スプレッドシート",
"button_newkanban": "新規カンバン",
"button_newkanban": "新規看板",
"button_newwhiteboard": "新規ホワイトボード",
"button_newslide": "新規プレゼンテーション",
"button_newpoll": "新規投票・アンケート",
@ -15,12 +15,159 @@
"media": "メディア",
"file": "ファイル",
"whiteboard": "ホワイトボード",
"drive": "ドライブ",
"drive": "CryptDrive",
"slide": "プレゼン",
"kanban": "看板",
"poll": "投票・アンケート",
"poll": "投票",
"code": "コード",
"pad": "リッチテキスト"
"pad": "リッチテキスト",
"sheet": "シート"
},
"main_title": " CryptPad - それは直感的なリアルタイム同期編集エディター -"
"main_title": "CryptPad - 安全にリアルタイム編集可能なコラボレーションツール",
"support_formButton": "送信",
"support_formMessage": "メッセージを入力...",
"support_formContentError": "エラー:内容が空です",
"support_formTitleError": "エラー:件名が空です",
"support_formTitle": "チケットの件名",
"support_cat_new": "新しいチケット",
"support_answer": "返信",
"support_remove": "チケットを削除",
"support_close": "チケットを閉じる",
"notifications_cat_all": "全て",
"openNotificationsApp": "通知パネルを開く",
"notificationsPage": "通知",
"notifications_empty": "通知がありません",
"copy_title": "{0} (コピー)",
"access_main": "アクセス",
"allow_disabled": "無効",
"allow_enabled": "有効",
"logoutEverywhere": "全ての場所でログアウト",
"settings_logoutEverywhereButton": "ログアウト",
"cancelButton": "キャンセル (Esc)",
"fm_recentPadsName": "最近使用したパッド",
"drive_active28Days": "最近4週間",
"drive_active7Days": "最近7日間",
"drive_active1Day": "最近24時間",
"fm_viewGridButton": "グリッド表示",
"fm_viewListButton": "一覧表示",
"fc_open": "開く",
"fc_newsharedfolder": "新しい共有フォルダ",
"fc_newfolder": "新しいフォルダ",
"fm_tags_name": "タグ名",
"fm_prop_tagsList": "タグ",
"fm_numberOfFiles": "ファイル数",
"fm_numberOfFolders": "フォルダ数",
"fm_folder": "フォルダ",
"fm_folderName": "フォルダ名",
"fm_fileName": "ファイル名",
"crowdfunding_button2": "CryptPad を助ける",
"fm_padIsOwned": "あなたはこのパッドの所有者です",
"creation_expiration": "有効期限",
"owner_removeText": "所有者",
"creation_owners": "所有者",
"download_mt_button": "ダウンロード",
"fc_rename": "名前を変更",
"forgotten": "ごみ箱へ移動",
"filePicker_close": "閉じる",
"upload_size": "容量",
"propertiesButton": "プロパティ",
"fm_creation": "作成日時",
"fm_lastAccess": "最終アクセス日時",
"fm_type": "種類",
"team_inviteLinkLoading": "あなたのリンクを生成中",
"loading_drive_3": "データの整合性を検証中",
"loading_drive_2": "データの形式を更新中",
"loading_drive_1": "データを読み込み中",
"loading_pad_2": "パッドの内容を読み込み中",
"download_step1": "ダウンロード中",
"loading": "読み込み中...",
"upload_type": "種類",
"fm_searchPlaceholder": "検索...",
"fm_searchName": "検索",
"fm_templateName": "テンプレート",
"fc_empty": "ごみ箱を空にする",
"fm_emptyTrashDialog": "ごみ箱を空にしてよろしいですか?",
"fm_trashName": "ごみ箱",
"settings_trimHistoryTitle": "履歴の削除",
"trimHistory_success": "履歴を削除しました",
"trimHistory_button": "履歴を削除",
"storageStatus": "ストレージ使用量:<br /><b>{1}</b> の内 <b>{0}</b>",
"formattedKB": "{0} KB",
"formattedGB": "{0} GB",
"formattedMB": "{0} MB",
"KB": "KB",
"GB": "GB",
"MB": "MB",
"upgradeAccount": "アカウントをアップグレード",
"upgrade": "アップグレード",
"settings_deleteButton": "アカウントを削除",
"settings_deleteTitle": "アカウントの削除",
"settings_changePasswordNewConfirm": "新しいパスワードの確認",
"settings_changePasswordNew": "新しいパスワード",
"settings_changePasswordCurrent": "現在のパスワード",
"settings_changePasswordButton": "パスワードを変更",
"settings_changePasswordHint": "アカウントのパスワードを変更します。「現在のパスワード」と、「新しいパスワード」および「新しいパスワードの確認」を入力してください。<br><b>あなたがパスワードを忘れた場合、パスワードをリセットする方法はありません。細心の注意を払って、パスワードを安全に管理してください。</b>",
"settings_changePasswordTitle": "パスワードの変更",
"languageButton": "言語",
"language": "言語",
"settings_publicSigningKey": "公開署名鍵",
"user_accountName": "アカウント名",
"settings_restore": "復元",
"settings_backupCategory": "バックアップ",
"settings_backup": "バックアップ",
"settings_title": "設定",
"settings_cat_subscription": "サブスクリプション",
"settings_cat_drive": "CryptDrive",
"settings_cat_account": "アカウント",
"settings_cat_creation": "新しいパッド",
"settings_save": "保存",
"slideOptionsButton": "保存 (Enter)",
"profile_viewMyProfile": "自分のプロフィールを表示",
"clickToEdit": "クリックして編集",
"profile_addLink": "あなたのウェブサイトへのリンクを追加",
"shareSuccess": "リンクをクリップボードにコピーしました",
"shareButton": "共有",
"login_hashing": "パスワードをハッシュ化しています、この処理には時間がかかる場合があります。",
"login_invalPass": "パスワードを入力してください",
"login_invalUser": "ユーザー名を入力してください",
"register_importRecent": "匿名セッション中のパッドをインポート",
"importButton": "インポート",
"policy_title": "CryptPad プライバシーポリシー",
"main_catch_phrase": "ゼロ知識クラウド",
"tos_3rdparties": "私たちは、法律で義務付けられている場合を除き、個別のデータを第三者に提供しません。",
"tos_logs": "あなたのブラウザからサーバーに送信されたメタデータは、サービスを維持するために記録される場合があります。",
"tos_availability": "私たちはこのサービスがあなたの役に立つことを願っていますが、可用性や性能は保証できません。定期的にデータをエクスポートしてください。",
"tos_legal": "悪意ある行為、乱用する行為、または何らかの違法な行為を行わないでください。",
"tos_title": "CryptPad サービス利用規約",
"whatis_title": "CryptPad とは何か",
"topbar_whatIsCryptpad": "CryptPad とは何か",
"faq_title": "よくある質問",
"faq_link": "よくある質問",
"footer_tos": "利用規約",
"footer_donate": "寄付",
"footer_legal": "法的情報",
"footer_aboutUs": "私たちについて",
"pricing": "料金",
"contact": "連絡先",
"privacy": "プライバシー",
"blog": "ブログ",
"driveReadmeTitle": "CryptPad とは何ですか?",
"readme_welcome": "CryptPad へようこそ!",
"register_header": "CryptPad へようこそ",
"login_confirm": "パスワードの確認",
"login_register": "新規登録",
"login_username": "ユーザー名",
"login_password": "パスワード",
"settingsButton": "設定",
"logoutButton": "ログアウト",
"login_login": "ログイン",
"autostore_hide": "保存しない",
"autostore_store": "保存する",
"autostore_notstored": "この {0} は、あなたの CryptDrive に保存されていません。今すぐ保存しますか?",
"user_displayName": "表示名",
"exportButton": "エクスポート",
"user_rename": "表示名を変更",
"users": "ユーザー",
"saved": "保存しました",
"error": "エラー"
}

View file

@ -1337,5 +1337,10 @@
"kanban_clearFilter": "Clear filter",
"kanban_editCard": "Edit this card",
"kanban_editBoard": "Edit this board",
"oo_isLocked": "syncing changes, please wait"
"oo_isLocked": "syncing changes, please wait",
"profile_copyKey": "Copy public key",
"admin_openFilesTitle": "Open Files",
"admin_openFilesHint": "Number of file descriptors currently open on the server.",
"canvas_brush": "Brush",
"canvas_select": "Select"
}

View file

@ -8,7 +8,171 @@
"slide": "Presentation",
"poll": "Röstning",
"code": "Kod",
"todo": "Att-göra"
"todo": "Att-göra",
"teams": "Teams",
"sheet": "Kalkylark",
"pad": "Rik text",
"kanban": "Kanban"
},
"main_title": "CryptPad: Nollkunskap, samarbete i realtid"
"main_title": "CryptPad: Nollkunskap, samarbete i realtid",
"fileEmbedScript": "För att bädda in denna fil, inkludera detta skript en gång på din sida för att ladda Media Tag:",
"fileEmbedTitle": "Bädda in filen i en extern sida",
"viewEmbedTag": "Bädda in detta dokument, inkludera denna iframe i din sida var du vill. Du kan anpassa den genom CSS- eller HTML-attribut.",
"getEmbedCode": "Visa inbäddningskod",
"fileShare": "Kopiera länk",
"viewOpenTitle": "Öppna detta dokument i skrivskyddat läge i en ny flik",
"viewOpen": "Öppna skrivskyddad länk i ny flik",
"viewShareTitle": "Kopiera skrivskyddad länk",
"viewShare": "Skrivskyddad länk",
"editOpenTitle": "Öppna detta dokument i redigeringsläge i en ny flik",
"editShareTitle": "Kopiera länk",
"editShare": "Redigerar länk",
"themeButtonTitle": "Välj färgtema för användning när du redigerar kod och presentationer",
"themeButton": "Tema",
"languageButtonTitle": "Välj språk för användning vid syntaxmarkering",
"languageButton": "Språk",
"slide_invalidLess": "Ogiltig anpassad stil",
"slideOptionsButton": "Spara (Enter)",
"slideOptionsTitle": "Anpassa dina bilder",
"slideOptionsText": "Alternativ",
"tags_noentry": "Du kan inte tagga ett raderat dokument!",
"tags_duplicate": "Duplicera tagg: {0}",
"tags_notShared": "Dina taggar är inte delade med andra användare",
"tags_searchHint": "Påbörja en sökning med # i din CryptDrive för att hitta dina taggade dokument.",
"tags_add": "Uppdatera taggar för denna sida",
"tags_title": "Taggar (endast för dig)",
"or": "eller",
"filePicker_filter": "Filtrera filer efter namn",
"filePicker_description": "Välj en fil från din CryptDrive för att bädda in den eller ladda upp en ny",
"filePicker_close": "Stäng",
"filePickerButton": "Bädda in en fil lagrad i CryptDrive",
"printBackgroundRemove": "Ta bort denna bakgrundsbild",
"printBackgroundNoValue": "<em>Ingen bakgrundsbild vald</em>",
"printBackgroundValue": "<b>Nuvarande bakgrund:</b> <em>{0}</em>",
"printBackgroundButton": "Välj en bild",
"printBackground": "Använd som bakgrundsbild",
"printTransition": "Aktivera övergångsanimationer",
"printCSS": "Anpassade stilregler (CSS):",
"printTitle": "Visa dokumenttiteln",
"printDate": "Visa datumet",
"printSlideNumber": "Visa sidnumret",
"printOptions": "Layout-alternativ",
"printButtonTitle2": "Skriv ut dokumentet eller exportera det som en PDF-fil",
"printButton": "Skriv ut (enter)",
"printText": "Skriv ut",
"propertiesButtonTitle": "Visa dokumentegenskaper",
"propertiesButton": "Egenskaper",
"colorButtonTitle": "Ändra textfärgen i presentationsläge",
"useTemplateCancel": "Starta tomt (Esc)",
"pinLimitReachedAlert": "Du har nått din lagringsgräns. Nya dokument kommer inte lagras i din CryptDrive.<br>Du kan antingen ta bort dokument från din CryptDrive eller <a href=\"https://accounts.cryptpad.fr/#!on={0}\" target=\"_blank\">eller prenumerera på premium</a> för att öka din lagringsgräns.",
"lag": "Fördröjning",
"anonymousStoreDisabled": "Administratören för denna CryptPad-instans har avaktiverat anonyma användare. Du måste logga in för att kunna använda CryptDrive.",
"wrongApp": "Kan inte visa innehållet av realtidssessionen i din webbläsare. Vänligen försök ladda om sidan.",
"common_connectionLost": "<b>Serveranslutning tappad</b><br>Du är nu i skrivskyddat läge tills anslutningen är tillbaka.",
"button_newcode": "Nytt kod-dokument",
"backgroundButtonTitle": "Ändra bakgrundfärgen i presentationen",
"presentButtonTitle": "Gå in i presentationsläge",
"previewButtonTitle": "Visa eller dölj Markdown förhandsvisning",
"template_empty": "Ingen mall tillgänglig",
"template_import": "Importera en mall",
"useTemplateOK": "Välj en mall (Enter)",
"useTemplate": "Vill du starta från en mall?",
"selectTemplate": "Välj en mall och tryck escape",
"templateSaved": "Mall sparad!",
"saveTemplatePrompt": "Välj ett namn på mallen",
"saveTemplateButton": "Spara som mall",
"uploadButtonTitle": "Ladda upp en ny fil till den nuvarande mappen",
"uploadFolderButton": "Ladda upp mapp",
"uploadButton": "Ladda upp filer",
"newButtonTitle": "Skapa ett dokument",
"newButton": "Ny",
"userAccountButton": "Ditt konto",
"chatButton": "Chatt",
"userListButton": "Användarlista",
"shareSuccess": "Länken är kopierad",
"shareButton": "Dela",
"movedToTrash": "Dokumentet har flyttats till papperskorgen.<br><a href=\"/drive/\">Gå till min Drive</a>",
"forgetPrompt": "Om du klickar OK flyttas detta dokument till papperskorgen. Är du säker?",
"forgetButtonTitle": "Flytta dokumentet till papperskorgen",
"forgetButton": "Radera",
"saveTitle": "Spara titeln (enter)",
"clickToEdit": "Klicka för att redigera",
"user_accountName": "Kontonamn",
"user_displayName": "Visningsnamn",
"user_rename": "Ändra ditt visningsnamn",
"changeNamePrompt": "Ändra ditt namn (lämna tomt för att vara anonym): ",
"exportPrompt": "Vad vill du kalla din fil?",
"exportButtonTitle": "Exportera detta dokument till en fil på din enhet",
"exportButton": "Exportera",
"importButtonTitle": "Importera ett dokument från en fil på din enhet",
"importButton": "Importera",
"moreActions": "Fler åtgärder",
"pinLimitDrive": "Du har nått din lagringsgräns.<br>Du kan inte skapa nya dokument.",
"pinLimitNotPinned": "Du har nått din lagringsgräns.<br>Detta dokument är inte lagrat i din CryptDrive.",
"pinLimitReachedAlertNoAccounts": "Du har nått din lagringsgräns",
"pinLimitReached": "Du har nått din lagringsgräns",
"redLight": "Du är frånkopplad från sessionen",
"orangeLight": "Din långsamma anslutning kan påverka din upplevelse",
"greenLight": "Allt fungerar fint",
"formattedKB": "{0} KB",
"formattedGB": "{0} GB",
"formattedMB": "{0} MB",
"supportCryptpad": "Stötta CryptPad",
"KB": "KB",
"GB": "GB",
"MB": "MB",
"storageStatus": "Lagring:<br /><b>{0}</b> använd av <b>{1}</b>",
"upgradeAccount": "Uppgradera konto",
"upgradeTitle": "Uppgradera ditt konto för att öka din lagringsgräns",
"upgrade": "Uppgradera",
"newVersion": "<b>CryptPad har uppdaterats!</b><br>Se vad som är nytt i den senaste versionen:<br><a href=\"https://github.com/xwiki-labs/cryptpad/releases/tag/{0}\" target=\"_blank\">Release notes för CryptPad {0}</a>",
"comingSoon": "Kommer snart...",
"language": "Språk",
"userlist_offline": "Du är för närvarande offline, användarlistan är inte tillgänglig.",
"editors": "redigerare",
"editor": "redigerare",
"viewers": "läsare",
"viewer": "läsare",
"and": "Och",
"users": "Användare",
"anonymousUser": "anonym redaktör",
"anonymousUsers": "anonyma redigerare",
"yourself": "Dig själv",
"anonymous": "Anonym",
"readonly": "Skrivskyddat",
"errorState": "Kritiskt fel: {0}",
"forgotten": "Flyttad till papperskorgen",
"initializing": "Initialiserar...",
"typing": "Redigerar",
"reconnecting": "Återuppkopplar",
"synchronizing": "Synkroniserar",
"disconnected": "Frånkopplad",
"realtime_unrecoverableError": "Ett oåterkalleligt fel har uppstått. Klicka OK för att ladda om.",
"disabledApp": "Programmet har blivit avaktiverat. Kontakta administratören av denna CryptPad för mer information.",
"mustLogin": "Du måste vara inloggad för att ha tillgång till denna sida",
"deletedFromServer": "Dokumentet är borttaget från servern",
"deleted": "Raderad",
"synced": "Allting är sparat",
"saved": "Sparad",
"error": "Fel",
"loading": "Laddar...",
"newVersionError": "En ny version av CryptPad är tillgänglig.<br><a href='#'>Ladda om</a> för att använda den nya versionen, eller tryck Esc för att nå ditt innehåll i <b>offline-läge</b>.",
"errorRedirectToHome": "Tryck <em>Esc</em> för att bli omdirigerad till din CryptDrive.",
"errorCopy": " Du kan fortfarande nå innehållet genom att trycka <em>Esc</em>.<br>När du stänger detta fönster kommer du inte längre ha tillgång till det.",
"invalidHashError": "Dokumentet du har begärt har en felaktig URL.",
"chainpadError": "Ett kritiskt fel har uppstått när ditt innehåll uppdaterades. Denna sida är i skrivskyddat läge för att säkerställa att du inte förlorar ditt arbete.<br>Tryck <em>Esc</em> för att fortsätta visa detta dokument, eller ladda om och försök redigera igen.",
"inactiveError": "Detta dokument har tagits bort på grund av inaktivitet. Vänligen tryck Esc för att skapa ett nytt dokument.",
"deletedError": "Detta dokument har tagits bort av sin ägare och är inte längre tillgängligt.",
"expiredError": "Detta dokument har nått sitt utgångsdatum och är inte längre tillgängligt.",
"padNotPinnedVariable": "Detta dokument kommer gå ut efter {4} dagar av inaktivitet, {0}logga in{1} eller {2}registrera{3} för att bevara det.",
"padNotPinned": "Detta dokument kommer automatiskt gå ut efter 3 månader av inaktivitet, {0}logga in{1} eller {2}registrera{3} för att bevara det.",
"onLogout": "Du är utloggad, {0}klicka här{1} för att logga in<br>eller tryck <em>Escape</em> för att öppna ditt dokument i skrivskyddat läge.",
"typeError": "Detta dokument är inte kompatibelt med det valda programmet",
"websocketError": "Kan inte ansluta till websocket-servern...",
"button_newsheet": "Nytt kalkylark",
"button_newkanban": "Ny Kanban",
"button_newwhiteboard": "Ny whiteboard",
"button_newslide": "Ny presentation",
"button_newpoll": "Ny votering",
"button_newpad": "Nytt textdokument"
}

View file

@ -459,7 +459,7 @@ define([
var addPublicKey = function ($container) {
if (!APP.readOnly) { return; }
if (!Messages.profile_copyKey) { return; } // XXX
if (!Messages.profile_copyKey) { return; }
var $div = $(h('div.cp-sidebarlayout-element')).appendTo($container);
APP.$edPublic = $('<button>', {
@ -473,7 +473,7 @@ define([
};
var setPublicKeyButton = function (data) {
if (!data.edPublic || APP.getEdPublic || !APP.readOnly) { return; }
if (!Messages.profile_copyKey) { return; } // XXX
if (!Messages.profile_copyKey) { return; }
APP.$edPublic.show();
APP.getEdPublic = function () {
var metadataMgr = APP.common.getMetadataMgr();
@ -506,7 +506,7 @@ define([
addFriendRequest($rightside);
addMuteButton($rightside);
addDescription(APP.$rightside);
addPublicKey(APP.$rightside);
addPublicKey($rightside);
addViewButton($rightside);
APP.initialized = true;
createLeftside();