Release version 1.8.0

This commit is contained in:
muxator 2019-10-20 04:18:06 +02:00
commit d967914341
178 changed files with 14595 additions and 13736 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
node_modules
settings.json
/settings.json
!settings.json.template
APIKEY.txt
SESSIONKEY.txt

View file

@ -1,5 +1,22 @@
# 1.8
* FEATURE: code was migrated to `async`/`await`, getting rid of a lot of callbacks (see https://github.com/ether/etherpad-lite/issues/3540)
* FEATURE: support configuration via environment variables
* FEATURE: include an official Dockerfile in the main repository
* FEATURE: support including plugins in custom Docker builds
* REQUIREMENTS: minimum required Node version is **8.9.0 LTS**. Release 1.8.3 will require at least Node **10.13.0** LTS
* MINOR: in the HTTP API, allow URL parameters and POST bodies to co-exist
* MINOR: fix Unicode bug in HTML export
* MINOR: bugfixes to colibris chat window
* MINOR: code simplification (avoided double negations, introduced early exits, ...)
* MINOR: reduced the size of the Windows package
* MINOR: upgraded the nodejs runtime to 10.16.3 in the Windows package
* SECURITY: avoided XSS in IE11
* SECURITY: the version is exposed in http header only when configured
* SECURITY: updated vendored jQuery version
* SECURITY: bumped dependencies
# 1.7.5
* FEATURE: introduced support for multiple skins. See http://etherpad.org/doc/v1.7.5/#index_skins
* FEATURE: introduced support for multiple skins. See https://etherpad.org/doc/v1.7.5/#index_skins
* FEATURE: added a new, optional skin. It can be activated choosing `skinName: "colibris"` in `settings.json`
* FEATURE: allow file import using LibreOffice
* SECURITY: updated many dependencies. No known high or moderate risk dependencies remain.
@ -297,7 +314,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
# 1.3
* NEW: We now follow the semantic versioning scheme!
* NEW: Option to disable IP logging
* NEW: Localisation updates from http://translatewiki.net.
* NEW: Localisation updates from https://translatewiki.net.
* Fix: Fix readOnly group pads
* Fix: don't fetch padList on every request

View file

@ -10,7 +10,7 @@
* contain meaningful and detailed **commit messages** in the form:
```
submodule: description
longer description of the change you have made, eventually mentioning the
number of the issue that is being fixed, in the form: Fixes #someIssueNumber
```
@ -130,5 +130,4 @@ Etherpad is much more than software. So if you aren't a developer then worry no
* Co-Author and Publish CVEs
* Work with SFC to maintain legal side of project
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
* Replying to messages on IRC / The Mailing list / Emails

View file

@ -1,25 +1,27 @@
# A really-real time collaborative word processor for the web
![Demo Etherpad Animated Jif](https://i.imgur.com/zYrGkg3.gif "Etherpad in action on PrimaryPad")
# A real-time collaborative editor for the web
![Demo Etherpad Animated Jif](https://i.imgur.com/zYrGkg3.gif "Etherpad in action")
# About
Etherpad is a really-real time collaborative editor scalable to thousands of simultaneous real time users. Unlike all other collaborative tools Etherpad provides full fidelity data export and portability making it fully GDPR compliant.
Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on _your_ server, under _your_ control.
**[Try it out](https://beta.etherpad.org)**
# Installation
## Requirements
- `nodejs` >= **6.9.0** (preferred: `nodejs` >= **8.9**)
- `nodejs` >= **8.9.0** (preferred: `nodejs` >= **10.13.0**). Please note that starting Jan 1st, 2020, nodejs 8.x is deprecated.
## Uber-Quick Ubuntu
## GNU/Linux and other UNIX-like systems
### Quick install on Debian/Ubuntu
```
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt-get install -y nodejs
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt install -y nodejs
git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh
```
## GNU/Linux and other UNIX-like systems
You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **6.9.0**, preferred: >= **8.9**).
### Manual install
You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **8.9.0**, preferred: >= **10.13.0**).
**As any user (we recommend creating a separate user called etherpad):**
@ -34,19 +36,19 @@ To update to the latest released version, execute `git pull origin`. The next st
## Windows
### Prebuilt Windows package
This package works out of the box on any windows machine, but it's not very useful for developing purposes...
This package runs on any Windows machine, but for development purposes, please do a manual install.
1. [Download the latest Windows package](http://etherpad.org/#download)
1. [Download the latest Windows package](https://etherpad.org/#download)
2. Extract the folder
Now, run `start.bat` and open <http://localhost:9001> in your browser. You like it? [Next steps](#next-steps).
Run `start.bat` and open <http://localhost:9001> in your browser. You like it? [Next steps](#next-steps).
### Manually install on Windows
You'll need [node.js](https://nodejs.org) and (optionally, though recommended) git.
1. Grab the source, either
- download <https://github.com/ether/etherpad-lite/zipball/master>
- or `git clone --branch master https://github.com/ether/etherpad-lite.git` (for this you need git, obviously)
- or `git clone --branch master https://github.com/ether/etherpad-lite.git`
2. start `bin\installOnWindows.bat`
Now, run `start.bat` and open <http://localhost:9001> in your browser.
@ -65,9 +67,10 @@ If cloning to a subdirectory within another project, you may need to do the foll
You can modify the settings in `settings.json`.
If you need to handle multiple settings files, you can pass the path to a settings file to `bin/run.sh` using the `-s|--settings` option: this allows you to run multiple Etherpad instances from the same installation.
Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt.
Once you have access to your /admin section settings can be modified through the web browser.
**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`.
Once you have access to your `/admin` section settings can be modified through the web browser.
You should use a dedicated database such as "mysql", if you are planning on using etherpad-in a production environment, since the "dirtyDB" database driver is only for testing and/or development purposes.
If you are planning to use Etherpad in a production environment, you should use a dedicated database such as `mysql`, since the `dirtyDB` database driver is only for testing and/or development purposes.
## Secure your installation
If you have enabled authentication in `users` section in `settings.json`, it is a good security practice to **store hashes instead of plain text passwords** in that file. This is _especially_ advised if you are running a production installation.
@ -87,10 +90,6 @@ Documentation can be found in `doc/`.
# Development
## Things you should know
Understand [git](https://training.github.com/) and watch this [video on getting started with Etherpad Development](https://youtu.be/67-Q26YH97E).
If you're new to node.js, start with Ryan Dahl's [Introduction to Node.js](https://youtu.be/jo_B4LTHi3I).
You can debug Etherpad using `bin/debugRun.sh`.
If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
@ -99,11 +98,9 @@ If you want to find out how Etherpad's `Easysync` works (the library that makes
Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md)
# Get in touch
[mailinglist](https://groups.google.com/group/etherpad-lite-dev)
[#etherpad-lite-dev freenode IRC](https://webchat.freenode.net?channels=#etherpad-lite-dev)!
The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues).
# Languages
Etherpad is written in JavaScript on both the server and client so it's easy for developers to maintain and add new features.
For **responsible disclosure of vulnerabilities**, please write a mail to the maintainer (a.mux@inwind.it).
# HTTP API
Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API)
@ -113,7 +110,7 @@ that allows your web application to manage pads, users and groups. It is recomme
There is a [jQuery plugin](https://github.com/ether/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website.
# Plugin Framework
Etherpad offers a plugin framework, allowing you to easily add your own features. By default your Etherpad is extremely light-weight and it's up to you to customize your experience. Once you have Etherpad installed you should visit the plugin page and take control.
Etherpad offers a plugin framework, allowing you to easily add your own features. By default your Etherpad is extremely light-weight and it's up to you to customize your experience. Once you have Etherpad installed you should visit the plugin page and take control.
# Translations / Localizations (i18n / l10n)
Etherpad comes with translations into all languages thanks to the team at TranslateWiki.
@ -121,12 +118,5 @@ Etherpad comes with translations into all languages thanks to the team at Transl
# FAQ
Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**.
# Donate!
* [Flattr](https://flattr.com/thing/71378/Etherpad-Foundation)
* Paypal - Press the donate button on [etherpad.org](http://etherpad.org)
* [Bitcoin](https://coinbase.com/checkouts/1e572bf8a82e4663499f7f1f66c2d15a)
All donations go to the Etherpad foundation which is part of Software Freedom Conservency
# License
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# IMPORTANT
# IMPORTANT
# Protect against misspelling a var and rm -rf /
set -u
set -e

View file

@ -1,6 +1,6 @@
#!/bin/sh
NODE_VERSION="8.9.0"
NODE_VERSION="10.16.3"
#Move to the folder where ep-lite is installed
cd `dirname $0`
@ -11,21 +11,21 @@ if [ -d "../bin" ]; then
fi
#Is wget installed?
hash wget > /dev/null 2>&1 || {
hash wget > /dev/null 2>&1 || {
echo "Please install wget" >&2
exit 1
exit 1
}
#Is zip installed?
hash zip > /dev/null 2>&1 || {
hash zip > /dev/null 2>&1 || {
echo "Please install zip" >&2
exit 1
exit 1
}
#Is zip installed?
hash unzip > /dev/null 2>&1 || {
hash unzip > /dev/null 2>&1 || {
echo "Please install unzip" >&2
exit 1
exit 1
}
START_FOLDER=$(pwd);
@ -37,6 +37,10 @@ cd $TMP_FOLDER
rm -rf node_modules
rm -f etherpad-lite-win.zip
# setting NODE_ENV=production ensures that dev dependencies are not installed,
# making the windows package smaller
export NODE_ENV=production
echo "do a normal unix install first..."
bin/installDeps.sh || exit 1

View file

@ -1,145 +1,94 @@
/*
This is a debug tool. It checks all revisions for data corruption
*/
* This is a debug tool. It checks all revisions for data corruption
*/
if(process.argv.length != 2)
{
if (process.argv.length != 2) {
console.error("Use: node bin/checkAllPads.js");
process.exit(1);
}
//initialize the variables
var db, settings, padManager;
var npm = require("../src/node_modules/npm");
var async = require("../src/node_modules/async");
// load and initialize NPM
let npm = require('../src/node_modules/npm');
npm.load({}, async function() {
var Changeset = require("../src/static/js/Changeset");
try {
// initialize the database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
async.series([
//load npm
function(callback) {
npm.load({}, callback);
},
//load modules
function(callback) {
settings = require('../src/node/utils/Settings');
db = require('../src/node/db/DB');
// load modules
let Changeset = require('../src/static/js/Changeset');
let padManager = require('../src/node/db/PadManager');
//initialize the database
db.init(callback);
},
//load pads
function (callback)
{
padManager = require('../src/node/db/PadManager');
padManager.listAllPads(function(err, res)
{
padIds = res.padIDs;
callback(err);
});
},
function (callback)
{
async.forEach(padIds, function(padId, callback)
{
padManager.getPad(padId, function(err, pad) {
if (err) {
callback(err);
}
//check if the pad has a pool
if(pad.pool === undefined )
{
console.error("[" + pad.id + "] Missing attribute pool");
callback();
return;
}
// get all pads
let res = await padManager.listAllPads();
//create an array with key kevisions
//key revisions always save the full pad atext
var head = pad.getHeadRevisionNumber();
var keyRevisions = [];
for(var i=0;i<head;i+=100)
{
keyRevisions.push(i);
}
//run trough all key revisions
async.forEachSeries(keyRevisions, function(keyRev, callback)
{
//create an array of revisions we need till the next keyRevision or the End
var revisionsNeeded = [];
for(var i=keyRev;i<=keyRev+100 && i<=head; i++)
{
revisionsNeeded.push(i);
}
//this array will hold all revision changesets
var revisions = [];
//run trough all needed revisions and get them from the database
async.forEach(revisionsNeeded, function(revNum, callback)
{
db.db.get("pad:"+pad.id+":revs:" + revNum, function(err, revision)
{
revisions[revNum] = revision;
callback(err);
});
}, function(err)
{
if(err)
{
callback(err);
return;
}
for (let padId of res.padIDs) {
//check if the revision exists
if (revisions[keyRev] == null) {
console.error("[" + pad.id + "] Missing revision " + keyRev);
callback();
return;
}
//check if there is a atext in the keyRevisions
if(revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined)
{
console.error("[" + pad.id + "] Missing atext in revision " + keyRev);
callback();
return;
}
var apool = pad.pool;
var atext = revisions[keyRev].meta.atext;
for(var i=keyRev+1;i<=keyRev+100 && i<=head; i++)
{
try
{
//console.log("[" + pad.id + "] check revision " + i);
var cs = revisions[i].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
}
catch(e)
{
console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message);
callback();
return;
}
}
callback();
});
}, callback);
});
}, callback);
}
], function (err)
{
if(err) throw err;
else
{
console.log("finished");
process.exit(0);
let pad = await padManager.getPad(padId);
// check if the pad has a pool
if (pad.pool === undefined) {
console.error("[" + pad.id + "] Missing attribute pool");
continue;
}
// create an array with key kevisions
// key revisions always save the full pad atext
let head = pad.getHeadRevisionNumber();
let keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (let keyRev of keyRevisions) {
// create an array of revisions we need till the next keyRevision or the End
var revisionsNeeded = [];
for (let rev = keyRev ; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
var revisions = [];
// run through all needed revisions and get them from the database
for (let revNum of revisionsNeeded) {
let revision = await db.get("pad:" + pad.id + ":revs:" + revNum);
revisions[revNum] = revision;
}
// check if the revision exists
if (revisions[keyRev] == null) {
console.error("[" + pad.id + "] Missing revision " + keyRev);
continue;
}
// check if there is a atext in the keyRevisions
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
console.error("[" + pad.id + "] Missing atext in revision " + keyRev);
continue;
}
let apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try {
let cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
} catch (e) {
console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message);
}
}
}
console.log("finished");
process.exit(0);
}
} catch (err) {
console.trace(err);
process.exit(1);
}
});

View file

@ -1,141 +1,95 @@
/*
This is a debug tool. It checks all revisions for data corruption
*/
* This is a debug tool. It checks all revisions for data corruption
*/
if(process.argv.length != 3)
{
if (process.argv.length != 3) {
console.error("Use: node bin/checkPad.js $PADID");
process.exit(1);
}
//get the padID
var padId = process.argv[2];
//initialize the variables
var db, settings, padManager;
var npm = require("../src/node_modules/npm");
var async = require("../src/node_modules/async");
// get the padID
const padId = process.argv[2];
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
// load and initialize NPM;
let npm = require('../src/node_modules/npm');
npm.load({}, async function() {
async.series([
//load npm
function(callback) {
npm.load({}, function(er) {
callback(er);
})
},
//load modules
function(callback) {
settings = require('../src/node/utils/Settings');
db = require('../src/node/db/DB');
try {
// initialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
//initialize the database
db.init(callback);
},
//get the pad
function (callback)
{
padManager = require('../src/node/db/PadManager');
padManager.doesPadExists(padId, function(err, exists)
{
if(!exists)
{
console.error("Pad does not exist");
// load modules
let Changeset = require('ep_etherpad-lite/static/js/Changeset');
let padManager = require('../src/node/db/PadManager');
let exists = await padManager.doesPadExists(padId);
if (!exists) {
console.error("Pad does not exist");
process.exit(1);
}
// get the pad
let pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
let head = pad.getHeadRevisionNumber();
let keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (let keyRev of keyRevisions) {
// create an array of revisions we need till the next keyRevision or the End
let revisionsNeeded = [];
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
var revisions = [];
// run through all needed revisions and get them from the database
for (let revNum of revisionsNeeded) {
let revision = await db.get("pad:" + padId + ":revs:" + revNum);
revisions[revNum] = revision;
}
// check if the pad has a pool
if (pad.pool === undefined ) {
console.error("Attribute pool is missing");
process.exit(1);
}
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
});
},
function (callback)
{
//create an array with key revisions
//key revisions always save the full pad atext
var head = pad.getHeadRevisionNumber();
var keyRevisions = [];
for(var i=0;i<head;i+=100)
{
keyRevisions.push(i);
}
//run trough all key revisions
async.forEachSeries(keyRevisions, function(keyRev, callback)
{
//create an array of revisions we need till the next keyRevision or the End
var revisionsNeeded = [];
for(var i=keyRev;i<=keyRev+100 && i<=head; i++)
{
revisionsNeeded.push(i);
// check if there is an atext in the keyRevisions
if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
console.error("No atext in key revision " + keyRev);
continue;
}
//this array will hold all revision changesets
var revisions = [];
//run trough all needed revisions and get them from the database
async.forEach(revisionsNeeded, function(revNum, callback)
{
db.db.get("pad:"+padId+":revs:" + revNum, function(err, revision)
{
revisions[revNum] = revision;
callback(err);
});
}, function(err)
{
if(err)
{
callback(err);
return;
let apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try {
// console.log("check revision " + rev);
let cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
} catch(e) {
console.error("Bad changeset at revision " + rev + " - " + e.message);
continue;
}
//check if the pad has a pool
if(pad.pool === undefined )
{
console.error("Attribute pool is missing");
process.exit(1);
}
//check if there is an atext in the keyRevisions
if(revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined)
{
console.error("No atext in key revision " + keyRev);
callback();
return;
}
var apool = pad.pool;
var atext = revisions[keyRev].meta.atext;
for(var i=keyRev+1;i<=keyRev+100 && i<=head; i++)
{
try
{
//console.log("check revision " + i);
var cs = revisions[i].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
}
catch(e)
{
console.error("Bad changeset at revision " + i + " - " + e.message);
callback();
return;
}
}
callback();
});
}, callback);
}
], function (err)
{
if(err) throw err;
else
{
console.log("finished");
process.exit(0);
}
console.log("finished");
process.exit(0);
}
} catch (e) {
console.trace(e);
process.exit(1);
}
});

View file

@ -1,4 +1,4 @@
var startTime = new Date().getTime();
var startTime = Date.now();
var fs = require("fs");
var ueberDB = require("../src/node_modules/ueberDB");
var mysql = require("../src/node_modules/ueberDB/node_modules/mysql");
@ -26,10 +26,10 @@ log("open output file...");
var sqlOutput = fs.openSync(sqlOutputFile, "w");
var sql = "SET CHARACTER SET UTF8;\n" +
"CREATE TABLE IF NOT EXISTS `store` ( \n" +
"`key` VARCHAR( 100 ) NOT NULL , \n" +
"`value` LONGTEXT NOT NULL , \n" +
"`key` VARCHAR( 100 ) NOT NULL , \n" +
"`value` LONGTEXT NOT NULL , \n" +
"PRIMARY KEY ( `key` ) \n" +
") ENGINE = INNODB;\n" +
") ENGINE = INNODB;\n" +
"START TRANSACTION;\n\n";
fs.writeSync(sqlOutput, sql);
log("done");
@ -43,7 +43,7 @@ var etherpadDB = mysql.createConnection({
});
//get the timestamp once
var timestamp = new Date().getTime();
var timestamp = Date.now();
var padIDs;
@ -52,7 +52,7 @@ async.series([
function(callback)
{
log("get all padIds out of the database...");
etherpadDB.query("SELECT ID FROM PAD_META", [], function(err, _padIDs)
{
padIDs = _padIDs;
@ -62,9 +62,9 @@ async.series([
function(callback)
{
log("done");
//create a queue with a concurrency 100
var queue = async.queue(function (padId, callback)
var queue = async.queue(function (padId, callback)
{
convertPad(padId, function(err)
{
@ -72,10 +72,10 @@ async.series([
callback(err);
});
}, 100);
//set the step callback as the queue callback
queue.drain = callback;
//add the padids to the worker queue
for(var i=0,length=padIDs.length;i<length;i++)
{
@ -85,32 +85,32 @@ async.series([
], function(err)
{
if(err) throw err;
//write the groups
var sql = "";
for(var proID in proID2groupID)
{
var groupID = proID2groupID[proID];
var subdomain = proID2subdomain[proID];
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("group:" + groupID) + ", " + etherpadDB.escape(JSON.stringify(groups[groupID]))+ ");\n";
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("mapper2group:subdomain:" + subdomain) + ", " + etherpadDB.escape(groupID)+ ");\n";
}
//close transaction
sql+="COMMIT;";
//end the sql file
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
fs.closeSync(sqlOutput);
log("finished.");
process.exit(0);
});
function log(str)
{
console.log((new Date().getTime() - startTime)/1000 + "\t" + str);
console.log((Date.now() - startTime)/1000 + "\t" + str);
}
var padsDone = 0;
@ -118,10 +118,10 @@ var padsDone = 0;
function incrementPadStats()
{
padsDone++;
if(padsDone%100 == 0)
{
var averageTime = Math.round(padsDone/((new Date().getTime() - startTime)/1000));
var averageTime = Math.round(padsDone/((Date.now() - startTime)/1000));
log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s")
}
}
@ -149,10 +149,10 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
if(!err)
{
try
{
@ -163,7 +163,7 @@ function convertPad(padId, callback)
}
}catch(e) {err = e}
}
callback(err);
});
},
@ -171,10 +171,10 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
if(!err)
{
try
{
@ -185,7 +185,7 @@ function convertPad(padId, callback)
}
}catch(e) {err = e}
}
callback(err);
});
},
@ -193,10 +193,10 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
if(!err)
{
try
{
@ -207,7 +207,7 @@ function convertPad(padId, callback)
}
}catch(e) {err = e}
}
callback(err);
});
},
@ -215,7 +215,7 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
@ -225,7 +225,7 @@ function convertPad(padId, callback)
apool=JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
}
callback(err);
});
},
@ -233,10 +233,10 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
if(!err)
{
try
{
@ -247,7 +247,7 @@ function convertPad(padId, callback)
}
}catch(e) {err = e}
}
callback(err);
});
},
@ -255,17 +255,17 @@ function convertPad(padId, callback)
function(callback)
{
var sql = "SELECT JSON FROM `PAD_META` WHERE ID=?";
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
if(!err)
{
try
{
padmeta = JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
}
callback(err);
});
},
@ -278,19 +278,19 @@ function convertPad(padId, callback)
callback();
return;
}
//get the proID out of this padID
var proID = padId.split("$")[0];
var sql = "SELECT subDomain FROM pro_domains WHERE ID = ?";
etherpadDB.query(sql, [proID], function(err, results)
{
if(!err)
{
subdomain = results[0].subDomain;
}
callback(err);
});
}
@ -300,105 +300,105 @@ function convertPad(padId, callback)
{
//saves all values that should be written to the database
var values = {};
//this is a pro pad, let's convert it to a group pad
if(padId.indexOf("$") != -1)
{
var padIdParts = padId.split("$");
var proID = padIdParts[0];
var padName = padIdParts[1];
var groupID
//this proID is not converted so far, do it
if(proID2groupID[proID] == null)
{
groupID = "g." + randomString(16);
//create the mappers for this new group
proID2groupID[proID] = groupID;
proID2subdomain[proID] = subdomain;
groups[groupID] = {pads: {}};
}
//use the generated groupID;
groupID = proID2groupID[proID];
//rename the pad
padId = groupID + "$" + padName;
//set the value for this pad in the group
groups[groupID].pads[padId] = 1;
}
try
{
var newAuthorIDs = {};
var oldName2newName = {};
//replace the authors with generated authors
// we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global
for(var i in apool.numToAttrib)
{
var key = apool.numToAttrib[i][0];
var value = apool.numToAttrib[i][1];
//skip non authors and anonymous authors
if(key != "author" || value == "")
continue;
//generate new author values
var authorID = "a." + randomString(16);
var authorColorID = authors[i].colorId || Math.floor(Math.random()*32);
var authorName = authors[i].name || null;
//overwrite the authorID of the attribute pool
apool.numToAttrib[i][1] = authorID;
//write the author to the database
values["globalAuthor:" + authorID] = {"colorId" : authorColorID, "name": authorName, "timestamp": timestamp};
//save in mappers
newAuthorIDs[i] = authorID;
oldName2newName[value] = authorID;
}
//save all revisions
for(var i=0;i<changesets.length;i++)
{
values["pad:" + padId + ":revs:" + i] = {changeset: changesets[i],
values["pad:" + padId + ":revs:" + i] = {changeset: changesets[i],
meta : {
author: newAuthorIDs[changesetsMeta[i].a],
timestamp: changesetsMeta[i].t,
atext: changesetsMeta[i].atext || undefined
}};
}
//save all chat messages
for(var i=0;i<chatMessages.length;i++)
{
values["pad:" + padId + ":chat:" + i] = {"text": chatMessages[i].lineText,
"userId": oldName2newName[chatMessages[i].userId],
values["pad:" + padId + ":chat:" + i] = {"text": chatMessages[i].lineText,
"userId": oldName2newName[chatMessages[i].userId],
"time": chatMessages[i].time}
}
//generate the latest atext
var fullAPool = (new AttributePool()).fromJsonable(apool);
var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
var atext = changesetsMeta[keyRev].atext;
var curRev = keyRev;
while (curRev < padmeta.head)
while (curRev < padmeta.head)
{
curRev++;
var changeset = changesets[curRev];
atext = Changeset.applyToAText(changeset, atext, fullAPool);
}
values["pad:" + padId] = {atext: atext,
pool: apool,
head: padmeta.head,
values["pad:" + padId] = {atext: atext,
pool: apool,
head: padmeta.head,
chatHead: padmeta.numChatMessages }
}
catch(e)
{
@ -407,13 +407,13 @@ function convertPad(padId, callback)
callback();
return;
}
var sql = "";
for(var key in values)
{
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape(key) + ", " + etherpadDB.escape(JSON.stringify(values[key]))+ ");\n";
}
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
callback();
}
@ -429,24 +429,24 @@ function parsePage(array, pageStart, offsets, data, json)
{
var start = 0;
var lengths = offsets.split(",");
for(var i=0;i<lengths.length;i++)
{
var unitLength = lengths[i];
//skip empty units
if(unitLength == "")
continue;
//parse the number
unitLength = Number(unitLength);
//cut the unit out of data
var unit = data.substr(start, unitLength);
//put it into the array
array[pageStart + i] = json ? JSON.parse(unit) : unit;
//update start
start+=unitLength;
}

View file

@ -1,5 +1,5 @@
{
"etherpadDB":
"etherpadDB":
{
"host": "localhost",
"port": 3306,

View file

@ -1,63 +1,41 @@
/*
A tool for deleting pads from the CLI, because sometimes a brick is required to fix a window.
*/
* A tool for deleting pads from the CLI, because sometimes a brick is required
* to fix a window.
*/
if(process.argv.length != 3)
{
if (process.argv.length != 3) {
console.error("Use: node deletePad.js $PADID");
process.exit(1);
}
//get the padID
var padId = process.argv[2];
var db, padManager, pad, settings;
var neededDBValues = ["pad:"+padId];
// get the padID
let padId = process.argv[2];
var npm = require("../src/node_modules/npm");
var async = require("../src/node_modules/async");
let npm = require('../src/node_modules/npm');
async.series([
// load npm
function(callback) {
npm.load({}, function(er) {
if(er)
{
console.error("Could not load NPM: " + er)
process.exit(1);
}
else
{
callback();
}
})
},
// load modules
function(callback) {
settings = require('../src/node/utils/Settings');
db = require('../src/node/db/DB');
callback();
},
// initialize the database
function (callback)
{
db.init(callback);
},
// delete the pad and its links
function (callback)
{
padManager = require('../src/node/db/PadManager');
padManager.removePad(padId, function(err){
callback(err);
});
callback();
npm.load({}, async function(er) {
if (er) {
console.error("Could not load NPM: " + er)
process.exit(1);
}
], function (err)
{
if(err) throw err;
else
{
console.log("Finished deleting padId: "+padId);
process.exit();
try {
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
padManager = require('../src/node/db/PadManager');
await padManager.removePad(padId);
console.log("Finished deleting padId: " + padId);
process.exit(0);
} catch (e) {
if (err.name === "apierror") {
console.error(e);
} else {
console.trace(e);
}
process.exit(1);
}
});

View file

@ -12,9 +12,9 @@ try:
assert(os.path.exists(dirtydb_input))
assert(not os.path.exists(dirtydb_output))
except:
print()
print()
print('Usage: %s /path/to/dirty.db' % sys.argv[0])
print()
print()
print('Note: Will create a file named dirty.db.new in the same folder,')
print(' please make sure permissions are OK and a file by that')
print(' name does not exist already. This script works by omitting')

View file

@ -65,7 +65,7 @@ function processIncludes(inputFile, input, cb) {
console.error(includes);
var incCount = includes.length;
if (incCount === 0) cb(null, input);
includes.forEach(function(include) {
var fname = include.replace(/^@include\s+/, '');
if (!fname.match(/\.md$/)) fname += '.md';

View file

@ -1,109 +1,75 @@
/*
This is a debug tool. It helps to extract all datas of a pad and move it from an productive environment and to a develop environment to reproduce bugs there. It outputs a dirtydb file
*/
* This is a debug tool. It helps to extract all datas of a pad and move it from
* a productive environment and to a develop environment to reproduce bugs
* there. It outputs a dirtydb file
*/
if(process.argv.length != 3)
{
if (process.argv.length != 3) {
console.error("Use: node extractPadData.js $PADID");
process.exit(1);
}
//get the padID
var padId = process.argv[2];
var db, dirty, padManager, pad, settings;
var neededDBValues = ["pad:"+padId];
// get the padID
let padId = process.argv[2];
var npm = require("../node_modules/ep_etherpad-lite/node_modules/npm");
var async = require("../node_modules/ep_etherpad-lite/node_modules/async");
let npm = require('../src/node_modules/npm');
async.series([
// load npm
function(callback) {
npm.load({}, function(er) {
if(er)
{
console.error("Could not load NPM: " + er)
process.exit(1);
}
else
{
callback();
}
})
},
// load modules
function(callback) {
settings = require('../node_modules/ep_etherpad-lite/node/utils/Settings');
db = require('../node_modules/ep_etherpad-lite/node/db/DB');
dirty = require("../node_modules/ep_etherpad-lite/node_modules/ueberDB/node_modules/dirty")(padId + ".db");
callback();
},
//initialize the database
function (callback)
{
db.init(callback);
},
//get the pad
function (callback)
{
padManager = require('../node_modules/ep_etherpad-lite/node/db/PadManager');
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
function (callback)
{
//add all authors
var authors = pad.getAllAuthors();
for(var i=0;i<authors.length;i++)
{
neededDBValues.push("globalAuthor:" + authors[i]);
}
//add all revisions
var revHead = pad.head;
for(var i=0;i<=revHead;i++)
{
neededDBValues.push("pad:"+padId+":revs:" + i);
}
//get all chat values
var chatHead = pad.chatHead;
for(var i=0;i<=chatHead;i++)
{
neededDBValues.push("pad:"+padId+":chat:" + i);
}
//get and set all values
async.forEach(neededDBValues, function(dbkey, callback)
{
db.db.db.wrappedDB.get(dbkey, function(err, dbvalue)
{
if(err) { callback(err); return}
if(dbvalue && typeof dbvalue != 'object'){
dbvalue=JSON.parse(dbvalue); // if it's not json then parse it as json
}
dirty.set(dbkey, dbvalue, callback);
});
}, callback);
npm.load({}, async function(er) {
if (er) {
console.error("Could not load NPM: " + er)
process.exit(1);
}
], function (err)
{
if(err) throw err;
else
{
console.log("finished");
process.exit();
try {
// initialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
// load extra modules
let dirtyDB = require('../src/node_modules/dirty');
let padManager = require('../src/node/db/PadManager');
let util = require('util');
// initialize output database
let dirty = dirtyDB(padId + '.db');
// Promise wrapped get and set function
let wrapped = db.db.db.wrappedDB;
let get = util.promisify(wrapped.get.bind(wrapped));
let set = util.promisify(dirty.set.bind(dirty));
// array in which required key values will be accumulated
let neededDBValues = ['pad:' + padId];
// get the actual pad object
let pad = await padManager.getPad(padId);
// add all authors
neededDBValues.push(...pad.getAllAuthors().map(author => 'globalAuthor:' + author));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push('pad:' + padId + ':revs:' + rev);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push('pad:' + padId + ':chat:' + chat);
}
for (let dbkey of neededDBValues) {
let dbvalue = await get(dbkey);
if (dbvalue && typeof dbvalue !== 'object') {
dbvalue = JSON.parse(dbvalue);
}
await set(dbkey, dbvalue);
}
console.log('finished');
process.exit(0);
} catch (er) {
console.error(er);
process.exit(1);
}
});
//get the pad object
//get all revisions of this pad
//get all authors related to this pad
//get the readonly link related to this pad
//get the chat entries related to this pad

View file

@ -1,4 +1,4 @@
var startTime = new Date().getTime();
var startTime = Date.now();
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
@ -73,7 +73,7 @@ require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
function log(str)
{
console.log((new Date().getTime() - startTime)/1000 + "\t" + str);
console.log((Date.now() - startTime)/1000 + "\t" + str);
}
unescape = function(val) {

View file

@ -1,12 +1,12 @@
#!/bin/sh
# minimum required node version
REQUIRED_NODE_MAJOR=6
REQUIRED_NODE_MAJOR=8
REQUIRED_NODE_MINOR=9
# minimum required npm version
REQUIRED_NPM_MAJOR=3
REQUIRED_NPM_MINOR=10
REQUIRED_NPM_MAJOR=5
REQUIRED_NPM_MINOR=5
require_minimal_version() {
PROGRAM_LABEL="$1"

View file

@ -1,106 +1,78 @@
/*
This is a repair tool. It extracts all datas of a pad, removes and inserts them again.
*/
* This is a repair tool. It extracts all datas of a pad, removes and inserts them again.
*/
console.warn("WARNING: This script must not be used while etherpad is running!");
if(process.argv.length != 3)
{
if (process.argv.length != 3) {
console.error("Use: node bin/repairPad.js $PADID");
process.exit(1);
}
//get the padID
// get the padID
var padId = process.argv[2];
var db, padManager, pad, settings;
var neededDBValues = ["pad:"+padId];
let npm = require("../src/node_modules/npm");
npm.load({}, async function(er) {
if (er) {
console.error("Could not load NPM: " + er)
process.exit(1);
}
var npm = require("../src/node_modules/npm");
var async = require("../src/node_modules/async");
try {
// intialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
async.series([
// load npm
function(callback) {
npm.load({}, function(er) {
if(er)
{
console.error("Could not load NPM: " + er)
process.exit(1);
}
else
{
callback();
}
})
},
// load modules
function(callback) {
settings = require('../src/node/utils/Settings');
db = require('../src/node/db/DB');
callback();
},
//initialize the database
function (callback)
{
db.init(callback);
},
//get the pad
function (callback)
{
padManager = require('../src/node/db/PadManager');
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
function (callback)
{
//add all authors
var authors = pad.getAllAuthors();
for(var i=0;i<authors.length;i++)
{
neededDBValues.push("globalAuthor:" + authors[i]);
// get the pad
let padManager = require('../src/node/db/PadManager');
let pad = await padManager.getPad(padId);
// accumulate the required keys
let neededDBValues = ["pad:" + padId];
// add all authors
neededDBValues.push(...pad.getAllAuthors().map(author => "globalAuthor:"));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push("pad:" + padId + ":revs:" + rev);
}
//add all revisions
var revHead = pad.head;
for(var i=0;i<=revHead;i++)
{
neededDBValues.push("pad:"+padId+":revs:" + i);
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push("pad:" + padId + ":chat:" + chat);
}
//get all chat values
var chatHead = pad.chatHead;
for(var i=0;i<=chatHead;i++)
{
neededDBValues.push("pad:"+padId+":chat:" + i);
}
callback();
},
function (callback) {
db = db.db;
//
// NB: this script doesn't actually does what's documented
// since the `value` fields in the following `.forEach`
// block are just the array index numbers
//
// the script therefore craps out now before it can do
// any damage.
//
// See gitlab issue #3545
//
console.info("aborting [gitlab #3545]");
process.exit(1);
// now fetch and reinsert every key
neededDBValues.forEach(function(key, value) {
console.debug("Key: "+key+", value: "+value);
console.log("Key: " + key+ ", value: " + value);
db.remove(key);
db.set(key, value);
});
callback();
}
], function (err)
{
if(err) throw err;
else
{
console.info("finished");
process.exit();
process.exit(0);
} catch (er) {
if (er.name === "apierror") {
console.error(er);
} else {
console.trace(er);
}
}
});
//get the pad object
//get all revisions of this pad
//get all authors related to this pad
//get the readonly link related to this pad
//get the chat entries related to this pad
//remove all keys from database and insert them again

View file

@ -8,8 +8,8 @@
ERROR_HANDLING=0
# Your email address which should receive the error messages
EMAIL_ADDRESS="no-reply@example.com"
# Sets the minimum amount of time between the sending of error emails.
# This ensures you do not get spammed during an endless reboot loop
# Sets the minimum amount of time between the sending of error emails.
# This ensures you do not get spammed during an endless reboot loop
# It's the time in seconds
TIME_BETWEEN_EMAILS=600 # 10 minutes
@ -39,7 +39,7 @@ do
if [ ! -f ${LOG} ]; then
touch ${LOG} || ( echo "Logfile '${LOG}' is not writeable" && exit 1 )
fi
#Check if the file is writeable
if [ ! -w ${LOG} ]; then
echo "Logfile '${LOG}' is not writeable"
@ -48,21 +48,21 @@ do
#Start the application
bin/run.sh $@ >>${LOG} 2>>${LOG}
#Send email
if [ $ERROR_HANDLING = 1 ]; then
TIME_NOW=$(date +%s)
TIME_SINCE_LAST_SEND=$(($TIME_NOW - $LAST_EMAIL_SEND))
if [ $TIME_SINCE_LAST_SEND -gt $TIME_BETWEEN_EMAILS ]; then
printf "Server was restarted at: $(date)\nThe last 50 lines of the log before the error happens:\n $(tail -n 50 ${LOG})" | mail -s "Pad Server was restarted" $EMAIL_ADDRESS
LAST_EMAIL_SEND=$TIME_NOW
fi
fi
echo "RESTART!" >>${LOG}
#Sleep 10 seconds before restart
sleep 10
done

View file

@ -70,11 +70,11 @@ This creates an empty apool. An apool saves which attributes were used during th
```
> apool.fromJsonable({"numToAttrib":{"0":["author","a.kVnWeomPADAT2pn9"],"1":["bold","true"],"2":["italic","true"]},"nextNum":3});
> console.log(apool)
{ numToAttrib:
{ numToAttrib:
{ '0': [ 'author', 'a.kVnWeomPADAT2pn9' ],
'1': [ 'bold', 'true' ],
'2': [ 'italic', 'true' ] },
attribToNum:
attribToNum:
{ 'author,a.kVnWeomPADAT2pn9': 0,
'bold,true': 1,
'italic,true': 2 },

View file

@ -62,7 +62,7 @@ Example: `lang=ar` (translates the interface into Arabic)
## rtl
* Boolean
Default: true
Displays pad text from right to left.

View file

@ -67,7 +67,28 @@ The current version can be queried via /api.
### Request Format
The API is accessible via HTTP. HTTP Requests are in the format /api/$APIVERSION/$FUNCTIONNAME. Parameters are transmitted via HTTP GET. $APIVERSION depends on the endpoints you want to use.
The API is accessible via HTTP. Starting from **1.8**, API endpoints can be invoked indifferently via GET or POST.
The URL of the HTTP request is of the form: `/api/$APIVERSION/$FUNCTIONNAME`. $APIVERSION depends on the endpoint you want to use. Depending on the verb you use (GET or POST) **parameters** can be passed differently.
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?apikey=<APIKEY>&param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
Starting from Etherpad **1.8** it is also possible to invoke the HTTP API via POST. In this case, querystring parameters will still be accepted, but **any parameter with the same name sent via POST will take precedence**. If you need to send large chunks of text (for example, for `setText()`) it is advisable to invoke via POST.
Example with cURL using GET (toy example, no encoding):
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
```
Example with cURL using GET (better example, encodes text):
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
```
Example with cURL using POST:
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
```
### Response Format
Responses are valid JSON in the following format:
@ -278,7 +299,9 @@ returns the text of a pad
#### setText(padID, text)
* API >= 1
sets the text of a pad
Sets the text of a pad.
If your text is long (>8 KB), please invoke via POST and include `text` parameter in the body of the request, not in the URL (since Etherpad **1.8**).
*Example returns:*
* `{code: 0, message:"ok", data: null}`
@ -288,7 +311,9 @@ sets the text of a pad
#### appendText(padID, text)
* API >= 1.2.13
appends text to a pad
Appends text to a pad.
If your text is long (>8 KB), please invoke via POST and include `text` parameter in the body of the request, not in the URL (since Etherpad **1.8**).
*Example returns:*
* `{code: 0, message:"ok", data: null}`
@ -309,6 +334,8 @@ returns the text of a pad formatted as HTML
sets the text of a pad based on HTML, HTML must be well-formed. Malformed HTML will send a warning to the API log.
If `html` is long (>8 KB), please invoke via POST and include `html` parameter in the body of the request, not in the URL (since Etherpad **1.8**).
*Example returns:*
* `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}`
@ -349,7 +376,7 @@ get the changeset at a given revision, or last revision if 'rev' is not defined.
*Example returns:*
* `{ "code" : 0,
"message" : "ok",
"data" : "Z:1>6b|5+6b$Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http://etherpad.org\n"
"data" : "Z:1>6b|5+6b$Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https://etherpad.org\n"
}`
* `{"code":1,"message":"padID does not exist","data":null}`
* `{"code":1,"message":"rev is higher than the head revision of the pad","data":null}`

View file

@ -40,7 +40,7 @@ Returns: {SelectButton}
* {String} value - The value of this option
* {String} text - the label text used for this option
* {Object} attributes - any additional html attributes go here (e.g. `data-l10n-id`)
## registerButton(name, item)
* {String} name - used to reference the item in the toolbar config in settings.json
* {Button|SelectButton} item - the button to add

View file

@ -115,7 +115,7 @@ Your plugin must also contain a [package definition file](https://docs.npmjs.com
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
"contributors": [],
"dependencies": {"MODULE": "0.3.20"},
"engines": { "node": ">= 6.9.0"}
"engines": { "node": ">= 8.9.0"}
}
```
@ -124,7 +124,7 @@ If your plugin adds or modifies the front end HTML (e.g. adding buttons or chang
## Writing and running front-end tests for your plugin
Etherpad allows you to easily create front-end tests for plugins.
Etherpad allows you to easily create front-end tests for plugins.
1. Create a new folder
```

65
docker/Dockerfile Normal file
View file

@ -0,0 +1,65 @@
# Etherpad Lite Dockerfile
#
# https://github.com/ether/etherpad-docker
#
# Author: muxator
#
# Version 0.1
FROM node:buster-slim
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
# git hash of the version to be built.
# If not given, build the latest development version.
ARG ETHERPAD_VERSION=develop
# plugins to install while building the container. By default no plugins are
# installed.
# If given a value, it has to be a space-separated, quoted list of plugin names.
#
# EXAMPLE:
# ETHERPAD_PLUGINS="ep_codepad ep_author_neat"
ARG ETHERPAD_PLUGINS=
# Set the following to production to avoid installing devDeps
# this can be done with build args (and is mandatory to build ARM version)
ENV NODE_ENV=development
# grab the ETHERPAD_VERSION tarball from github (no need to clone the whole
# repository)
RUN echo "Getting version: ${ETHERPAD_VERSION}" && \
curl \
--location \
--fail \
--silent \
--show-error \
--output /opt/etherpad-lite.tar.gz \
https://github.com/ether/etherpad-lite/archive/"${ETHERPAD_VERSION}".tar.gz && \
mkdir /opt/etherpad-lite && \
tar xf /opt/etherpad-lite.tar.gz \
--directory /opt/etherpad-lite \
--strip-components=1 && \
rm /opt/etherpad-lite.tar.gz
WORKDIR /opt/etherpad-lite
# install node dependencies for Etherpad
RUN bin/installDeps.sh && \
rm -rf ~/.npm/_cacache
# Install the plugins, if ETHERPAD_PLUGINS is not empty.
#
# Bash trick: in the for loop ${ETHERPAD_PLUGINS} is NOT quoted, in order to be
# able to split at spaces.
RUN for PLUGIN_NAME in ${ETHERPAD_PLUGINS}; do npm install "${PLUGIN_NAME}"; done
# Copy the custom configuration file, if present. The configuration file has to
# be manually put inside the same directory containing the Dockerfile (we cannot
# directly point to "../settings.json" for Docker's security restrictions).
#
# For the conditional COPY trick, see:
# https://stackoverflow.com/questions/31528384/conditional-copy-add-in-dockerfile#46801962
COPY ./settings.json /opt/etherpad-lite/
EXPOSE 9001
CMD ["node", "node_modules/ep_etherpad-lite/node/server.js"]

120
docker/README.md Normal file
View file

@ -0,0 +1,120 @@
# Docker image
This directory contains the files that are used to build the official Docker image on https://hub.docker.com/r/etherpad/etherpad.
# Downloading from Docker Hub
If you are ok downloading a [prebuilt image from Docker Hub](https://hub.docker.com/r/etherpad/etherpad), these are the commands:
```bash
# gets the latest published version
docker pull etherpad/etherpad
# gets a specific version
docker pull etherpad/etherpad:1.7.5
```
# Build a personalized container
If you want to use a personalized settings file, **you will have to rebuild your image**.
All of the following instructions are as a member of the `docker` group.
## Rebuilding with custom settings
Prepare your custom `settings.json` file:
```bash
cd <BASEDIR>/docker
cp ../settings.json.template settings.json
[ further edit your settings.json as needed]
```
**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`.
## Rebuilding including some plugins
If you want to install some plugins in your container, it is sufficient to list them in the ETHERPAD_PLUGINS build variable.
The variable value has to be a space separated, double quoted list of plugin names (see examples).
Some plugins will need personalized settings in the `settings.json` file. Just refer to the previous section, and include them in your custom `settings.json`.
## Examples
Build the latest development version:
```bash
docker build --tag <YOUR_USERNAME>/etherpad .
```
Build the latest stable version:
```bash
docker build --build-arg ETHERPAD_VERSION=master --build-arg NODE_ENV=production --tag <YOUR_USERNAME>/etherpad .
```
Build a specific tagged version:
```bash
docker build --build-arg ETHERPAD_VERSION=1.7.5 --build-arg NODE_ENV=production --tag <YOUR_USERNAME>/etherpad .
```
Build a specific git hash:
```bash
docker build --build-arg ETHERPAD_VERSION=4c45ac3cb1ae --tag <YOUR_USERNAME>/etherpad .
```
Include two plugins in the container:
```bash
docker build --build-arg ETHERPAD_PLUGINS="ep_codepad ep_author_neat" --tag <YOUR_USERNAME>/etherpad .
```
# Running your instance:
To run your instance:
```bash
docker run --detach --publish <DESIDERED_PORT>:9001 <YOUR_USERNAME>/etherpad
```
And point your browser to `http://<YOUR_IP>:<DESIDERED_PORT>`
# Options available by default
The `settings.json` available by default enables some configuration to be set from the environment.
Available options:
* `TITLE`: The name of the instance
* `FAVICON`: favicon default name, or a fully specified URL to your own favicon
* `SKIN_NAME`: either `no-skin`, `colibris` or an existing directory under `src/static/skins`.
* `IP`: IP which etherpad should bind at. Change to `::` for IPv6
* `PORT`: port which etherpad should bind at
* `SHOW_SETTINGS_IN_ADMIN_PAGE`: hide/show the settings.json in admin page
* `DB_TYPE`: a database supported by https://www.npmjs.com/package/ueberdb2
* `DB_HOST`: the host of the database
* `DB_PORT`: the port of the database
* `DB_NAME`: the database name
* `DB_USER`: a database user with sufficient permissions to create tables
* `DB_PASS`: the password for the database username
* `DB_CHARSET`: the character set for the tables (only required for MySQL)
* `DB_FILENAME`: in case `DB_TYPE` is `DirtyDB`, the database filename. Default: `var/dirty.db`
* `ADMIN_PASSWORD`: the password for the `admin` user (leave unspecified if you do not want to create it)
* `USER_PASSWORD`: the password for the first user `user` (leave unspecified if you do not want to create it)
* `LOGLEVEL`: valid values are `DEBUG`, `INFO`, `WARN` and `ERROR`
## Examples
Use a Postgres database, no admin user enabled:
```shell
docker run -d \
--name etherpad \
-p 9001:9001 \
-e 'DB_TYPE=postgres' \
-e 'DB_HOST=db.local' \
-e 'DB_PORT=4321' \
-e 'DB_NAME=etherpad' \
-e 'DB_USER=dbusername' \
-e 'DB_PASS=mypassword' \
etherpad/etherpad
```
Run enabling the administrative user `admin`:
```shell
docker run -d \
--name etherpad \
-p 9001:9001 \
-e 'ADMIN_PASSWORD=supersecret' \
etherpad/etherpad
```

475
docker/settings.json Normal file
View file

@ -0,0 +1,475 @@
/*
* This file must be valid JSON. But comments are allowed
*
* Please edit settings.json, not settings.json.template
*
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
* a separate file (credentials.json).
*
*
* ENVIRONMENT VARIABLE SUBSTITUTION
* =================================
*
* All the configuration values can be read from environment variables using the
* syntax "${ENV_VAR}" or "${ENV_VAR:default_value}".
*
* This is useful, for example, when running in a Docker container.
*
* EXAMPLE:
* "port": "${PORT:9001}"
* "minify": "${MINIFY}"
* "skinName": "${SKIN_NAME:colibris}"
*
* Would read the configuration values for those items from the environment
* variables PORT, MINIFY and SKIN_NAME.
*
* If PORT and SKIN_NAME variables were not defined, the default values 9001 and
* "colibris" would be used.
* The configuration value "minify", on the other hand, does not have a
* designated default value. Thus, if the environment variable MINIFY were
* undefined, "minify" would be null.
*
* REMARKS:
* 1) please note that variable substitution always needs to be quoted.
*
* "port": 9001, <-- Literal values. When not using
* "minify": false substitution, only strings must be
* "skinName": "colibris" quoted. Booleans and numbers must not.
*
* "port": "${PORT:9001}" <-- CORRECT: if you want to use a variable
* "minify": "${MINIFY:true}" substitution, put quotes around its name,
* "skinName": "${SKIN_NAME}" even if the required value is a number or
* a boolean.
* Etherpad will take care of rewriting it
* to the proper type if necessary.
*
* "port": ${PORT:9001} <-- ERROR: this is not valid json. Quotes
* "minify": ${MINIFY} around variable names are missing.
* "skinName": ${SKIN_NAME}
*
* 2) Beware of undefined variables and default values: nulls and empty strings
* are different!
*
* This is particularly important for user's passwords (see the relevant
* section):
*
* "password": "${PASSW}" // if PASSW is not defined would result in password === null
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
*
*/
{
/*
* Name your instance!
*/
"title": "${TITLE:Etherpad}",
/*
* favicon default name
* alternatively, set up a fully specified Url to your own favicon
*/
"favicon": "${FAVICON:favicon.ico}",
/*
* Skin name.
*
* Its value has to be an existing directory under src/static/skins.
* You can write your own, or use one of the included ones:
*
* - "no-skin": an empty skin (default). This yields the unmodified,
* traditional Etherpad theme.
* - "colibris": the new experimental skin (since Etherpad 1.8), candidate to
* become the default in Etherpad 2.0
*/
"skinName": "${SKIN_NAME:colibris}",
/*
* IP and port which etherpad should bind at
*/
"ip": "${IP:0.0.0.0}",
"port": "${PORT:9001}",
/*
* Option to hide/show the settings.json in admin page.
*
* Default option is set to true
*/
"showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}",
/*
* Node native SSL support
*
* This is disabled by default.
* Make sure to have the minimum and correct file access permissions set so
* that the Etherpad server can access them
*/
/*
"ssl" : {
"key" : "/path-to-your/epl-server.key",
"cert" : "/path-to-your/epl-server.crt",
"ca": ["/path-to-your/epl-intermediate-cert1.crt", "/path-to-your/epl-intermediate-cert2.crt"]
},
*/
/*
* The type of the database.
*
* You can choose between many DB drivers, for example: dirty, postgres,
* sqlite, mysql.
*
* You shouldn't use "dirty" for for anything else than testing or
* development.
*
*
* Database specific settings are dependent on dbType, and go in dbSettings.
* Remember that since Etherpad 1.6.0 you can also store these informations in
* credentials.json.
*
* For a complete list of the supported drivers, please refer to:
* https://www.npmjs.com/package/ueberdb2
*/
"dbType": "${DB_TYPE:dirty}",
"dbSettings": {
"host": "${DB_HOST}",
"port": "${DB_PORT}",
"database": "${DB_NAME}",
"user": "${DB_USER}",
"password": "${DB_PASS}",
"charset": "${DB_CHARSET}",
"filename": "${DB_FILENAME:var/dirty.db}"
},
/*
* The default text of a pad
*/
"defaultPadText" : "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at http:\/\/etherpad.org\n",
/*
* Default Pad behavior.
*
* Change them if you want to override.
*/
"padOptions": {
"noColors": false,
"showControls": true,
"showChat": true,
"showLineNumbers": true,
"useMonospaceFont": false,
"userName": false,
"userColor": false,
"rtl": false,
"alwaysShowChat": false,
"chatAndUsers": false,
"lang": "en-gb"
},
/*
* Pad Shortcut Keys
*/
"padShortcutEnabled" : {
"altF9": true, /* focus on the File Menu and/or editbar */
"altC": true, /* focus on the Chat window */
"cmdShift2": true, /* shows a gritter popup showing a line author */
"delete": true,
"return": true,
"esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */
"cmdS": true, /* save a revision */
"tab": true, /* indent */
"cmdZ": true, /* undo/redo */
"cmdY": true, /* redo */
"cmdI": true, /* italic */
"cmdB": true, /* bold */
"cmdU": true, /* underline */
"cmd5": true, /* strike through */
"cmdShiftL": true, /* unordered list */
"cmdShiftN": true, /* ordered list */
"cmdShift1": true, /* ordered list */
"cmdShiftC": true, /* clear authorship */
"cmdH": true, /* backspace */
"ctrlHome": true, /* scroll to top of pad */
"pageUp": true,
"pageDown": true
},
/*
* Should we suppress errors from being visible in the default Pad Text?
*/
"suppressErrorsInPadText": false,
/*
* If this option is enabled, a user must have a session to access pads.
* This effectively allows only group pads to be accessed.
*/
"requireSession": false,
/*
* Users may edit pads but not create new ones.
*
* Pad creation is only via the API.
* This applies both to group pads and regular pads.
*/
"editOnly": false,
/*
* If set to true, those users who have a valid session will automatically be
* granted access to password protected pads.
*/
"sessionNoPassword": false,
/*
* If true, all css & js will be minified before sending to the client.
*
* This will improve the loading performance massively, but makes it difficult
* to debug the javascript/css
*/
"minify": true,
/*
* How long may clients use served javascript code (in seconds)?
*
* Not setting this may cause problems during deployment.
* Set to 0 to disable caching.
*/
"maxAge": 21600, // 60 * 60 * 6 = 6 hours
/*
* Absolute path to the Abiword executable.
*
* Abiword is needed to get advanced import/export features of pads. Setting
* it to null disables Abiword and will only allow plain text and HTML
* import/exports.
*/
"abiword": null,
/*
* This is the absolute path to the soffice executable.
*
* LibreOffice can be used in lieu of Abiword to export pads.
* Setting it to null disables LibreOffice exporting.
*/
"soffice": null,
/*
* Path to the Tidy executable.
*
* Tidy is used to improve the quality of exported pads.
* Setting it to null disables Tidy.
*/
"tidyHtml": null,
/*
* Allow import of file types other than the supported ones:
* txt, doc, docx, rtf, odt, html & htm
*/
"allowUnknownFileEnds": true,
/*
* This setting is used if you require authentication of all users.
*
* Note: "/admin" always requires authentication.
*/
"requireAuthentication": false,
/*
* Require authorization by a module, or a user with is_admin set, see below.
*/
"requireAuthorization": false,
/*
* When you use NGINX or another proxy/load-balancer set this to true.
*/
"trustProxy": false,
/*
* Privacy: disable IP logging
*/
"disableIPlogging": false,
/*
* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
* message is shown to user.
*
* Set to 0 to disable automatic reconnection.
*/
"automaticReconnectionTimeout": 0,
/*
* By default, when caret is moved out of viewport, it scrolls the minimum
* height needed to make this line visible.
*/
"scrollWhenFocusLineIsOutOfViewport": {
/*
* Percentage of viewport height to be additionally scrolled.
*
* E.g.: use "percentage.editionAboveViewport": 0.5, to place caret line in
* the middle of viewport, when user edits a line above of the
* viewport
*
* Set to 0 to disable extra scrolling
*/
"percentage": {
"editionAboveViewport": 0,
"editionBelowViewport": 0
},
/*
* Time (in milliseconds) used to animate the scroll transition.
* Set to 0 to disable animation
*/
"duration": 0,
/*
* Flag to control if it should scroll when user places the caret in the
* last line of the viewport
*/
"scrollWhenCaretIsInTheLastLineOfViewport": false,
/*
* Percentage of viewport height to be additionally scrolled when user
* presses arrow up in the line of the top of the viewport.
*
* Set to 0 to let the scroll to be handled as default by Etherpad
*/
"percentageToScrollWhenUserPressesArrowUp": 0
},
/*
* Users for basic authentication.
*
* is_admin = true gives access to /admin.
* If you do not uncomment this, /admin will not be available!
*
* WARNING: passwords should not be stored in plaintext in this file.
* If you want to mitigate this, please install ep_hash_auth and
* follow the section "secure your installation" in README.md
*/
"users": {
"admin": {
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
// 2) please note that if password is null, the user will not be created
"password": "${ADMIN_PASSWORD}",
"is_admin": true
},
"user": {
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
// 2) please note that if password is null, the user will not be created
"password": "${USER_PASSWORD}",
"is_admin": false
}
},
/*
* Restrict socket.io transport methods
*/
"socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],
/*
* Allow Load Testing tools to hit the Etherpad Instance.
*
* WARNING: this will disable security on the instance.
*/
"loadTest": false,
/*
* Disable indentation on new line when previous line ends with some special
* chars (':', '[', '(', '{')
*/
/*
"indentationOnNewLine": false,
*/
/*
* Toolbar buttons configuration.
*
* Uncomment to customize.
*/
/*
"toolbar": {
"left": [
["bold", "italic", "underline", "strikethrough"],
["orderedlist", "unorderedlist", "indent", "outdent"],
["undo", "redo"],
["clearauthorship"]
],
"right": [
["importexport", "timeslider", "savedrevision"],
["settings", "embed"],
["showusers"]
],
"timeslider": [
["timeslider_export", "timeslider_returnToPad"]
]
},
*/
/*
* Expose Etherpad version in the web interface and in the Server http header.
*
* Do not enable on production machines.
*/
"exposeVersion": false,
/*
* The log level we are using.
*
* Valid values: DEBUG, INFO, WARN, ERROR
*/
"loglevel": "${LOGLEVEL:INFO}",
/*
* Logging configuration. See log4js documentation for further information:
* https://github.com/nomiddlename/log4js-node
*
* You can add as many appenders as you want here.
*/
"logconfig" :
{ "appenders": [
{ "type": "console"
//, "category": "access"// only logs pad access
}
/*
, { "type": "file"
, "filename": "your-log-file-here.log"
, "maxLogSize": 1024
, "backups": 3 // how many log files there're gonna be at max
//, "category": "test" // only log a specific category
}
*/
/*
, { "type": "logLevelFilter"
, "level": "warn" // filters out all log messages that have a lower level than "error"
, "appender":
{ Use whatever appender you want here }
}
*/
/*
, { "type": "logLevelFilter"
, "level": "error" // filters out all log messages that have a lower level than "error"
, "appender":
{ "type": "smtp"
, "subject": "An error occurred in your EPL instance!"
, "recipients": "bar@blurdybloop.com, baz@blurdybloop.com"
, "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message
, "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods
"host": "smtp.example.com", "port": 465,
"secureConnection": true,
"auth": {
"user": "foo@example.com",
"pass": "bar_foo"
}
}
}
}
*/
]
} // logconfig
}

View file

@ -3,8 +3,59 @@
*
* Please edit settings.json, not settings.json.template
*
* Please note that since Etherpad 1.6.0 you can store DB credentials in a
* separate file (credentials.json).
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
* a separate file (credentials.json).
*
*
* ENVIRONMENT VARIABLE SUBSTITUTION
* =================================
*
* All the configuration values can be read from environment variables using the
* syntax "${ENV_VAR}" or "${ENV_VAR:default_value}".
*
* This is useful, for example, when running in a Docker container.
*
* EXAMPLE:
* "port": "${PORT:9001}"
* "minify": "${MINIFY}"
* "skinName": "${SKIN_NAME:colibris}"
*
* Would read the configuration values for those items from the environment
* variables PORT, MINIFY and SKIN_NAME.
*
* If PORT and SKIN_NAME variables were not defined, the default values 9001 and
* "colibris" would be used.
* The configuration value "minify", on the other hand, does not have a
* designated default value. Thus, if the environment variable MINIFY were
* undefined, "minify" would be null.
*
* REMARKS:
* 1) please note that variable substitution always needs to be quoted.
*
* "port": 9001, <-- Literal values. When not using
* "minify": false substitution, only strings must be
* "skinName": "colibris" quoted. Booleans and numbers must not.
*
* "port": "${PORT:9001}" <-- CORRECT: if you want to use a variable
* "minify": "${MINIFY:true}" substitution, put quotes around its name,
* "skinName": "${SKIN_NAME}" even if the required value is a number or
* a boolean.
* Etherpad will take care of rewriting it
* to the proper type if necessary.
*
* "port": ${PORT:9001} <-- ERROR: this is not valid json. Quotes
* "minify": ${MINIFY} around variable names are missing.
* "skinName": ${SKIN_NAME}
*
* 2) Beware of undefined variables and default values: nulls and empty strings
* are different!
*
* This is particularly important for user's passwords (see the relevant
* section):
*
* "password": "${PASSW}" // if PASSW is not defined would result in password === null
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
*
*/
{
/*
@ -35,14 +86,14 @@
* IP and port which etherpad should bind at
*/
"ip": "0.0.0.0",
"port" : 9001,
"port": 9001,
/*
* Option to hide/show the settings.json in admin page.
*
* Default option is set to true
*/
"showSettingsInAdminPage" : true,
"showSettingsInAdminPage": true,
/*
* Node native SSL support
@ -78,10 +129,10 @@
* https://www.npmjs.com/package/ueberdb2
*/
"dbType" : "dirty",
"dbSettings" : {
"filename" : "var/dirty.db"
},
"dbType": "dirty",
"dbSettings": {
"filename": "var/dirty.db"
},
/*
* An Example of MySQL Configuration (commented out).
@ -92,13 +143,13 @@
/*
"dbType" : "mysql",
"dbSettings" : {
"user" : "etherpaduser",
"host" : "localhost",
"port" : 3306,
"password": "PASSWORD",
"database": "etherpad_lite_db",
"charset" : "utf8mb4"
},
"user": "etherpaduser",
"host": "localhost",
"port": 3306,
"password": "PASSWORD",
"database": "etherpad_lite_db",
"charset": "utf8mb4"
},
*/
/*
@ -112,57 +163,57 @@
* Change them if you want to override.
*/
"padOptions": {
"noColors": false,
"showControls": true,
"showChat": true,
"showLineNumbers": true,
"noColors": false,
"showControls": true,
"showChat": true,
"showLineNumbers": true,
"useMonospaceFont": false,
"userName": false,
"userColor": false,
"rtl": false,
"alwaysShowChat": false,
"chatAndUsers": false,
"lang": "en-gb"
"userName": false,
"userColor": false,
"rtl": false,
"alwaysShowChat": false,
"chatAndUsers": false,
"lang": "en-gb"
},
/*
* Pad Shortcut Keys
*/
"padShortcutEnabled" : {
"altF9" : true, /* focus on the File Menu and/or editbar */
"altC" : true, /* focus on the Chat window */
"cmdShift2" : true, /* shows a gritter popup showing a line author */
"delete" : true,
"return" : true,
"esc" : true, /* in mozilla versions 14-19 avoid reconnecting pad */
"cmdS" : true, /* save a revision */
"tab" : true, /* indent */
"cmdZ" : true, /* undo/redo */
"cmdY" : true, /* redo */
"cmdI" : true, /* italic */
"cmdB" : true, /* bold */
"cmdU" : true, /* underline */
"cmd5" : true, /* strike through */
"cmdShiftL" : true, /* unordered list */
"cmdShiftN" : true, /* ordered list */
"cmdShift1" : true, /* ordered list */
"cmdShiftC" : true, /* clear authorship */
"cmdH" : true, /* backspace */
"ctrlHome" : true, /* scroll to top of pad */
"pageUp" : true,
"pageDown" : true
"altF9": true, /* focus on the File Menu and/or editbar */
"altC": true, /* focus on the Chat window */
"cmdShift2": true, /* shows a gritter popup showing a line author */
"delete": true,
"return": true,
"esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */
"cmdS": true, /* save a revision */
"tab": true, /* indent */
"cmdZ": true, /* undo/redo */
"cmdY": true, /* redo */
"cmdI": true, /* italic */
"cmdB": true, /* bold */
"cmdU": true, /* underline */
"cmd5": true, /* strike through */
"cmdShiftL": true, /* unordered list */
"cmdShiftN": true, /* ordered list */
"cmdShift1": true, /* ordered list */
"cmdShiftC": true, /* clear authorship */
"cmdH": true, /* backspace */
"ctrlHome": true, /* scroll to top of pad */
"pageUp": true,
"pageDown": true
},
/*
* Should we suppress errors from being visible in the default Pad Text?
*/
"suppressErrorsInPadText" : false,
"suppressErrorsInPadText": false,
/*
* If this option is enabled, a user must have a session to access pads.
* This effectively allows only group pads to be accessed.
*/
"requireSession" : false,
"requireSession": false,
/*
* Users may edit pads but not create new ones.
@ -170,13 +221,13 @@
* Pad creation is only via the API.
* This applies both to group pads and regular pads.
*/
"editOnly" : false,
"editOnly": false,
/*
* If set to true, those users who have a valid session will automatically be
* granted access to password protected pads.
*/
"sessionNoPassword" : false,
"sessionNoPassword": false,
/*
* If true, all css & js will be minified before sending to the client.
@ -184,7 +235,7 @@
* This will improve the loading performance massively, but makes it difficult
* to debug the javascript/css
*/
"minify" : true,
"minify": true,
/*
* How long may clients use served javascript code (in seconds)?
@ -192,7 +243,7 @@
* Not setting this may cause problems during deployment.
* Set to 0 to disable caching.
*/
"maxAge" : 21600, // 60 * 60 * 6 = 6 hours
"maxAge": 21600, // 60 * 60 * 6 = 6 hours
/*
* Absolute path to the Abiword executable.
@ -201,7 +252,7 @@
* it to null disables Abiword and will only allow plain text and HTML
* import/exports.
*/
"abiword" : null,
"abiword": null,
/*
* This is the absolute path to the soffice executable.
@ -209,7 +260,7 @@
* LibreOffice can be used in lieu of Abiword to export pads.
* Setting it to null disables LibreOffice exporting.
*/
"soffice" : null,
"soffice": null,
/*
* Path to the Tidy executable.
@ -217,35 +268,35 @@
* Tidy is used to improve the quality of exported pads.
* Setting it to null disables Tidy.
*/
"tidyHtml" : null,
"tidyHtml": null,
/*
* Allow import of file types other than the supported ones:
* txt, doc, docx, rtf, odt, html & htm
*/
"allowUnknownFileEnds" : true,
"allowUnknownFileEnds": true,
/*
* This setting is used if you require authentication of all users.
*
* Note: "/admin" always requires authentication.
*/
"requireAuthentication" : false,
"requireAuthentication": false,
/*
* Require authorization by a module, or a user with is_admin set, see below.
*/
"requireAuthorization" : false,
"requireAuthorization": false,
/*
* When you use NGINX or another proxy/load-balancer set this to true.
*/
"trustProxy" : false,
"trustProxy": false,
/*
* Privacy: disable IP logging
*/
"disableIPlogging" : false,
"disableIPlogging": false,
/*
* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
@ -253,7 +304,7 @@
*
* Set to 0 to disable automatic reconnection.
*/
"automaticReconnectionTimeout" : 0,
"automaticReconnectionTimeout": 0,
/*
* By default, when caret is moved out of viewport, it scrolls the minimum
@ -310,12 +361,14 @@
/*
"users": {
"admin": {
// "password" can be replaced with "hash" if you install ep_hash_auth
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
// 2) please note that if password is null, the user will not be created
"password": "changeme1",
"is_admin": true
},
"user": {
// "password" can be replaced with "hash" if you install ep_hash_auth
// 1) "password" can be replaced with "hash" if you install ep_hash_auth
// 2) please note that if password is null, the user will not be created
"password": "changeme1",
"is_admin": false
}
@ -368,6 +421,13 @@
},
*/
/*
* Expose Etherpad version in the web interface and in the Server http header.
*
* Do not enable on production machines.
*/
"exposeVersion": false,
/*
* The log level we are using.
*

View file

@ -15,16 +15,16 @@
},
"index.newPad": "باد جديد",
"index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:",
"pad.toolbar.bold.title": "سميك (Ctrl-B)",
"pad.toolbar.italic.title": "مائل (Ctrl-I)",
"pad.toolbar.underline.title": "تسطير (Ctrl-U)",
"pad.toolbar.bold.title": "سميك (Ctrl+B)",
"pad.toolbar.italic.title": "مائل (Ctrl+I)",
"pad.toolbar.underline.title": "تسطير (Ctrl+U)",
"pad.toolbar.strikethrough.title": "شطب (Ctrl+5)",
"pad.toolbar.ol.title": "قائمة مرتبة (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "قائمة غير مرتبة (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "إزاحة",
"pad.toolbar.unindent.title": "حذف الإزاحة",
"pad.toolbar.undo.title": "فك (Ctrl-Z)",
"pad.toolbar.redo.title": "تكرار (Ctrl-Y)",
"pad.toolbar.indent.title": "إزاحة (TAB)",
"pad.toolbar.unindent.title": "حذف الإزاحة (Shift+TAB)",
"pad.toolbar.undo.title": "فك (Ctrl+Z)",
"pad.toolbar.redo.title": "تكرار (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "مسح ألوان التأليف (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "استيراد/تصدير من/إلى تنسيقات ملفات مختلفة",
"pad.toolbar.timeslider.title": "متصفح التاريخ",
@ -32,13 +32,13 @@
"pad.toolbar.settings.title": "الإعدادات",
"pad.toolbar.embed.title": "تبادل و تضمين هذا الباد",
"pad.toolbar.showusers.title": "عرض المستخدمين على هذا الباد",
"pad.colorpicker.save": "تسجيل",
"pad.colorpicker.save": "حفظ",
"pad.colorpicker.cancel": "إلغاء",
"pad.loading": "جارٍ التحميل...",
"pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!",
"pad.passwordRequired": "تحتاج إلى كلمة مرور للوصول إلى هذا الباد",
"pad.passwordRequired": "تحتاج إلى كلمة سر للوصول إلى هذا الباد",
"pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد",
"pad.wrongPassword": "كانت كلمة المرور خاطئة",
"pad.wrongPassword": "كانت كلمة السر خاطئة",
"pad.settings.padSettings": "إعدادات الباد",
"pad.settings.myView": "رؤيتي",
"pad.settings.stickychat": "الدردشة دائما على الشاشة",
@ -63,9 +63,9 @@
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)",
"pad.importExport.abiword.innerHTML": "لا يمكنك الاستيراد إلا من نص عادي أو من تنسيقات HTML. للحصول على المزيد من ميزات الاستيراد المتقدمة، يرجى <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">تثبيت AbiWord</a>.",
"pad.modals.connected": "متصل.",
"pad.modals.reconnecting": "إعادة الاتصال ببادك",
"pad.modals.reconnecting": "إعادة الاتصال ببادك..",
"pad.modals.forcereconnect": "فرض إعادة الاتصال",
"pad.modals.reconnecttimer": "حاول إعادة الاتصال",
"pad.modals.reconnecttimer": "جاري محاولة إعادة الاتصال",
"pad.modals.cancel": "إلغاء",
"pad.modals.userdup": "مفتوح في نافذة أخرى",
"pad.modals.userdup.explanation": "يبدو أن هذا الباد تم فتحه في أكثر من نافذة متصفح في هذا الحاسوب.",
@ -74,9 +74,9 @@
"pad.modals.unauth.explanation": "لقد تغيرت الأذونات الخاصة بك أثناء عرض هذه الصفحة. أعد محاولة الاتصال.",
"pad.modals.looping.explanation": "هناك مشاكل في الاتصال مع ملقم التزامن.",
"pad.modals.looping.cause": "ربما كنت متصلاً من خلال وكيل أو جدار حماية غير متوافق.",
"pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم",
"pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم.",
"pad.modals.initsocketfail.explanation": "تعذر الاتصال بخادم المزامنة.",
"pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بإنترنت.",
"pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بالإنترنت.",
"pad.modals.slowcommit.explanation": "الخادم لا يستجيب.",
"pad.modals.slowcommit.cause": "يمكن أن يكون هذا بسبب مشاكل في الاتصال بالشبكة.",
"pad.modals.badChangeset.explanation": "لقد صُنفَت إحدى عمليات التحرير التي قمت بها كعملية غير مسموح بها من قبل ملقم التزامن.",
@ -84,17 +84,19 @@
"pad.modals.corruptPad.explanation": "الباد الذي تحاول الوصول إليه تالف.",
"pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.",
"pad.modals.deleted": "محذوف.",
"pad.modals.deleted.explanation": "تمت إزالة هذا الباد",
"pad.modals.deleted.explanation": "تمت إزالة هذا الباد.",
"pad.modals.disconnected": "لم تعد متصلا.",
"pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم",
"pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.",
"pad.share": "شارك هذه الباد",
"pad.share.readonly": "للقراءة فقط",
"pad.share.link": "رابط",
"pad.share.link": "وصلة",
"pad.share.emebdcode": "URL للتضمين",
"pad.chat": "دردشة",
"pad.chat.title": "فتح الدردشة لهذا الباد",
"pad.chat.title": "فتح الدردشة لهذا الباد.",
"pad.chat.loadmessages": "تحميل المزيد من الرسائل",
"pad.chat.stick.title": "ألصق الدردشة بالشاشة",
"pad.chat.writeMessage.placeholder": "اكتب رسالتك هنا",
"timeslider.pageTitle": "{{appTitle}} متصفح التاريخ",
"timeslider.toolbar.returnbutton": "العودة إلى الباد",
"timeslider.toolbar.authors": "المؤلفون:",

View file

@ -52,7 +52,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Sólo se pue importar dende los formatos de testu planu o html. Pa carauterístiques d'importación más avanzaes <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">instala abiword</a>.",
"pad.importExport.abiword.innerHTML": "Sólo se pue importar dende los formatos de testu planu o HTML. Pa carauterístiques d'importación más avanzaes <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instala Abiword</a>.",
"pad.modals.connected": "Coneutáu.",
"pad.modals.reconnecting": "Reconeutando col to bloc...",
"pad.modals.forcereconnect": "Forzar la reconexón",
@ -86,6 +86,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Abrir el chat d'esti bloc.",
"pad.chat.loadmessages": "Cargar más mensaxes",
"pad.chat.stick.title": "Pegar charra a la pantalla",
"pad.chat.writeMessage.placeholder": "Escribi'l mensaxe equí",
"timeslider.pageTitle": "Eslizador de tiempu de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Tornar al bloc",
"timeslider.toolbar.authors": "Autores:",

View file

@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"Baloch Afghanistan"
"Baloch Afghanistan",
"Sultanselim baloch"
]
},
"index.newPad": "دفترچه یادداشت تازه",
@ -30,7 +31,7 @@
"pad.permissionDenied": "شرمنده، شما را اجازت په دسترسی ای صفحه نیست.",
"pad.wrongPassword": "گذرواژه‌ی شما درست نیست",
"pad.settings.padSettings": "تنظیمات دفترچه یادداشت",
"pad.settings.myView": "نمای من",
"pad.settings.myView": "منی سۏج",
"pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد",
"pad.settings.colorcheck": "رنگ‌های نویسندگی",
"pad.settings.linenocheck": "شماره‌ی خطوط",
@ -71,7 +72,7 @@
"pad.modals.corruptPad.cause": "این احتمالاً به دلیل تنظیمات اشتباه کارساز یا سایر رفتارهای غیرمنتظره است. لطفاً با مدیر خدمت تماس حاصل کنید.",
"pad.modals.deleted": "پاک کورتین",
"pad.modals.deleted.explanation": "این دفترچه یادداشت پاک شده‌است.",
"pad.modals.disconnected": "اتصال شما قطع شده‌است.",
"pad.modals.disconnected": "شمئی سکّی کھت اِنت۔",
"pad.modals.disconnected.explanation": "اتصال به سرور قطع شده‌است.",
"pad.modals.disconnected.cause": "ممکن است سرور در دسترس نباشد. اگر این مشکل باز هم رخ داد مدیر حدمت را آگاه کنید.",
"pad.share": "به اشتراک‌گذاری این دفترچه یادداشت",
@ -80,7 +81,7 @@
"pad.share.emebdcode": "جاسازی نشانی",
"pad.chat": "گفتگو",
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
"pad.chat.loadmessages": "بارگیری پیام‌های بیشتر",
"pad.chat.loadmessages": "گݔشترݔں پیگامء چارگ",
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
"timeslider.toolbar.authors": "نویسوک:",

View file

@ -88,6 +88,8 @@
"pad.chat": "Чат",
"pad.chat.title": "Адкрыць чат для гэтага дакумэнту.",
"pad.chat.loadmessages": "Загрузіць болей паведамленьняў",
"pad.chat.stick.title": "Замацаваць чат на экране",
"pad.chat.writeMessage.placeholder": "Напішыце вашае паведамленьне тут",
"timeslider.pageTitle": "Часавая шкала {{appTitle}}",
"timeslider.toolbar.returnbutton": "Вярнуцца да дакумэнту",
"timeslider.toolbar.authors": "Аўтары:",

View file

@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Vodnokon4e",
"StanProg"
"StanProg",
"Vlad5250"
]
},
"index.newPad": "Нов пад",
@ -46,6 +47,8 @@
"pad.chat": "Чат",
"pad.chat.title": "Отваряне на чат за този пад.",
"pad.chat.loadmessages": "Зареждане на повече съобщения",
"pad.chat.stick.title": "Залепяне на разговора на екрана",
"pad.chat.writeMessage.placeholder": "Тук напишете съобщение",
"timeslider.toolbar.returnbutton": "Връщане към пада",
"timeslider.toolbar.authors": "Автори:",
"timeslider.toolbar.authorsList": "Няма автори",

View file

@ -75,6 +75,7 @@
"pad.chat": "চ্যাট",
"pad.chat.title": "এই প্যাডের জন্য চ্যাট চালু করুন।",
"pad.chat.loadmessages": "আরও বার্তা লোড করুন",
"pad.chat.writeMessage.placeholder": "আপনার বার্তাটি এখানে লিখুন",
"timeslider.toolbar.returnbutton": "প্যাডে ফিরে যাও",
"timeslider.toolbar.authors": "লেখকগণ:",
"timeslider.toolbar.authorsList": "কোনো লেখক নেই",

View file

@ -17,8 +17,8 @@
"pad.toolbar.ul.title": "Listenn en dizurzh (Ktrl+Pennlizherenn+L)",
"pad.toolbar.indent.title": "Endantañ (TAB)",
"pad.toolbar.unindent.title": "Diendantañ (Shift+TAB)",
"pad.toolbar.undo.title": "Dizober (Ktrl-Z)",
"pad.toolbar.redo.title": "Adober (Ktrl-Y)",
"pad.toolbar.undo.title": "Dizober (Ktrl+Z)",
"pad.toolbar.redo.title": "Adober (Ktrl+Y)",
"pad.toolbar.clearAuthorship.title": "Diverkañ al livioù oc'h anaout an aozerien (Ktrl+Pennlizherenn+C)",
"pad.toolbar.import_export.title": "Enporzhiañ/Ezporzhiañ eus/war-zu ur furmad restr disheñvel",
"pad.toolbar.timeslider.title": "Istor dinamek",
@ -63,7 +63,7 @@
"pad.modals.cancel": "Nullañ",
"pad.modals.userdup": "Digor en ur prenestr all",
"pad.modals.userdup.explanation": "Digor eo ho pad, war a seblant, e meur a brenestr eus ho merdeer en urzhiataer-mañ.",
"pad.modals.userdup.advice": "Kevreañ en ur implijout ar prenestr-mañ.",
"pad.modals.userdup.advice": "Kevreañ en-dro en ur implijout ar prenestr-mañ.",
"pad.modals.unauth": "N'eo ket aotreet",
"pad.modals.unauth.explanation": "Kemmet e vo hoc'h aotreoù pa vo diskwelet ar bajenn.-mañ Klaskit kevreañ en-dro.",
"pad.modals.looping.explanation": "Kudennoù kehentiñ zo gant ar servijer sinkronelekaat.",
@ -89,6 +89,8 @@
"pad.chat": "Flap",
"pad.chat.title": "Digeriñ ar flap kevelet gant ar pad-mañ.",
"pad.chat.loadmessages": "Kargañ muioc'h a gemennadennoù",
"pad.chat.stick.title": "Gwriziennañ an diviz war ar skramm",
"pad.chat.writeMessage.placeholder": "Skrivañ ho kemenadenn amañ",
"timeslider.pageTitle": "Istor dinamek eus {{appTitle}}",
"timeslider.toolbar.returnbutton": "Distreiñ d'ar pad-mañ.",
"timeslider.toolbar.authors": "Aozerien :",
@ -126,7 +128,7 @@
"pad.impexp.importing": "Oc'h enporzhiañ...",
"pad.impexp.confirmimport": "Ma vez enporzhiet ur restr e vo diverket ar pezh zo en teul a-vremañ. Ha sur oc'h e fell deoc'h mont betek penn ?",
"pad.impexp.convertFailed": "N'eus ket bet gallet enporzhiañ ar restr. Ober gant ur furmad teul all pe eilañ/pegañ gant an dorn.",
"pad.impexp.padHasData": "N'hon eus ket gallet enporzhiañ ar restr-mañdre ma'z eus bet degaset kemmoù er bloc'h-se ; enporzhiit anezhi war-zu ur bloc'h nevez, mar plij.",
"pad.impexp.padHasData": "N'hon eus ket gallet enporzhiañ ar restr-mañ dre ma'z eus bet degaset kemmoù er bloc'h-se ; enporzhiit anezhi war-zu ur bloc'h nevez, mar plij.",
"pad.impexp.uploadFailed": "C'hwitet eo bet an enporzhiañ. Klaskit en-dro.",
"pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn",
"pad.impexp.copypaste": "Eilit/pegit, mar plij",

View file

@ -60,7 +60,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Només podeu importar de text net o html. Per a opcions d'importació més avançades <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">instal·leu l'Abiword</a>.",
"pad.importExport.abiword.innerHTML": "Només podeu importar de text net o HTML. Per a opcions d'importació més avançades <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instal·leu l'Abiword</a>.",
"pad.modals.connected": "Connectat.",
"pad.modals.reconnecting": "S'està tornant a connectar al vostre pad…",
"pad.modals.forcereconnect": "Força tornar a connectar",
@ -94,6 +94,8 @@
"pad.chat": "Xat",
"pad.chat.title": "Obre el xat d'aquest pad.",
"pad.chat.loadmessages": "Carrega més missatges",
"pad.chat.stick.title": "Ancora el xat a la pantalla",
"pad.chat.writeMessage.placeholder": "Escriviu el vostre missatge a continuació",
"timeslider.pageTitle": "Línia temporal — {{appTitle}}",
"timeslider.toolbar.returnbutton": "Torna al pad",
"timeslider.toolbar.authors": "Autors:",

View file

@ -92,6 +92,8 @@
"pad.chat": "Unterhaltung",
"pad.chat.title": "Den Chat für dieses Pad öffnen.",
"pad.chat.loadmessages": "Weitere Nachrichten laden",
"pad.chat.stick.title": "Chat an den Bildschirm anheften",
"pad.chat.writeMessage.placeholder": "Schreibe hier deine Nachricht",
"timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
"timeslider.toolbar.authors": "Autoren:",

View file

@ -6,12 +6,13 @@
"Mirzali",
"Kumkumuk",
"1917 Ekim Devrimi",
"Gırd"
"Gırd",
"Orbot707"
]
},
"index.newPad": "Pedo newe",
"index.createOpenPad": "Yana eno bamaeya bloknot vıraz/ak:",
"pad.toolbar.bold.title": "Qalın (Ctrl-B)",
"index.newPad": "Bloknoto newe",
"index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:",
"pad.toolbar.bold.title": "Qalınd (Ctrl-B)",
"pad.toolbar.italic.title": "Namıte (Ctrl-I)",
"pad.toolbar.underline.title": "Bınxetın (Ctrl-U)",
"pad.toolbar.strikethrough.title": "Serxetın (Ctrl+5)",
@ -20,12 +21,12 @@
"pad.toolbar.indent.title": "Serrêze (TAB)",
"pad.toolbar.unindent.title": "Teberdayış (Shift+TAB)",
"pad.toolbar.undo.title": "Meke (Ctrl-Z)",
"pad.toolbar.redo.title": "Fına bıke (Ctrl-Y)",
"pad.toolbar.redo.title": "Newe ke (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "Rengê Nuştoğiê Arıstey (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Babaetna tewranê dosyaya azere/ateber ke",
"pad.toolbar.timeslider.title": ızagê zemani",
"pad.toolbar.savedRevision.title": ımraviyarnayışi qeyd ke",
"pad.toolbar.settings.title": "Sazkerdışi",
"pad.toolbar.settings.title": "Eyari",
"pad.toolbar.embed.title": "Na bloknot degusn u bıhesrne",
"pad.toolbar.showusers.title": "Karbera ena bloknot dı bımotné",
"pad.colorpicker.save": "Qeyd ke",
@ -58,7 +59,7 @@
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": ıma şenê tenya metınanê zelalan ya zi formatanê HTML-i biyarê. Seba vêşi xısusiyetanê arezekerdışi ra gırey <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord-i bar kerên</a>.",
"pad.modals.connected": "Gırediya.",
"pad.modals.connected": "Gıre diya.",
"pad.modals.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
"pad.modals.reconnecttimer": "Anciya gırê beno",
@ -91,6 +92,8 @@
"pad.chat": "Mıhebet",
"pad.chat.title": "Qandê ena ped mıhebet ake.",
"pad.chat.loadmessages": "Dehana zaf mesaci bar keri",
"pad.chat.stick.title": "Mobet ekran de bıvındarne",
"pad.chat.writeMessage.placeholder": "Mesacê xo tiya bınusne",
"timeslider.pageTitle": ızagê zemani {{appTitle}}",
"timeslider.toolbar.returnbutton": "Peyser şo ped",
"timeslider.toolbar.authors": "Nuştoği:",
@ -104,7 +107,7 @@
"timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "Çele",
"timeslider.month.february": "Zemherı",
"timeslider.month.february": "Sıbate",
"timeslider.month.march": "Adar",
"timeslider.month.april": "Nisane",
"timeslider.month.may": "Gulane",
@ -119,7 +122,7 @@
"pad.savedrevs.marked": "Eno vurriyayış henda qeyd bıyaye yew vurriyayış deyne nışan bıyo",
"pad.savedrevs.timeslider": "Xızberê zemani ziyer kerdış ra şıma şenê revizyonanê qeyd bıyayan bıvinê",
"pad.userlist.entername": "Namey xo cıkewe",
"pad.userlist.unnamed": "Name nébıyo",
"pad.userlist.unnamed": "bêname",
"pad.userlist.guest": "Meyman",
"pad.userlist.deny": "Red ke",
"pad.userlist.approve": "Tesdiq ke",

View file

@ -56,7 +56,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Μορφή Open Document)",
"pad.importExport.abiword.innerHTML": "Μπορείτε να κάνετε εισαγωγή απλού κειμένου ή μορφής html. Για πιο προηγμένες δυνατότητες εισαγωγής παρακαλώ <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">εγκαταστήστε το abiword</a>.",
"pad.importExport.abiword.innerHTML": "Μπορείτε να εισάγετε απλό κείμενο ή HTML. Για προηγμένες δυνατότητες εισαγωγής παρακαλούμε <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">εγκαταστήστε το AbiWord</a>.",
"pad.modals.connected": "Συνδεμένοι.",
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας...",
"pad.modals.forcereconnect": "Επιβολή επανασύνδεσης",
@ -90,6 +90,8 @@
"pad.chat": "Συνομιλία",
"pad.chat.title": "Άνοιγμα της συνομιλίας για αυτό το pad.",
"pad.chat.loadmessages": "Φόρτωση περισσότερων μηνυμάτων",
"pad.chat.stick.title": "Κρατήστε τη συνομιλία στην οθόνη",
"pad.chat.writeMessage.placeholder": "Γράψτε το μήνυμα σας εδώ",
"timeslider.pageTitle": "{{appTitle}} Χρονοδιάγραμμα",
"timeslider.toolbar.returnbutton": "Επιστροφή στο pad",
"timeslider.toolbar.authors": "Συντάκτες:",

View file

@ -4,7 +4,8 @@
"Chase me ladies, I'm the Cavalry",
"Shirayuki",
"Andibing",
"HairyFotr"
"HairyFotr",
"Cblair91"
]
},
"index.newPad": "New Pad",
@ -89,6 +90,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Open the chat for this pad.",
"pad.chat.loadmessages": "Load more messages",
"pad.chat.stick.title": "Stick chat to screen",
"pad.chat.writeMessage.placeholder": "Write your message here",
"timeslider.pageTitle": "{{appTitle}} Timeslider",
"timeslider.toolbar.returnbutton": "Return to pad",
"timeslider.toolbar.authors": "Authors:",

View file

@ -116,6 +116,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Open the chat for this pad.",
"pad.chat.loadmessages": "Load more messages",
"pad.chat.stick.title": "Stick chat to screen",
"pad.chat.writeMessage.placeholder": "Write your message here",
"timeslider.pageTitle": "{{appTitle}} Timeslider",
"timeslider.toolbar.returnbutton": "Return to pad",

View file

@ -4,7 +4,8 @@
"Eliovir",
"Mschmitt",
"Objectivesea",
"Robin van der Vliet"
"Robin van der Vliet",
"Mirin"
]
},
"index.newPad": "Nova Teksto",
@ -89,6 +90,8 @@
"pad.chat": "Babilejo",
"pad.chat.title": "Malfermi la babilejon por ĉi tiu teksto.",
"pad.chat.loadmessages": "Ŝargi pliajn mesaĝojn",
"pad.chat.stick.title": "Alpingli babilejon al ekrano",
"pad.chat.writeMessage.placeholder": "Verki vian mesaĝon ĉi tie",
"timeslider.pageTitle": "{{appTitle}} Tempoŝovilo",
"timeslider.toolbar.returnbutton": "Reiri al teksto",
"timeslider.toolbar.authors": "Aŭtoroj:",

View file

@ -99,6 +99,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Abrir el chat para este pad.",
"pad.chat.loadmessages": "Cargar más mensajes",
"pad.chat.stick.title": "Ampliar",
"pad.chat.writeMessage.placeholder": "Enviar un mensaje",
"timeslider.pageTitle": "{{appTitle}} Línea de tiempo",
"timeslider.toolbar.returnbutton": "Volver al pad",
"timeslider.toolbar.authors": "Autores:",

View file

@ -91,6 +91,8 @@
"pad.chat": "Txata",
"pad.chat.title": "Pad honetarako txata ireki.",
"pad.chat.loadmessages": "Mezu gehiago kargatu",
"pad.chat.stick.title": "Handitu",
"pad.chat.writeMessage.placeholder": "Zure mezua hemen idatzi",
"timeslider.pageTitle": "{{appTitle}} denbora lerroa",
"timeslider.toolbar.returnbutton": "Padera itzuli",
"timeslider.toolbar.authors": "Egileak:",

View file

@ -7,7 +7,8 @@
"Reza1615",
"ZxxZxxZ",
"الناز",
"Omid.koli"
"Omid.koli",
"FarsiNevis"
]
},
"index.newPad": "دفترچه یادداشت تازه",
@ -92,6 +93,8 @@
"pad.chat": "گفتگو",
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
"pad.chat.loadmessages": "بارگیری پیام‌های بیشتر",
"pad.chat.stick.title": "چسباندن چت به صفحه",
"pad.chat.writeMessage.placeholder": "پیام خود را این‌جا بنویسید",
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
"timeslider.toolbar.authors": "نویسندگان:",

View file

@ -24,11 +24,12 @@
"C13m3n7",
"Wladek92",
"Urhixidur",
"Envlh"
"Envlh",
"Verdy p"
]
},
"index.newPad": "Nouveau pad",
"index.createOpenPad": "ou créer/ouvrir un pad intitulé :",
"index.newPad": "Nouveau bloc-notes",
"index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé:",
"pad.toolbar.bold.title": "Gras (Ctrl+B)",
"pad.toolbar.italic.title": "Italique (Ctrl+I)",
"pad.toolbar.underline.title": "Souligné (Ctrl+U)",
@ -39,87 +40,89 @@
"pad.toolbar.unindent.title": "Désindenter (Maj+TAB)",
"pad.toolbar.undo.title": "Annuler (Ctrl+Z)",
"pad.toolbar.redo.title": "Rétablir (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Effacer les couleurs identifiant les auteurs (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importer/Exporter de/vers un format de fichier différent",
"pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importer de/Exporter vers un format de fichier différent",
"pad.toolbar.timeslider.title": "Historique dynamique",
"pad.toolbar.savedRevision.title": "Enregistrer la révision",
"pad.toolbar.settings.title": "Paramètres",
"pad.toolbar.embed.title": "Partager et intégrer ce pad",
"pad.toolbar.showusers.title": "Afficher les utilisateurs du pad",
"pad.toolbar.embed.title": "Partager et intégrer ce bloc-notes",
"pad.toolbar.showusers.title": "Afficher les utilisateurs du bloc-notes",
"pad.colorpicker.save": "Enregistrer",
"pad.colorpicker.cancel": "Annuler",
"pad.loading": "Chargement",
"pad.noCookie": "Le cookie na pas pu être trouvé. Veuillez autoriser les cookies dans votre navigateur!",
"pad.passwordRequired": "Vous avez besoin d'un mot de passe pour accéder à ce pad",
"pad.permissionDenied": "Vous n'avez pas la permission daccéder à ce pad",
"pad.loading": "Chargement...",
"pad.noCookie": "Un cookie na pas pu être trouvé. Veuillez autoriser les fichiers témoins (ou cookies) dans votre navigateur!",
"pad.passwordRequired": "Vous avez besoin d'un mot de passe pour accéder à ce bloc-note",
"pad.permissionDenied": "Vous nêtes pas autorisé à accéder à ce bloc-notes",
"pad.wrongPassword": "Votre mot de passe est incorrect",
"pad.settings.padSettings": "Paramètres du pad",
"pad.settings.padSettings": "Paramètres du bloc-notes",
"pad.settings.myView": "Ma vue",
"pad.settings.stickychat": "Toujours afficher le clavardage",
"pad.settings.chatandusers": "Afficher la discussion et les utilisateurs",
"pad.settings.colorcheck": "Couleurs didentification",
"pad.settings.chatandusers": "Afficher le clavardage et les utilisateurs",
"pad.settings.colorcheck": "Surlignage par auteur",
"pad.settings.linenocheck": "Numéros de lignes",
"pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche ?",
"pad.settings.fontType": "Police :",
"pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche?",
"pad.settings.fontType": "Police:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.fontType.monospaced": "Monospace",
"pad.settings.globalView": "Vue densemble",
"pad.settings.language": "Langue :",
"pad.settings.language": "Langue:",
"pad.importExport.import_export": "Importer/Exporter",
"pad.importExport.import": "Charger un texte ou un document",
"pad.importExport.importSuccessful": "Réussi !",
"pad.importExport.export": "Exporter le pad actuel comme :",
"pad.importExport.importSuccessful": "Réussi!",
"pad.importExport.export": "Exporter le bloc-notes actuel en:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Texte brut",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Vous ne pouvez importer que des formats texte brut ou HTML. Pour des fonctionnalités d'importation plus évoluées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord</a>.",
"pad.importExport.abiword.innerHTML": "Vous ne pouvez importer que des formats texte brut ou HTML. Pour des fonctionnalités dimportation plus évoluées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord</a>.",
"pad.modals.connected": "Connecté.",
"pad.modals.reconnecting": "Reconnexion vers votre pad...",
"pad.modals.reconnecting": "Reconnexion à votre bloc-notes...",
"pad.modals.forcereconnect": "Forcer la reconnexion",
"pad.modals.reconnecttimer": "Essai de reconnexion",
"pad.modals.cancel": "Annuler",
"pad.modals.userdup": "Ouvert dans une autre fenêtre",
"pad.modals.userdup.explanation": "Ce pad semble être ouvert dans plusieurs fenêtres sur cet ordinateur.",
"pad.modals.userdup.explanation": "Ce bloc-notes semble être ouvert dans plusieurs fenêtres sur cet ordinateur.",
"pad.modals.userdup.advice": "Se reconnecter en utilisant cette fenêtre.",
"pad.modals.unauth": "Non autorisé",
"pad.modals.unauth.explanation": "Vos permissions ont été changées lors de l'affichage de cette page. Essayez de vous reconnecter.",
"pad.modals.looping.explanation": "Nous éprouvons un problème de communication au serveur de synchronisation.",
"pad.modals.unauth.explanation": "Vos autorisations ont été changées lors de laffichage de cette page. Essayez de vous reconnecter.",
"pad.modals.looping.explanation": "Nous éprouvons des problèmes de communication au serveur de synchronisation.",
"pad.modals.looping.cause": "Il est possible que vous soyez connecté avec un pare-feu ou un mandataire incompatible.",
"pad.modals.initsocketfail": "Le serveur est introuvable.",
"pad.modals.initsocketfail.explanation": "Impossible de se connecter au serveur de synchronisation.",
"pad.modals.initsocketfail.cause": "Ceci est probablement dû à un problème avec votre navigateur ou votre connexion internet.",
"pad.modals.slowcommit.explanation": "Le serveur ne répond pas.",
"pad.modals.slowcommit.cause": "Ce problème peut venir d'une mauvaise connectivité au réseau.",
"pad.modals.badChangeset.explanation": "Une modification que vous avez effectuée a été classée comme impossible par le serveur de synchronisation.",
"pad.modals.slowcommit.cause": "Ce problème peut venir dune mauvaise connectivité au réseau.",
"pad.modals.badChangeset.explanation": "Une modification que vous avez effectuée a été classée comme interdite par le serveur de synchronisation.",
"pad.modals.badChangeset.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter ladministrateur du service si vous pensez que cest une erreur. Essayez de vous reconnecter pour continuer à modifier.",
"pad.modals.corruptPad.explanation": "Le pad auquel vous essayez daccéder est corrompu.",
"pad.modals.corruptPad.explanation": "Le bloc-notes auquel vous essayez daccéder est corrompu.",
"pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter ladministrateur du service.",
"pad.modals.deleted": "Supprimé.",
"pad.modals.deleted.explanation": "Ce pad a été supprimé.",
"pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.",
"pad.modals.disconnected": "Vous avez été déconnecté.",
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer ladministrateur du service.",
"pad.share": "Partager ce pad",
"pad.share": "Partager ce bloc-notes",
"pad.share.readonly": "Lecture seule",
"pad.share.link": "Lien",
"pad.share.emebdcode": "Incorporer un lien",
"pad.chat": "Clavardage",
"pad.chat.title": "Ouvrir le clavardoir de ce pad.",
"pad.chat.title": "Ouvrir le clavardage sur ce bloc-notes.",
"pad.chat.loadmessages": "Charger davantage de messages",
"pad.chat.stick.title": "Ancrer la discussion sur lécran",
"pad.chat.writeMessage.placeholder": "Entrez votre message ici",
"timeslider.pageTitle": "Historique dynamique de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Retourner au pad",
"timeslider.toolbar.authors": "Auteurs :",
"timeslider.toolbar.returnbutton": "Retourner au bloc-notes",
"timeslider.toolbar.authors": "Auteurs:",
"timeslider.toolbar.authorsList": "Aucun auteur",
"timeslider.toolbar.exportlink.title": "Exporter",
"timeslider.exportCurrent": "Exporter la version actuelle sous :",
"timeslider.exportCurrent": "Exporter la version actuelle sous:",
"timeslider.version": "Version {{version}}",
"timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}",
"timeslider.playPause": "Lecture / Pause des contenus du pad",
"timeslider.backRevision": "Reculer dune révision dans ce pad",
"timeslider.forwardRevision": "Avancer dune révision dans ce pad",
"timeslider.playPause": "Lecture / Pause des contenus du bloc-notes",
"timeslider.backRevision": "Reculer dune révision dans ce bloc-notes",
"timeslider.forwardRevision": "Avancer dune révision dans ce bloc-notes",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "janvier",
"timeslider.month.february": "février",
@ -135,20 +138,20 @@
"timeslider.month.december": "décembre",
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: auteur anonyme, other: auteurs anonymes ]}",
"pad.savedrevs.marked": "Cette révision est maintenant marquée comme révision enregistrée",
"pad.savedrevs.timeslider": "Vous pouvez voir les révisions enregistrées en ouvrant l'historique",
"pad.savedrevs.timeslider": "Vous pouvez voir les révisions enregistrées en ouvrant lhistorique",
"pad.userlist.entername": "Entrez votre nom",
"pad.userlist.unnamed": "anonyme",
"pad.userlist.guest": "Invité",
"pad.userlist.deny": "Refuser",
"pad.userlist.approve": "Approuver",
"pad.editbar.clearcolors": "Effacer les couleurs de paternité des auteurs dans tout le document ?",
"pad.editbar.clearcolors": "Effacer le surlignage par auteur dans tout le document?",
"pad.impexp.importbutton": "Importer maintenant",
"pad.impexp.importing": "Import en cours...",
"pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du pad. Êtes-vous sûr de vouloir le faire ?",
"pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du bloc-notes. Êtes-vous sûr de vouloir le faire?",
"pad.impexp.convertFailed": "Nous ne pouvons pas importer ce fichier. Veuillez utiliser un autre format de document ou faire manuellement un copier/coller du texte brut",
"pad.impexp.padHasData": "Nous navons pas pu importer ce fichier parce que ce pad a déjà eu des modifications; veuillez donc créer un nouveau pad",
"pad.impexp.padHasData": "Nous navons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié; veuillez limporter vers un nouveau bloc-notes",
"pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer",
"pad.impexp.importfailed": "Échec de l'importation",
"pad.impexp.copypaste": "Veuillez copier/coller",
"pad.impexp.exportdisabled": "L'option d'export au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails."
"pad.impexp.importfailed": "Échec de limportation",
"pad.impexp.copypaste": "Veuillez copier-coller",
"pad.impexp.exportdisabled": "Lexportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails."
}

View file

@ -89,6 +89,8 @@
"pad.chat": "שיחה",
"pad.chat.title": "פתיחת השיחה של הפנקס הזה.",
"pad.chat.loadmessages": "טעינת הודעות נוספות",
"pad.chat.stick.title": "הצמדת צ׳אט למסך",
"pad.chat.writeMessage.placeholder": "מקום לכתיבת ההודעה שלך",
"timeslider.pageTitle": "גולל זמן של {{appTitle}}",
"timeslider.toolbar.returnbutton": "חזרה אל הפנקס",
"timeslider.toolbar.authors": "כותבים:",

View file

@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"Bugoslav"
"Bugoslav",
"Hmxhmx"
]
},
"index.newPad": "Novi blokić",
@ -84,6 +85,8 @@
"pad.chat": "Čavrljanje",
"pad.chat.title": "Otvori čavrljanje uz ovaj blokić.",
"pad.chat.loadmessages": "Učitaj više poruka",
"pad.chat.stick.title": "Prilijepi razgovor na zaslon",
"pad.chat.writeMessage.placeholder": "Napišite Vašu poruku ovdje",
"timeslider.pageTitle": "{{appTitle}} Vremenska lenta",
"timeslider.toolbar.returnbutton": "Vrati se natrag na blokić",
"timeslider.toolbar.authors": "Autori:",

View file

@ -93,6 +93,8 @@
"pad.chat": "Csevegés",
"pad.chat.title": "A noteszhez tartozó csevegés megnyitása.",
"pad.chat.loadmessages": "További üzenetek betöltése",
"pad.chat.stick.title": "Csevegés a képernyőre",
"pad.chat.writeMessage.placeholder": "Írja az üzenetét ide",
"timeslider.pageTitle": "{{appTitle}} időcsúszka",
"timeslider.toolbar.returnbutton": "Vissza a noteszhez",
"timeslider.toolbar.authors": "Szerzők:",

View file

@ -87,6 +87,8 @@
"pad.chat": "Spjall",
"pad.chat.title": "Opna spjallið fyrir þessa skrifblokk.",
"pad.chat.loadmessages": "Hlaða inn fleiri skeytum",
"pad.chat.stick.title": "Festa spjallið á skjáinn",
"pad.chat.writeMessage.placeholder": "Skrifaðu skilaboðin þín hér",
"timeslider.pageTitle": "Tímalína {{appTitle}}",
"timeslider.toolbar.returnbutton": "Fara til baka í skrifblokk",
"timeslider.toolbar.authors": "Höfundar:",

View file

@ -17,8 +17,8 @@
"pad.toolbar.strikethrough.title": "Barrato (Ctrl+5)",
"pad.toolbar.ol.title": "Elenco numerato (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Elenco puntato (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Rientro (TAB)",
"pad.toolbar.unindent.title": "Riduci rientro (Shift+TAB)",
"pad.toolbar.indent.title": "Indentazione (TAB)",
"pad.toolbar.unindent.title": "Riduci indentazione (Shift+TAB)",
"pad.toolbar.undo.title": "Annulla (Ctrl-Z)",
"pad.toolbar.redo.title": "Ripeti (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "Elimina i colori che indicano gli autori (Ctrl+Shift+C)",
@ -91,6 +91,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Apri la chat per questo Pad.",
"pad.chat.loadmessages": "Carica altri messaggi",
"pad.chat.stick.title": "Ancora chat nello schermo",
"pad.chat.writeMessage.placeholder": "Scrivi il tuo messaggio qui",
"timeslider.pageTitle": "Cronologia {{appTitle}}",
"timeslider.toolbar.returnbutton": "Ritorna al Pad",
"timeslider.toolbar.authors": "Autori:",

View file

@ -4,7 +4,8 @@
"Shirayuki",
"Torinky",
"Omotecho",
"Aefgh39622"
"Aefgh39622",
"Afaz"
]
},
"index.newPad": "新規作成",
@ -89,6 +90,8 @@
"pad.chat": "チャット",
"pad.chat.title": "このパッドのチャットを開きます。",
"pad.chat.loadmessages": "その他のメッセージを読み込む",
"pad.chat.stick.title": "チャットを画面に貼り付ける",
"pad.chat.writeMessage.placeholder": "ここにメッセージを書き込んでください",
"timeslider.pageTitle": "{{appTitle}} タイムスライダー",
"timeslider.toolbar.returnbutton": "パッドに戻る",
"timeslider.toolbar.authors": "作者:",

View file

@ -84,6 +84,8 @@
"pad.chat": "Asqerdec",
"pad.chat.title": "Ldi asqerdec deg upad-agi.",
"pad.chat.loadmessages": "Sali-d ugar n yiznan",
"pad.chat.stick.title": "Senṭeḍ adiwenni deg ugdil",
"pad.chat.writeMessage.placeholder": "Aru izen dagi",
"timeslider.pageTitle": "Amazray asmussan n {{appTitle}}",
"timeslider.toolbar.returnbutton": "Uqal ar upad",
"timeslider.toolbar.authors": "Imeskaren:",

View file

@ -92,6 +92,8 @@
"pad.chat": "대화",
"pad.chat.title": "이 패드에 대화를 엽니다.",
"pad.chat.loadmessages": "더 많은 메시지 불러오기",
"pad.chat.stick.title": "채팅을 화면에 고정",
"pad.chat.writeMessage.placeholder": "여기에 메시지를 적으십시오",
"timeslider.pageTitle": "{{appTitle}} 시간슬라이더",
"timeslider.toolbar.returnbutton": "패드로 돌아가기",
"timeslider.toolbar.authors": "저자:",

View file

@ -6,7 +6,8 @@
"George Animal",
"Gomada",
"Mehk63",
"Ghybu"
"Ghybu",
"MikaelF"
]
},
"index.newPad": "Bloknota nû",
@ -60,18 +61,18 @@
"timeslider.version": "Guhertoya {{version}}",
"timeslider.saved": "Di dîroka {{day}} {{month}} {{year}} de hate tomarkirin",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}",
"timeslider.month.january": "rêbendan",
"timeslider.month.february": "reşemî",
"timeslider.month.january": "kanûna paşîn",
"timeslider.month.february": "sibat",
"timeslider.month.march": "adar",
"timeslider.month.april": "avrêl",
"timeslider.month.april": "nîsan",
"timeslider.month.may": "gulan",
"timeslider.month.june": "pûşper",
"timeslider.month.june": "hezîran",
"timeslider.month.july": "tîrmeh",
"timeslider.month.august": "gelawêj",
"timeslider.month.september": "rezber",
"timeslider.month.october": "kewçêr",
"timeslider.month.november": "Mijdar",
"timeslider.month.december": "berfanbar",
"timeslider.month.august": "tebax",
"timeslider.month.september": "îlon",
"timeslider.month.october": "çiriya pêşîn",
"timeslider.month.november": "Çiriya paşîn",
"timeslider.month.december": "kanûna pêşîn",
"pad.userlist.entername": "Navê xwe têkeve",
"pad.userlist.unnamed": "nenavkirî",
"pad.userlist.guest": "Mêvan",

View file

@ -12,8 +12,8 @@
"pad.toolbar.italic.title": "Schréi (Ctrl+I)",
"pad.toolbar.underline.title": "Ënnerstrach (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Duerchgestrach (Ctrl+5)",
"pad.toolbar.ol.title": "Numeréiert Lëscht (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Net-numeréiert Lëscht (Ctrl+Shift+L)",
"pad.toolbar.ol.title": "Nummeréiert Lëscht (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Net-nummeréiert Lëscht (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Aréckelen (TAB)",
"pad.toolbar.unindent.title": "Erausréckelen (Shift+TAB)",
"pad.toolbar.undo.title": "Réckgängeg (Ctrl-Z)",
@ -59,6 +59,7 @@
"pad.share.link": "Link",
"pad.chat": "Chat",
"pad.chat.loadmessages": "Méi Message lueden",
"pad.chat.writeMessage.placeholder": "Schreift Äre Message hei",
"timeslider.toolbar.authors": "Auteuren:",
"timeslider.toolbar.authorsList": "Keng Auteuren",
"timeslider.toolbar.exportlink.title": "Exportéieren",

View file

@ -1,78 +1,80 @@
{
"@metadata": {
"authors": [
"Mogoeilor"
"Mogoeilor",
"Lorestani"
]
},
"index.newPad": "دشته تازه",
"pad.toolbar.bold.title": "توپر",
"pad.toolbar.italic.title": "کج کوله(ctrl-l)",
"pad.toolbar.underline.title": "زیر خط دار بین (Ctrl-U)",
"pad.toolbar.ol.title": "نوم گه منظم",
"pad.toolbar.ul.title": "نوم گه بی نظم",
"pad.toolbar.indent.title": "مئن رئته(TAB)",
"pad.toolbar.unindent.title": "وه در رئته (Shift+TAB)",
"pad.toolbar.undo.title": "رد انجوم دئین (Ctrl-Z)",
"pad.toolbar.redo.title": "د نو انجوم دئین(Ctrl-Y)",
"index.newPad": "دٱفتٱرچٱ تازٱ",
"pad.toolbar.bold.title": "تۊپور",
"pad.toolbar.italic.title": "هٱلٛ هار(ctrl-l)",
"pad.toolbar.underline.title": "زؽر خٱت (Ctrl-U)",
"pad.toolbar.ol.title": "نومگٱ مورٱتٱب بیٱ",
"pad.toolbar.ul.title": "نومگٱ مورٱتٱب ناٛیٱ",
"pad.toolbar.indent.title": "قوپساٛیی(TAB)",
"pad.toolbar.unindent.title": "ڤ دٱر رٱتاٛیی (Shift+TAB)",
"pad.toolbar.undo.title": "رٱد ٱنجوم داٛئن (Ctrl-Z)",
"pad.toolbar.redo.title": "د نۊ ٱنجوم داٛئن(Ctrl-Y)",
"pad.toolbar.savedRevision.title": "ڤانری بٱلگٱ",
"pad.toolbar.settings.title": "میزوکاری",
"pad.colorpicker.save": "ذخيره كردن",
"pad.colorpicker.cancel": "انجوم شیو كردن",
"pad.loading": حالت سوار كرد",
"pad.wrongPassword": اسوردتو اشتوائه",
"pad.settings.padSettings": "میزوکاری دشته",
"pad.settings.myView": ظرگه مه",
"pad.settings.stickychat": "همیشه د بلگه چک چنه بکید",
"pad.settings.linenocheck": "شماره خطیا",
"pad.colorpicker.save": "زٱخیرٱ كردن",
"pad.colorpicker.cancel": "ٱنجوم شؽڤ كردن",
"pad.loading": هالٱت سڤار كرد...",
"pad.wrongPassword": ٱسڤردتو اْشتبائٱ",
"pad.settings.padSettings": "میزوکاری دٱفتٱرچٱ",
"pad.settings.myView": ٱزٱرگٱ ماْ",
"pad.settings.stickychat": "همیشٱ د بٱلگٱ چٱک چنٱ بٱکؽت",
"pad.settings.linenocheck": "شمارٱ خٱتؽا",
"pad.settings.fontType": "نوع فونت:",
"pad.settings.fontType.normal": "عادی",
"pad.settings.fontType.monospaced": "تک جاگه",
"pad.settings.globalView": "دیئن جهونی",
"pad.settings.language": ون:",
"pad.importExport.import_export": "وامین آوردن/د در دئن",
"pad.importExport.importSuccessful": "موفق بی!",
"pad.importExport.export": شته تازه چی وه در بیه:",
"pad.importExport.exporthtml": "اچ تی ام ال",
"pad.importExport.exportplain": "نیسسه ساده",
"pad.importExport.exportword": "واجه پالایشتگر مایکروسافت",
"pad.importExport.exportpdf": "پی دی اف",
"pad.importExport.exportopen": "او دی اف(قالو سند وا بیه)",
"pad.modals.connected": "وصل بیه",
"pad.modals.forcereconnect": "سی وصل بین مژبور کو",
"pad.modals.userdup": "د نیمدری هنی واز بیه",
"pad.modals.initsocketfail": "سرور د دسرسی نئ.",
"pad.modals.deleted": "پاک بیه",
"pad.modals.deleted.explanation": "ای دشته جا وه جا بیه",
"pad.modals.disconnected": "ارتواطتو قطع بیه",
"pad.share": "ای دشته نه بهر کو",
"pad.share.readonly": "فقط بحون",
"pad.share.link": "هوم پیوند",
"pad.chat": "گپ زئن",
"pad.chat.title": "گپ چنه نه سی دشته وا کو.",
"pad.chat.loadmessages": یغومیا بیشتر نه سوار کو",
"timeslider.toolbar.returnbutton": "ورگرد د دشته",
"timeslider.toolbar.authors": "نیسنه یا:",
"timeslider.toolbar.authorsList": ی نیسنه",
"timeslider.toolbar.exportlink.title": "وه در ديئن",
"timeslider.version": سقه{{نسقه}}",
"timeslider.month.january": "جانويه",
"timeslider.month.february": وريه",
"pad.settings.language": ڤون:",
"pad.importExport.import_export": "ڤامین آوئردن/ڤ دٱر داٛئن",
"pad.importExport.importSuccessful": "موئٱفٱق بی!",
"pad.importExport.export": ٱفتٱرچٱ تازٱ چی ڤ دٱر بیٱ:",
"pad.importExport.exporthtml": "اْچ تی اْم اْل",
"pad.importExport.exportplain": "نیسسٱ سادٱ",
"pad.importExport.exportword": "ڤاژٱ پالایشگٱر مایکروسافت",
"pad.importExport.exportpdf": "پی دی اْف",
"pad.importExport.exportopen": "او دی اْف(قالب سٱنٱد ڤاز)",
"pad.modals.connected": "ڤٱسل بیٱ",
"pad.modals.forcereconnect": "سی ڤٱسل بیئن دوئارٱ مٱجبۊر کو",
"pad.modals.userdup": "د نیمدری هنی ڤاز بیٱ",
"pad.modals.initsocketfail": "سرور د دٱسرسی نؽ.",
"pad.modals.deleted": "پاک بیٱ",
"pad.modals.deleted.explanation": "اؽ دٱفتٱرچٱ جا ڤ جا بیٱ",
"pad.modals.disconnected": "اْرتبات تو قٱت بیٱ.",
"pad.share": "اؽ دٱفتٱرچٱ ناْ بٱئر کو",
"pad.share.readonly": "فقٱت ڤٱننی",
"pad.share.link": "هوم پاٛڤٱن",
"pad.chat": "سالفٱ",
"pad.chat.title": "سالفٱ ناْ سی دٱفتٱرچٱ ڤاز کو.",
"pad.chat.loadmessages": اٛغومؽا ؽشتر ناْ سڤار کو",
"timeslider.toolbar.returnbutton": "ڤرگٱشتن ڤ دٱفتٱرچٱ",
"timeslider.toolbar.authors": "نیسٱنٱ یا:",
"timeslider.toolbar.authorsList": ؽ نیسٱنٱ",
"timeslider.toolbar.exportlink.title": "ڤ دٱر داٛئن",
"timeslider.version": ۏسخٱ{{نۏسخٱ}}",
"timeslider.month.january": "ژانڤیٱ",
"timeslider.month.february": ڤریٱ",
"timeslider.month.march": "مارس",
"timeslider.month.april": وريل",
"timeslider.month.may": "ما",
"timeslider.month.june": "جوئن",
"timeslider.month.july": ولای",
"timeslider.month.august": "اگوست",
"timeslider.month.april": ڤريل",
"timeslider.month.may": "ماٛی",
"timeslider.month.june": "ژوئٱن",
"timeslider.month.july": ۊلای",
"timeslider.month.august": "آگوست",
"timeslider.month.september": "سپتامر",
"timeslider.month.october": "اكتور",
"timeslider.month.november": "نوامر",
"timeslider.month.october": "اوكتوبر",
"timeslider.month.november": "نوڤامر",
"timeslider.month.december": "دسامر",
"pad.userlist.entername": "نوم تونه وارد بکید",
"pad.userlist.unnamed": "نوم نهشته",
"pad.userlist.guest": یزوان",
"pad.userlist.deny": "پرو کردن",
"pad.userlist.approve": "اصلا کردن",
"pad.impexp.importbutton": "ایسه وارد کو",
"pad.impexp.importing": حالت وارد کردن",
"pad.impexp.importfailed": "وامین آوردن شکست حرد",
"pad.impexp.copypaste": واهشن وردار بدیسن"
"pad.userlist.entername": "نوم توناْ ڤارد بٱکؽت",
"pad.userlist.unnamed": "بؽ نوم",
"pad.userlist.guest": اٛموݩ",
"pad.userlist.deny": "رٱد کردن",
"pad.userlist.approve": "قبۊل کردن",
"pad.impexp.importbutton": "ایساْ ڤارد کو",
"pad.impexp.importing": هالٱت ڤارد کردن...",
"pad.impexp.importfailed": "ڤامؽن آوئردن شکٱست هٱرد",
"pad.impexp.copypaste": اهشٱن ڤردار بٱدیسن"
}

View file

@ -5,7 +5,8 @@
"Mantak111",
"I-svetaines",
"Zygimantus",
"Vogone"
"Vogone",
"Naktis"
]
},
"index.newPad": "Naujas bloknotas",
@ -59,6 +60,8 @@
"pad.modals.connected": "Prisijungta.",
"pad.modals.reconnecting": "Iš naujo prisijungiama prie Jūsų bloknoto",
"pad.modals.forcereconnect": "Priversti prisijungti iš naujo",
"pad.modals.reconnecttimer": "Bandoma vėl prisijungti",
"pad.modals.cancel": "Atšaukti",
"pad.modals.userdup": "Atidaryta kitame lange",
"pad.modals.userdup.explanation": "Šis bloknotas, atrodo yra atidarytas daugiau nei viename šio kompiuterio naršyklės lange.",
"pad.modals.userdup.advice": "Prisijunkite iš naujo, kad vietoj to naudotumėte šį langą.",
@ -87,6 +90,8 @@
"pad.chat": "Pokalbiai",
"pad.chat.title": "Atverti šio bloknoto pokalbį.",
"pad.chat.loadmessages": "Įkrauti daugiau pranešimų",
"pad.chat.stick.title": "Priklijuoti pokalbį",
"pad.chat.writeMessage.placeholder": "Rašykite savo žinutę čia",
"timeslider.pageTitle": "{{appTitle}} Laiko slinkiklis",
"timeslider.toolbar.returnbutton": "Grįžti į bloknotą",
"timeslider.toolbar.authors": "Autoriai:",

View file

@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Bjankuloski06",
"Brest"
"Brest",
"Vlad5250"
]
},
"index.newPad": "Нова тетратка",
@ -23,7 +24,7 @@
"pad.toolbar.savedRevision.title": "Зачувај преработка",
"pad.toolbar.settings.title": "Поставки",
"pad.toolbar.embed.title": "Споделете и вметнете ја тетраткава",
"pad.toolbar.showusers.title": "Прикаж. корисниците на тетраткава",
"pad.toolbar.showusers.title": "Прикажи корисниците на тетраткава",
"pad.colorpicker.save": "Зачувај",
"pad.colorpicker.cancel": "Откажи",
"pad.loading": "Вчитувам...",
@ -87,6 +88,8 @@
"pad.chat": "Разговор",
"pad.chat.title": "Отвори го разговорот за оваа тетратка.",
"pad.chat.loadmessages": "Вчитај уште пораки",
"pad.chat.stick.title": "Залепи го разговорот на екранот",
"pad.chat.writeMessage.placeholder": "Тука напишете порака",
"timeslider.pageTitle": "{{appTitle}} Историски преглед",
"timeslider.toolbar.returnbutton": "Назад на тетратката",
"timeslider.toolbar.authors": "Автори:",
@ -122,7 +125,7 @@
"pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ?",
"pad.impexp.importbutton": "Увези сега",
"pad.impexp.importing": "Увезувам...",
"pad.impexp.confirmimport": "Увезувајќи ја податотеката ќе го замените целиот досегашен текст во тетратката. Дали сте сигурни дека сакате да продолжите?",
"pad.impexp.confirmimport": "Увезувањето на податотека ќе го презапише тековниот текст на тетратката. Дали сте сигурни дека сакате да продолжите?",
"pad.impexp.convertFailed": "Не можев да ја увезам податотеката. Послужете се со поинаков формат или прекопирајте го текстот рачно.",
"pad.impexp.padHasData": "Не можевме да ја увеземе оваа податотека бидејќи оваа тетратка веќе има промени. Увезете ја во нова тетратка.",
"pad.impexp.uploadFailed": "Подигањето не успеа. Обидете се повторно.",

View file

@ -90,6 +90,8 @@
"pad.chat": "Chat",
"pad.chat.title": "Åpne chatten for denne blokken.",
"pad.chat.loadmessages": "Last flere beskjeder",
"pad.chat.stick.title": "Fest chatten til skjermen",
"pad.chat.writeMessage.placeholder": "Skriv beskjeden din her",
"timeslider.pageTitle": "{{appTitle}} Tidslinje",
"timeslider.toolbar.returnbutton": "Gå tilbake til blokk",
"timeslider.toolbar.authors": "Forfattere:",

View file

@ -7,7 +7,8 @@
"Robin van der Vliet",
"Mainframe98",
"KlaasZ4usV",
"Rickvl"
"Rickvl",
"Marcelhospers"
]
},
"index.newPad": "Nieuw pad",
@ -92,6 +93,8 @@
"pad.chat": "Chatten",
"pad.chat.title": "Chat voor dit pad opnenen",
"pad.chat.loadmessages": "Meer berichten laden",
"pad.chat.stick.title": "Zet de chat op het scherm",
"pad.chat.writeMessage.placeholder": "Schrijf je bericht hier",
"timeslider.pageTitle": "Tijdlijn voor {{appTitle}}",
"timeslider.toolbar.returnbutton": "Terug naar pad",
"timeslider.toolbar.authors": "Auteurs:",

View file

@ -4,7 +4,8 @@
"Aalam",
"Babanwalia",
"ਪ੍ਰਚਾਰਕ",
"Tow"
"Tow",
"ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ"
]
},
"index.newPad": "ਨਵਾਂ ਪੈਡ",
@ -22,7 +23,7 @@
"pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ",
"pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ",
"pad.toolbar.savedRevision.title": "ਰੀਵਿਜ਼ਨ ਸੰਭਾਲੋ",
"pad.toolbar.savedRevision.title": "ਦੁਹਰਾਅ ਸਾਂਭੋ",
"pad.toolbar.settings.title": "ਸੈਟਿੰਗ",
"pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ",
"pad.toolbar.showusers.title": "ਇਹ ਪੈਡ ਉੱਤੇ ਯੂਜ਼ਰ ਵੇਖਾਓ",
@ -30,9 +31,9 @@
"pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ",
"pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
"pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।",
"pad.passwordRequired": "ਇਹ ਪੈਡ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ",
"pad.passwordRequired": "ਇਸ ਪੈਡ ਤੱਕ ਅਪੜਨ ਲਈ ਤੁਹਾਨੂੰ ਇੱਕ ਲੰਘ-ਸ਼ਬਦ ਦੀ ਲੋੜ ਹੈ",
"pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ",
"pad.wrongPassword": "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਗਲਤੀ ਸੀ",
"pad.wrongPassword": "ਤੁਹਾਡਾ ਲੰਘ-ਸ਼ਬਦ ਗਲਤ ਸੀ",
"pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ",
"pad.settings.myView": "ਮੇਰੀ ਝਲਕ",
"pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ",
@ -97,7 +98,7 @@
"timeslider.saved": "{{day}} {{month}} {{year}} ਨੂੰ ਸੰਭਾਲਿਆ",
"timeslider.playPause": "ਪੈਡ ਸਮੱਗਰੀ ਚਲਾਓ / ਵਿਰਾਮ ਕਰੋ",
"timeslider.backRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਪਿਛਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ",
"timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ",
"timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਦੁਹਰਾਅ ਤੇ ਜਾਓ",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "ਜਨਵਰੀ",
"timeslider.month.february": "ਫ਼ਰਵਰੀ",

View file

@ -84,6 +84,8 @@
"pad.chat": "Ciaciarada",
"pad.chat.title": "Duverté la ciaciarada për cost feuj.",
"pad.chat.loadmessages": "Carié pi 'd mëssagi",
"pad.chat.stick.title": "Taché la ciaciarada an slë scren",
"pad.chat.writeMessage.placeholder": "Ch'a scriva sò mëssage ambelessì",
"timeslider.pageTitle": "Stòria dinàmica ëd {{appTitle}}",
"timeslider.toolbar.returnbutton": "Torné al feuj",
"timeslider.toolbar.authors": "Autor:",

View file

@ -101,6 +101,8 @@
"pad.chat": "Bate-papo",
"pad.chat.title": "Abrir o bate-papo desta nota.",
"pad.chat.loadmessages": "Carregar mais mensagens",
"pad.chat.stick.title": "Cole o bate-papo na tela",
"pad.chat.writeMessage.placeholder": "Escreva sua mensagem aqui",
"timeslider.pageTitle": "Linha do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Retornar para a nota",
"timeslider.toolbar.authors": "Autores:",

View file

@ -9,38 +9,39 @@
"Macofe",
"Ti4goc",
"Cainamarques",
"Athena in Wonderland"
"Athena in Wonderland",
"Waldyrious"
]
},
"index.newPad": "Nova Nota",
"index.createOpenPad": "ou crie/abra uma Nota com o nome:",
"pad.toolbar.bold.title": "Negrito (Ctrl-B)",
"pad.toolbar.italic.title": "Itálico (Ctrl-I)",
"pad.toolbar.underline.title": "Sublinhado (Ctrl-U)",
"index.createOpenPad": "ou crie/abra uma nota com o nome:",
"pad.toolbar.bold.title": "Negrito (Ctrl+B)",
"pad.toolbar.italic.title": "Itálico (Ctrl+I)",
"pad.toolbar.underline.title": "Sublinhado (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Avançar (TAB)",
"pad.toolbar.unindent.title": "Recuar (Shift+TAB)",
"pad.toolbar.undo.title": "Desfazer (Ctrl-Z)",
"pad.toolbar.redo.title": "Refazer (Ctrl-Y)",
"pad.toolbar.indent.title": "Indentar (TAB)",
"pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)",
"pad.toolbar.undo.title": "Desfazer (Ctrl+Z)",
"pad.toolbar.redo.title": "Refazer (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importar/exportar de/para diferentes formatos de ficheiro",
"pad.toolbar.timeslider.title": "Linha de tempo",
"pad.toolbar.savedRevision.title": "Salvar revisão",
"pad.toolbar.savedRevision.title": "Gravar revisão",
"pad.toolbar.settings.title": "Configurações",
"pad.toolbar.embed.title": "Compartilhar e incorporar este pad",
"pad.toolbar.showusers.title": "Mostrar os utilizadores nesta Nota",
"pad.toolbar.embed.title": "Partilhar e incorporar esta nota",
"pad.toolbar.showusers.title": "Mostrar os utilizadores nesta nota",
"pad.colorpicker.save": "Gravar",
"pad.colorpicker.cancel": "Cancelar",
"pad.loading": "A carregar…",
"pad.noCookie": "O cookie não foi encontrado. Por favor, ative os cookies no seu navegador!",
"pad.passwordRequired": "Precisa de uma senha para aceder a este pad",
"pad.permissionDenied": "Não tem permissão para aceder a este pad.",
"pad.wrongPassword": "A palavra-chave está errada",
"pad.settings.padSettings": "Configurações da Nota",
"pad.settings.myView": "Minha vista",
"pad.settings.stickychat": "Bate-papo sempre no ecrã",
"pad.passwordRequired": "Precisa de uma palavra-passe para aceder a esta nota",
"pad.permissionDenied": "Não tem permissão para aceder a esta nota",
"pad.wrongPassword": "A sua palavra-passe estava errada",
"pad.settings.padSettings": "Configurações da nota",
"pad.settings.myView": "A minha vista",
"pad.settings.stickychat": "Conversação sempre no ecrã",
"pad.settings.chatandusers": "Mostrar a conversação e os utilizadores",
"pad.settings.colorcheck": "Cores de autoria",
"pad.settings.linenocheck": "Números de linha",
@ -52,8 +53,8 @@
"pad.settings.language": "Língua:",
"pad.importExport.import_export": "Importar/Exportar",
"pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento",
"pad.importExport.importSuccessful": "Bem sucedido!",
"pad.importExport.export": "Exportar a Nota atual como:",
"pad.importExport.importSuccessful": "Completo!",
"pad.importExport.export": "Exportar a nota atual como:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Texto simples",
@ -62,19 +63,19 @@
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Só é possível importar texto sem formatação ou HTML. Para obter funcionalidades de importação mais avançadas, por favor <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale o AbiWord</a>.",
"pad.modals.connected": "Ligado.",
"pad.modals.reconnecting": "Reconectando-se ao seu bloco…",
"pad.modals.forcereconnect": "Forçar reconexão",
"pad.modals.reconnecttimer": "A tentar religar",
"pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…",
"pad.modals.forcereconnect": "Forçar restabelecimento de ligação",
"pad.modals.reconnecttimer": "A tentar restabelecer ligação",
"pad.modals.cancel": "Cancelar",
"pad.modals.userdup": "Aberto noutra janela",
"pad.modals.userdup.explanation": "Este pad parece estar aberto em mais do que uma janela do navegador neste computador.",
"pad.modals.userdup.explanation": "Esta nota parece estar aberta em mais do que uma janela do navegador neste computador.",
"pad.modals.userdup.advice": "Religar para utilizar esta janela.",
"pad.modals.unauth": "Não autorizado",
"pad.modals.unauth.explanation": "As suas permissões foram alteradas enquanto revia esta página. Tente religar.",
"pad.modals.looping.explanation": "Existem problemas de comunicação com o servidor de sincronização.",
"pad.modals.looping.cause": "Talvez tenha ligado por um firewall ou proxy incompatível.",
"pad.modals.initsocketfail": "O servidor está inacessível.",
"pad.modals.initsocketfail.explanation": "Não foi possível a conexão ao servidor de sincronização.",
"pad.modals.initsocketfail.explanation": "Não foi possível ligar ao servidor de sincronização.",
"pad.modals.initsocketfail.cause": "Isto provavelmente ocorreu por um problema no seu navegador ou na sua ligação de Internet.",
"pad.modals.slowcommit.explanation": "O servidor não está a responder.",
"pad.modals.slowcommit.cause": "Isto pode ser por problemas com a ligação de rede.",
@ -83,28 +84,30 @@
"pad.modals.corruptPad.explanation": "A nota que está a tentar aceder está corrompida.",
"pad.modals.corruptPad.cause": "Isto pode ocorrer devido a uma configuração errada do servidor ou algum outro comportamento inesperado. Por favor contacte o administrador.",
"pad.modals.deleted": "Eliminado.",
"pad.modals.deleted.explanation": "Este pad foi removido.",
"pad.modals.disconnected": "Você foi desconectado.",
"pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida",
"pad.modals.deleted.explanation": "Esta nota foi removida.",
"pad.modals.disconnected": "Você foi desligado.",
"pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida",
"pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.",
"pad.share": "Compartilhar este pad",
"pad.share": "Partilhar esta nota",
"pad.share.readonly": "Somente para leitura",
"pad.share.link": "Ligação",
"pad.share.link": "Hiperligação",
"pad.share.emebdcode": "Incorporar o URL",
"pad.chat": "Bate-papo",
"pad.chat.title": "Abrir o bate-papo para este pad.",
"pad.chat": "Conversação",
"pad.chat.title": "Abrir a conversação para esta nota.",
"pad.chat.loadmessages": "Carregar mais mensagens",
"pad.chat.stick.title": "Colar conversação no ecrã",
"pad.chat.writeMessage.placeholder": "Escreva a sua mensagem aqui",
"timeslider.pageTitle": "Linha do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Voltar ao pad",
"timeslider.toolbar.returnbutton": "Voltar à nota",
"timeslider.toolbar.authors": "Autores:",
"timeslider.toolbar.authorsList": "Sem Autores",
"timeslider.toolbar.exportlink.title": "Exportar",
"timeslider.exportCurrent": "Exportar versão atual como:",
"timeslider.version": "Versão {{version}}",
"timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
"timeslider.playPause": "Reproduzir / Pausar conteúdo do Pad",
"timeslider.backRevision": "Voltar a uma revisão anterior neste Pad",
"timeslider.forwardRevision": "Ir a uma revisão posterior neste Pad",
"timeslider.playPause": "Reproduzir / pausar conteúdo da nota",
"timeslider.backRevision": "Voltar a uma revisão anterior desta nota",
"timeslider.forwardRevision": "Avançar para uma revisão posterior desta nota",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "Janeiro",
"timeslider.month.february": "Fevereiro",
@ -129,11 +132,11 @@
"pad.editbar.clearcolors": "Deseja limpar as cores de autoria em todo o documento?",
"pad.impexp.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...",
"pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual do pad. Tem certeza que deseja continuar?",
"pad.impexp.confirmimport": "A importação de um ficheiro irá substituir o texto atual da nota. Tem certeza que deseja continuar?",
"pad.impexp.convertFailed": "Não foi possível importar este ficheiro. Utilize outro formato ou copie e insira manualmente",
"pad.impexp.padHasData": "Não fomos capazes de importar este ficheiro porque este Pad já tinha alterações, por favor importe para um novo pad",
"pad.impexp.uploadFailed": "O upload falhou. Por favor, tente novamente",
"pad.impexp.padHasData": "Não fomos capazes de importar este ficheiro porque esta nota já tinha alterações; importe para uma nota nova, por favor",
"pad.impexp.uploadFailed": "O carregamento falhou; tente novamente, por favor",
"pad.impexp.importfailed": "A importação falhou",
"pad.impexp.copypaste": "Por favor, copie e cole",
"pad.impexp.copypaste": "Copie e insira, por favor",
"pad.impexp.exportdisabled": "A exportação no formato {{type}} está desativada. Por favor, contacte o administrador do sistema para mais informações."
}

View file

@ -83,6 +83,8 @@
"pad.chat": "Used as button text and as title of Chat window.\n{{Identical|Chat}}",
"pad.chat.title": "Used as tooltip for the Chat button",
"pad.chat.loadmessages": "chat messages",
"pad.chat.stick.title": "Tooltip for the stick chat button",
"pad.chat.writeMessage.placeholder": "Placeholder for the chat input",
"timeslider.pageTitle": "{{doc-important|Please leave <code><nowiki>{{appTitle}}</nowiki></code> parameter untouched. It will be replaced by app title.}}\nInserted into HTML title tag.",
"timeslider.toolbar.returnbutton": "Used as link title",
"timeslider.toolbar.authors": "A list of Authors follows after the colon.\n{{Identical|Author}}",

View file

@ -9,7 +9,8 @@
"Nzeemin",
"Facenapalm",
"Patrick Star",
"Movses"
"Movses",
"Diralik"
]
},
"index.newPad": "Создать",
@ -82,8 +83,8 @@
"pad.modals.badChangeset.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы, если вы считаете, что это ошибка. Попробуйте переподключиться для того, чтобы продолжить редактирование.",
"pad.modals.corruptPad.explanation": "Документ, к которому вы пытаетесь получить доступ, повреждён.",
"pad.modals.corruptPad.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы.",
"pad.modals.deleted": "Удален.",
"pad.modals.deleted.explanation": "Этот документ был удален.",
"pad.modals.deleted": "Удалён.",
"pad.modals.deleted.explanation": "Этот документ был удалён.",
"pad.modals.disconnected": "Соединение разорвано.",
"pad.modals.disconnected.explanation": "Подключение к серверу потеряно",
"pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.",
@ -93,7 +94,9 @@
"pad.share.emebdcode": "Вставить URL",
"pad.chat": "Чат",
"pad.chat.title": "Открыть чат для этого документа.",
"pad.chat.loadmessages": "Еще сообщения",
"pad.chat.loadmessages": "Ещё сообщения",
"pad.chat.stick.title": "Закрепить чат на экране",
"pad.chat.writeMessage.placeholder": "Напишите своё сообщение сюда",
"timeslider.pageTitle": "Временная шкала {{appTitle}}",
"timeslider.toolbar.returnbutton": "К документу",
"timeslider.toolbar.authors": "Авторы:",
@ -135,5 +138,5 @@
"pad.impexp.uploadFailed": "Загрузка не удалась, пожалуйста, попробуйте ещё раз",
"pad.impexp.importfailed": "Ошибка при импорте",
"pad.impexp.copypaste": "Пожалуйста, скопируйте",
"pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключен. Для подробной информации обратитесь к системному администратору."
"pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключён. Для подробной информации обратитесь к системному администратору."
}

View file

@ -66,7 +66,7 @@
"timeslider.toolbar.authorsList": "ڪوبه ليکڪ ناهي",
"timeslider.toolbar.exportlink.title": "برآمد ڪريو",
"timeslider.version": "ورزن {{version}}",
"timeslider.saved": "شانڍيل {{مهينو}} {{ڏينهن}}, {{سال}}",
"timeslider.saved": "سانڍيل {{month}} {{day}}، {{year}}",
"timeslider.dateformat": "{{مهينو}}/{{ڏينهن}}/{{سال}} {{ڪلاڪ}}:{{منٽ}}:{{سيڪنڊ}}",
"timeslider.month.january": "جنوري",
"timeslider.month.february": "فيبروري",

132
src/locales/sh.json Normal file
View file

@ -0,0 +1,132 @@
{
"@metadata": {
"authors": [
"Conquistador",
"Vlad5250"
]
},
"index.newPad": "Novi blokić",
"index.createOpenPad": "ili napravite/otvorite blokić s imenom:",
"pad.toolbar.bold.title": "Podebljano (Ctrl+B)",
"pad.toolbar.italic.title": "Ukošeno (Ctrl+I)",
"pad.toolbar.underline.title": "Podcrtano (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Precrtano (Ctrl+5)",
"pad.toolbar.ol.title": "Poredani spisak (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Neporedani spisak (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Uvlaka (TAB)",
"pad.toolbar.unindent.title": "Izvlaka (Shift+TAB)",
"pad.toolbar.undo.title": "Vrati (Ctrl+Z)",
"pad.toolbar.redo.title": "Ponovi (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Ukloni boje autorstva (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Uvoz/Izvoz iz/na različite datotečne formate",
"pad.toolbar.timeslider.title": "Historijski pregled",
"pad.toolbar.savedRevision.title": "Snimi inačicu",
"pad.toolbar.settings.title": "Postavke",
"pad.toolbar.embed.title": "Dijelite i umetnite ovaj blokić",
"pad.toolbar.showusers.title": "Pokaži korisnike ovoga blokića",
"pad.colorpicker.save": "Snimi",
"pad.colorpicker.cancel": "Otkaži",
"pad.loading": "Učitavam...",
"pad.noCookie": "Nisam mogao pronaći kolačić. Omogućite kolačiće u vašem pregledniku!",
"pad.passwordRequired": "Potrebna je lozinka za pristup",
"pad.permissionDenied": "Za ovdje nije potrebna dozvola za pristup",
"pad.wrongPassword": "Pogrešna lozinka",
"pad.settings.padSettings": "Postavke blokića",
"pad.settings.myView": "Moj prikaz",
"pad.settings.stickychat": "Ćaskanje uvijek na ekranu",
"pad.settings.chatandusers": "Prikaži ćaskanje i korisnike",
"pad.settings.colorcheck": "Boje autorstva",
"pad.settings.linenocheck": "Brojevi redova",
"pad.settings.rtlcheck": "Da prikažem sadržaj zdesna ulijevo?",
"pad.settings.fontType": "Tip fonta:",
"pad.settings.globalView": "Globalni prikaz",
"pad.settings.language": "Jezik:",
"pad.importExport.import_export": "Uvoz/Izvoz",
"pad.importExport.import": "Otpremanje bilo koje tekstualne datoteke ili dokumenta",
"pad.importExport.importSuccessful": "Uspješno!",
"pad.importExport.export": "Izvezi trenutni blokić kao:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Obični tekst",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Možete uvoziti samo iz običnog teksta te HTML-formata. Naprednije mogućnosti uvoza dobit ćete ako <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">uspostavite AbiWord</a>.",
"pad.modals.connected": "Povezano.",
"pad.modals.reconnecting": "Prepovezujemo Vas s blokićem...",
"pad.modals.forcereconnect": "Nametni prepovezivanje",
"pad.modals.reconnecttimer": "Se prepovezivam za",
"pad.modals.cancel": "Otkaži",
"pad.modals.userdup": "Otvoreno u drugom prozoru",
"pad.modals.userdup.explanation": "Ovaj je blokić otvoren u više od jednoga prozora (u pregledniku) na računalu.",
"pad.modals.userdup.advice": "Prepovežite se da biste koristili ovaj prozor.",
"pad.modals.unauth": "Neovlašteno",
"pad.modals.unauth.explanation": "Vaše su dozvole izmijenjene za vrijeme dok ste pregledavali ovu stranicu. Pokušajte se prepovezati.",
"pad.modals.looping.explanation": "Postoje problemi s vezom sa usklađivnim poslužiteljem.",
"pad.modals.looping.cause": "Možda ste se spojili preko neskladne sigurnosne stijene ili proxyja.",
"pad.modals.initsocketfail": "Server je nedostupan.",
"pad.modals.initsocketfail.explanation": "Nisam mogao se povezati sa usklađivnim serverom.",
"pad.modals.initsocketfail.cause": "Ovo je vjerojatno zbog problema s vašim preglednikom ili svemrežnom vezom.",
"pad.modals.slowcommit.explanation": "Server se ne odaziva.",
"pad.modals.slowcommit.cause": "Ovo je vjerojatno zbog problema s mrežnim povezivanjem.",
"pad.modals.badChangeset.explanation": "Poslužitelj za usklađivanje smatra da je izmjena koju ste napravili nedopuštena.",
"pad.modals.badChangeset.cause": "Ovo može biti zbog pogrešne postavljenosti poslužitelja ili nekog drugog neočekivanog ponašanja. Obratite se administratoru ukoliko držite da je to greška. Pokušajte se preuključiti kako biste nastavili s uređivanjem.",
"pad.modals.corruptPad.explanation": "Blokić što pokušavate otvoriti je oštećen.",
"pad.modals.corruptPad.cause": "Ovo može biti zbog pogrešne postavljenosti poslužitelja ili nekog drugog neočekivanog ponašanja. Obratite se administratoru.",
"pad.modals.deleted": "Obrisano.",
"pad.modals.deleted.explanation": "Ovaj blokić je uklonjen.",
"pad.modals.disconnected": "Veza je prekinuta.",
"pad.modals.disconnected.explanation": "Veza s poslužiteljem je prekinuta",
"pad.modals.disconnected.cause": "Moguće je da server nije dostupan. Obavijestite administratora ako se ovo nastavi događati.",
"pad.share": "Dijeli ovaj blokić",
"pad.share.readonly": "Samo čitanje",
"pad.share.link": "Link",
"pad.share.emebdcode": "Umetni URL",
"pad.chat": "Ćaskanje",
"pad.chat.title": "Otvori ćaskanje uz ovaj blokić.",
"pad.chat.loadmessages": "Učitaj više poruka",
"pad.chat.stick.title": "Zalijepi ćaskanje na ekranu",
"pad.chat.writeMessage.placeholder": "Ovdje napišite poruku",
"timeslider.pageTitle": "{{appTitle}} Historijski pregled",
"timeslider.toolbar.returnbutton": "Natrag na blokić",
"timeslider.toolbar.authors": "Autori:",
"timeslider.toolbar.authorsList": "Nema autora",
"timeslider.toolbar.exportlink.title": "Izvoz",
"timeslider.exportCurrent": "Izvezi trenutnu verziju kao:",
"timeslider.version": "Verzija {{version}}",
"timeslider.saved": "Spremljeno {{day}}. {{month}} {{year}}.",
"timeslider.playPause": "Izvrti/pauziraj sadržaj blokića",
"timeslider.backRevision": "Nazad na jednu inačicu ovog blokića",
"timeslider.forwardRevision": "Naprijed na jednu inačicu ovog blokića",
"timeslider.dateformat": "{{day}}. {{month}}. {{year}}. {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "januara",
"timeslider.month.february": "februara",
"timeslider.month.march": "marta",
"timeslider.month.april": "aprila",
"timeslider.month.may": "maja",
"timeslider.month.june": "juna",
"timeslider.month.july": "jula",
"timeslider.month.august": "augusta",
"timeslider.month.september": "septembra",
"timeslider.month.october": "oktobra",
"timeslider.month.november": "novembra",
"timeslider.month.december": "decembra",
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: neimenovani autor, plural(num) two: neimenovana autora, plural(num) other: neimenovanih autora ]}",
"pad.savedrevs.marked": "Ova inačica označena je sada kao spremljena",
"pad.savedrevs.timeslider": "Možete pogledati spremljene inačice rabeći vremesledni klizač",
"pad.userlist.entername": "Upišite svoje ime",
"pad.userlist.unnamed": "bez imena",
"pad.userlist.guest": "Gost",
"pad.userlist.deny": "Odbij",
"pad.userlist.approve": "Odobri",
"pad.editbar.clearcolors": "Ukloniti boje autorstva sa cijelog dokumenta?",
"pad.impexp.importbutton": "Uvezi odmah",
"pad.impexp.importing": "Uvozim...",
"pad.impexp.confirmimport": "Uvoženje datoteke presnimit će trenutni sadržaj blokića.\nJeste li sigurni da želite nastaviti?",
"pad.impexp.convertFailed": "Nisam mogao uvesti datoteku. Poslužite se uz neki drugi format ili prekopirajte tekst ručno.",
"pad.impexp.padHasData": "Nismo mogli uvesti ovu datoteku jer je ovaj blokić već ima promjene. Uvezite je u novi blokić.",
"pad.impexp.uploadFailed": "Postavljanje nije uspjelo. Pokušajte ponovo.",
"pad.impexp.importfailed": "Uvoz nije uspio",
"pad.impexp.copypaste": "Prekopirajte",
"pad.impexp.exportdisabled": "Izvoz u formatu {{type}} je onemogućen. Ako želite saznati više o ovome, obratite se administratoru sustava."
}

View file

@ -38,6 +38,7 @@
"pad.share.emebdcode": "امنیڈ یو آر ایل",
"pad.chat": "چیٹ",
"pad.chat.loadmessages": "ٻئے سنیہے لوڈ کرو",
"pad.chat.writeMessage.placeholder": "آپݨاں سنیہا اتھ لکھو",
"timeslider.toolbar.returnbutton": "واپس پیڈ تے ونڄو",
"timeslider.toolbar.authors": "مصنف:",
"timeslider.toolbar.authorsList": "کوئی مصنف کائنی",

View file

@ -90,6 +90,8 @@
"pad.chat": "Klepet",
"pad.chat.title": "Odpri klepetalno okno dokumenta.",
"pad.chat.loadmessages": "Naloži več sporočil",
"pad.chat.stick.title": "Prilepi klepet na zaslon",
"pad.chat.writeMessage.placeholder": "Napišite sporočilo",
"timeslider.pageTitle": "Časovni trak {{appTitle}}",
"timeslider.toolbar.returnbutton": "Vrni se na dokument",
"timeslider.toolbar.authors": "Avtorji:",

View file

@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Besnik b",
"Kosovastar"
"Kosovastar",
"Liridon"
]
},
"index.newPad": "Bllok i ri",
@ -87,6 +88,8 @@
"pad.chat": "Fjalosje",
"pad.chat.title": "Hapni fjalosjen për këtë bllok.",
"pad.chat.loadmessages": "Ngarko më tepër mesazhe",
"pad.chat.stick.title": "Ngjit bisedën në ekran",
"pad.chat.writeMessage.placeholder": "Shkruajeni mesazhin tuaj këtu",
"timeslider.pageTitle": "Rrjedhë kohore e {{appTitle}}",
"timeslider.toolbar.returnbutton": "Rikthehuni te blloku",
"timeslider.toolbar.authors": "Autorë:",

View file

@ -6,7 +6,8 @@
"Милан Јелисавчић",
"Srdjan m",
"Obsuser",
"Acamicamacaraca"
"Acamicamacaraca",
"BadDog"
]
},
"index.newPad": "Нови Пад",
@ -91,6 +92,8 @@
"pad.chat": "Ћаскање",
"pad.chat.title": "Отворите ћаскање за овај пад.",
"pad.chat.loadmessages": "Учитај више порука",
"pad.chat.stick.title": "Залепите ћаскање на екран",
"pad.chat.writeMessage.placeholder": "Напишите поруку овде",
"timeslider.pageTitle": "{{appTitle}} временска линија",
"timeslider.toolbar.returnbutton": "Врати се на пад",
"timeslider.toolbar.authors": "Аутори:",

129
src/locales/sr-el.json Normal file
View file

@ -0,0 +1,129 @@
{
"@metadata": [],
"index.newPad": "Novi Pad",
"index.createOpenPad": "ili napravite/otvorite pad sledećeg naziva:",
"pad.toolbar.bold.title": "Podebljano (Ctrl+B)",
"pad.toolbar.italic.title": "Iskošeno (Ctrl+I)",
"pad.toolbar.underline.title": "Podvučeno (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Precrtano (Ctrl+5)",
"pad.toolbar.ol.title": "Uređen spisak (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Neuređen spisak (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Uvlačenje (TAB)",
"pad.toolbar.unindent.title": "Izvlačenje (Shift+TAB)",
"pad.toolbar.undo.title": "Opozovi (Ctrl+Z)",
"pad.toolbar.redo.title": "Ponovi (Ctrl+Z)",
"pad.toolbar.clearAuthorship.title": "Očisti autorske boje (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Uvezi/izvezi iz/na druge datotečne formate",
"pad.toolbar.timeslider.title": "Vremenska linija",
"pad.toolbar.savedRevision.title": "Sačuvaj verziju",
"pad.toolbar.settings.title": "Podešavanja",
"pad.toolbar.embed.title": "Sačuvaj i ugradi ovaj pad",
"pad.toolbar.showusers.title": "Prikaži korisnike na ovom padu",
"pad.colorpicker.save": "Sačuvaj",
"pad.colorpicker.cancel": "Otkaži",
"pad.loading": "Učitavam…",
"pad.noCookie": "Kolačić nije pronađen. Molimo da uključite kolačiće u vašem pregledavaču!",
"pad.passwordRequired": "Trebate imati lozinku kako biste pristupili ovom padu",
"pad.permissionDenied": "Nemate dozvolu da pristupite ovom padu",
"pad.wrongPassword": "Vaša lozinka nije ispravna",
"pad.settings.padSettings": "Podešavanja pada",
"pad.settings.myView": "Moj prikaz",
"pad.settings.stickychat": "Ćaskanje uvek na ekranu",
"pad.settings.chatandusers": "Prikaži ćaskanje i korisnike",
"pad.settings.colorcheck": "Autorske boje",
"pad.settings.linenocheck": "Brojevi redova",
"pad.settings.rtlcheck": "Čitaj sadržaj s desna na levo?",
"pad.settings.fontType": "Vrsta fonta:",
"pad.settings.fontType.normal": "Normalno",
"pad.settings.fontType.monospaced": "Monospace",
"pad.settings.globalView": "Globalni prikaz",
"pad.settings.language": "Jezik:",
"pad.importExport.import_export": "Uvoz/izvoz",
"pad.importExport.import": "Otpremite bilo koju tekstualnu datoteku ili dokument",
"pad.importExport.importSuccessful": "Uspešno!",
"pad.importExport.export": "Izvezi trenutni pad kao:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Čist tekst",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Jedino možete uvesti sa jednostavnog tekstualnog formata ili HTML formata. Za komplikovanije funkcije o uvozu, molimo da <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalirate AbiWord</a>.",
"pad.modals.connected": "Povezano.",
"pad.modals.reconnecting": "Ponovo se povezujem na vaš pad..",
"pad.modals.forcereconnect": "Prisilno se ponovo poveži",
"pad.modals.reconnecttimer": "Pokušavam se ponovo povezati",
"pad.modals.cancel": "Otkaži",
"pad.modals.userdup": "Otvoreno u drugom prozoru",
"pad.modals.userdup.explanation": "Izgleda da je ovaj pad otvoren u dva ili više prozora na ovom računaru.",
"pad.modals.userdup.advice": "Ponovo se povežite na ovoj prozor.",
"pad.modals.unauth": "Niste ovlašćeni",
"pad.modals.unauth.explanation": "Vaša dopuštenja se se promenila dok ste pregledavali stranicu. Pokušajte se ponovo povezati.",
"pad.modals.looping.explanation": "Postoje komunikacijski problemi sa sinhronizacionim serverom.",
"pad.modals.looping.cause": "Možda ste se povezali preko nepodržanog zaštitnog zida ili proksija.",
"pad.modals.initsocketfail": "Server je nedostupan.",
"pad.modals.initsocketfail.explanation": "Ne mogu se povezati na sinhronizacioni server.",
"pad.modals.initsocketfail.cause": "Najverovatnije je došlo do problem sa vašim pregledačem ili vašom internetskom vezom.",
"pad.modals.slowcommit.explanation": "Server ne odgovara.",
"pad.modals.slowcommit.cause": "Najverovatnije je došlo do problema sa mrežnom povezanošću.",
"pad.modals.badChangeset.explanation": "Sinhronizacioni server je uređivanje koje ste načili označio kao neispravno.",
"pad.modals.badChangeset.cause": "Moguće da je došlo do pogrešne konfiguracije servera ili nekog drugog neočekivanog događaja. Molimo vas da kontaktirate servisnog administratora ako mislite da je ovo greška. Pokušajte se ponovo povezati kako biste nastavili s uređivanjem.",
"pad.modals.corruptPad.explanation": "Pad kojem pokušavate pristupiti je oštećen.",
"pad.modals.corruptPad.cause": "Moguće da je došlo do pogrešne konfiguracije servera ili nekog drugog neočekivanog događaja. Molimo vas da kontaktirate servisnog administratora.",
"pad.modals.deleted": "Obrisano.",
"pad.modals.deleted.explanation": "Ovaj pad je uklonjen.",
"pad.modals.disconnected": "Veza je prekinuta.",
"pad.modals.disconnected.explanation": "Izgubljena je veza sa serverom",
"pad.modals.disconnected.cause": "Server nije dostupan. Obavestite servisnog administratora ako se ovo nastavi dešavati.",
"pad.share": "Pofeli ovaj pad",
"pad.share.readonly": "Samo za čitanje",
"pad.share.link": "Veza",
"pad.share.emebdcode": "Ugradi vezu",
"pad.chat": "Ćaskanje",
"pad.chat.title": "Otvorite ćaskanje za ovaj pad.",
"pad.chat.loadmessages": "Učitaj više poruka",
"pad.chat.stick.title": "Postavi čet na ekran",
"pad.chat.writeMessage.placeholder": "Napiši poruku ovde",
"timeslider.pageTitle": "{{appTitle}} vremenska linija",
"timeslider.toolbar.returnbutton": "Vrati se na pad",
"timeslider.toolbar.authors": "Autori:",
"timeslider.toolbar.authorsList": "Nema autora",
"timeslider.toolbar.exportlink.title": "Izvezi",
"timeslider.exportCurrent": "Izvezi trenutnu verziju kao:",
"timeslider.version": "Izdanje {{version}}",
"timeslider.saved": "Sačuvano na {{day}}. {{month}}. {{year}}",
"timeslider.playPause": "Pusti/pauziraj sadržaj pada",
"timeslider.backRevision": "Idi na prethodnu verziju ovog pada",
"timeslider.forwardRevision": "Idi na sledeće izdanje pada",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "januar",
"timeslider.month.february": "februar",
"timeslider.month.march": "mart",
"timeslider.month.april": "april",
"timeslider.month.may": "maj",
"timeslider.month.june": "jun",
"timeslider.month.july": "jul",
"timeslider.month.august": "avgust",
"timeslider.month.september": "septembar",
"timeslider.month.october": "oktobar",
"timeslider.month.november": "novembar",
"timeslider.month.december": "decembar",
"timeslider.unnamedauthors": "{{num}} neimenovan(i) {[plural(num) one: autor, other: autori ]}",
"pad.savedrevs.marked": "Ova izmena je sada označena kao sačuvana",
"pad.savedrevs.timeslider": "Možete videti sačuvane izmene koristeći se vremenskom linijom",
"pad.userlist.entername": "Upišite svoje ime",
"pad.userlist.unnamed": "neimenovan",
"pad.userlist.guest": "Gost",
"pad.userlist.deny": "Odbij",
"pad.userlist.approve": "Odobri",
"pad.editbar.clearcolors": "Očisti autorske boje za celi dokument?",
"pad.impexp.importbutton": "Uvezi odmah",
"pad.impexp.importing": "Uvozim...",
"pad.impexp.confirmimport": "Uvoz datoteke će prepisati trenutni tekst pada. Da li ste sigurni da želite nastaviti?",
"pad.impexp.convertFailed": "Ne mogu da uvezem ovu datoteku. Molimo da koristite drugi format dokumenta ili da dokument kopirate ručno",
"pad.impexp.padHasData": "Ne mogu da uvezem ovu datoteku zato što je već bilo promena na ovom padu, molimo da uvezete novi pad",
"pad.impexp.uploadFailed": "Nisam uspeo da otpremim, molimo pokušate ponovo",
"pad.impexp.importfailed": "Nisam uspeo da uvezem",
"pad.impexp.copypaste": "Kopirajte i zalepite",
"pad.impexp.exportdisabled": "Izvoz u formatu {{type}} nije dozvoljen. Kontaktirajte sistemskog administratora za detalje."
}

View file

@ -88,6 +88,8 @@
"pad.chat": "Chatt",
"pad.chat.title": "Öppna chatten för detta block.",
"pad.chat.loadmessages": "Läs in fler meddelanden",
"pad.chat.stick.title": "Fäst chatten på skärmen",
"pad.chat.writeMessage.placeholder": "Skriv ditt meddelande här",
"timeslider.pageTitle": "{{appTitle}} tidsreglage",
"timeslider.toolbar.returnbutton": "Återvänd till blocket",
"timeslider.toolbar.authors": "Författare:",

View file

@ -7,7 +7,9 @@
"Meelo",
"Trockya",
"McAang",
"Vito Genovese"
"Vito Genovese",
"Hedda",
"Grkn gll"
]
},
"index.newPad": "Yeni Bloknot",
@ -48,7 +50,7 @@
"pad.settings.fontType.monospaced": "Tek aralıklı",
"pad.settings.globalView": "Genel Görünüm",
"pad.settings.language": "Dil:",
"pad.importExport.import_export": "İçerik/Dışarı Aktar",
"pad.importExport.import_export": "İçeri aktar/Dışarı aktar",
"pad.importExport.import": "Herhangi bir metin dosyası ya da belgesi yükle",
"pad.importExport.importSuccessful": "Başarılı!",
"pad.importExport.export": "Mevcut bloknotu şu olarak dışa aktar:",
@ -92,6 +94,8 @@
"pad.chat": "Sohbet",
"pad.chat.title": "Bu bloknot için sohbeti açın.",
"pad.chat.loadmessages": "Daha fazla mesaj yükle",
"pad.chat.stick.title": "Sohbeti ekrana yapıştır",
"pad.chat.writeMessage.placeholder": "Mesajını buraya yaz",
"timeslider.pageTitle": "{{appTitle}} Zaman Çizelgesi",
"timeslider.toolbar.returnbutton": "Bloknota geri dön",
"timeslider.toolbar.authors": "Yazarlar:",

View file

@ -95,6 +95,8 @@
"pad.chat": "Чат",
"pad.chat.title": "Відкрити чат для цього документа.",
"pad.chat.loadmessages": "Завантажити більше повідомлень",
"pad.chat.stick.title": "Закріпити чат на екрані",
"pad.chat.writeMessage.placeholder": "Напишіть своє повідомлення сюди",
"timeslider.pageTitle": "Часова шкала {{appTitle}}",
"timeslider.toolbar.returnbutton": "Повернутись до документа",
"timeslider.toolbar.authors": "Автори:",

View file

@ -11,7 +11,9 @@
"Yfdyh000",
"乌拉跨氪",
"燃玉",
"JuneAugust"
"JuneAugust",
"94rain",
"VulpesVulpes825"
]
},
"index.newPad": "新记事本",
@ -96,6 +98,8 @@
"pad.chat": "聊天",
"pad.chat.title": "打开此记事本的聊天窗口。",
"pad.chat.loadmessages": "加载更多信息",
"pad.chat.stick.title": "在屏幕上固定聊天界面",
"pad.chat.writeMessage.placeholder": "在此写下您的消息",
"timeslider.pageTitle": "{{appTitle}} 时间轴",
"timeslider.toolbar.returnbutton": "返回记事本",
"timeslider.toolbar.authors": "作者:",

View file

@ -93,6 +93,8 @@
"pad.chat": "聊天功能",
"pad.chat.title": "打開記事本聊天功能",
"pad.chat.loadmessages": "載入更多訊息",
"pad.chat.stick.title": "釘住聊天在螢幕上",
"pad.chat.writeMessage.placeholder": "在此編寫您的訊息",
"timeslider.pageTitle": "{{appTitle}}時間軸",
"timeslider.toolbar.returnbutton": "返回到記事本",
"timeslider.toolbar.authors": "協作者:",

View file

@ -1,7 +1,7 @@
# About the folder structure
* **db** - all modules that are accessing the data structure and are communicating directly to the database
* **handler** - all modules that responds directly to requests/messages of the browser
* **handler** - all modules that respond directly to requests/messages of the browser
* **utils** - helper modules
# Module name conventions

File diff suppressed because it is too large Load diff

View file

@ -18,211 +18,189 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var db = require("./DB");
var customError = require("../utils/customError");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
exports.getColorPalette = function(){
return ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ffa8a8", "#ffe699", "#cfff9e", "#99ffb3", "#a3ffff", "#99b3ff", "#cc99ff", "#ff99e5", "#e7b1b1", "#e9dcAf", "#cde9af", "#bfedcc", "#b1e7e7", "#c3cdee", "#d2b8ea", "#eec3e6", "#e9cece", "#e7e0ca", "#d3e5c7", "#bce1c5", "#c1e2e2", "#c1c9e2", "#cfc1e2", "#e0bdd9", "#baded3", "#a0f8eb", "#b1e7e0", "#c3c8e4", "#cec5e2", "#b1d5e7", "#cda8f0", "#f0f0a8", "#f2f2a6", "#f5a8eb", "#c5f9a9", "#ececbb", "#e7c4bc", "#daf0b2", "#b0a0fd", "#bce2e7", "#cce2bb", "#ec9afe", "#edabbd", "#aeaeea", "#c4e7b1", "#d722bb", "#f3a5e7", "#ffa8a8", "#d8c0c5", "#eaaedd", "#adc6eb", "#bedad1", "#dee9af", "#e9afc2", "#f8d2a0", "#b3b3e6"];
exports.getColorPalette = function() {
return [
"#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1",
"#ffa8a8", "#ffe699", "#cfff9e", "#99ffb3", "#a3ffff", "#99b3ff", "#cc99ff", "#ff99e5",
"#e7b1b1", "#e9dcAf", "#cde9af", "#bfedcc", "#b1e7e7", "#c3cdee", "#d2b8ea", "#eec3e6",
"#e9cece", "#e7e0ca", "#d3e5c7", "#bce1c5", "#c1e2e2", "#c1c9e2", "#cfc1e2", "#e0bdd9",
"#baded3", "#a0f8eb", "#b1e7e0", "#c3c8e4", "#cec5e2", "#b1d5e7", "#cda8f0", "#f0f0a8",
"#f2f2a6", "#f5a8eb", "#c5f9a9", "#ececbb", "#e7c4bc", "#daf0b2", "#b0a0fd", "#bce2e7",
"#cce2bb", "#ec9afe", "#edabbd", "#aeaeea", "#c4e7b1", "#d722bb", "#f3a5e7", "#ffa8a8",
"#d8c0c5", "#eaaedd", "#adc6eb", "#bedad1", "#dee9af", "#e9afc2", "#f8d2a0", "#b3b3e6"
];
};
/**
* Checks if the author exists
*/
exports.doesAuthorExists = function (authorID, callback)
exports.doesAuthorExist = async function(authorID)
{
//check if the database entry of this author exists
db.get("globalAuthor:" + authorID, function (err, author)
{
if(ERR(err, callback)) return;
callback(null, author != null);
});
let author = await db.get("globalAuthor:" + authorID);
return author !== null;
}
/* exported for backwards compatibility */
exports.doesAuthorExists = exports.doesAuthorExist;
/**
* Returns the AuthorID for a token.
* @param {String} token The token
* @param {Function} callback callback (err, author)
*/
exports.getAuthor4Token = function (token, callback)
exports.getAuthor4Token = async function(token)
{
mapAuthorWithDBKey("token2author", token, function(err, author)
{
if(ERR(err, callback)) return;
//return only the sub value authorID
callback(null, author ? author.authorID : author);
});
let author = await mapAuthorWithDBKey("token2author", token);
// return only the sub value authorID
return author ? author.authorID : author;
}
/**
* Returns the AuthorID for a mapper.
* @param {String} token The mapper
* @param {String} name The name of the author (optional)
* @param {Function} callback callback (err, author)
*/
exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback)
exports.createAuthorIfNotExistsFor = async function(authorMapper, name)
{
mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author)
{
if(ERR(err, callback)) return;
let author = await mapAuthorWithDBKey("mapper2author", authorMapper);
//set the name of this author
if(name)
exports.setAuthorName(author.authorID, name);
if (name) {
// set the name of this author
await exports.setAuthorName(author.authorID, name);
}
//return the authorID
callback(null, author);
});
}
return author;
};
/**
* Returns the AuthorID for a mapper. We can map using a mapperkey,
* so far this is token2author and mapper2author
* @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper
* @param {Function} callback callback (err, author)
*/
function mapAuthorWithDBKey (mapperkey, mapper, callback)
async function mapAuthorWithDBKey (mapperkey, mapper)
{
//try to map to an author
db.get(mapperkey + ":" + mapper, function (err, author)
{
if(ERR(err, callback)) return;
// try to map to an author
let author = await db.get(mapperkey + ":" + mapper);
//there is no author with this mapper, so create one
if(author == null)
{
exports.createAuthor(null, function(err, author)
{
if(ERR(err, callback)) return;
if (author === null) {
// there is no author with this mapper, so create one
let author = await exports.createAuthor(null);
//create the token2author relation
db.set(mapperkey + ":" + mapper, author.authorID);
// create the token2author relation
await db.set(mapperkey + ":" + mapper, author.authorID);
//return the author
callback(null, author);
});
// return the author
return author;
}
return;
}
// there is an author with this mapper
// update the timestamp of this author
await db.setSub("globalAuthor:" + author, ["timestamp"], Date.now());
//there is a author with this mapper
//update the timestamp of this author
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
//return the author
callback(null, {authorID: author});
});
// return the author
return { authorID: author};
}
/**
* Internal function that creates the database entry for an author
* @param {String} name The name of the author
*/
exports.createAuthor = function(name, callback)
exports.createAuthor = function(name)
{
//create the new author name
var author = "a." + randomString(16);
// create the new author name
let author = "a." + randomString(16);
//create the globalAuthors db entry
var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": new Date().getTime()};
// create the globalAuthors db entry
let authorObj = {
"colorId": Math.floor(Math.random() * (exports.getColorPalette().length)),
"name": name,
"timestamp": Date.now()
};
//set the global author db entry
// set the global author db entry
// NB: no await, since we're not waiting for the DB set to finish
db.set("globalAuthor:" + author, authorObj);
callback(null, {authorID: author});
return { authorID: author };
}
/**
* Returns the Author Obj of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, authorObj)
*/
exports.getAuthor = function (author, callback)
exports.getAuthor = function(author)
{
db.get("globalAuthor:" + author, callback);
// NB: result is already a Promise
return db.get("globalAuthor:" + author);
}
/**
* Returns the color Id of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, colorId)
*/
exports.getAuthorColorId = function (author, callback)
exports.getAuthorColorId = function(author)
{
db.getSub("globalAuthor:" + author, ["colorId"], callback);
return db.getSub("globalAuthor:" + author, ["colorId"]);
}
/**
* Sets the color Id of the author
* @param {String} author The id of the author
* @param {String} colorId The color id of the author
* @param {Function} callback (optional)
*/
exports.setAuthorColorId = function (author, colorId, callback)
exports.setAuthorColorId = function(author, colorId)
{
db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback);
return db.setSub("globalAuthor:" + author, ["colorId"], colorId);
}
/**
* Returns the name of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, name)
*/
exports.getAuthorName = function (author, callback)
exports.getAuthorName = function(author)
{
db.getSub("globalAuthor:" + author, ["name"], callback);
return db.getSub("globalAuthor:" + author, ["name"]);
}
/**
* Sets the name of the author
* @param {String} author The id of the author
* @param {String} name The name of the author
* @param {Function} callback (optional)
*/
exports.setAuthorName = function (author, name, callback)
exports.setAuthorName = function(author, name)
{
db.setSub("globalAuthor:" + author, ["name"], name, callback);
return db.setSub("globalAuthor:" + author, ["name"], name);
}
/**
* Returns an array of all pads this author contributed to
* @param {String} author The id of the author
* @param {Function} callback (optional)
*/
exports.listPadsOfAuthor = function (authorID, callback)
exports.listPadsOfAuthor = async function(authorID)
{
/* There are two other places where this array is manipulated:
* (1) When the author is added to a pad, the author object is also updated
* (2) When a pad is deleted, each author of that pad is also updated
*/
//get the globalAuthor
db.get("globalAuthor:" + authorID, function(err, author)
{
if(ERR(err, callback)) return;
//author does not exists
if(author == null)
{
callback(new customError("authorID does not exist","apierror"))
return;
}
// get the globalAuthor
let author = await db.get("globalAuthor:" + authorID);
//everything is fine, return the pad IDs
var pads = [];
if(author.padIDs != null)
{
for (var padId in author.padIDs)
{
pads.push(padId);
}
}
callback(null, {padIDs: pads});
});
if (author === null) {
// author does not exist
throw new customError("authorID does not exist", "apierror");
}
// everything is fine, return the pad IDs
let padIDs = Object.keys(author.padIDs || {});
return { padIDs };
}
/**
@ -230,26 +208,27 @@ exports.listPadsOfAuthor = function (authorID, callback)
* @param {String} author The id of the author
* @param {String} padID The id of the pad the author contributes to
*/
exports.addPad = function (authorID, padID)
exports.addPad = async function(authorID, padID)
{
//get the entry
db.get("globalAuthor:" + authorID, function(err, author)
{
if(ERR(err)) return;
if(author == null) return;
// get the entry
let author = await db.get("globalAuthor:" + authorID);
//the entry doesn't exist so far, let's create it
if(author.padIDs == null)
{
author.padIDs = {};
}
if (author === null) return;
//add the entry for this pad
author.padIDs[padID] = 1;// anything, because value is not used
/*
* ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
* to perform a strict check here
*/
if (!author.padIDs) {
// the entry doesn't exist so far, let's create it
author.padIDs = {};
}
//save the new element back
db.set("globalAuthor:" + authorID, author);
});
// add the entry for this pad
author.padIDs[padID] = 1; // anything, because value is not used
// save the new element back
db.set("globalAuthor:" + authorID, author);
}
/**
@ -257,18 +236,15 @@ exports.addPad = function (authorID, padID)
* @param {String} author The id of the author
* @param {String} padID The id of the pad the author contributes to
*/
exports.removePad = function (authorID, padID)
exports.removePad = async function(authorID, padID)
{
db.get("globalAuthor:" + authorID, function (err, author)
{
if(ERR(err)) return;
if(author == null) return;
let author = await db.get("globalAuthor:" + authorID);
if(author.padIDs != null)
{
//remove pad from author
delete author.padIDs[padID];
db.set("globalAuthor:" + authorID, author);
}
});
if (author === null) return;
if (author.padIDs !== null) {
// remove pad from author
delete author.padIDs[padID];
db.set("globalAuthor:" + authorID, author);
}
}

View file

@ -1,5 +1,5 @@
/**
* The DB Module provides a database initalized with the settings
* The DB Module provides a database initalized with the settings
* provided by the settings module
*/
@ -22,9 +22,10 @@
var ueberDB = require("ueberdb2");
var settings = require("../utils/Settings");
var log4js = require('log4js');
const util = require("util");
//set database settings
var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
// set database settings
let db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
/**
* The UeberDB Object that provides the database functions
@ -33,25 +34,40 @@ exports.db = null;
/**
* Initalizes the database with the settings provided by the settings module
* @param {Function} callback
* @param {Function} callback
*/
exports.init = function(callback)
{
//initalize the database async
db.init(function(err)
{
//there was an error while initializing the database, output it and stop
if(err)
{
console.error("ERROR: Problem while initalizing the database");
console.error(err.stack ? err.stack : err);
process.exit(1);
}
//everything ok
else
{
exports.db = db;
callback(null);
}
exports.init = function() {
// initalize the database async
return new Promise((resolve, reject) => {
db.init(function(err) {
if (err) {
// there was an error while initializing the database, output it and stop
console.error("ERROR: Problem while initalizing the database");
console.error(err.stack ? err.stack : err);
process.exit(1);
} else {
// everything ok, set up Promise-based methods
['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove', 'doShutdown'].forEach(fn => {
exports[fn] = util.promisify(db[fn].bind(db));
});
// set up wrappers for get and getSub that can't return "undefined"
let get = exports.get;
exports.get = async function(key) {
let result = await get(key);
return (result === undefined) ? null : result;
};
let getSub = exports.getSub;
exports.getSub = async function(key, sub) {
let result = await getSub(key, sub);
return (result === undefined) ? null : result;
};
// exposed for those callers that need the underlying raw API
exports.db = db;
resolve();
}
});
});
}

View file

@ -17,319 +17,167 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var db = require("./DB");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
exports.listAllGroups = function(callback) {
db.get("groups", function (err, groups) {
if(ERR(err, callback)) return;
// there are no groups
if(groups == null) {
callback(null, {groupIDs: []});
return;
}
var groupIDs = [];
for ( var groupID in groups ) {
groupIDs.push(groupID);
}
callback(null, {groupIDs: groupIDs});
});
}
exports.deleteGroup = function(groupID, callback)
exports.listAllGroups = async function()
{
var group;
let groups = await db.get("groups");
groups = groups || {};
async.series([
//ensure group exists
function (callback)
{
//try to get the group entry
db.get("group:" + groupID, function (err, _group)
{
if(ERR(err, callback)) return;
//group does not exist
if(_group == null)
{
callback(new customError("groupID does not exist","apierror"));
return;
}
//group exists, everything is fine
group = _group;
callback();
});
},
//iterate trough all pads of this groups and delete them
function(callback)
{
//collect all padIDs in an array, that allows us to use async.forEach
var padIDs = [];
for(var i in group.pads)
{
padIDs.push(i);
}
//loop trough all pads and delete them
async.forEach(padIDs, function(padID, callback)
{
padManager.getPad(padID, function(err, pad)
{
if(ERR(err, callback)) return;
pad.remove(callback);
});
}, callback);
},
//iterate trough group2sessions and delete all sessions
function(callback)
{
//try to get the group entry
db.get("group2sessions:" + groupID, function (err, group2sessions)
{
if(ERR(err, callback)) return;
//skip if there is no group2sessions entry
if(group2sessions == null) {callback(); return}
//collect all sessions in an array, that allows us to use async.forEach
var sessions = [];
for(var i in group2sessions.sessionsIDs)
{
sessions.push(i);
}
//loop trough all sessions and delete them
async.forEach(sessions, function(session, callback)
{
sessionManager.deleteSession(session, callback);
}, callback);
});
},
//remove group and group2sessions entry
function(callback)
{
db.remove("group2sessions:" + groupID);
db.remove("group:" + groupID);
callback();
},
//unlist the group
function(callback)
{
exports.listAllGroups(function(err, groups) {
if(ERR(err, callback)) return;
groups = groups? groups.groupIDs : [];
// it's not listed
if(groups.indexOf(groupID) == -1) {
callback();
return;
}
groups.splice(groups.indexOf(groupID), 1);
// store empty groupe list
if(groups.length == 0) {
db.set("groups", {});
callback();
return;
}
// regenerate group list
var newGroups = {};
async.forEach(groups, function(group, cb) {
newGroups[group] = 1;
cb();
},function() {
db.set("groups", newGroups);
callback();
});
});
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
});
}
exports.doesGroupExist = function(groupID, callback)
{
//try to get the group entry
db.get("group:" + groupID, function (err, group)
{
if(ERR(err, callback)) return;
callback(null, group != null);
});
let groupIDs = Object.keys(groups);
return { groupIDs };
}
exports.createGroup = function(callback)
exports.deleteGroup = async function(groupID)
{
//search for non existing groupID
var groupID = "g." + randomString(16);
//create the group
db.set("group:" + groupID, {pads: {}});
//list the group
exports.listAllGroups(function(err, groups) {
if(ERR(err, callback)) return;
groups = groups? groups.groupIDs : [];
groups.push(groupID);
// regenerate group list
var newGroups = {};
async.forEach(groups, function(group, cb) {
newGroups[group] = 1;
cb();
},function() {
db.set("groups", newGroups);
callback(null, {groupID: groupID});
});
});
}
let group = await db.get("group:" + groupID);
// ensure group exists
if (group == null) {
// group does not exist
throw new customError("groupID does not exist", "apierror");
}
// iterate through all pads of this group and delete them (in parallel)
await Promise.all(Object.keys(group.pads).map(padID => {
return padManager.getPad(padID).then(pad => pad.remove());
}));
// iterate through group2sessions and delete all sessions
let group2sessions = await db.get("group2sessions:" + groupID);
let sessions = group2sessions ? group2sessions.sessionsIDs : {};
// loop through all sessions and delete them (in parallel)
await Promise.all(Object.keys(sessions).map(session => {
return sessionManager.deleteSession(session);
}));
// remove group and group2sessions entry
await db.remove("group2sessions:" + groupID);
await db.remove("group:" + groupID);
// unlist the group
let groups = await exports.listAllGroups();
groups = groups ? groups.groupIDs : [];
let index = groups.indexOf(groupID);
if (index === -1) {
// it's not listed
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
//ensure mapper is optional
if(typeof groupMapper != "string")
{
callback(new customError("groupMapper is no string","apierror"));
return;
}
//try to get a group for this mapper
db.get("mapper2group:"+groupMapper, function(err, groupID)
{
function createGroupForMapper(cb) {
exports.createGroup(function(err, responseObj)
{
if(ERR(err, cb)) return;
//create the mapper entry for this group
db.set("mapper2group:"+groupMapper, responseObj.groupID);
cb(null, responseObj);
});
}
if(ERR(err, callback)) return;
// remove from the list
groups.splice(index, 1);
// regenerate group list
var newGroups = {};
groups.forEach(group => newGroups[group] = 1);
await db.set("groups", newGroups);
}
exports.doesGroupExist = async function(groupID)
{
// try to get the group entry
let group = await db.get("group:" + groupID);
return (group != null);
}
exports.createGroup = async function()
{
// search for non existing groupID
var groupID = "g." + randomString(16);
// create the group
await db.set("group:" + groupID, {pads: {}});
// list the group
let groups = await exports.listAllGroups();
groups = groups? groups.groupIDs : [];
groups.push(groupID);
// regenerate group list
var newGroups = {};
groups.forEach(group => newGroups[group] = 1);
await db.set("groups", newGroups);
return { groupID };
}
exports.createGroupIfNotExistsFor = async function(groupMapper)
{
// ensure mapper is optional
if (typeof groupMapper !== "string") {
throw new customError("groupMapper is not a string", "apierror");
}
// try to get a group for this mapper
let groupID = await db.get("mapper2group:" + groupMapper);
if (groupID) {
// there is a group for this mapper
if(groupID) {
exports.doesGroupExist(groupID, function(err, exists) {
if(ERR(err, callback)) return;
if(exists) return callback(null, {groupID: groupID});
let exists = await exports.doesGroupExist(groupID);
// hah, the returned group doesn't exist, let's create one
createGroupForMapper(callback)
})
if (exists) return { groupID };
}
return;
}
// hah, the returned group doesn't exist, let's create one
let result = await exports.createGroup();
//there is no group for this mapper, let's create a group
createGroupForMapper(callback)
});
// create the mapper entry for this group
await db.set("mapper2group:" + groupMapper, result.groupID);
return result;
}
exports.createGroupPad = function(groupID, padName, text, callback)
exports.createGroupPad = async function(groupID, padName, text)
{
//create the padID
var padID = groupID + "$" + padName;
// create the padID
let padID = groupID + "$" + padName;
async.series([
//ensure group exists
function (callback)
{
exports.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
return;
}
// ensure group exists
let groupExists = await exports.doesGroupExist(groupID);
//group exists, everything is fine
callback();
});
},
//ensure pad does not exists
function (callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
//pad exists already
if(exists == true)
{
callback(new customError("padName does already exist","apierror"));
return;
}
if (!groupExists) {
throw new customError("groupID does not exist", "apierror");
}
//pad does not exist, everything is fine
callback();
});
},
//create the pad
function (callback)
{
padManager.getPad(padID, text, function(err)
{
if(ERR(err, callback)) return;
callback();
});
},
//create an entry in the group for this pad
function (callback)
{
db.setSub("group:" + groupID, ["pads", padID], 1);
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, {padID: padID});
});
// ensure pad doesn't exist already
let padExists = await padManager.doesPadExists(padID);
if (padExists) {
// pad exists already
throw new customError("padName does already exist", "apierror");
}
// create the pad
await padManager.getPad(padID, text);
//create an entry in the group for this pad
await db.setSub("group:" + groupID, ["pads", padID], 1);
return { padID };
}
exports.listPads = function(groupID, callback)
exports.listPads = async function(groupID)
{
exports.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
return;
}
let exists = await exports.doesGroupExist(groupID);
//group exists, let's get the pads
db.getSub("group:" + groupID, ["pads"], function(err, result)
{
if(ERR(err, callback)) return;
var pads = [];
for ( var padId in result ) {
pads.push(padId);
}
callback(null, {padIDs: pads});
});
});
// ensure the group exists
if (!exists) {
throw new customError("groupID does not exist", "apierror");
}
// group exists, let's get the pads
let result = await db.getSub("group:" + groupID, ["pads"]);
let padIDs = Object.keys(result);
return { padIDs };
}

View file

@ -3,11 +3,9 @@
*/
var ERR = require("async-stacktrace");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
var db = require("./DB").db;
var async = require("async");
var db = require("./DB");
var settings = require('../utils/Settings');
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
@ -19,7 +17,7 @@ var crypto = require("crypto");
var randomString = require("../utils/randomstring");
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
//serialization/deserialization attributes
// serialization/deserialization attributes
var attributeBlackList = ["id"];
var jsonableList = ["pool"];
@ -32,8 +30,7 @@ exports.cleanText = function (txt) {
};
var Pad = function Pad(id) {
let Pad = function Pad(id) {
this.atext = Changeset.makeAText("\n");
this.pool = new AttributePool();
this.head = -1;
@ -60,7 +57,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() {
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
var savedRev = new Array();
for(var rev in this.savedRevisions){
for (var rev in this.savedRevisions) {
savedRev.push(this.savedRevisions[rev].revNum);
}
savedRev.sort(function(a, b) {
@ -74,8 +71,9 @@ Pad.prototype.getPublicStatus = function getPublicStatus() {
};
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
if(!author)
if (!author) {
author = '';
}
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
Changeset.copyAText(newAText, this.atext);
@ -86,23 +84,24 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
newRevData.changeset = aChangeset;
newRevData.meta = {};
newRevData.meta.author = author;
newRevData.meta.timestamp = new Date().getTime();
newRevData.meta.timestamp = Date.now();
//ex. getNumForAuthor
if(author != '')
// ex. getNumForAuthor
if (author != '') {
this.pool.putAttrib(['author', author || '']);
}
if(newRev % 100 == 0)
{
if (newRev % 100 == 0) {
newRevData.meta.atext = this.atext;
}
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
db.set("pad:" + this.id + ":revs:" + newRev, newRevData);
this.saveToDatabase();
// set the author to pad
if(author)
if (author) {
authorManager.addPad(author, this.id);
}
if (this.head == 0) {
hooks.callAll("padCreate", {'pad':this, 'author': author});
@ -111,49 +110,47 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
}
};
//save all attributes to the database
Pad.prototype.saveToDatabase = function saveToDatabase(){
// save all attributes to the database
Pad.prototype.saveToDatabase = function saveToDatabase() {
var dbObject = {};
for(var attr in this){
if(typeof this[attr] === "function") continue;
if(attributeBlackList.indexOf(attr) !== -1) continue;
for (var attr in this) {
if (typeof this[attr] === "function") continue;
if (attributeBlackList.indexOf(attr) !== -1) continue;
dbObject[attr] = this[attr];
if(jsonableList.indexOf(attr) !== -1){
if (jsonableList.indexOf(attr) !== -1) {
dbObject[attr] = dbObject[attr].toJsonable();
}
}
db.set("pad:"+this.id, dbObject);
db.set("pad:" + this.id, dbObject);
}
// get time of last edit (changeset application)
Pad.prototype.getLastEdit = function getLastEdit(callback){
Pad.prototype.getLastEdit = function getLastEdit() {
var revNum = this.getHeadRevisionNumber();
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]);
}
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
};
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum) {
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"]);
}
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
};
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum) {
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"]);
}
Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
};
Pad.prototype.getRevisionDate = function getRevisionDate(revNum) {
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]);
}
Pad.prototype.getAllAuthors = function getAllAuthors() {
var authors = [];
for(var key in this.pool.numToAttrib)
{
if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
{
for(var key in this.pool.numToAttrib) {
if (this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") {
authors.push(this.pool.numToAttrib[key][1]);
}
}
@ -161,120 +158,77 @@ Pad.prototype.getAllAuthors = function getAllAuthors() {
return authors;
};
Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) {
var _this = this;
Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText(targetRev) {
let keyRev = this.getKeyRevisionNumber(targetRev);
var keyRev = this.getKeyRevisionNumber(targetRev);
var atext;
var changesets = [];
//find out which changesets are needed
var neededChangesets = [];
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
neededChangesets.push(curRev);
// find out which changesets are needed
let neededChangesets = [];
for (let curRev = keyRev; curRev < targetRev; ) {
neededChangesets.push(++curRev);
}
async.series([
//get all needed data out of the database
function(callback)
{
async.parallel([
//get the atext of the key revision
function (callback)
{
db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext)
{
if(ERR(err, callback)) return;
try {
atext = Changeset.cloneAText(_atext);
} catch (e) {
return callback(e);
}
// get all needed data out of the database
callback();
});
},
//get all needed changesets
function (callback)
{
async.forEach(neededChangesets, function(item, callback)
{
_this.getRevisionChangeset(item, function(err, changeset)
{
if(ERR(err, callback)) return;
changesets[item] = changeset;
callback();
});
}, callback);
}
], callback);
},
//apply all changesets to the key changeset
function(callback)
{
var apool = _this.apool();
var curRev = keyRev;
// start to get the atext of the key revision
let p_atext = db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]);
while (curRev < targetRev)
{
curRev++;
var cs = changesets[curRev];
try{
atext = Changeset.applyToAText(cs, atext, apool);
}catch(e) {
return callback(e)
}
}
callback(null);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, atext);
});
};
Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) {
db.get("pad:"+this.id+":revs:"+revNum, callback);
};
Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback){
var authors = this.getAllAuthors();
var returnTable = {};
var colorPalette = authorManager.getColorPalette();
async.forEach(authors, function(author, callback){
authorManager.getAuthorColorId(author, function(err, colorId){
if(err){
return callback(err);
}
//colorId might be a hex color or an number out of the palette
returnTable[author]=colorPalette[colorId] || colorId;
callback();
// get all needed changesets
let changesets = [];
await Promise.all(neededChangesets.map(item => {
return this.getRevisionChangeset(item).then(changeset => {
changesets[item] = changeset;
});
}, function(err){
callback(err, returnTable);
});
};
}));
// we should have the atext by now
let atext = await p_atext;
atext = Changeset.cloneAText(atext);
// apply all changesets to the key changeset
let apool = this.apool();
for (let curRev = keyRev; curRev < targetRev; ) {
let cs = changesets[++curRev];
atext = Changeset.applyToAText(cs, atext, apool);
}
return atext;
}
Pad.prototype.getRevision = function getRevisionChangeset(revNum) {
return db.get("pad:" + this.id + ":revs:" + revNum);
}
Pad.prototype.getAllAuthorColors = async function getAllAuthorColors() {
let authors = this.getAllAuthors();
let returnTable = {};
let colorPalette = authorManager.getColorPalette();
await Promise.all(authors.map(author => {
return authorManager.getAuthorColorId(author).then(colorId => {
// colorId might be a hex color or an number out of the palette
returnTable[author] = colorPalette[colorId] || colorId;
});
}));
return returnTable;
}
Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) {
startRev = parseInt(startRev, 10);
var head = this.getHeadRevisionNumber();
endRev = endRev ? parseInt(endRev, 10) : head;
if(isNaN(startRev) || startRev < 0 || startRev > head) {
if (isNaN(startRev) || startRev < 0 || startRev > head) {
startRev = null;
}
if(isNaN(endRev) || endRev < startRev) {
if (isNaN(endRev) || endRev < startRev) {
endRev = null;
} else if(endRev > head) {
} else if (endRev > head) {
endRev = head;
}
if(startRev !== null && endRev !== null) {
if (startRev !== null && endRev !== null) {
return { startRev: startRev , endRev: endRev }
}
return null;
@ -289,12 +243,12 @@ Pad.prototype.text = function text() {
};
Pad.prototype.setText = function setText(newText) {
//clean the new text
// clean the new text
newText = exports.cleanText(newText);
var oldText = this.text();
//create the changeset
// create the changeset
// We want to ensure the pad still ends with a \n, but otherwise keep
// getText() and setText() consistent.
var changeset;
@ -304,165 +258,112 @@ Pad.prototype.setText = function setText(newText) {
changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
}
//append the changeset
// append the changeset
this.appendRevision(changeset);
};
Pad.prototype.appendText = function appendText(newText) {
//clean the new text
// clean the new text
newText = exports.cleanText(newText);
var oldText = this.text();
//create the changeset
// create the changeset
var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText);
//append the changeset
// append the changeset
this.appendRevision(changeset);
};
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
this.chatHead++;
//save the chat entry in the database
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
// save the chat entry in the database
db.set("pad:" + this.id + ":chat:" + this.chatHead, { "text": text, "userId": userId, "time": time });
this.saveToDatabase();
};
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
var _this = this;
var entry;
Pad.prototype.getChatMessage = async function getChatMessage(entryNum) {
// get the chat entry
let entry = await db.get("pad:" + this.id + ":chat:" + entryNum);
async.series([
//get the chat entry
function(callback)
{
db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry)
{
if(ERR(err, callback)) return;
entry = _entry;
callback();
});
},
//add the authorName
function(callback)
{
//this chat message doesn't exist, return null
if(entry == null)
{
callback();
return;
}
//get the authorName
authorManager.getAuthorName(entry.userId, function(err, authorName)
{
if(ERR(err, callback)) return;
entry.userName = authorName;
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, entry);
});
};
Pad.prototype.getChatMessages = function getChatMessages(start, end, callback) {
//collect the numbers of chat entries and in which order we need them
var neededEntries = [];
var order = 0;
for(var i=start;i<=end; i++)
{
neededEntries.push({entryNum:i, order: order});
order++;
// get the authorName if the entry exists
if (entry != null) {
entry.userName = await authorManager.getAuthorName(entry.userId);
}
var _this = this;
//get all entries out of the database
var entries = [];
async.forEach(neededEntries, function(entryObject, callback)
{
_this.getChatMessage(entryObject.entryNum, function(err, entry)
{
if(ERR(err, callback)) return;
entries[entryObject.order] = entry;
callback();
});
}, function(err)
{
if(ERR(err, callback)) return;
//sort out broken chat entries
//it looks like in happend in the past that the chat head was
//incremented, but the chat message wasn't added
var cleanedEntries = [];
for(var i=0;i<entries.length;i++)
{
if(entries[i]!=null)
cleanedEntries.push(entries[i]);
else
console.warn("WARNING: Found broken chat entry in pad " + _this.id);
}
callback(null, cleanedEntries);
});
return entry;
};
Pad.prototype.init = function init(text, callback) {
var _this = this;
Pad.prototype.getChatMessages = async function getChatMessages(start, end) {
//replace text with default text if text isn't set
if(text == null)
{
// collect the numbers of chat entries and in which order we need them
let neededEntries = [];
for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) {
neededEntries.push({ entryNum, order });
}
// get all entries out of the database
let entries = [];
await Promise.all(neededEntries.map(entryObject => {
return this.getChatMessage(entryObject.entryNum).then(entry => {
entries[entryObject.order] = entry;
});
}));
// sort out broken chat entries
// it looks like in happened in the past that the chat head was
// incremented, but the chat message wasn't added
let cleanedEntries = entries.filter(entry => {
let pass = (entry != null);
if (!pass) {
console.warn("WARNING: Found broken chat entry in pad " + this.id);
}
return pass;
});
return cleanedEntries;
}
Pad.prototype.init = async function init(text) {
// replace text with default text if text isn't set
if (text == null) {
text = settings.defaultPadText;
}
//try to load the pad
db.get("pad:"+this.id, function(err, value)
{
if(ERR(err, callback)) return;
// try to load the pad
let value = await db.get("pad:" + this.id);
//if this pad exists, load it
if(value != null)
{
//copy all attr. To a transfrom via fromJsonable if necassary
for(var attr in value){
if(jsonableList.indexOf(attr) !== -1){
_this[attr] = _this[attr].fromJsonable(value[attr]);
} else {
_this[attr] = value[attr];
}
// if this pad exists, load it
if (value != null) {
// copy all attr. To a transfrom via fromJsonable if necassary
for (var attr in value) {
if (jsonableList.indexOf(attr) !== -1) {
this[attr] = this[attr].fromJsonable(value[attr]);
} else {
this[attr] = value[attr];
}
}
//this pad doesn't exist, so create it
else
{
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
} else {
// this pad doesn't exist, so create it
let firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
_this.appendRevision(firstChangeset, '');
}
hooks.callAll("padLoad", {'pad':_this});
callback(null);
});
};
Pad.prototype.copy = function copy(destinationID, force, callback) {
var sourceID = this.id;
var _this = this;
var destGroupID;
// make force optional
if (typeof force == "function") {
callback = force;
force = false;
this.appendRevision(firstChangeset, '');
}
else if (force == undefined || force.toLowerCase() != "true") {
force = false;
hooks.callAll("padLoad", { 'pad': this });
}
Pad.prototype.copy = async function copy(destinationID, force) {
let sourceID = this.id;
// allow force to be a string
if (typeof force === "string") {
force = (force.toLowerCase() === "true");
} else {
force = !!force;
}
else force = true;
// Kick everyone from this pad.
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
@ -470,247 +371,137 @@ Pad.prototype.copy = function copy(destinationID, force, callback) {
// padMessageHandler.kickSessionsFromPad(sourceID);
// flush the source pad:
_this.saveToDatabase();
this.saveToDatabase();
async.series([
// if it's a group pad, let's make sure the group exists.
function(callback)
{
if (destinationID.indexOf("$") === -1)
{
callback();
return;
}
// if it's a group pad, let's make sure the group exists.
let destGroupID;
if (destinationID.indexOf("$") >= 0) {
destGroupID = destinationID.split("$")[0]
groupManager.doesGroupExist(destGroupID, function (err, exists)
{
if(ERR(err, callback)) return;
destGroupID = destinationID.split("$")[0]
let groupExists = await groupManager.doesGroupExist(destGroupID);
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist for destinationID","apierror"));
return;
}
//everything is fine, continue
callback();
});
},
// if the pad exists, we should abort, unless forced.
function(callback)
{
padManager.doesPadExists(destinationID, function (err, exists)
{
if(ERR(err, callback)) return;
/*
* this is the negation of a truthy comparison. Has been left in this
* wonky state to keep the old (possibly buggy) behaviour
*/
if (!(exists == true))
{
callback();
return;
}
if (!force)
{
console.error("erroring out without force");
callback(new customError("destinationID already exists","apierror"));
console.error("erroring out without force - after");
return;
}
// exists and forcing
padManager.getPad(destinationID, function(err, pad) {
if (ERR(err, callback)) return;
pad.remove(callback);
});
});
},
// copy the 'pad' entry
function(callback)
{
db.get("pad:"+sourceID, function(err, pad) {
db.set("pad:"+destinationID, pad);
});
callback();
},
//copy all relations
function(callback)
{
async.parallel([
//copy all chat messages
function(callback)
{
var chatHead = _this.chatHead;
for(var i=0;i<=chatHead;i++)
{
db.get("pad:"+sourceID+":chat:"+i, function (err, chat) {
if (ERR(err, callback)) return;
db.set("pad:"+destinationID+":chat:"+i, chat);
});
}
callback();
},
//copy all revisions
function(callback)
{
var revHead = _this.head;
for(var i=0;i<=revHead;i++)
{
db.get("pad:"+sourceID+":revs:"+i, function (err, rev) {
if (ERR(err, callback)) return;
db.set("pad:"+destinationID+":revs:"+i, rev);
});
}
callback();
},
//add the new pad to all authors who contributed to the old one
function(callback)
{
var authorIDs = _this.getAllAuthors();
authorIDs.forEach(function (authorID)
{
authorManager.addPad(authorID, destinationID);
});
callback();
},
// parallel
], callback);
},
function(callback) {
// Group pad? Add it to the group's list
if(destGroupID) db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
// Initialize the new pad (will update the listAllPads cache)
setTimeout(function(){
padManager.getPad(destinationID, null, callback) // this runs too early.
},10);
},
// let the plugins know the pad was copied
function(callback) {
hooks.callAll('padCopy', { 'originalPad': _this, 'destinationID': destinationID });
callback();
// group does not exist
if (!groupExists) {
throw new customError("groupID does not exist for destinationID", "apierror");
}
// series
], function(err)
{
if(ERR(err, callback)) return;
callback(null, {padID: destinationID});
}
// if the pad exists, we should abort, unless forced.
let exists = await padManager.doesPadExist(destinationID);
if (exists) {
if (!force) {
console.error("erroring out without force");
throw new customError("destinationID already exists", "apierror");
}
// exists and forcing
let pad = await padManager.getPad(destinationID);
await pad.remove();
}
// copy the 'pad' entry
let pad = await db.get("pad:" + sourceID);
db.set("pad:" + destinationID, pad);
// copy all relations in parallel
let promises = [];
// copy all chat messages
let chatHead = this.chatHead;
for (let i = 0; i <= chatHead; ++i) {
let p = db.get("pad:" + sourceID + ":chat:" + i).then(chat => {
return db.set("pad:" + destinationID + ":chat:" + i, chat);
});
promises.push(p);
}
// copy all revisions
let revHead = this.head;
for (let i = 0; i <= revHead; ++i) {
let p = db.get("pad:" + sourceID + ":revs:" + i).then(rev => {
return db.set("pad:" + destinationID + ":revs:" + i, rev);
});
promises.push(p);
}
// add the new pad to all authors who contributed to the old one
this.getAllAuthors().forEach(authorID => {
authorManager.addPad(authorID, destinationID);
});
};
Pad.prototype.remove = function remove(callback) {
// wait for the above to complete
await Promise.all(promises);
// Group pad? Add it to the group's list
if (destGroupID) {
await db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
}
// delay still necessary?
await new Promise(resolve => setTimeout(resolve, 10));
// Initialize the new pad (will update the listAllPads cache)
await padManager.getPad(destinationID, null); // this runs too early.
// let the plugins know the pad was copied
hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID });
return { padID: destinationID };
}
Pad.prototype.remove = async function remove() {
var padID = this.id;
var _this = this;
//kick everyone from this pad
// kick everyone from this pad
padMessageHandler.kickSessionsFromPad(padID);
async.series([
//delete all relations
function(callback)
{
async.parallel([
//is it a group pad? -> delete the entry of this pad in the group
function(callback)
{
if(padID.indexOf("$") === -1)
{
// it isn't a group pad, nothing to do here
callback();
return;
}
// delete all relations - the original code used async.parallel but
// none of the operations except getting the group depended on callbacks
// so the database operations here are just started and then left to
// run to completion
// it is a group pad
var groupID = padID.substring(0,padID.indexOf("$"));
// is it a group pad? -> delete the entry of this pad in the group
if (padID.indexOf("$") >= 0) {
db.get("group:" + groupID, function (err, group)
{
if(ERR(err, callback)) return;
// it is a group pad
let groupID = padID.substring(0, padID.indexOf("$"));
let group = await db.get("group:" + groupID);
//remove the pad entry
delete group.pads[padID];
// remove the pad entry
delete group.pads[padID];
//set the new value
db.set("group:" + groupID, group);
// set the new value
db.set("group:" + groupID, group);
}
callback();
});
},
//remove the readonly entries
function(callback)
{
readOnlyManager.getReadOnlyId(padID, function(err, readonlyID)
{
if(ERR(err, callback)) return;
// remove the readonly entries
let readonlyID = readOnlyManager.getReadOnlyId(padID);
db.remove("pad2readonly:" + padID);
db.remove("readonly2pad:" + readonlyID);
db.remove("pad2readonly:" + padID);
db.remove("readonly2pad:" + readonlyID);
callback();
});
},
//delete all chat messages
function(callback)
{
var chatHead = _this.chatHead;
// delete all chat messages
for (let i = 0, n = this.chatHead; i <= n; ++i) {
db.remove("pad:" + padID + ":chat:" + i);
}
for(var i=0;i<=chatHead;i++)
{
db.remove("pad:"+padID+":chat:"+i);
}
// delete all revisions
for (let i = 0, n = this.head; i <= n; ++i) {
db.remove("pad:" + padID + ":revs:" + i);
}
callback();
},
//delete all revisions
function(callback)
{
var revHead = _this.head;
for(var i=0;i<=revHead;i++)
{
db.remove("pad:"+padID+":revs:"+i);
}
callback();
},
//remove pad from all authors who contributed
function(callback)
{
var authorIDs = _this.getAllAuthors();
authorIDs.forEach(function (authorID)
{
authorManager.removePad(authorID, padID);
});
callback();
}
], callback);
},
//delete the pad entry and delete pad from padManager
function(callback)
{
padManager.removePad(padID);
hooks.callAll("padRemove", {'padID':padID});
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
// remove pad from all authors who contributed
this.getAllAuthors().forEach(authorID => {
authorManager.removePad(authorID, padID);
});
};
//set in db
// delete the pad entry and delete pad from padManager
padManager.removePad(padID);
hooks.callAll("padRemove", { padID });
}
// set in db
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
this.publicStatus = publicStatus;
this.saveToDatabase();
@ -730,22 +521,22 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() {
};
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
//if this revision is already saved, return silently
for(var i in this.savedRevisions){
if(this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum){
// if this revision is already saved, return silently
for (var i in this.savedRevisions) {
if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
return;
}
}
//build the saved revision object
// build the saved revision object
var savedRevision = {};
savedRevision.revNum = revNum;
savedRevision.savedById = savedById;
savedRevision.label = label || "Revision " + revNum;
savedRevision.timestamp = new Date().getTime();
savedRevision.timestamp = Date.now();
savedRevision.id = randomString(10);
//save this new saved revision
// save this new saved revision
this.savedRevisions.push(savedRevision);
this.saveToDatabase();
};
@ -756,19 +547,17 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() {
/* Crypto helper methods */
function hash(password, salt)
{
function hash(password, salt) {
var shasum = crypto.createHash('sha512');
shasum.update(password + salt);
return shasum.digest("hex") + "$" + salt;
}
function generateSalt()
{
function generateSalt() {
return randomString(86);
}
function compare(hashStr, password)
{
function compare(hashStr, password) {
return hash(password, hashStr.split("$")[1]) === hashStr;
}

View file

@ -18,12 +18,11 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var Pad = require("../db/Pad").Pad;
var db = require("./DB").db;
var db = require("./DB");
/**
/**
* A cache of all loaded Pads.
*
* Provides "get" and "set" functions,
@ -35,12 +34,11 @@ var db = require("./DB").db;
* that's defined somewhere more sensible.
*/
var globalPads = {
get: function (name) { return this[':'+name]; },
set: function (name, value)
{
get: function(name) { return this[':'+name]; },
set: function(name, value) {
this[':'+name] = value;
},
remove: function (name) {
remove: function(name) {
delete this[':'+name];
}
};
@ -50,183 +48,151 @@ var globalPads = {
*
* Updated without db access as new pads are created/old ones removed.
*/
var padList = {
let padList = {
list: [],
sorted : false,
initiated: false,
init: function(cb)
{
db.findKeys("pad:*", "*:*:*", function(err, dbData)
{
if(ERR(err, cb)) return;
if(dbData != null){
padList.initiated = true
dbData.forEach(function(val){
padList.addPad(val.replace(/pad:/,""),false);
});
cb && cb()
init: async function() {
let dbData = await db.findKeys("pad:*", "*:*:*");
if (dbData != null) {
this.initiated = true;
for (let val of dbData) {
this.addPad(val.replace(/pad:/,""), false);
}
});
}
return this;
},
load: function(cb) {
if(this.initiated) cb && cb()
else this.init(cb)
load: async function() {
if (!this.initiated) {
return this.init();
}
return this;
},
/**
* Returns all pads in alphabetical order as array.
*/
getPads: function(cb){
this.load(function() {
if(!padList.sorted){
padList.list = padList.list.sort();
padList.sorted = true;
}
cb && cb(padList.list);
})
getPads: async function() {
await this.load();
if (!this.sorted) {
this.list.sort();
this.sorted = true;
}
return this.list;
},
addPad: function(name)
{
if(!this.initiated) return;
if(this.list.indexOf(name) == -1){
addPad: function(name) {
if (!this.initiated) return;
if (this.list.indexOf(name) == -1) {
this.list.push(name);
this.sorted=false;
this.sorted = false;
}
},
removePad: function(name)
{
if(!this.initiated) return;
removePad: function(name) {
if (!this.initiated) return;
var index = this.list.indexOf(name);
if(index>-1){
this.list.splice(index,1);
this.sorted=false;
if (index > -1) {
this.list.splice(index, 1);
this.sorted = false;
}
}
};
//initialises the allknowing data structure
// initialises the all-knowing data structure
/**
* Returns a Pad Object with the callback
* @param id A String with the id of the pad
* @param {Function} callback
*/
exports.getPad = async function(id, text)
{
// check if this is a valid padId
if (!exports.isValidPadId(id)) {
throw new customError(id + " is not a valid padId", "apierror");
}
// check if this is a valid text
if (text != null) {
// check if text is a string
if (typeof text != "string") {
throw new customError("text is not a string", "apierror");
}
// check if text is less than 100k chars
if (text.length > 100000) {
throw new customError("text must be less than 100k chars", "apierror");
}
}
let pad = globalPads.get(id);
// return pad if it's already loaded
if (pad != null) {
return pad;
}
// try to load pad
pad = new Pad(id);
// initalize the pad
await pad.init(text);
globalPads.set(id, pad);
padList.addPad(id);
return pad;
}
exports.listAllPads = async function()
{
let padIDs = await padList.getPads();
return { padIDs };
}
// checks if a pad exists
exports.doesPadExist = async function(padId)
{
let value = await db.get("pad:" + padId);
return (value != null && value.atext);
}
// alias for backwards compatibility
exports.doesPadExists = exports.doesPadExist;
/**
* An array of padId transformations. These represent changes in pad name policy over
* time, and allow us to "play back" these changes so legacy padIds can be found.
*/
var padIdTransforms = [
const padIdTransforms = [
[/\s+/g, '_'],
[/:+/g, '_']
];
/**
* Returns a Pad Object with the callback
* @param id A String with the id of the pad
* @param {Function} callback
*/
exports.getPad = function(id, text, callback)
{
//check if this is a valid padId
if(!exports.isValidPadId(id))
{
callback(new customError(id + " is not a valid padId","apierror"));
return;
}
//make text an optional parameter
if(typeof text == "function")
{
callback = text;
text = null;
}
//check if this is a valid text
if(text != null)
{
//check if text is a string
if(typeof text != "string")
{
callback(new customError("text is not a string","apierror"));
return;
// returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = async function sanitizePadId(padId) {
for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
let exists = await exports.doesPadExist(padId);
if (exists) {
return padId;
}
//check if text is less than 100k chars
if(text.length > 100000)
{
callback(new customError("text must be less than 100k chars","apierror"));
return;
}
}
var pad = globalPads.get(id);
//return pad if its already loaded
if(pad != null)
{
callback(null, pad);
return;
let [from, to] = padIdTransforms[i];
padId = padId.replace(from, to);
}
//try to load pad
pad = new Pad(id);
//initalize the pad
pad.init(text, function(err)
{
if(ERR(err, callback)) return;
globalPads.set(id, pad);
padList.addPad(id);
callback(null, pad);
});
}
exports.listAllPads = function(cb)
{
padList.getPads(function(list) {
cb && cb(null, {padIDs: list});
});
}
//checks if a pad exists
exports.doesPadExists = function(padId, callback)
{
db.get("pad:"+padId, function(err, value)
{
if(ERR(err, callback)) return;
if(value != null && value.atext){
callback(null, true);
}
else
{
callback(null, false);
}
});
}
//returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = function(padId, callback) {
var transform_index = arguments[2] || 0;
//we're out of possible transformations, so just return it
if(transform_index >= padIdTransforms.length)
{
callback(padId);
return;
}
//check if padId exists
exports.doesPadExists(padId, function(junk, exists)
{
if(exists)
{
callback(padId);
return;
}
//get the next transformation *that's different*
var transformedPadId = padId;
while(transformedPadId == padId && transform_index < padIdTransforms.length)
{
transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]);
transform_index += 1;
}
//check the next transform
exports.sanitizePadId(transformedPadId, callback, transform_index);
});
// we're out of possible transformations, so just return it
return padId;
}
exports.isValidPadId = function(padId)
@ -237,13 +203,13 @@ exports.isValidPadId = function(padId)
/**
* Removes the pad from database and unloads it.
*/
exports.removePad = function(padId){
db.remove("pad:"+padId);
exports.removePad = function(padId) {
db.remove("pad:" + padId);
exports.unloadPad(padId);
padList.removePad(padId);
}
//removes a pad from the cache
// removes a pad from the cache
exports.unloadPad = function(padId)
{
globalPads.remove(padId);

View file

@ -19,80 +19,47 @@
*/
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
var db = require("./DB");
var randomString = require("../utils/randomstring");
/**
* returns a read only id for a pad
* @param {String} padId the id of the pad
*/
exports.getReadOnlyId = function (padId, callback)
{
var readOnlyId;
async.waterfall([
//check if there is a pad2readonly entry
function(callback)
{
db.get("pad2readonly:" + padId, callback);
},
function(dbReadOnlyId, callback)
{
//there is no readOnly Entry in the database, let's create one
if(dbReadOnlyId == null)
{
readOnlyId = "r." + randomString(16);
db.set("pad2readonly:" + padId, readOnlyId);
db.set("readonly2pad:" + readOnlyId, padId);
}
//there is a readOnly Entry in the database, let's take this one
else
{
readOnlyId = dbReadOnlyId;
}
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
//return the results
callback(null, readOnlyId);
})
exports.getReadOnlyId = async function (padId)
{
// check if there is a pad2readonly entry
let readOnlyId = await db.get("pad2readonly:" + padId);
// there is no readOnly Entry in the database, let's create one
if (readOnlyId == null) {
readOnlyId = "r." + randomString(16);
db.set("pad2readonly:" + padId, readOnlyId);
db.set("readonly2pad:" + readOnlyId, padId);
}
return readOnlyId;
}
/**
* returns a the padId for a read only id
* returns the padId for a read only id
* @param {String} readOnlyId read only id
*/
exports.getPadId = function(readOnlyId, callback)
exports.getPadId = function(readOnlyId)
{
db.get("readonly2pad:" + readOnlyId, callback);
return db.get("readonly2pad:" + readOnlyId);
}
/**
* returns a the padId and readonlyPadId in an object for any id
* returns the padId and readonlyPadId in an object for any id
* @param {String} padIdOrReadonlyPadId read only id or real pad id
*/
exports.getIds = function(id, callback) {
if (id.indexOf("r.") == 0)
exports.getPadId(id, function (err, value) {
if(ERR(err, callback)) return;
callback(null, {
readOnlyPadId: id,
padId: value, // Might be null, if this is an unknown read-only id
readonly: true
});
});
else
exports.getReadOnlyId(id, function (err, value) {
callback(null, {
readOnlyPadId: value,
padId: id,
readonly: false
});
});
exports.getIds = async function(id) {
let readonly = (id.indexOf("r.") === 0);
// Might be null, if this is an unknown read-only id
let readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
let padId = readonly ? await exports.getPadId(id) : id;
return { readOnlyPadId, padId, readonly };
}

View file

@ -18,9 +18,6 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var async = require("async");
var authorManager = require("./AuthorManager");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
var padManager = require("./PadManager");
@ -34,296 +31,231 @@ var authLogger = log4js.getLogger("auth");
* @param padID the pad the user wants to access
* @param sessionCookie the session the user has (set via api)
* @param token the token of the author (randomly generated at client side, used for public pads)
* @param password the password the user has given to access this pad, can be null
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
*/
exports.checkAccess = function (padID, sessionCookie, token, password, callback)
{
var statusObject;
if(!padID) {
callback(null, {accessStatus: "deny"});
return;
* @param password the password the user has given to access this pad, can be null
* @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
*/
exports.checkAccess = async function(padID, sessionCookie, token, password)
{
// immutable object
let deny = Object.freeze({ accessStatus: "deny" });
if (!padID) {
return deny;
}
// allow plugins to deny access
var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1;
if(deniedByHook)
{
callback(null, {accessStatus: "deny"});
return;
if (deniedByHook) {
return deny;
}
// a valid session is required (api-only mode)
if(settings.requireSession)
{
// without sessionCookie, access is denied
if(!sessionCookie)
{
callback(null, {accessStatus: "deny"});
return;
// start to get author for this token
let p_tokenAuthor = authorManager.getAuthor4Token(token);
// start to check if pad exists
let p_padExists = padManager.doesPadExist(padID);
if (settings.requireSession) {
// a valid session is required (api-only mode)
if (!sessionCookie) {
// without sessionCookie, access is denied
return deny;
}
}
// a session is not required, so we'll check if it's a public pad
else
{
// it's not a group pad, means we can grant access
if(padID.indexOf("$") == -1)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
if(ERR(err, callback)) return;
// assume user has access
statusObject = {accessStatus: "grant", authorID: author};
} else {
// a session is not required, so we'll check if it's a public pad
if (padID.indexOf("$") === -1) {
// it's not a group pad, means we can grant access
// assume user has access
let authorID = await p_tokenAuthor;
let statusObject = { accessStatus: "grant", authorID };
if (settings.editOnly) {
// user can't create pads
if(settings.editOnly)
{
// check if pad exists
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
// pad doesn't exist - user can't have access
if(!exists) statusObject.accessStatus = "deny";
// grant or deny access, with author of token
callback(null, statusObject);
});
return;
}
let padExists = await p_padExists;
// user may create new pads - no need to check anything
// grant access, with author of token
callback(null, statusObject);
});
//don't continue
return;
}
}
var groupID = padID.split("$")[0];
var padExists = false;
var validSession = false;
var sessionAuthor;
var tokenAuthor;
var isPublic;
var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
async.series([
//get basic informations from the database
function(callback)
{
async.parallel([
//does pad exists
function(callback)
{
padManager.doesPadExists(padID, function(err, exists)
{
if(ERR(err, callback)) return;
padExists = exists;
callback();
});
},
//get information about all sessions contained in this cookie
function(callback)
{
if (!sessionCookie)
{
callback();
return;
}
var sessionIDs = sessionCookie.split(',');
async.forEach(sessionIDs, function(sessionID, callback)
{
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
{
//skip session if it doesn't exist
if(err && err.message == "sessionID does not exist")
{
authLogger.debug("Auth failed: unknown session");
callback();
return;
}
if(ERR(err, callback)) return;
var now = Math.floor(new Date().getTime()/1000);
//is it for this group?
if(sessionInfo.groupID != groupID)
{
authLogger.debug("Auth failed: wrong group");
callback();
return;
}
//is validUntil still ok?
if(sessionInfo.validUntil <= now)
{
authLogger.debug("Auth failed: validUntil");
callback();
return;
}
// There is a valid session
validSession = true;
sessionAuthor = sessionInfo.authorID;
callback();
});
}, callback);
},
//get author for token
function(callback)
{
//get author for this token
authorManager.getAuthor4Token(token, function(err, author)
{
if(ERR(err, callback)) return;
tokenAuthor = author;
callback();
});
}
], callback);
},
//get more informations of this pad, if avaiable
function(callback)
{
//skip this if the pad doesn't exists
if(padExists == false)
{
callback();
return;
}
padManager.getPad(padID, function(err, pad)
{
if(ERR(err, callback)) return;
//is it a public pad?
isPublic = pad.getPublicStatus();
//is it password protected?
isPasswordProtected = pad.isPasswordProtected();
//is password correct?
if(isPasswordProtected && password && pad.isCorrectPassword(password))
{
passwordStatus = "correct";
}
callback();
});
},
function(callback)
{
//- a valid session for this group is avaible AND pad exists
if(validSession && padExists)
{
//- the pad is not password protected
if(!isPasswordProtected)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the setting to bypass password validation is set
else if(settings.sessionNoPassword)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected and password is correct
else if(isPasswordProtected && passwordStatus == "correct")
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
}
//- the pad is password protected but wrong password given
else if(isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- the pad is password protected but no password given
else if(isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
//- a valid session for this group avaible but pad doesn't exists
else if(validSession && !padExists)
{
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
//--> deny access if user isn't allowed to create the pad
if(settings.editOnly)
{
authLogger.debug("Auth failed: valid session & pad does not exist");
if (!padExists) {
// pad doesn't exist - user can't have access
statusObject.accessStatus = "deny";
}
}
// there is no valid session avaiable AND pad exists
else if(!validSession && padExists)
{
//-- its public and not password protected
if(isPublic && !isPasswordProtected)
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and password protected and password is correct
else if(isPublic && isPasswordProtected && passwordStatus == "correct")
{
//--> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
}
//- its public and the pad is password protected but wrong password given
else if(isPublic && isPasswordProtected && passwordStatus == "wrong")
{
//--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"};
}
//- its public and the pad is password protected but no password given
else if(isPublic && isPasswordProtected && passwordStatus == "notGiven")
{
//--> ask for password
statusObject = {accessStatus: "needPassword"};
}
//- its not public
else if(!isPublic)
{
authLogger.debug("Auth failed: invalid session & pad is not public");
//--> deny access
statusObject = {accessStatus: "deny"};
}
else
{
throw new Error("Ops, something wrong happend");
}
}
// there is no valid session avaiable AND pad doesn't exists
else
{
authLogger.debug("Auth failed: invalid session & pad does not exist");
//--> deny access
statusObject = {accessStatus: "deny"};
}
callback();
// user may create new pads - no need to check anything
// grant access, with author of token
return statusObject;
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, statusObject);
});
};
}
let validSession = false;
let sessionAuthor;
let isPublic;
let isPasswordProtected;
let passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
// get information about all sessions contained in this cookie
if (sessionCookie) {
let groupID = padID.split("$")[0];
let sessionIDs = sessionCookie.split(',');
// was previously iterated in parallel using async.forEach
let sessionInfos = await Promise.all(sessionIDs.map(sessionID => {
return sessionManager.getSessionInfo(sessionID);
}));
// seperated out the iteration of sessioninfos from the (parallel) fetches from the DB
for (let sessionInfo of sessionInfos) {
try {
// is it for this group?
if (sessionInfo.groupID != groupID) {
authLogger.debug("Auth failed: wrong group");
continue;
}
// is validUntil still ok?
let now = Math.floor(Date.now() / 1000);
if (sessionInfo.validUntil <= now) {
authLogger.debug("Auth failed: validUntil");
continue;
}
// fall-through - there is a valid session
validSession = true;
sessionAuthor = sessionInfo.authorID;
break;
} catch (err) {
// skip session if it doesn't exist
if (err.message == "sessionID does not exist") {
authLogger.debug("Auth failed: unknown session");
} else {
throw err;
}
}
}
}
let padExists = await p_padExists;
if (padExists) {
let pad = await padManager.getPad(padID);
// is it a public pad?
isPublic = pad.getPublicStatus();
// is it password protected?
isPasswordProtected = pad.isPasswordProtected();
// is password correct?
if (isPasswordProtected && password && pad.isCorrectPassword(password)) {
passwordStatus = "correct";
}
}
// - a valid session for this group is avaible AND pad exists
if (validSession && padExists) {
let authorID = sessionAuthor;
let grant = Object.freeze({ accessStatus: "grant", authorID });
if (!isPasswordProtected) {
// - the pad is not password protected
// --> grant access
return grant;
}
if (settings.sessionNoPassword) {
// - the setting to bypass password validation is set
// --> grant access
return grant;
}
if (isPasswordProtected && passwordStatus === "correct") {
// - the pad is password protected and password is correct
// --> grant access
return grant;
}
if (isPasswordProtected && passwordStatus === "wrong") {
// - the pad is password protected but wrong password given
// --> deny access, ask for new password and tell them that the password is wrong
return { accessStatus: "wrongPassword" };
}
if (isPasswordProtected && passwordStatus === "notGiven") {
// - the pad is password protected but no password given
// --> ask for password
return { accessStatus: "needPassword" };
}
throw new Error("Oops, something wrong happend");
}
if (validSession && !padExists) {
// - a valid session for this group avaible but pad doesn't exist
// --> grant access by default
let accessStatus = "grant";
let authorID = sessionAuthor;
// --> deny access if user isn't allowed to create the pad
if (settings.editOnly) {
authLogger.debug("Auth failed: valid session & pad does not exist");
accessStatus = "deny";
}
return { accessStatus, authorID };
}
if (!validSession && padExists) {
// there is no valid session avaiable AND pad exists
let authorID = await p_tokenAuthor;
let grant = Object.freeze({ accessStatus: "grant", authorID });
if (isPublic && !isPasswordProtected) {
// -- it's public and not password protected
// --> grant access, with author of token
return grant;
}
if (isPublic && isPasswordProtected && passwordStatus === "correct") {
// - it's public and password protected and password is correct
// --> grant access, with author of token
return grant;
}
if (isPublic && isPasswordProtected && passwordStatus === "wrong") {
// - it's public and the pad is password protected but wrong password given
// --> deny access, ask for new password and tell them that the password is wrong
return { accessStatus: "wrongPassword" };
}
if (isPublic && isPasswordProtected && passwordStatus === "notGiven") {
// - it's public and the pad is password protected but no password given
// --> ask for password
return { accessStatus: "needPassword" };
}
if (!isPublic) {
// - it's not public
authLogger.debug("Auth failed: invalid session & pad is not public");
// --> deny access
return { accessStatus: "deny" };
}
throw new Error("Oops, something wrong happend");
}
// there is no valid session avaiable AND pad doesn't exist
authLogger.debug("Auth failed: invalid session & pad does not exist");
return { accessStatus: "deny" };
}

View file

@ -17,361 +17,208 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = require("../utils/randomstring");
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
var authorMangager = require("./AuthorManager");
exports.doesSessionExist = function(sessionID, callback)
var db = require("./DB");
var groupManager = require("./GroupManager");
var authorManager = require("./AuthorManager");
exports.doesSessionExist = async function(sessionID)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
callback(null, session != null);
});
let session = await db.get("session:" + sessionID);
return (session !== null);
}
/**
* Creates a new session between an author and a group
*/
exports.createSession = function(groupID, authorID, validUntil, callback)
exports.createSession = async function(groupID, authorID, validUntil)
{
var sessionID;
// check if the group exists
let groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) {
throw new customError("groupID does not exist", "apierror");
}
async.series([
//check if group exists
function(callback)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//everything is fine, continue
else
{
callback();
}
});
},
//check if author exists
function(callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
if(ERR(err, callback)) return;
//author does not exist
if(exists == false)
{
callback(new customError("authorID does not exist","apierror"));
}
//everything is fine, continue
else
{
callback();
}
});
},
//check validUntil and create the session db entry
function(callback)
{
//check if rev is a number
if(typeof validUntil != "number")
{
//try to parse the number
if(isNaN(parseInt(validUntil)))
{
callback(new customError("validUntil is not a number","apierror"));
return;
}
// check if the author exists
let authorExists = await authorManager.doesAuthorExist(authorID);
if (!authorExists) {
throw new customError("authorID does not exist", "apierror");
}
validUntil = parseInt(validUntil);
}
//ensure this is not a negativ number
if(validUntil < 0)
{
callback(new customError("validUntil is a negativ number","apierror"));
return;
}
//ensure this is not a float value
if(!is_int(validUntil))
{
callback(new customError("validUntil is a float value","apierror"));
return;
}
//check if validUntil is in the future
if(Math.floor(new Date().getTime()/1000) > validUntil)
{
callback(new customError("validUntil is in the past","apierror"));
return;
}
//generate sessionID
sessionID = "s." + randomString(16);
//set the session into the database
db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
callback();
},
//set the group2sessions entry
function(callback)
{
//get the entry
db.get("group2sessions:" + groupID, function(err, group2sessions)
{
if(ERR(err, callback)) return;
//the entry doesn't exist so far, let's create it
if(group2sessions == null || group2sessions.sessionIDs == null)
{
group2sessions = {sessionIDs : {}};
}
//add the entry for this session
group2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("group2sessions:" + groupID, group2sessions);
callback();
});
},
//set the author2sessions entry
function(callback)
{
//get the entry
db.get("author2sessions:" + authorID, function(err, author2sessions)
{
if(ERR(err, callback)) return;
//the entry doesn't exist so far, let's create it
if(author2sessions == null || author2sessions.sessionIDs == null)
{
author2sessions = {sessionIDs : {}};
}
//add the entry for this session
author2sessions.sessionIDs[sessionID] = 1;
//save the new element back
db.set("author2sessions:" + authorID, author2sessions);
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
//return error and sessionID
callback(null, {sessionID: sessionID});
})
// try to parse validUntil if it's not a number
if (typeof validUntil !== "number") {
validUntil = parseInt(validUntil);
}
// check it's a valid number
if (isNaN(validUntil)) {
throw new customError("validUntil is not a number", "apierror");
}
// ensure this is not a negative number
if (validUntil < 0) {
throw new customError("validUntil is a negative number", "apierror");
}
// ensure this is not a float value
if (!is_int(validUntil)) {
throw new customError("validUntil is a float value", "apierror");
}
// check if validUntil is in the future
if (validUntil < Math.floor(Date.now() / 1000)) {
throw new customError("validUntil is in the past", "apierror");
}
// generate sessionID
let sessionID = "s." + randomString(16);
// set the session into the database
await db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
// get the entry
let group2sessions = await db.get("group2sessions:" + groupID);
/*
* In some cases, the db layer could return "undefined" as well as "null".
* Thus, it is not possible to perform strict null checks on group2sessions.
* In a previous version of this code, a strict check broke session
* management.
*
* See: https://github.com/ether/etherpad-lite/issues/3567#issuecomment-468613960
*/
if (!group2sessions || !group2sessions.sessionIDs) {
// the entry doesn't exist so far, let's create it
group2sessions = {sessionIDs : {}};
}
// add the entry for this session
group2sessions.sessionIDs[sessionID] = 1;
// save the new element back
await db.set("group2sessions:" + groupID, group2sessions);
// get the author2sessions entry
let author2sessions = await db.get("author2sessions:" + authorID);
if (author2sessions == null || author2sessions.sessionIDs == null) {
// the entry doesn't exist so far, let's create it
author2sessions = {sessionIDs : {}};
}
// add the entry for this session
author2sessions.sessionIDs[sessionID] = 1;
//save the new element back
await db.set("author2sessions:" + authorID, author2sessions);
return { sessionID };
}
exports.getSessionInfo = function(sessionID, callback)
exports.getSessionInfo = async function(sessionID)
{
//check if the database entry of this session exists
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
//session does not exists
if(session == null)
{
callback(new customError("sessionID does not exist","apierror"))
}
//everything is fine, return the sessioninfos
else
{
callback(null, session);
}
});
// check if the database entry of this session exists
let session = await db.get("session:" + sessionID);
if (session == null) {
// session does not exist
throw new customError("sessionID does not exist", "apierror");
}
// everything is fine, return the sessioninfos
return session;
}
/**
* Deletes a session
*/
exports.deleteSession = function(sessionID, callback)
exports.deleteSession = async function(sessionID)
{
var authorID, groupID;
var group2sessions, author2sessions;
// ensure that the session exists
let session = await db.get("session:" + sessionID);
if (session == null) {
throw new customError("sessionID does not exist", "apierror");
}
async.series([
function(callback)
{
//get the session entry
db.get("session:" + sessionID, function (err, session)
{
if(ERR(err, callback)) return;
//session does not exists
if(session == null)
{
callback(new customError("sessionID does not exist","apierror"))
}
//everything is fine, return the sessioninfos
else
{
authorID = session.authorID;
groupID = session.groupID;
callback();
}
});
},
//get the group2sessions entry
function(callback)
{
db.get("group2sessions:" + groupID, function (err, _group2sessions)
{
if(ERR(err, callback)) return;
group2sessions = _group2sessions;
callback();
});
},
//get the author2sessions entry
function(callback)
{
db.get("author2sessions:" + authorID, function (err, _author2sessions)
{
if(ERR(err, callback)) return;
author2sessions = _author2sessions;
callback();
});
},
//remove the values from the database
function(callback)
{
//remove the session
db.remove("session:" + sessionID);
//remove session from group2sessions
if(group2sessions != null) { // Maybe the group was already deleted
delete group2sessions.sessionIDs[sessionID];
db.set("group2sessions:" + groupID, group2sessions);
}
// everything is fine, use the sessioninfos
let groupID = session.groupID;
let authorID = session.authorID;
//remove session from author2sessions
if(author2sessions != null) { // Maybe the author was already deleted
delete author2sessions.sessionIDs[sessionID];
db.set("author2sessions:" + authorID, author2sessions);
}
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
callback();
})
// get the group2sessions and author2sessions entries
let group2sessions = await db.get("group2sessions:" + groupID);
let author2sessions = await db.get("author2sessions:" + authorID);
// remove the session
await db.remove("session:" + sessionID);
// remove session from group2sessions
if (group2sessions != null) { // Maybe the group was already deleted
delete group2sessions.sessionIDs[sessionID];
await db.set("group2sessions:" + groupID, group2sessions);
}
// remove session from author2sessions
if (author2sessions != null) { // Maybe the author was already deleted
delete author2sessions.sessionIDs[sessionID];
await db.set("author2sessions:" + authorID, author2sessions);
}
}
exports.listSessionsOfGroup = function(groupID, callback)
exports.listSessionsOfGroup = async function(groupID)
{
groupMangager.doesGroupExist(groupID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist","apierror"));
}
//everything is fine, continue
else
{
listSessionsWithDBKey("group2sessions:" + groupID, callback);
}
});
// check that the group exists
let exists = await groupManager.doesGroupExist(groupID);
if (!exists) {
throw new customError("groupID does not exist", "apierror");
}
let sessions = await listSessionsWithDBKey("group2sessions:" + groupID);
return sessions;
}
exports.listSessionsOfAuthor = function(authorID, callback)
{
authorMangager.doesAuthorExists(authorID, function(err, exists)
{
if(ERR(err, callback)) return;
//group does not exist
if(exists == false)
{
callback(new customError("authorID does not exist","apierror"));
}
//everything is fine, continue
else
{
listSessionsWithDBKey("author2sessions:" + authorID, callback);
}
});
}
//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common
function listSessionsWithDBKey (dbkey, callback)
exports.listSessionsOfAuthor = async function(authorID)
{
var sessions;
// check that the author exists
let exists = await authorManager.doesAuthorExist(authorID)
if (!exists) {
throw new customError("authorID does not exist", "apierror");
}
async.series([
function(callback)
{
//get the group2sessions entry
db.get(dbkey, function(err, sessionObject)
{
if(ERR(err, callback)) return;
sessions = sessionObject ? sessionObject.sessionIDs : null;
callback();
});
},
function(callback)
{
//collect all sessionIDs in an arrary
var sessionIDs = [];
for (var i in sessions)
{
sessionIDs.push(i);
}
//foreach trough the sessions and get the sessioninfos
async.forEach(sessionIDs, function(sessionID, callback)
{
exports.getSessionInfo(sessionID, function(err, sessionInfo)
{
if (err == "apierror: sessionID does not exist")
{
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
}
else if(ERR(err, callback))
{
return;
}
sessions[sessionID] = sessionInfo;
callback();
});
}, callback);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, sessions);
});
let sessions = await listSessionsWithDBKey("author2sessions:" + authorID);
return sessions;
}
//checks if a number is an int
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
// required to return null rather than an empty object if there are none
async function listSessionsWithDBKey(dbkey)
{
// get the group2sessions entry
let sessionObject = await db.get(dbkey);
let sessions = sessionObject ? sessionObject.sessionIDs : null;
// iterate through the sessions and get the sessioninfos
for (let sessionID in sessions) {
try {
let sessionInfo = await exports.getSessionInfo(sessionID);
sessions[sessionID] = sessionInfo;
} catch (err) {
if (err == "apierror: sessionID does not exist") {
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
sessions[sessionID] = null;
} else {
throw err;
}
}
}
return sessions;
}
// checks if a number is an int
function is_int(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
{
return (parseFloat(value) == parseInt(value)) && !isNaN(value);
}

View file

@ -1,7 +1,10 @@
/*
/*
* Stores session data in the database
* Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js
* This is not used for authors that are created via the API at current
*
* RPB: this module was not migrated to Promises, because it is only used via
* express-session, which can't actually use promises anyway.
*/
var Store = require('ep_etherpad-lite/node_modules/express-session').Store,
@ -13,11 +16,12 @@ var SessionStore = module.exports = function SessionStore() {};
SessionStore.prototype.__proto__ = Store.prototype;
SessionStore.prototype.get = function(sid, fn){
SessionStore.prototype.get = function(sid, fn) {
messageLogger.debug('GET ' + sid);
var self = this;
db.get("sessionstorage:" + sid, function (err, sess)
{
db.get("sessionstorage:" + sid, function(err, sess) {
if (sess) {
sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires;
if (!sess.cookie.expires || new Date() < sess.cookie.expires) {
@ -31,50 +35,64 @@ SessionStore.prototype.get = function(sid, fn){
});
};
SessionStore.prototype.set = function(sid, sess, fn){
SessionStore.prototype.set = function(sid, sess, fn) {
messageLogger.debug('SET ' + sid);
db.set("sessionstorage:" + sid, sess);
process.nextTick(function(){
if(fn) fn();
});
if (fn) {
process.nextTick(fn);
}
};
SessionStore.prototype.destroy = function(sid, fn){
SessionStore.prototype.destroy = function(sid, fn) {
messageLogger.debug('DESTROY ' + sid);
db.remove("sessionstorage:" + sid);
process.nextTick(function(){
if(fn) fn();
});
if (fn) {
process.nextTick(fn);
}
};
SessionStore.prototype.all = function(fn){
messageLogger.debug('ALL');
var sessions = [];
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
sessions.push(value);
}
});
fn(null, sessions);
};
/*
* RPB: the following methods are optional requirements for a compatible session
* store for express-session, but in any case appear to depend on a
* non-existent feature of ueberdb2
*/
if (db.forEach) {
SessionStore.prototype.all = function(fn) {
messageLogger.debug('ALL');
SessionStore.prototype.clear = function(fn){
messageLogger.debug('CLEAR');
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
db.db.remove("session:" + key);
}
});
if(fn) fn();
};
var sessions = [];
SessionStore.prototype.length = function(fn){
messageLogger.debug('LENGTH');
var i = 0;
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
i++;
}
});
fn(null, i);
db.forEach(function(key, value) {
if (key.substr(0,15) === "sessionstorage:") {
sessions.push(value);
}
});
fn(null, sessions);
};
SessionStore.prototype.clear = function(fn) {
messageLogger.debug('CLEAR');
db.forEach(function(key, value) {
if (key.substr(0,15) === "sessionstorage:") {
db.remove("session:" + key);
}
});
if (fn) fn();
};
SessionStore.prototype.length = function(fn) {
messageLogger.debug('LENGTH');
var i = 0;
db.forEach(function(key, value) {
if (key.substr(0,15) === "sessionstorage:") {
i++;
}
});
fn(null, i);
}
};

View file

@ -19,7 +19,6 @@
*/
var absolutePaths = require('../utils/AbsolutePaths');
var ERR = require("async-stacktrace");
var fs = require("fs");
var api = require("../db/API");
var log4js = require('log4js');
@ -32,19 +31,17 @@ var apiHandlerLogger = log4js.getLogger('APIHandler');
//ensure we have an apikey
var apikey = null;
var apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || "./APIKEY.txt");
try
{
try {
apikey = fs.readFileSync(apikeyFilename,"utf8");
apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`);
}
catch(e)
{
} catch(e) {
apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`);
apikey = randomString(32);
fs.writeFileSync(apikeyFilename,apikey,"utf8");
}
//a list of all functions
// a list of all functions
var version = {};
version["1"] = Object.assign({},
@ -152,110 +149,73 @@ exports.version = version;
* @req express request object
* @res express response object
*/
exports.handle = function(apiVersion, functionName, fields, req, res)
exports.handle = async function(apiVersion, functionName, fields, req, res)
{
//check if this is a valid apiversion
var isKnownApiVersion = false;
for(var knownApiVersion in version)
{
if(knownApiVersion == apiVersion)
{
isKnownApiVersion = true;
break;
}
}
//say goodbye if this is an unknown API version
if(!isKnownApiVersion)
{
// say goodbye if this is an unknown API version
if (!(apiVersion in version)) {
res.statusCode = 404;
res.send({code: 3, message: "no such api version", data: null});
return;
}
//check if this is a valid function name
var isKnownFunctionname = false;
for(var knownFunctionname in version[apiVersion])
{
if(knownFunctionname == functionName)
{
isKnownFunctionname = true;
break;
}
}
//say goodbye if this is a unknown function
if(!isKnownFunctionname)
{
// say goodbye if this is an unknown function
if (!(functionName in version[apiVersion])) {
// no status code?!
res.send({code: 3, message: "no such function", data: null});
return;
}
//check the api key!
// check the api key!
fields["apikey"] = fields["apikey"] || fields["api_key"];
if(fields["apikey"] != apikey.trim())
{
if (fields["apikey"] !== apikey.trim()) {
res.statusCode = 401;
res.send({code: 4, message: "no or wrong API Key", data: null});
return;
}
//sanitize any pad id's before continuing
if(fields["padID"])
{
padManager.sanitizePadId(fields["padID"], function(padId)
{
fields["padID"] = padId;
callAPI(apiVersion, functionName, fields, req, res);
});
// sanitize any padIDs before continuing
if (fields["padID"]) {
fields["padID"] = await padManager.sanitizePadId(fields["padID"]);
}
else if(fields["padName"])
{
padManager.sanitizePadId(fields["padName"], function(padId)
{
fields["padName"] = padId;
callAPI(apiVersion, functionName, fields, req, res);
});
}
else
{
callAPI(apiVersion, functionName, fields, req, res);
// there was an 'else' here before - removed it to ensure
// that this sanitize step can't be circumvented by forcing
// the first branch to be taken
if (fields["padName"]) {
fields["padName"] = await padManager.sanitizePadId(fields["padName"]);
}
// no need to await - callAPI returns a promise
return callAPI(apiVersion, functionName, fields, req, res);
}
//calls the api function
function callAPI(apiVersion, functionName, fields, req, res)
// calls the api function
async function callAPI(apiVersion, functionName, fields, req, res)
{
//put the function parameters in an array
// put the function parameters in an array
var functionParams = version[apiVersion][functionName].map(function (field) {
return fields[field]
})
//add a callback function to handle the response
functionParams.push(function(err, data)
{
// no error happend, everything is fine
if(err == null)
{
if(!data)
data = null;
res.send({code: 0, message: "ok", data: data});
}
// parameters were wrong and the api stopped execution, pass the error
else if(err.name == "apierror")
{
res.send({code: 1, message: err.message, data: null});
}
//an unknown error happend
else
{
res.send({code: 2, message: "internal error", data: null});
ERR(err);
}
});
//call the api function
api[functionName].apply(this, functionParams);
try {
// call the api function
let data = await api[functionName].apply(this, functionParams);
if (!data) {
data = null;
}
res.send({code: 0, message: "ok", data: data});
} catch (err) {
if (err.name == "apierror") {
// parameters were wrong and the api stopped execution, pass the error
res.send({code: 1, message: err.message, data: null});
} else {
// an unknown error happened
res.send({code: 2, message: "internal error", data: null});
throw err;
}
}
}

View file

@ -19,169 +19,122 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var exporthtml = require("../utils/ExportHtml");
var exporttxt = require("../utils/ExportTxt");
var exportEtherpad = require("../utils/ExportEtherpad");
var async = require("async");
var fs = require("fs");
var settings = require('../utils/Settings');
var os = require('os');
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
var TidyHtml = require('../utils/TidyHtml');
const util = require("util");
var convertor = null;
const fsp_writeFile = util.promisify(fs.writeFile);
const fsp_unlink = util.promisify(fs.unlink);
//load abiword only if its enabled
if(settings.abiword != null)
let convertor = null;
// load abiword only if it is enabled
if (settings.abiword != null) {
convertor = require("../utils/Abiword");
}
// Use LibreOffice if an executable has been defined in the settings
if(settings.soffice != null)
if (settings.soffice != null) {
convertor = require("../utils/LibreOffice");
var tempDirectory = "/tmp";
//tempDirectory changes if the operating system is windows
if(os.type().indexOf("Windows") > -1)
{
tempDirectory = process.env.TEMP;
}
const tempDirectory = os.tmpdir();
/**
* do a requested export
*/
exports.doExport = function(req, res, padId, type)
async function doExport(req, res, padId, type)
{
var fileName = padId;
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
hooks.aCallFirst("exportFileName", padId,
function(err, hookFileName){
// if fileName is set then set it to the padId, note that fileName is returned as an array.
if(hookFileName.length) fileName = hookFileName;
let hookFileName = await hooks.aCallFirst("exportFileName", padId);
//tell the browser that this is a downloadable file
res.attachment(fileName + "." + type);
// if fileName is set then set it to the padId, note that fileName is returned as an array.
if (hookFileName.length) {
fileName = hookFileName;
}
//if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text
if(type == "etherpad"){
exportEtherpad.getPadRaw(padId, function(err, pad){
if(!err){
res.send(pad);
// return;
}
});
}
else if(type == "txt")
{
exporttxt.getPadTXTDocument(padId, req.params.rev, function(err, txt)
{
if(!err) {
res.send(txt);
}
});
}
else
{
var html;
var randNum;
var srcFile, destFile;
// tell the browser that this is a downloadable file
res.attachment(fileName + "." + type);
async.series([
//render the html document
function(callback)
{
exporthtml.getPadHTMLDocument(padId, req.params.rev, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
},
//decide what to do with the html export
function(callback)
{
//if this is a html export, we can send this from here directly
if(type == "html")
{
// do any final changes the plugin might want to make
hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML){
if(newHTML.length) html = newHTML;
res.send(html);
callback("stop");
});
}
else //write the html export to a file
{
randNum = Math.floor(Math.random()*0xFFFFFFFF);
srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html";
fs.writeFile(srcFile, html, callback);
}
},
// if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text
if (type === "etherpad") {
let pad = await exportEtherpad.getPadRaw(padId);
res.send(pad);
} else if (type === "txt") {
let txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
res.send(txt);
} else {
// render the html document
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev);
// Tidy up the exported HTML
function(callback)
{
//ensure html can be collected by the garbage collector
html = null;
// decide what to do with the html export
TidyHtml.tidy(srcFile, callback);
},
//send the convert job to the convertor (abiword, libreoffice, ..)
function(callback)
{
destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type;
// Allow plugins to overwrite the convert in export process
hooks.aCallAll("exportConvert", {srcFile: srcFile, destFile: destFile, req: req, res: res}, function(err, result){
if(!err && result.length > 0){
// console.log("export handled by plugin", destFile);
handledByPlugin = true;
callback();
}else{
convertor.convertFile(srcFile, destFile, type, callback);
}
});
},
//send the file
function(callback)
{
res.sendFile(destFile, null, callback);
},
//clean up temporary files
function(callback)
{
async.parallel([
function(callback)
{
fs.unlink(srcFile, callback);
},
function(callback)
{
//100ms delay to accomidate for slow windows fs
if(os.type().indexOf("Windows") > -1)
{
setTimeout(function()
{
fs.unlink(destFile, callback);
}, 100);
}
else
{
fs.unlink(destFile, callback);
}
}
], callback);
}
], function(err)
{
if(err && err != "stop") ERR(err);
})
}
// if this is a html export, we can send this from here directly
if (type === "html") {
// do any final changes the plugin might want to make
let newHTML = await hooks.aCallFirst("exportHTMLSend", html);
if (newHTML.length) html = newHTML;
res.send(html);
throw "stop";
}
);
};
// else write the html export to a file
let randNum = Math.floor(Math.random()*0xFFFFFFFF);
let srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html";
await fsp_writeFile(srcFile, html);
// Tidy up the exported HTML
// ensure html can be collected by the garbage collector
html = null;
await TidyHtml.tidy(srcFile);
// send the convert job to the convertor (abiword, libreoffice, ..)
let destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type;
// Allow plugins to overwrite the convert in export process
let result = await hooks.aCallAll("exportConvert", { srcFile, destFile, req, res });
if (result.length > 0) {
// console.log("export handled by plugin", destFile);
handledByPlugin = true;
} else {
// @TODO no Promise interface for convertors (yet)
await new Promise((resolve, reject) => {
convertor.convertFile(srcFile, destFile, type, function(err) {
err ? reject("convertFailed") : resolve();
});
});
}
// send the file
let sendFile = util.promisify(res.sendFile);
await res.sendFile(destFile, null);
// clean up temporary files
await fsp_unlink(srcFile);
// 100ms delay to accommodate for slow windows fs
if (os.type().indexOf("Windows") > -1) {
await new Promise(resolve => setTimeout(resolve, 100));
}
await fsp_unlink(destFile);
}
}
exports.doExport = function(req, res, padId, type)
{
doExport(req, res, padId, type).catch(err => {
if (err !== "stop") {
throw err;
}
});
}

View file

@ -20,10 +20,8 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace")
, padManager = require("../db/PadManager")
var padManager = require("../db/PadManager")
, padMessageHandler = require("./PadMessageHandler")
, async = require("async")
, fs = require("fs")
, path = require("path")
, settings = require('../utils/Settings')
@ -32,301 +30,241 @@ var ERR = require("async-stacktrace")
, importHtml = require("../utils/ImportHtml")
, importEtherpad = require("../utils/ImportEtherpad")
, log4js = require("log4js")
, hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
, hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js")
, util = require("util");
var convertor = null;
var exportExtension = "htm";
let fsp_exists = util.promisify(fs.exists);
let fsp_rename = util.promisify(fs.rename);
let fsp_readFile = util.promisify(fs.readFile);
let fsp_unlink = util.promisify(fs.unlink)
//load abiword only if its enabled and if soffice is disabled
if(settings.abiword != null && settings.soffice === null)
let convertor = null;
let exportExtension = "htm";
// load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice === null) {
convertor = require("../utils/Abiword");
}
//load soffice only if its enabled
if(settings.soffice != null) {
// load soffice only if it is enabled
if (settings.soffice != null) {
convertor = require("../utils/LibreOffice");
exportExtension = "html";
}
//for node 0.6 compatibily, os.tmpDir() only works from 0.8
var tmpDirectory = process.env.TEMP || process.env.TMPDIR || process.env.TMP || '/tmp';
const tmpDirectory = os.tmpdir();
/**
* do a requested import
*/
exports.doImport = function(req, res, padId)
*/
async function doImport(req, res, padId)
{
var apiLogger = log4js.getLogger("ImportHandler");
//pipe to a file
//convert file to html via abiword or soffice
//set html in the pad
var srcFile, destFile
, pad
, text
, importHandledByPlugin
, directDatabaseAccess
, useConvertor;
// pipe to a file
// convert file to html via abiword or soffice
// set html in the pad
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
// setting flag for whether to use convertor or not
useConvertor = (convertor != null);
let useConvertor = (convertor != null);
async.series([
//save the uploaded file to /tmp
function(callback) {
var form = new formidable.IncomingForm();
form.keepExtensions = true;
form.uploadDir = tmpDirectory;
form.parse(req, function(err, fields, files) {
//the upload failed, stop at this point
if(err || files.file === undefined) {
if(err) console.warn("Uploading Error: " + err.stack);
callback("uploadFailed");
let form = new formidable.IncomingForm();
form.keepExtensions = true;
form.uploadDir = tmpDirectory;
return;
// locally wrapped Promise, since form.parse requires a callback
let srcFile = await new Promise((resolve, reject) => {
form.parse(req, function(err, fields, files) {
if (err || files.file === undefined) {
// the upload failed, stop at this point
if (err) {
console.warn("Uploading Error: " + err.stack);
}
//everything ok, continue
//save the path of the uploaded file
srcFile = files.file.path;
callback();
});
},
//ensure this is a file ending we know, else we change the file ending to .txt
//this allows us to accept source code files like .c or .java
function(callback) {
var fileEnding = path.extname(srcFile).toLowerCase()
, knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"]
, fileEndingKnown = (knownFileEndings.indexOf(fileEnding) > -1);
//if the file ending is known, continue as normal
if(fileEndingKnown) {
callback();
return;
reject("uploadFailed");
}
resolve(files.file.path);
});
});
//we need to rename this file with a .txt ending
if(settings.allowUnknownFileEnds === true){
var oldSrcFile = srcFile;
srcFile = path.join(path.dirname(srcFile),path.basename(srcFile, fileEnding)+".txt");
fs.rename(oldSrcFile, srcFile, callback);
}else{
console.warn("Not allowing unknown file type to be imported", fileEnding);
callback("uploadFailed");
}
},
function(callback){
destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension);
// ensure this is a file ending we know, else we change the file ending to .txt
// this allows us to accept source code files like .c or .java
let fileEnding = path.extname(srcFile).toLowerCase()
, knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"]
, fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
// Logic for allowing external Import Plugins
hooks.aCallAll("import", {srcFile: srcFile, destFile: destFile}, function(err, result){
if(ERR(err, callback)) return callback();
if(result.length > 0){ // This feels hacky and wrong..
importHandledByPlugin = true;
}
callback();
});
},
function(callback) {
var fileEnding = path.extname(srcFile).toLowerCase()
var fileIsNotEtherpad = (fileEnding !== ".etherpad");
if (fileEndingUnknown) {
// the file ending is not known
if (fileIsNotEtherpad) {
callback();
if (settings.allowUnknownFileEnds === true) {
// we need to rename this file with a .txt ending
let oldSrcFile = srcFile;
return;
}
srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt");
await fs.rename(oldSrcFile, srcFile);
} else {
console.warn("Not allowing unknown file type to be imported", fileEnding);
throw "uploadFailed";
}
}
// we do this here so we can see if the pad has quit ea few edits
padManager.getPad(padId, function(err, _pad){
var headCount = _pad.head;
if(headCount >= 10){
apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this")
return callback("padHasData");
}
let destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension);
fs.readFile(srcFile, "utf8", function(err, _text){
directDatabaseAccess = true;
importEtherpad.setPadRaw(padId, _text, function(err){
callback();
});
});
});
},
//convert file to html
function(callback) {
if (importHandledByPlugin || directDatabaseAccess) {
callback();
// Logic for allowing external Import Plugins
let result = await hooks.aCallAll("import", { srcFile, destFile });
let importHandledByPlugin = (result.length > 0); // This feels hacky and wrong..
return;
}
let fileIsEtherpad = (fileEnding === ".etherpad");
let fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm");
let fileIsTXT = (fileEnding === ".txt");
var fileEnding = path.extname(srcFile).toLowerCase();
var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm");
var fileIsTXT = (fileEnding === ".txt");
if (fileIsTXT) useConvertor = false; // Don't use convertor for text files
// See https://github.com/ether/etherpad-lite/issues/2572
if (fileIsHTML || (useConvertor === false)) {
// if no convertor only rename
fs.rename(srcFile, destFile, callback);
return;
}
if (fileIsEtherpad) {
// we do this here so we can see if the pad has quite a few edits
let _pad = await padManager.getPad(padId);
let headCount = _pad.head;
convertor.convertFile(srcFile, destFile, exportExtension, function(err) {
//catch convert errors
if(err) {
console.warn("Converting Error:", err);
return callback("convertFailed");
}
if (headCount >= 10) {
apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this");
throw "padHasData";
}
callback();
});
},
function(callback) {
if (useConvertor || directDatabaseAccess) {
callback();
const fsp_readFile = util.promisify(fs.readFile);
let _text = await fsp_readFile(srcFile, "utf8");
req.directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, _text);
}
return;
}
// convert file to html if necessary
if (!importHandledByPlugin && !req.directDatabaseAccess) {
if (fileIsTXT) {
// Don't use convertor for text files
useConvertor = false;
}
// Read the file with no encoding for raw buffer access.
fs.readFile(destFile, function(err, buf) {
if (err) throw err;
var isAscii = true;
// Check if there are only ascii chars in the uploaded file
for (var i=0, len=buf.length; i<len; i++) {
if (buf[i] > 240) {
isAscii=false;
break;
// See https://github.com/ether/etherpad-lite/issues/2572
if (fileIsHTML || !useConvertor) {
// if no convertor only rename
fs.renameSync(srcFile, destFile);
} else {
// @TODO - no Promise interface for convertors (yet)
await new Promise((resolve, reject) => {
convertor.convertFile(srcFile, destFile, exportExtension, function(err) {
// catch convert errors
if (err) {
console.warn("Converting Error:", err);
reject("convertFailed");
}
}
if (!isAscii) {
callback("uploadFailed");
return;
}
callback();
});
},
//get the pad object
function(callback) {
padManager.getPad(padId, function(err, _pad){
if(ERR(err, callback)) return;
pad = _pad;
callback();
});
},
//read the text
function(callback) {
if (directDatabaseAccess) {
callback();
return;
}
fs.readFile(destFile, "utf8", function(err, _text){
if(ERR(err, callback)) return;
text = _text;
// Title needs to be stripped out else it appends it to the pad..
text = text.replace("<title>", "<!-- <title>");
text = text.replace("</title>","</title>-->");
//node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this
if(os.type().indexOf("Windows") > -1){
setTimeout(function() {callback();}, 100);
} else {
callback();
}
});
},
//change text of the pad and broadcast the changeset
function(callback) {
if(!directDatabaseAccess){
var fileEnding = path.extname(srcFile).toLowerCase();
if (importHandledByPlugin || useConvertor || fileEnding == ".htm" || fileEnding == ".html") {
importHtml.setPadHTML(pad, text, function(e){
if(e) apiLogger.warn("Error importing, possibly caused by malformed HTML");
});
} else {
pad.setText(text);
}
}
// Load the Pad into memory then brodcast updates to all clients
padManager.unloadPad(padId);
padManager.getPad(padId, function(err, _pad){
var pad = _pad;
padManager.unloadPad(padId);
// direct Database Access means a pad user should perform a switchToPad
// and not attempt to recieve updated pad data..
if (directDatabaseAccess) {
callback();
return;
}
padMessageHandler.updatePadClients(pad, function(){
callback();
resolve();
});
});
}
}
},
//clean up temporary files
function(callback) {
if (directDatabaseAccess) {
callback();
if (!useConvertor && !req.directDatabaseAccess) {
// Read the file with no encoding for raw buffer access.
let buf = await fsp_readFile(destFile);
return;
// Check if there are only ascii chars in the uploaded file
let isAscii = ! Array.prototype.some.call(buf, c => (c > 240));
if (!isAscii) {
throw "uploadFailed";
}
}
// get the pad object
let pad = await padManager.getPad(padId);
// read the text
let text;
if (!req.directDatabaseAccess) {
text = await fsp_readFile(destFile, "utf8");
// Title needs to be stripped out else it appends it to the pad..
text = text.replace("<title>", "<!-- <title>");
text = text.replace("</title>","</title>-->");
// node on windows has a delay on releasing of the file lock.
// We add a 100ms delay to work around this
if (os.type().indexOf("Windows") > -1){
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// change text of the pad and broadcast the changeset
if (!req.directDatabaseAccess) {
if (importHandledByPlugin || useConvertor || fileIsHTML) {
try {
importHtml.setPadHTML(pad, text);
} catch (e) {
apiLogger.warn("Error importing, possibly caused by malformed HTML");
}
//for node < 0.7 compatible
var fileExists = fs.exists || path.exists;
async.parallel([
function(callback){
fileExists (srcFile, function(exist) { (exist)? fs.unlink(srcFile, callback): callback(); });
},
function(callback){
fileExists (destFile, function(exist) { (exist)? fs.unlink(destFile, callback): callback(); });
}
], callback);
} else {
pad.setText(text);
}
], function(err) {
var status = "ok";
//check for known errors and replace the status
if(err == "uploadFailed" || err == "convertFailed" || err == "padHasData")
{
}
// Load the Pad into memory then broadcast updates to all clients
padManager.unloadPad(padId);
pad = await padManager.getPad(padId);
padManager.unloadPad(padId);
// direct Database Access means a pad user should perform a switchToPad
// and not attempt to receive updated pad data
if (req.directDatabaseAccess) {
return;
}
// tell clients to update
await padMessageHandler.updatePadClients(pad);
// clean up temporary files
/*
* TODO: directly delete the file and handle the eventual error. Checking
* before for existence is prone to race conditions, and does not handle any
* errors anyway.
*/
if (await fsp_exists(srcFile)) {
fsp_unlink(srcFile);
}
if (await fsp_exists(destFile)) {
fsp_unlink(destFile);
}
}
exports.doImport = function (req, res, padId)
{
/**
* NB: abuse the 'req' object by storing an additional
* 'directDatabaseAccess' property on it so that it can
* be passed back in the HTML below.
*
* this is necessary because in the 'throw' paths of
* the function above there's no other way to return
* a value to the caller.
*/
let status = "ok";
doImport(req, res, padId).catch(err => {
// check for known errors and replace the status
if (err == "uploadFailed" || err == "convertFailed" || err == "padHasData") {
status = err;
err = null;
} else {
throw err;
}
ERR(err);
//close the connection
}).then(() => {
// close the connection
res.send(
"<head> \
<script type='text/javascript' src='../../static/js/jquery.js'></script> \
</head> \
<script> \
$(window).load(function(){ \
var impexp = window.parent.padimpexp.handleFrameCall('" + directDatabaseAccess +"', '" + status + "'); \
}) \
</script>"
"<head> \
<script type='text/javascript' src='../../static/js/jquery.js'></script> \
</head> \
<script> \
$(window).load(function(){ \
var impexp = window.parent.padimpexp.handleFrameCall('" + req.directDatabaseAccess +"', '" + status + "'); \
}) \
</script>"
);
});
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
/**
* This is the Socket.IO Router. It routes the Messages between the
* This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider
*/
@ -19,7 +19,6 @@
* limitations under the License.
*/
var ERR = require("async-stacktrace");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
var securityManager = require("../db/SecurityManager");
@ -31,20 +30,20 @@ var settings = require('../utils/Settings');
* Saves all components
* key is the component name
* value is the component module
*/
*/
var components = {};
var socket;
/**
* adds a component
*/
exports.addComponent = function(moduleName, module)
{
//save the component
// save the component
components[moduleName] = module;
//give the module the socket
// give the module the socket
module.setSocketIO(socket);
}
@ -52,115 +51,102 @@ exports.addComponent = function(moduleName, module)
* sets the socket.io and adds event functions for routing
*/
exports.setSocketIO = function(_socket) {
//save this socket internaly
// save this socket internaly
socket = _socket;
socket.sockets.on('connection', function(client)
{
// Broken: See http://stackoverflow.com/questions/4647348/send-message-to-specific-client-with-socket-io-and-node-js
// Fixed by having a persistant object, ideally this would actually be in the database layer
// TODO move to database layer
if(settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined){
if (settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined) {
remoteAddress[client.id] = client.handshake.headers['x-forwarded-for'];
}
else{
} else {
remoteAddress[client.id] = client.handshake.address;
}
var clientAuthorized = false;
//wrap the original send function to log the messages
// wrap the original send function to log the messages
client._send = client.send;
client.send = function(message) {
messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message));
client._send(message);
}
//tell all components about this connect
for(var i in components) {
components[i].handleConnect(client);
}
client.on('message', function(message)
{
if(message.protocolVersion && message.protocolVersion != 2) {
// tell all components about this connect
for (let i in components) {
components[i].handleConnect(client);
}
client.on('message', async function(message) {
if (message.protocolVersion && message.protocolVersion != 2) {
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
return;
}
//client is authorized, everything ok
if(clientAuthorized) {
if (clientAuthorized) {
// client is authorized, everything ok
handleMessage(client, message);
} else { //try to authorize the client
if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
var checkAccessCallback = function(err, statusObject) {
ERR(err);
//access was granted, mark the client as authorized and handle the message
if(statusObject.accessStatus == "grant") {
clientAuthorized = true;
handleMessage(client, message);
}
//no access, send the client a message that tell him why
else {
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({accessStatus: statusObject.accessStatus});
}
};
if (message.padId.indexOf("r.") === 0) {
readOnlyManager.getPadId(message.padId, function(err, value) {
ERR(err);
securityManager.checkAccess (value, message.sessionID, message.token, message.password, checkAccessCallback);
});
} else {
//this message has everything to try an authorization
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback);
} else {
// try to authorize the client
if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
// check for read-only pads
let padId = message.padId;
if (padId.indexOf("r.") === 0) {
padId = await readOnlyManager.getPadId(message.padId);
}
} else { //drop message
messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
let { accessStatus } = await securityManager.checkAccess(padId, message.sessionID, message.token, message.password);
if (accessStatus === "grant") {
// access was granted, mark the client as authorized and handle the message
clientAuthorized = true;
handleMessage(client, message);
} else {
// no access, send the client a message that tells him why
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({ accessStatus });
}
} else {
// drop message
messageLogger.warn("Dropped message because of bad permissions:" + stringifyWithoutPassword(message));
}
}
});
client.on('disconnect', function()
{
//tell all components about this disconnect
for(var i in components)
{
client.on('disconnect', function() {
// tell all components about this disconnect
for (let i in components) {
components[i].handleDisconnect(client);
}
});
});
}
//try to handle the message of this client
// try to handle the message of this client
function handleMessage(client, message)
{
if(message.component && components[message.component]) {
//check if component is registered in the components array
if(components[message.component]) {
if (message.component && components[message.component]) {
// check if component is registered in the components array
if (components[message.component]) {
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
components[message.component].handleMessage(client, message);
}
} else {
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
}
}
}
//returns a stringified representation of a message, removes the password
//this ensures there are no passwords in the log
// returns a stringified representation of a message, removes the password
// this ensures there are no passwords in the log
function stringifyWithoutPassword(message)
{
var newMessage = {};
for(var i in message)
{
if(i == "password" && message[i] != null)
newMessage["password"] = "xxx";
else
newMessage[i]=message[i];
let newMessage = Object.assign({}, message);
if (newMessage.password != null) {
newMessage.password = "xxx";
}
return JSON.stringify(newMessage);
}

View file

@ -12,27 +12,27 @@ var serverName;
exports.createServer = function () {
console.log("Report bugs at https://github.com/ether/etherpad-lite/issues")
serverName = `Etherpad ${settings.getGitCommit()} (http://etherpad.org)`;
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
exports.restartServer();
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
if(!_.isEmpty(settings.users)){
if (!_.isEmpty(settings.users)) {
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`);
}
else{
} else {
console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json");
}
var env = process.env.NODE_ENV || 'development';
if(env !== 'production'){
if (env !== 'production') {
console.warn("Etherpad is running in Development mode. This mode is slower for users and less secure than production mode. You should set the NODE_ENV environment variable to production by using: export NODE_ENV=production");
}
}
exports.restartServer = function () {
if (server) {
console.log("Restarting express server");
server.close();
@ -41,46 +41,50 @@ exports.restartServer = function () {
var app = express(); // New syntax for express v3
if (settings.ssl) {
console.log("SSL -- enabled");
console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
var options = {
key: fs.readFileSync( settings.ssl.key ),
cert: fs.readFileSync( settings.ssl.cert )
};
if (settings.ssl.ca) {
options.ca = [];
for(var i = 0; i < settings.ssl.ca.length; i++) {
for (var i = 0; i < settings.ssl.ca.length; i++) {
var caFileName = settings.ssl.ca[i];
options.ca.push(fs.readFileSync(caFileName));
}
}
var https = require('https');
server = https.createServer(options, app);
} else {
var http = require('http');
server = http.createServer(app);
}
app.use(function (req, res, next) {
app.use(function(req, res, next) {
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
if(settings.ssl){ // if we use SSL
if (settings.ssl) {
// we use SSL
res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
}
// Stop IE going into compatability mode
// https://github.com/ether/etherpad-lite/issues/2547
res.header("X-UA-Compatible", "IE=Edge,chrome=1");
res.header("Server", serverName);
// send git version in the Server response header if exposeVersion is true.
if (settings.exposeVersion) {
res.header("Server", serverName);
}
next();
});
if(settings.trustProxy){
if (settings.trustProxy) {
app.enable('trust proxy');
}

View file

@ -5,7 +5,7 @@ var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
var _ = require('underscore');
var semver = require('semver');
exports.expressCreateServer = function (hook_name, args, cb) {
exports.expressCreateServer = function(hook_name, args, cb) {
args.app.get('/admin/plugins', function(req, res) {
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var render_args = {
@ -13,91 +13,99 @@ exports.expressCreateServer = function (hook_name, args, cb) {
search_results: {},
errors: [],
};
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args) );
res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args));
});
args.app.get('/admin/plugins/info', function(req, res) {
var gitCommit = settings.getGitCommit();
var epVersion = settings.getEpVersion();
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html",
{
gitCommit: gitCommit,
epVersion: epVersion
})
);
res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {
gitCommit: gitCommit,
epVersion: epVersion
}));
});
}
exports.socketio = function (hook_name, args, cb) {
exports.socketio = function(hook_name, args, cb) {
var io = args.io.of("/pluginfw/installer");
io.on('connection', function (socket) {
io.on('connection', function(socket) {
if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return;
socket.on("getInstalled", function (query) {
socket.on("getInstalled", function(query) {
// send currently installed plugins
var installed = Object.keys(plugins.plugins).map(function(plugin) {
return plugins.plugins[plugin].package
})
});
socket.emit("results:installed", {installed: installed});
});
socket.on("checkUpdates", function() {
socket.on("checkUpdates", async function() {
// Check plugins for updates
installer.getAvailablePlugins(/*maxCacheAge:*/60*10, function(er, results) {
if(er) {
console.warn(er);
socket.emit("results:updatable", {updatable: {}});
return;
}
try {
let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10);
var updatable = _(plugins.plugins).keys().filter(function(plugin) {
if(!results[plugin]) return false;
var latestVersion = results[plugin].version
var currentVersion = plugins.plugins[plugin].package.version
return semver.gt(latestVersion, currentVersion)
if (!results[plugin]) return false;
var latestVersion = results[plugin].version;
var currentVersion = plugins.plugins[plugin].package.version;
return semver.gt(latestVersion, currentVersion);
});
socket.emit("results:updatable", {updatable: updatable});
});
})
socket.on("getAvailable", function (query) {
installer.getAvailablePlugins(/*maxCacheAge:*/false, function (er, results) {
if(er) {
console.error(er)
results = {}
}
socket.emit("results:available", results);
});
} catch (er) {
console.warn(er);
socket.emit("results:updatable", {updatable: {}});
}
});
socket.on("search", function (query) {
installer.search(query.searchTerm, /*maxCacheAge:*/60*10, function (er, results) {
if(er) {
console.error(er)
results = {}
}
socket.on("getAvailable", async function(query) {
try {
let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ false);
socket.emit("results:available", results);
} catch (er) {
console.error(er);
socket.emit("results:available", {});
}
});
socket.on("search", async function(query) {
try {
let results = await installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10);
var res = Object.keys(results)
.map(function(pluginName) {
return results[pluginName]
return results[pluginName];
})
.filter(function(plugin) {
return !plugins.plugins[plugin.name]
return !plugins.plugins[plugin.name];
});
res = sortPluginList(res, query.sortBy, query.sortDir)
.slice(query.offset, query.offset+query.limit);
socket.emit("results:search", {results: res, query: query});
});
} catch (er) {
console.error(er);
socket.emit("results:search", {results: {}, query: query});
}
});
socket.on("install", function (plugin_name) {
installer.install(plugin_name, function (er) {
if(er) console.warn(er)
socket.on("install", function(plugin_name) {
installer.install(plugin_name, function(er) {
if (er) console.warn(er);
socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null});
});
});
socket.on("uninstall", function (plugin_name) {
installer.uninstall(plugin_name, function (er) {
if(er) console.warn(er)
socket.on("uninstall", function(plugin_name) {
installer.uninstall(plugin_name, function(er) {
if (er) console.warn(er);
socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null});
});
});
@ -106,11 +114,15 @@ exports.socketio = function (hook_name, args, cb) {
function sortPluginList(plugins, property, /*ASC?*/dir) {
return plugins.sort(function(a, b) {
if (a[property] < b[property])
return dir? -1 : 1;
if (a[property] > b[property])
return dir? 1 : -1;
if (a[property] < b[property]) {
return dir? -1 : 1;
}
if (a[property] > b[property]) {
return dir? 1 : -1;
}
// a must be equal to b
return 0;
})
});
}

View file

@ -40,7 +40,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//This is a api POST call, collect all post informations and pass it to the apiHandler
args.app.post('/api/:version/:func', function(req, res) {
new formidable.IncomingForm().parse(req, function (err, fields, files) {
apiCaller(req, res, fields)
apiCaller(req, res, Object.assign(req.query, fields))
});
});

View file

@ -11,20 +11,23 @@ exports.gracefulShutdown = function(err) {
console.error(err);
}
//ensure there is only one graceful shutdown running
if(exports.onShutdown) return;
// ensure there is only one graceful shutdown running
if (exports.onShutdown) {
return;
}
exports.onShutdown = true;
console.log("graceful shutdown...");
//do the db shutdown
db.db.doShutdown(function() {
// do the db shutdown
db.doShutdown().then(function() {
console.log("db sucessfully closed.");
process.exit(0);
});
setTimeout(function(){
setTimeout(function() {
process.exit(1);
}, 3000);
}
@ -35,22 +38,36 @@ exports.expressCreateServer = function (hook_name, args, cb) {
exports.app = args.app;
// Handle errors
args.app.use(function(err, req, res, next){
args.app.use(function(err, req, res, next) {
// if an error occurs Connect will pass it down
// through these "error-handling" middleware
// allowing you to respond however you like
res.status(500).send({ error: 'Sorry, something bad happened!' });
console.error(err.stack? err.stack : err.toString());
stats.meter('http500').mark()
})
});
//connect graceful shutdown with sigint and uncaughtexception
if(os.type().indexOf("Windows") == -1) {
//sigint is so far not working on windows
//https://github.com/joyent/node/issues/1553
process.on('SIGINT', exports.gracefulShutdown);
// when running as PID1 (e.g. in docker container)
// allow graceful shutdown on SIGTERM c.f. #3265
process.on('SIGTERM', exports.gracefulShutdown);
}
/*
* Connect graceful shutdown with sigint and uncaught exception
*
* Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were
* not hooked up under Windows, because old nodejs versions did not support
* them.
*
* According to nodejs 6.x documentation, it is now safe to do so. This
* allows to gracefully close the DB connection when hitting CTRL+C under
* Windows, for example.
*
* Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events
*
* - SIGTERM is not supported on Windows, it can be listened on.
* - SIGINT from the terminal is supported on all platforms, and can usually
* be generated with <Ctrl>+C (though this may be configurable). It is not
* generated when terminal raw mode is enabled.
*/
process.on('SIGINT', exports.gracefulShutdown);
// when running as PID1 (e.g. in docker container)
// allow graceful shutdown on SIGTERM c.f. #3265
process.on('SIGTERM', exports.gracefulShutdown);
}

View file

@ -5,15 +5,14 @@ var importHandler = require('../../handler/ImportHandler');
var padManager = require("../../db/PadManager");
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
args.app.get('/p/:pad/:rev?/export/:type', async function(req, res, next) {
var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
//send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) == -1) {
next();
return;
return next();
}
//if abiword is disabled, and this is a format we only support with abiword, output a message
// if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.exportAvailable() == "no" &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
res.send("This export is not enabled at this Etherpad instance. Set the path to Abiword or SOffice in settings.json to enable this feature");
@ -22,30 +21,26 @@ exports.expressCreateServer = function (hook_name, args, cb) {
res.header("Access-Control-Allow-Origin", "*");
hasPadAccess(req, res, function() {
if (await hasPadAccess(req, res)) {
console.log('req.params.pad', req.params.pad);
padManager.doesPadExists(req.params.pad, function(err, exists)
{
if(!exists) {
return next();
}
let exists = await padManager.doesPadExists(req.params.pad);
if (!exists) {
return next();
}
exportHandler.doExport(req, res, req.params.pad, req.params.type);
});
});
exportHandler.doExport(req, res, req.params.pad, req.params.type);
}
});
//handle import requests
args.app.post('/p/:pad/import', function(req, res, next) {
hasPadAccess(req, res, function() {
padManager.doesPadExists(req.params.pad, function(err, exists)
{
if(!exists) {
return next();
}
// handle import requests
args.app.post('/p/:pad/import', async function(req, res, next) {
if (await hasPadAccess(req, res)) {
let exists = await padManager.doesPadExists(req.params.pad);
if (!exists) {
return next();
}
importHandler.doImport(req, res, req.params.pad);
});
});
importHandler.doImport(req, res, req.params.pad);
}
});
}

View file

@ -1,64 +1,26 @@
var async = require('async');
var ERR = require("async-stacktrace");
var readOnlyManager = require("../../db/ReadOnlyManager");
var hasPadAccess = require("../../padaccess");
var exporthtml = require("../../utils/ExportHtml");
exports.expressCreateServer = function (hook_name, args, cb) {
//serve read only pad
args.app.get('/ro/:id', function(req, res)
{
var html;
var padId;
// serve read only pad
args.app.get('/ro/:id', async function(req, res) {
async.series([
//translate the read only pad to a padId
function(callback)
{
readOnlyManager.getPadId(req.params.id, function(err, _padId)
{
if(ERR(err, callback)) return;
// translate the read only pad to a padId
let padId = await readOnlyManager.getPadId(req.params.id);
if (padId == null) {
res.status(404).send('404 - Not Found');
return;
}
padId = _padId;
// we need that to tell hasPadAcess about the pad
req.params.pad = padId;
//we need that to tell hasPadAcess about the pad
req.params.pad = padId;
callback();
});
},
//render the html document
function(callback)
{
//return if the there is no padId
if(padId == null)
{
callback("notfound");
return;
}
hasPadAccess(req, res, function()
{
//render the html document
exporthtml.getPadHTMLDocument(padId, null, function(err, _html)
{
if(ERR(err, callback)) return;
html = _html;
callback();
});
});
}
], function(err)
{
//throw any unexpected error
if(err && err != "notfound")
ERR(err);
if(err == "notfound")
res.status(404).send('404 - Not Found');
else
res.send(html);
});
if (await hasPadAccess(req, res)) {
// render the html document
let html = await exporthtml.getPadHTMLDocument(padId, null);
res.send(html);
}
});
}

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