mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-20 22:49:53 +01:00
Release version 1.8.0
This commit is contained in:
commit
d967914341
178 changed files with 14595 additions and 13736 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
settings.json
|
/settings.json
|
||||||
!settings.json.template
|
!settings.json.template
|
||||||
APIKEY.txt
|
APIKEY.txt
|
||||||
SESSIONKEY.txt
|
SESSIONKEY.txt
|
||||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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
|
# 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: added a new, optional skin. It can be activated choosing `skinName: "colibris"` in `settings.json`
|
||||||
* FEATURE: allow file import using LibreOffice
|
* FEATURE: allow file import using LibreOffice
|
||||||
* SECURITY: updated many dependencies. No known high or moderate risk dependencies remain.
|
* 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
|
# 1.3
|
||||||
* NEW: We now follow the semantic versioning scheme!
|
* NEW: We now follow the semantic versioning scheme!
|
||||||
* NEW: Option to disable IP logging
|
* 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: Fix readOnly group pads
|
||||||
* Fix: don't fetch padList on every request
|
* Fix: don't fetch padList on every request
|
||||||
|
|
||||||
|
|
|
@ -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
|
* Co-Author and Publish CVEs
|
||||||
* Work with SFC to maintain legal side of project
|
* Work with SFC to maintain legal side of project
|
||||||
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
|
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
|
||||||
* Replying to messages on IRC / The Mailing list / Emails
|
|
||||||
|
|
||||||
|
|
50
README.md
50
README.md
|
@ -1,25 +1,27 @@
|
||||||
# A really-real time collaborative word processor for the web
|
# A real-time collaborative editor for the web
|
||||||
![Demo Etherpad Animated Jif](https://i.imgur.com/zYrGkg3.gif "Etherpad in action on PrimaryPad")
|
![Demo Etherpad Animated Jif](https://i.imgur.com/zYrGkg3.gif "Etherpad in action")
|
||||||
|
|
||||||
# About
|
# 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)**
|
**[Try it out](https://beta.etherpad.org)**
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
## Requirements
|
## 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 -
|
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
||||||
sudo apt-get install -y nodejs
|
sudo apt install -y nodejs
|
||||||
git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh
|
git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## GNU/Linux and other UNIX-like systems
|
### Manual install
|
||||||
You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **6.9.0**, preferred: >= **8.9**).
|
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):**
|
**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
|
## Windows
|
||||||
|
|
||||||
### Prebuilt Windows package
|
### 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
|
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
|
### Manually install on Windows
|
||||||
You'll need [node.js](https://nodejs.org) and (optionally, though recommended) git.
|
You'll need [node.js](https://nodejs.org) and (optionally, though recommended) git.
|
||||||
|
|
||||||
1. Grab the source, either
|
1. Grab the source, either
|
||||||
- download <https://github.com/ether/etherpad-lite/zipball/master>
|
- 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`
|
2. start `bin\installOnWindows.bat`
|
||||||
|
|
||||||
Now, run `start.bat` and open <http://localhost:9001> in your browser.
|
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`.
|
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.
|
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.
|
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
|
## 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.
|
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
|
# Development
|
||||||
|
|
||||||
## Things you should know
|
## 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`.
|
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).
|
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)
|
Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
# Get in touch
|
# Get in touch
|
||||||
[mailinglist](https://groups.google.com/group/etherpad-lite-dev)
|
The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues).
|
||||||
[#etherpad-lite-dev freenode IRC](https://webchat.freenode.net?channels=#etherpad-lite-dev)!
|
|
||||||
|
|
||||||
# Languages
|
For **responsible disclosure of vulnerabilities**, please write a mail to the maintainer (a.mux@inwind.it).
|
||||||
Etherpad is written in JavaScript on both the server and client so it's easy for developers to maintain and add new features.
|
|
||||||
|
|
||||||
# HTTP API
|
# HTTP API
|
||||||
Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API)
|
Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API)
|
||||||
|
@ -121,12 +118,5 @@ Etherpad comes with translations into all languages thanks to the team at Transl
|
||||||
# FAQ
|
# FAQ
|
||||||
Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/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
|
# License
|
||||||
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
|
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
NODE_VERSION="8.9.0"
|
NODE_VERSION="10.16.3"
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
#Move to the folder where ep-lite is installed
|
||||||
cd `dirname $0`
|
cd `dirname $0`
|
||||||
|
@ -37,6 +37,10 @@ cd $TMP_FOLDER
|
||||||
rm -rf node_modules
|
rm -rf node_modules
|
||||||
rm -f etherpad-lite-win.zip
|
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..."
|
echo "do a normal unix install first..."
|
||||||
bin/installDeps.sh || exit 1
|
bin/installDeps.sh || exit 1
|
||||||
|
|
||||||
|
|
|
@ -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");
|
console.error("Use: node bin/checkAllPads.js");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
//initialize the variables
|
// load and initialize NPM
|
||||||
var db, settings, padManager;
|
let npm = require('../src/node_modules/npm');
|
||||||
var npm = require("../src/node_modules/npm");
|
npm.load({}, async function() {
|
||||||
var async = require("../src/node_modules/async");
|
|
||||||
|
|
||||||
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 modules
|
||||||
//load npm
|
let Changeset = require('../src/static/js/Changeset');
|
||||||
function(callback) {
|
let padManager = require('../src/node/db/PadManager');
|
||||||
npm.load({}, callback);
|
|
||||||
},
|
|
||||||
//load modules
|
|
||||||
function(callback) {
|
|
||||||
settings = require('../src/node/utils/Settings');
|
|
||||||
db = require('../src/node/db/DB');
|
|
||||||
|
|
||||||
//initialize the database
|
// get all pads
|
||||||
db.init(callback);
|
let res = await padManager.listAllPads();
|
||||||
},
|
|
||||||
//load pads
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager = require('../src/node/db/PadManager');
|
|
||||||
|
|
||||||
padManager.listAllPads(function(err, res)
|
for (let padId of res.padIDs) {
|
||||||
{
|
|
||||||
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
|
let pad = await padManager.getPad(padId);
|
||||||
if(pad.pool === undefined )
|
|
||||||
{
|
// check if the pad has a pool
|
||||||
|
if (pad.pool === undefined) {
|
||||||
console.error("[" + pad.id + "] Missing attribute pool");
|
console.error("[" + pad.id + "] Missing attribute pool");
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//create an array with key kevisions
|
// create an array with key kevisions
|
||||||
//key revisions always save the full pad atext
|
// key revisions always save the full pad atext
|
||||||
var head = pad.getHeadRevisionNumber();
|
let head = pad.getHeadRevisionNumber();
|
||||||
var keyRevisions = [];
|
let keyRevisions = [];
|
||||||
for(var i=0;i<head;i+=100)
|
for (let rev = 0; rev < head; rev += 100) {
|
||||||
{
|
keyRevisions.push(rev);
|
||||||
keyRevisions.push(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//run trough all key revisions
|
// run through all key revisions
|
||||||
async.forEachSeries(keyRevisions, function(keyRev, callback)
|
for (let keyRev of keyRevisions) {
|
||||||
{
|
|
||||||
//create an array of revisions we need till the next keyRevision or the End
|
// create an array of revisions we need till the next keyRevision or the End
|
||||||
var revisionsNeeded = [];
|
var revisionsNeeded = [];
|
||||||
for(var i=keyRev;i<=keyRev+100 && i<=head; i++)
|
for (let rev = keyRev ; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||||
{
|
revisionsNeeded.push(rev);
|
||||||
revisionsNeeded.push(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//this array will hold all revision changesets
|
// this array will hold all revision changesets
|
||||||
var revisions = [];
|
var revisions = [];
|
||||||
|
|
||||||
//run trough all needed revisions and get them from the database
|
// run through all needed revisions and get them from the database
|
||||||
async.forEach(revisionsNeeded, function(revNum, callback)
|
for (let revNum of revisionsNeeded) {
|
||||||
{
|
let revision = await db.get("pad:" + pad.id + ":revs:" + revNum);
|
||||||
db.db.get("pad:"+pad.id+":revs:" + revNum, function(err, revision)
|
|
||||||
{
|
|
||||||
revisions[revNum] = revision;
|
revisions[revNum] = revision;
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}, function(err)
|
|
||||||
{
|
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if the revision exists
|
// check if the revision exists
|
||||||
if (revisions[keyRev] == null) {
|
if (revisions[keyRev] == null) {
|
||||||
console.error("[" + pad.id + "] Missing revision " + keyRev);
|
console.error("[" + pad.id + "] Missing revision " + keyRev);
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if there is a atext in the keyRevisions
|
// check if there is a atext in the keyRevisions
|
||||||
if(revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined)
|
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
|
||||||
{
|
|
||||||
console.error("[" + pad.id + "] Missing atext in revision " + keyRev);
|
console.error("[" + pad.id + "] Missing atext in revision " + keyRev);
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var apool = pad.pool;
|
let apool = pad.pool;
|
||||||
var atext = revisions[keyRev].meta.atext;
|
let atext = revisions[keyRev].meta.atext;
|
||||||
|
|
||||||
for(var i=keyRev+1;i<=keyRev+100 && i<=head; i++)
|
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||||
{
|
try {
|
||||||
try
|
let cs = revisions[rev].changeset;
|
||||||
{
|
|
||||||
//console.log("[" + pad.id + "] check revision " + i);
|
|
||||||
var cs = revisions[i].changeset;
|
|
||||||
atext = Changeset.applyToAText(cs, atext, apool);
|
atext = Changeset.applyToAText(cs, atext, apool);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
console.error("[" + pad.id + "] Bad changeset at revision " + i + " - " + e.message);
|
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");
|
console.log("finished");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.trace(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
160
bin/checkPad.js
160
bin/checkPad.js
|
@ -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");
|
console.error("Use: node bin/checkPad.js $PADID");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
//get the padID
|
|
||||||
var padId = process.argv[2];
|
|
||||||
|
|
||||||
//initialize the variables
|
// get the padID
|
||||||
var db, settings, padManager;
|
const padId = process.argv[2];
|
||||||
var npm = require("../src/node_modules/npm");
|
|
||||||
var async = require("../src/node_modules/async");
|
|
||||||
|
|
||||||
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([
|
try {
|
||||||
//load npm
|
// initialize database
|
||||||
function(callback) {
|
let settings = require('../src/node/utils/Settings');
|
||||||
npm.load({}, function(er) {
|
let db = require('../src/node/db/DB');
|
||||||
callback(er);
|
await db.init();
|
||||||
})
|
|
||||||
},
|
|
||||||
//load modules
|
|
||||||
function(callback) {
|
|
||||||
settings = require('../src/node/utils/Settings');
|
|
||||||
db = require('../src/node/db/DB');
|
|
||||||
|
|
||||||
//initialize the database
|
// load modules
|
||||||
db.init(callback);
|
let Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||||
},
|
let padManager = require('../src/node/db/PadManager');
|
||||||
//get the pad
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager = require('../src/node/db/PadManager');
|
|
||||||
|
|
||||||
padManager.doesPadExists(padId, function(err, exists)
|
let exists = await padManager.doesPadExists(padId);
|
||||||
{
|
if (!exists) {
|
||||||
if(!exists)
|
|
||||||
{
|
|
||||||
console.error("Pad does not exist");
|
console.error("Pad does not exist");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
// get the pad
|
||||||
{
|
let pad = await padManager.getPad(padId);
|
||||||
pad = _pad;
|
|
||||||
callback(err);
|
// create an array with key revisions
|
||||||
});
|
// key revisions always save the full pad atext
|
||||||
});
|
let head = pad.getHeadRevisionNumber();
|
||||||
},
|
let keyRevisions = [];
|
||||||
function (callback)
|
for (let rev = 0; rev < head; rev += 100) {
|
||||||
{
|
keyRevisions.push(rev);
|
||||||
//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
|
// run through all key revisions
|
||||||
async.forEachSeries(keyRevisions, function(keyRev, callback)
|
for (let keyRev of keyRevisions) {
|
||||||
{
|
|
||||||
//create an array of revisions we need till the next keyRevision or the End
|
// create an array of revisions we need till the next keyRevision or the End
|
||||||
var revisionsNeeded = [];
|
let revisionsNeeded = [];
|
||||||
for(var i=keyRev;i<=keyRev+100 && i<=head; i++)
|
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||||
{
|
revisionsNeeded.push(rev);
|
||||||
revisionsNeeded.push(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//this array will hold all revision changesets
|
// this array will hold all revision changesets
|
||||||
var revisions = [];
|
var revisions = [];
|
||||||
|
|
||||||
//run trough all needed revisions and get them from the database
|
// run through all needed revisions and get them from the database
|
||||||
async.forEach(revisionsNeeded, function(revNum, callback)
|
for (let revNum of revisionsNeeded) {
|
||||||
{
|
let revision = await db.get("pad:" + padId + ":revs:" + revNum);
|
||||||
db.db.get("pad:"+padId+":revs:" + revNum, function(err, revision)
|
|
||||||
{
|
|
||||||
revisions[revNum] = revision;
|
revisions[revNum] = revision;
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}, function(err)
|
|
||||||
{
|
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if the pad has a pool
|
// check if the pad has a pool
|
||||||
if(pad.pool === undefined )
|
if (pad.pool === undefined ) {
|
||||||
{
|
|
||||||
console.error("Attribute pool is missing");
|
console.error("Attribute pool is missing");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if there is an atext in the keyRevisions
|
// check if there is an atext in the keyRevisions
|
||||||
if(revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined)
|
if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
|
||||||
{
|
|
||||||
console.error("No atext in key revision " + keyRev);
|
console.error("No atext in key revision " + keyRev);
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var apool = pad.pool;
|
let apool = pad.pool;
|
||||||
var atext = revisions[keyRev].meta.atext;
|
let atext = revisions[keyRev].meta.atext;
|
||||||
|
|
||||||
for(var i=keyRev+1;i<=keyRev+100 && i<=head; i++)
|
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
|
||||||
{
|
try {
|
||||||
try
|
// console.log("check revision " + rev);
|
||||||
{
|
let cs = revisions[rev].changeset;
|
||||||
//console.log("check revision " + i);
|
|
||||||
var cs = revisions[i].changeset;
|
|
||||||
atext = Changeset.applyToAText(cs, atext, apool);
|
atext = Changeset.applyToAText(cs, atext, apool);
|
||||||
}
|
} catch(e) {
|
||||||
catch(e)
|
console.error("Bad changeset at revision " + rev + " - " + e.message);
|
||||||
{
|
continue;
|
||||||
console.error("Bad changeset at revision " + i + " - " + e.message);
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], function (err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("finished");
|
console.log("finished");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.trace(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
var startTime = new Date().getTime();
|
var startTime = Date.now();
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var ueberDB = require("../src/node_modules/ueberDB");
|
var ueberDB = require("../src/node_modules/ueberDB");
|
||||||
var mysql = require("../src/node_modules/ueberDB/node_modules/mysql");
|
var mysql = require("../src/node_modules/ueberDB/node_modules/mysql");
|
||||||
|
@ -43,7 +43,7 @@ var etherpadDB = mysql.createConnection({
|
||||||
});
|
});
|
||||||
|
|
||||||
//get the timestamp once
|
//get the timestamp once
|
||||||
var timestamp = new Date().getTime();
|
var timestamp = Date.now();
|
||||||
|
|
||||||
var padIDs;
|
var padIDs;
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ async.series([
|
||||||
|
|
||||||
function log(str)
|
function log(str)
|
||||||
{
|
{
|
||||||
console.log((new Date().getTime() - startTime)/1000 + "\t" + str);
|
console.log((Date.now() - startTime)/1000 + "\t" + str);
|
||||||
}
|
}
|
||||||
|
|
||||||
var padsDone = 0;
|
var padsDone = 0;
|
||||||
|
@ -121,7 +121,7 @@ function incrementPadStats()
|
||||||
|
|
||||||
if(padsDone%100 == 0)
|
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")
|
log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
console.error("Use: node deletePad.js $PADID");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
//get the padID
|
|
||||||
var padId = process.argv[2];
|
|
||||||
|
|
||||||
var db, padManager, pad, settings;
|
// get the padID
|
||||||
var neededDBValues = ["pad:"+padId];
|
let padId = process.argv[2];
|
||||||
|
|
||||||
var npm = require("../src/node_modules/npm");
|
let npm = require('../src/node_modules/npm');
|
||||||
var async = require("../src/node_modules/async");
|
|
||||||
|
|
||||||
async.series([
|
npm.load({}, async function(er) {
|
||||||
// load npm
|
if (er) {
|
||||||
function(callback) {
|
|
||||||
npm.load({}, function(er) {
|
|
||||||
if(er)
|
|
||||||
{
|
|
||||||
console.error("Could not load NPM: " + er)
|
console.error("Could not load NPM: " + er)
|
||||||
process.exit(1);
|
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){
|
try {
|
||||||
callback(err);
|
let settings = require('../src/node/utils/Settings');
|
||||||
});
|
let db = require('../src/node/db/DB');
|
||||||
callback();
|
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);
|
||||||
}
|
}
|
||||||
], function (err)
|
process.exit(1);
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("Finished deleting padId: "+padId);
|
|
||||||
process.exit();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
console.error("Use: node extractPadData.js $PADID");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
//get the padID
|
|
||||||
var padId = process.argv[2];
|
|
||||||
|
|
||||||
var db, dirty, padManager, pad, settings;
|
// get the padID
|
||||||
var neededDBValues = ["pad:"+padId];
|
let padId = process.argv[2];
|
||||||
|
|
||||||
var npm = require("../node_modules/ep_etherpad-lite/node_modules/npm");
|
let npm = require('../src/node_modules/npm');
|
||||||
var async = require("../node_modules/ep_etherpad-lite/node_modules/async");
|
|
||||||
|
|
||||||
async.series([
|
npm.load({}, async function(er) {
|
||||||
// load npm
|
if (er) {
|
||||||
function(callback) {
|
|
||||||
npm.load({}, function(er) {
|
|
||||||
if(er)
|
|
||||||
{
|
|
||||||
console.error("Could not load NPM: " + er)
|
console.error("Could not load NPM: " + er)
|
||||||
process.exit(1);
|
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)
|
try {
|
||||||
{
|
// initialize database
|
||||||
pad = _pad;
|
let settings = require('../src/node/utils/Settings');
|
||||||
callback(err);
|
let db = require('../src/node/db/DB');
|
||||||
});
|
await db.init();
|
||||||
},
|
|
||||||
function (callback)
|
// load extra modules
|
||||||
{
|
let dirtyDB = require('../src/node_modules/dirty');
|
||||||
//add all authors
|
let padManager = require('../src/node/db/PadManager');
|
||||||
var authors = pad.getAllAuthors();
|
let util = require('util');
|
||||||
for(var i=0;i<authors.length;i++)
|
|
||||||
{
|
// initialize output database
|
||||||
neededDBValues.push("globalAuthor:" + authors[i]);
|
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 revisions
|
// add all chat values
|
||||||
var revHead = pad.head;
|
for (let chat = 0; chat <= pad.chatHead; ++chat) {
|
||||||
for(var i=0;i<=revHead;i++)
|
neededDBValues.push('pad:' + padId + ':chat:' + chat);
|
||||||
{
|
|
||||||
neededDBValues.push("pad:"+padId+":revs:" + i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//get all chat values
|
for (let dbkey of neededDBValues) {
|
||||||
var chatHead = pad.chatHead;
|
let dbvalue = await get(dbkey);
|
||||||
for(var i=0;i<=chatHead;i++)
|
if (dbvalue && typeof dbvalue !== 'object') {
|
||||||
{
|
dbvalue = JSON.parse(dbvalue);
|
||||||
neededDBValues.push("pad:"+padId+":chat:" + i);
|
}
|
||||||
|
await set(dbkey, dbvalue);
|
||||||
}
|
}
|
||||||
|
|
||||||
//get and set all values
|
console.log('finished');
|
||||||
async.forEach(neededDBValues, function(dbkey, callback)
|
process.exit(0);
|
||||||
{
|
} catch (er) {
|
||||||
db.db.db.wrappedDB.get(dbkey, function(err, dbvalue)
|
console.error(er);
|
||||||
{
|
process.exit(1);
|
||||||
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);
|
|
||||||
}
|
|
||||||
], function (err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("finished");
|
|
||||||
process.exit();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//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
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
var startTime = new Date().getTime();
|
var startTime = Date.now();
|
||||||
|
|
||||||
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
|
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)
|
function log(str)
|
||||||
{
|
{
|
||||||
console.log((new Date().getTime() - startTime)/1000 + "\t" + str);
|
console.log((Date.now() - startTime)/1000 + "\t" + str);
|
||||||
}
|
}
|
||||||
|
|
||||||
unescape = function(val) {
|
unescape = function(val) {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# minimum required node version
|
# minimum required node version
|
||||||
REQUIRED_NODE_MAJOR=6
|
REQUIRED_NODE_MAJOR=8
|
||||||
REQUIRED_NODE_MINOR=9
|
REQUIRED_NODE_MINOR=9
|
||||||
|
|
||||||
# minimum required npm version
|
# minimum required npm version
|
||||||
REQUIRED_NPM_MAJOR=3
|
REQUIRED_NPM_MAJOR=5
|
||||||
REQUIRED_NPM_MINOR=10
|
REQUIRED_NPM_MINOR=5
|
||||||
|
|
||||||
require_minimal_version() {
|
require_minimal_version() {
|
||||||
PROGRAM_LABEL="$1"
|
PROGRAM_LABEL="$1"
|
||||||
|
|
136
bin/repairPad.js
136
bin/repairPad.js
|
@ -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!");
|
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");
|
console.error("Use: node bin/repairPad.js $PADID");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
//get the padID
|
|
||||||
|
// get the padID
|
||||||
var padId = process.argv[2];
|
var padId = process.argv[2];
|
||||||
|
|
||||||
var db, padManager, pad, settings;
|
let npm = require("../src/node_modules/npm");
|
||||||
var neededDBValues = ["pad:"+padId];
|
npm.load({}, async function(er) {
|
||||||
|
if (er) {
|
||||||
var npm = require("../src/node_modules/npm");
|
|
||||||
var async = require("../src/node_modules/async");
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
// load npm
|
|
||||||
function(callback) {
|
|
||||||
npm.load({}, function(er) {
|
|
||||||
if(er)
|
|
||||||
{
|
|
||||||
console.error("Could not load NPM: " + er)
|
console.error("Could not load NPM: " + er)
|
||||||
process.exit(1);
|
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)
|
try {
|
||||||
{
|
// intialize database
|
||||||
pad = _pad;
|
let settings = require('../src/node/utils/Settings');
|
||||||
callback(err);
|
let db = require('../src/node/db/DB');
|
||||||
});
|
await db.init();
|
||||||
},
|
|
||||||
function (callback)
|
// get the pad
|
||||||
{
|
let padManager = require('../src/node/db/PadManager');
|
||||||
//add all authors
|
let pad = await padManager.getPad(padId);
|
||||||
var authors = pad.getAllAuthors();
|
|
||||||
for(var i=0;i<authors.length;i++)
|
// accumulate the required keys
|
||||||
{
|
let neededDBValues = ["pad:" + padId];
|
||||||
neededDBValues.push("globalAuthor:" + authors[i]);
|
|
||||||
|
// 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
|
// add all chat values
|
||||||
var revHead = pad.head;
|
for (let chat = 0; chat <= pad.chatHead; ++chat) {
|
||||||
for(var i=0;i<=revHead;i++)
|
neededDBValues.push("pad:" + padId + ":chat:" + chat);
|
||||||
{
|
|
||||||
neededDBValues.push("pad:"+padId+":revs:" + i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//get all chat values
|
//
|
||||||
var chatHead = pad.chatHead;
|
// NB: this script doesn't actually does what's documented
|
||||||
for(var i=0;i<=chatHead;i++)
|
// since the `value` fields in the following `.forEach`
|
||||||
{
|
// block are just the array index numbers
|
||||||
neededDBValues.push("pad:"+padId+":chat:" + i);
|
//
|
||||||
}
|
// the script therefore craps out now before it can do
|
||||||
callback();
|
// any damage.
|
||||||
},
|
//
|
||||||
function (callback) {
|
// See gitlab issue #3545
|
||||||
db = db.db;
|
//
|
||||||
|
console.info("aborting [gitlab #3545]");
|
||||||
|
process.exit(1);
|
||||||
|
|
||||||
|
// now fetch and reinsert every key
|
||||||
neededDBValues.forEach(function(key, value) {
|
neededDBValues.forEach(function(key, value) {
|
||||||
console.debug("Key: "+key+", value: "+value);
|
console.log("Key: " + key+ ", value: " + value);
|
||||||
db.remove(key);
|
db.remove(key);
|
||||||
db.set(key, value);
|
db.set(key, value);
|
||||||
});
|
});
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function (err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.info("finished");
|
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
|
|
||||||
|
|
|
@ -67,7 +67,28 @@ The current version can be queried via /api.
|
||||||
|
|
||||||
### Request Format
|
### 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>¶m1=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
|
### Response Format
|
||||||
Responses are valid JSON in the following format:
|
Responses are valid JSON in the following format:
|
||||||
|
@ -278,7 +299,9 @@ returns the text of a pad
|
||||||
#### setText(padID, text)
|
#### setText(padID, text)
|
||||||
* API >= 1
|
* 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:*
|
*Example returns:*
|
||||||
* `{code: 0, message:"ok", data: null}`
|
* `{code: 0, message:"ok", data: null}`
|
||||||
|
@ -288,7 +311,9 @@ sets the text of a pad
|
||||||
#### appendText(padID, text)
|
#### appendText(padID, text)
|
||||||
* API >= 1.2.13
|
* 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:*
|
*Example returns:*
|
||||||
* `{code: 0, message:"ok", data: null}`
|
* `{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.
|
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:*
|
*Example returns:*
|
||||||
* `{code: 0, message:"ok", data: null}`
|
* `{code: 0, message:"ok", data: null}`
|
||||||
* `{code: 1, message:"padID does not exist", 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:*
|
*Example returns:*
|
||||||
* `{ "code" : 0,
|
* `{ "code" : 0,
|
||||||
"message" : "ok",
|
"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":"padID does not exist","data":null}`
|
||||||
* `{"code":1,"message":"rev is higher than the head revision of the pad","data":null}`
|
* `{"code":1,"message":"rev is higher than the head revision of the pad","data":null}`
|
||||||
|
|
|
@ -115,7 +115,7 @@ Your plugin must also contain a [package definition file](https://docs.npmjs.com
|
||||||
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
|
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
|
||||||
"contributors": [],
|
"contributors": [],
|
||||||
"dependencies": {"MODULE": "0.3.20"},
|
"dependencies": {"MODULE": "0.3.20"},
|
||||||
"engines": { "node": ">= 6.9.0"}
|
"engines": { "node": ">= 8.9.0"}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
65
docker/Dockerfile
Normal file
65
docker/Dockerfile
Normal 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
120
docker/README.md
Normal 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
475
docker/settings.json
Normal 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
|
||||||
|
}
|
|
@ -3,8 +3,59 @@
|
||||||
*
|
*
|
||||||
* Please edit settings.json, not settings.json.template
|
* Please edit settings.json, not settings.json.template
|
||||||
*
|
*
|
||||||
* Please note that since Etherpad 1.6.0 you can store DB credentials in a
|
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
|
||||||
* separate file (credentials.json).
|
* 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 and port which etherpad should bind at
|
||||||
*/
|
*/
|
||||||
"ip": "0.0.0.0",
|
"ip": "0.0.0.0",
|
||||||
"port" : 9001,
|
"port": 9001,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Option to hide/show the settings.json in admin page.
|
* Option to hide/show the settings.json in admin page.
|
||||||
*
|
*
|
||||||
* Default option is set to true
|
* Default option is set to true
|
||||||
*/
|
*/
|
||||||
"showSettingsInAdminPage" : true,
|
"showSettingsInAdminPage": true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Node native SSL support
|
* Node native SSL support
|
||||||
|
@ -78,9 +129,9 @@
|
||||||
* https://www.npmjs.com/package/ueberdb2
|
* https://www.npmjs.com/package/ueberdb2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"dbType" : "dirty",
|
"dbType": "dirty",
|
||||||
"dbSettings" : {
|
"dbSettings": {
|
||||||
"filename" : "var/dirty.db"
|
"filename": "var/dirty.db"
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -92,12 +143,12 @@
|
||||||
/*
|
/*
|
||||||
"dbType" : "mysql",
|
"dbType" : "mysql",
|
||||||
"dbSettings" : {
|
"dbSettings" : {
|
||||||
"user" : "etherpaduser",
|
"user": "etherpaduser",
|
||||||
"host" : "localhost",
|
"host": "localhost",
|
||||||
"port" : 3306,
|
"port": 3306,
|
||||||
"password": "PASSWORD",
|
"password": "PASSWORD",
|
||||||
"database": "etherpad_lite_db",
|
"database": "etherpad_lite_db",
|
||||||
"charset" : "utf8mb4"
|
"charset": "utf8mb4"
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -129,40 +180,40 @@
|
||||||
* Pad Shortcut Keys
|
* Pad Shortcut Keys
|
||||||
*/
|
*/
|
||||||
"padShortcutEnabled" : {
|
"padShortcutEnabled" : {
|
||||||
"altF9" : true, /* focus on the File Menu and/or editbar */
|
"altF9": true, /* focus on the File Menu and/or editbar */
|
||||||
"altC" : true, /* focus on the Chat window */
|
"altC": true, /* focus on the Chat window */
|
||||||
"cmdShift2" : true, /* shows a gritter popup showing a line author */
|
"cmdShift2": true, /* shows a gritter popup showing a line author */
|
||||||
"delete" : true,
|
"delete": true,
|
||||||
"return" : true,
|
"return": true,
|
||||||
"esc" : true, /* in mozilla versions 14-19 avoid reconnecting pad */
|
"esc": true, /* in mozilla versions 14-19 avoid reconnecting pad */
|
||||||
"cmdS" : true, /* save a revision */
|
"cmdS": true, /* save a revision */
|
||||||
"tab" : true, /* indent */
|
"tab": true, /* indent */
|
||||||
"cmdZ" : true, /* undo/redo */
|
"cmdZ": true, /* undo/redo */
|
||||||
"cmdY" : true, /* redo */
|
"cmdY": true, /* redo */
|
||||||
"cmdI" : true, /* italic */
|
"cmdI": true, /* italic */
|
||||||
"cmdB" : true, /* bold */
|
"cmdB": true, /* bold */
|
||||||
"cmdU" : true, /* underline */
|
"cmdU": true, /* underline */
|
||||||
"cmd5" : true, /* strike through */
|
"cmd5": true, /* strike through */
|
||||||
"cmdShiftL" : true, /* unordered list */
|
"cmdShiftL": true, /* unordered list */
|
||||||
"cmdShiftN" : true, /* ordered list */
|
"cmdShiftN": true, /* ordered list */
|
||||||
"cmdShift1" : true, /* ordered list */
|
"cmdShift1": true, /* ordered list */
|
||||||
"cmdShiftC" : true, /* clear authorship */
|
"cmdShiftC": true, /* clear authorship */
|
||||||
"cmdH" : true, /* backspace */
|
"cmdH": true, /* backspace */
|
||||||
"ctrlHome" : true, /* scroll to top of pad */
|
"ctrlHome": true, /* scroll to top of pad */
|
||||||
"pageUp" : true,
|
"pageUp": true,
|
||||||
"pageDown" : true
|
"pageDown": true
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Should we suppress errors from being visible in the default Pad Text?
|
* 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.
|
* If this option is enabled, a user must have a session to access pads.
|
||||||
* This effectively allows only group pads to be accessed.
|
* This effectively allows only group pads to be accessed.
|
||||||
*/
|
*/
|
||||||
"requireSession" : false,
|
"requireSession": false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Users may edit pads but not create new ones.
|
* Users may edit pads but not create new ones.
|
||||||
|
@ -170,13 +221,13 @@
|
||||||
* Pad creation is only via the API.
|
* Pad creation is only via the API.
|
||||||
* This applies both to group pads and regular pads.
|
* 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
|
* If set to true, those users who have a valid session will automatically be
|
||||||
* granted access to password protected pads.
|
* granted access to password protected pads.
|
||||||
*/
|
*/
|
||||||
"sessionNoPassword" : false,
|
"sessionNoPassword": false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If true, all css & js will be minified before sending to the client.
|
* 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
|
* This will improve the loading performance massively, but makes it difficult
|
||||||
* to debug the javascript/css
|
* to debug the javascript/css
|
||||||
*/
|
*/
|
||||||
"minify" : true,
|
"minify": true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* How long may clients use served javascript code (in seconds)?
|
* How long may clients use served javascript code (in seconds)?
|
||||||
|
@ -192,7 +243,7 @@
|
||||||
* Not setting this may cause problems during deployment.
|
* Not setting this may cause problems during deployment.
|
||||||
* Set to 0 to disable caching.
|
* 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.
|
* Absolute path to the Abiword executable.
|
||||||
|
@ -201,7 +252,7 @@
|
||||||
* it to null disables Abiword and will only allow plain text and HTML
|
* it to null disables Abiword and will only allow plain text and HTML
|
||||||
* import/exports.
|
* import/exports.
|
||||||
*/
|
*/
|
||||||
"abiword" : null,
|
"abiword": null,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This is the absolute path to the soffice executable.
|
* This is the absolute path to the soffice executable.
|
||||||
|
@ -209,7 +260,7 @@
|
||||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||||
* Setting it to null disables LibreOffice exporting.
|
* Setting it to null disables LibreOffice exporting.
|
||||||
*/
|
*/
|
||||||
"soffice" : null,
|
"soffice": null,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Path to the Tidy executable.
|
* Path to the Tidy executable.
|
||||||
|
@ -217,35 +268,35 @@
|
||||||
* Tidy is used to improve the quality of exported pads.
|
* Tidy is used to improve the quality of exported pads.
|
||||||
* Setting it to null disables Tidy.
|
* Setting it to null disables Tidy.
|
||||||
*/
|
*/
|
||||||
"tidyHtml" : null,
|
"tidyHtml": null,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allow import of file types other than the supported ones:
|
* Allow import of file types other than the supported ones:
|
||||||
* txt, doc, docx, rtf, odt, html & htm
|
* txt, doc, docx, rtf, odt, html & htm
|
||||||
*/
|
*/
|
||||||
"allowUnknownFileEnds" : true,
|
"allowUnknownFileEnds": true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This setting is used if you require authentication of all users.
|
* This setting is used if you require authentication of all users.
|
||||||
*
|
*
|
||||||
* Note: "/admin" always requires authentication.
|
* Note: "/admin" always requires authentication.
|
||||||
*/
|
*/
|
||||||
"requireAuthentication" : false,
|
"requireAuthentication": false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Require authorization by a module, or a user with is_admin set, see below.
|
* 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.
|
* When you use NGINX or another proxy/load-balancer set this to true.
|
||||||
*/
|
*/
|
||||||
"trustProxy" : false,
|
"trustProxy": false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Privacy: disable IP logging
|
* Privacy: disable IP logging
|
||||||
*/
|
*/
|
||||||
"disableIPlogging" : false,
|
"disableIPlogging": false,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
|
* Time (in seconds) to automatically reconnect pad when a "Force reconnect"
|
||||||
|
@ -253,7 +304,7 @@
|
||||||
*
|
*
|
||||||
* Set to 0 to disable automatic reconnection.
|
* Set to 0 to disable automatic reconnection.
|
||||||
*/
|
*/
|
||||||
"automaticReconnectionTimeout" : 0,
|
"automaticReconnectionTimeout": 0,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* By default, when caret is moved out of viewport, it scrolls the minimum
|
* By default, when caret is moved out of viewport, it scrolls the minimum
|
||||||
|
@ -310,12 +361,14 @@
|
||||||
/*
|
/*
|
||||||
"users": {
|
"users": {
|
||||||
"admin": {
|
"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",
|
"password": "changeme1",
|
||||||
"is_admin": true
|
"is_admin": true
|
||||||
},
|
},
|
||||||
"user": {
|
"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",
|
"password": "changeme1",
|
||||||
"is_admin": false
|
"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.
|
* The log level we are using.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,16 +15,16 @@
|
||||||
},
|
},
|
||||||
"index.newPad": "باد جديد",
|
"index.newPad": "باد جديد",
|
||||||
"index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:",
|
"index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:",
|
||||||
"pad.toolbar.bold.title": "سميك (Ctrl-B)",
|
"pad.toolbar.bold.title": "سميك (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "مائل (Ctrl-I)",
|
"pad.toolbar.italic.title": "مائل (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "تسطير (Ctrl-U)",
|
"pad.toolbar.underline.title": "تسطير (Ctrl+U)",
|
||||||
"pad.toolbar.strikethrough.title": "شطب (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "شطب (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "قائمة مرتبة (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "قائمة مرتبة (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "قائمة غير مرتبة (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "قائمة غير مرتبة (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "إزاحة",
|
"pad.toolbar.indent.title": "إزاحة (TAB)",
|
||||||
"pad.toolbar.unindent.title": "حذف الإزاحة",
|
"pad.toolbar.unindent.title": "حذف الإزاحة (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "فك (Ctrl-Z)",
|
"pad.toolbar.undo.title": "فك (Ctrl+Z)",
|
||||||
"pad.toolbar.redo.title": "تكرار (Ctrl-Y)",
|
"pad.toolbar.redo.title": "تكرار (Ctrl+Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "مسح ألوان التأليف (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "مسح ألوان التأليف (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "استيراد/تصدير من/إلى تنسيقات ملفات مختلفة",
|
"pad.toolbar.import_export.title": "استيراد/تصدير من/إلى تنسيقات ملفات مختلفة",
|
||||||
"pad.toolbar.timeslider.title": "متصفح التاريخ",
|
"pad.toolbar.timeslider.title": "متصفح التاريخ",
|
||||||
|
@ -32,13 +32,13 @@
|
||||||
"pad.toolbar.settings.title": "الإعدادات",
|
"pad.toolbar.settings.title": "الإعدادات",
|
||||||
"pad.toolbar.embed.title": "تبادل و تضمين هذا الباد",
|
"pad.toolbar.embed.title": "تبادل و تضمين هذا الباد",
|
||||||
"pad.toolbar.showusers.title": "عرض المستخدمين على هذا الباد",
|
"pad.toolbar.showusers.title": "عرض المستخدمين على هذا الباد",
|
||||||
"pad.colorpicker.save": "تسجيل",
|
"pad.colorpicker.save": "حفظ",
|
||||||
"pad.colorpicker.cancel": "إلغاء",
|
"pad.colorpicker.cancel": "إلغاء",
|
||||||
"pad.loading": "جارٍ التحميل...",
|
"pad.loading": "جارٍ التحميل...",
|
||||||
"pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!",
|
"pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!",
|
||||||
"pad.passwordRequired": "تحتاج إلى كلمة مرور للوصول إلى هذا الباد",
|
"pad.passwordRequired": "تحتاج إلى كلمة سر للوصول إلى هذا الباد",
|
||||||
"pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد",
|
"pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد",
|
||||||
"pad.wrongPassword": "كانت كلمة المرور خاطئة",
|
"pad.wrongPassword": "كانت كلمة السر خاطئة",
|
||||||
"pad.settings.padSettings": "إعدادات الباد",
|
"pad.settings.padSettings": "إعدادات الباد",
|
||||||
"pad.settings.myView": "رؤيتي",
|
"pad.settings.myView": "رؤيتي",
|
||||||
"pad.settings.stickychat": "الدردشة دائما على الشاشة",
|
"pad.settings.stickychat": "الدردشة دائما على الشاشة",
|
||||||
|
@ -63,9 +63,9 @@
|
||||||
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)",
|
"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.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.connected": "متصل.",
|
||||||
"pad.modals.reconnecting": "إعادة الاتصال ببادك",
|
"pad.modals.reconnecting": "إعادة الاتصال ببادك..",
|
||||||
"pad.modals.forcereconnect": "فرض إعادة الاتصال",
|
"pad.modals.forcereconnect": "فرض إعادة الاتصال",
|
||||||
"pad.modals.reconnecttimer": "حاول إعادة الاتصال",
|
"pad.modals.reconnecttimer": "جاري محاولة إعادة الاتصال",
|
||||||
"pad.modals.cancel": "إلغاء",
|
"pad.modals.cancel": "إلغاء",
|
||||||
"pad.modals.userdup": "مفتوح في نافذة أخرى",
|
"pad.modals.userdup": "مفتوح في نافذة أخرى",
|
||||||
"pad.modals.userdup.explanation": "يبدو أن هذا الباد تم فتحه في أكثر من نافذة متصفح في هذا الحاسوب.",
|
"pad.modals.userdup.explanation": "يبدو أن هذا الباد تم فتحه في أكثر من نافذة متصفح في هذا الحاسوب.",
|
||||||
|
@ -74,9 +74,9 @@
|
||||||
"pad.modals.unauth.explanation": "لقد تغيرت الأذونات الخاصة بك أثناء عرض هذه الصفحة. أعد محاولة الاتصال.",
|
"pad.modals.unauth.explanation": "لقد تغيرت الأذونات الخاصة بك أثناء عرض هذه الصفحة. أعد محاولة الاتصال.",
|
||||||
"pad.modals.looping.explanation": "هناك مشاكل في الاتصال مع ملقم التزامن.",
|
"pad.modals.looping.explanation": "هناك مشاكل في الاتصال مع ملقم التزامن.",
|
||||||
"pad.modals.looping.cause": "ربما كنت متصلاً من خلال وكيل أو جدار حماية غير متوافق.",
|
"pad.modals.looping.cause": "ربما كنت متصلاً من خلال وكيل أو جدار حماية غير متوافق.",
|
||||||
"pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم",
|
"pad.modals.initsocketfail": "لا يمكن الوصول إلى الخادم.",
|
||||||
"pad.modals.initsocketfail.explanation": "تعذر الاتصال بخادم المزامنة.",
|
"pad.modals.initsocketfail.explanation": "تعذر الاتصال بخادم المزامنة.",
|
||||||
"pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بإنترنت.",
|
"pad.modals.initsocketfail.cause": "هذا على الأرجح بسبب مشكلة في المستعرض الخاص بك أو الاتصال بالإنترنت.",
|
||||||
"pad.modals.slowcommit.explanation": "الخادم لا يستجيب.",
|
"pad.modals.slowcommit.explanation": "الخادم لا يستجيب.",
|
||||||
"pad.modals.slowcommit.cause": "يمكن أن يكون هذا بسبب مشاكل في الاتصال بالشبكة.",
|
"pad.modals.slowcommit.cause": "يمكن أن يكون هذا بسبب مشاكل في الاتصال بالشبكة.",
|
||||||
"pad.modals.badChangeset.explanation": "لقد صُنفَت إحدى عمليات التحرير التي قمت بها كعملية غير مسموح بها من قبل ملقم التزامن.",
|
"pad.modals.badChangeset.explanation": "لقد صُنفَت إحدى عمليات التحرير التي قمت بها كعملية غير مسموح بها من قبل ملقم التزامن.",
|
||||||
|
@ -84,17 +84,19 @@
|
||||||
"pad.modals.corruptPad.explanation": "الباد الذي تحاول الوصول إليه تالف.",
|
"pad.modals.corruptPad.explanation": "الباد الذي تحاول الوصول إليه تالف.",
|
||||||
"pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.",
|
"pad.modals.corruptPad.cause": "قد يكون هذا بسبب تكوين ملقم خاطئ أو بسبب سلوك آخر غير متوقع. يرجى الاتصال بمسؤول الخدمة.",
|
||||||
"pad.modals.deleted": "محذوف.",
|
"pad.modals.deleted": "محذوف.",
|
||||||
"pad.modals.deleted.explanation": "تمت إزالة هذا الباد",
|
"pad.modals.deleted.explanation": "تمت إزالة هذا الباد.",
|
||||||
"pad.modals.disconnected": "لم تعد متصلا.",
|
"pad.modals.disconnected": "لم تعد متصلا.",
|
||||||
"pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم",
|
"pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم",
|
||||||
"pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.",
|
"pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.",
|
||||||
"pad.share": "شارك هذه الباد",
|
"pad.share": "شارك هذه الباد",
|
||||||
"pad.share.readonly": "للقراءة فقط",
|
"pad.share.readonly": "للقراءة فقط",
|
||||||
"pad.share.link": "رابط",
|
"pad.share.link": "وصلة",
|
||||||
"pad.share.emebdcode": "URL للتضمين",
|
"pad.share.emebdcode": "URL للتضمين",
|
||||||
"pad.chat": "دردشة",
|
"pad.chat": "دردشة",
|
||||||
"pad.chat.title": "فتح الدردشة لهذا الباد",
|
"pad.chat.title": "فتح الدردشة لهذا الباد.",
|
||||||
"pad.chat.loadmessages": "تحميل المزيد من الرسائل",
|
"pad.chat.loadmessages": "تحميل المزيد من الرسائل",
|
||||||
|
"pad.chat.stick.title": "ألصق الدردشة بالشاشة",
|
||||||
|
"pad.chat.writeMessage.placeholder": "اكتب رسالتك هنا",
|
||||||
"timeslider.pageTitle": "{{appTitle}} متصفح التاريخ",
|
"timeslider.pageTitle": "{{appTitle}} متصفح التاريخ",
|
||||||
"timeslider.toolbar.returnbutton": "العودة إلى الباد",
|
"timeslider.toolbar.returnbutton": "العودة إلى الباد",
|
||||||
"timeslider.toolbar.authors": "المؤلفون:",
|
"timeslider.toolbar.authors": "المؤلفون:",
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"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.connected": "Coneutáu.",
|
||||||
"pad.modals.reconnecting": "Reconeutando col to bloc...",
|
"pad.modals.reconnecting": "Reconeutando col to bloc...",
|
||||||
"pad.modals.forcereconnect": "Forzar la reconexón",
|
"pad.modals.forcereconnect": "Forzar la reconexón",
|
||||||
|
@ -86,6 +86,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Abrir el chat d'esti bloc.",
|
"pad.chat.title": "Abrir el chat d'esti bloc.",
|
||||||
"pad.chat.loadmessages": "Cargar más mensaxes",
|
"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.pageTitle": "Eslizador de tiempu de {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Tornar al bloc",
|
"timeslider.toolbar.returnbutton": "Tornar al bloc",
|
||||||
"timeslider.toolbar.authors": "Autores:",
|
"timeslider.toolbar.authors": "Autores:",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Baloch Afghanistan"
|
"Baloch Afghanistan",
|
||||||
|
"Sultanselim baloch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "دفترچه یادداشت تازه",
|
"index.newPad": "دفترچه یادداشت تازه",
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
"pad.permissionDenied": "شرمنده، شما را اجازت په دسترسی ای صفحه نیست.",
|
"pad.permissionDenied": "شرمنده، شما را اجازت په دسترسی ای صفحه نیست.",
|
||||||
"pad.wrongPassword": "گذرواژهی شما درست نیست",
|
"pad.wrongPassword": "گذرواژهی شما درست نیست",
|
||||||
"pad.settings.padSettings": "تنظیمات دفترچه یادداشت",
|
"pad.settings.padSettings": "تنظیمات دفترچه یادداشت",
|
||||||
"pad.settings.myView": "نمای من",
|
"pad.settings.myView": "منی سۏج",
|
||||||
"pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد",
|
"pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد",
|
||||||
"pad.settings.colorcheck": "رنگهای نویسندگی",
|
"pad.settings.colorcheck": "رنگهای نویسندگی",
|
||||||
"pad.settings.linenocheck": "شمارهی خطوط",
|
"pad.settings.linenocheck": "شمارهی خطوط",
|
||||||
|
@ -71,7 +72,7 @@
|
||||||
"pad.modals.corruptPad.cause": "این احتمالاً به دلیل تنظیمات اشتباه کارساز یا سایر رفتارهای غیرمنتظره است. لطفاً با مدیر خدمت تماس حاصل کنید.",
|
"pad.modals.corruptPad.cause": "این احتمالاً به دلیل تنظیمات اشتباه کارساز یا سایر رفتارهای غیرمنتظره است. لطفاً با مدیر خدمت تماس حاصل کنید.",
|
||||||
"pad.modals.deleted": "پاک کورتین",
|
"pad.modals.deleted": "پاک کورتین",
|
||||||
"pad.modals.deleted.explanation": "این دفترچه یادداشت پاک شدهاست.",
|
"pad.modals.deleted.explanation": "این دفترچه یادداشت پاک شدهاست.",
|
||||||
"pad.modals.disconnected": "اتصال شما قطع شدهاست.",
|
"pad.modals.disconnected": "شمئی سکّی کھت اِنت۔",
|
||||||
"pad.modals.disconnected.explanation": "اتصال به سرور قطع شدهاست.",
|
"pad.modals.disconnected.explanation": "اتصال به سرور قطع شدهاست.",
|
||||||
"pad.modals.disconnected.cause": "ممکن است سرور در دسترس نباشد. اگر این مشکل باز هم رخ داد مدیر حدمت را آگاه کنید.",
|
"pad.modals.disconnected.cause": "ممکن است سرور در دسترس نباشد. اگر این مشکل باز هم رخ داد مدیر حدمت را آگاه کنید.",
|
||||||
"pad.share": "به اشتراکگذاری این دفترچه یادداشت",
|
"pad.share": "به اشتراکگذاری این دفترچه یادداشت",
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
"pad.share.emebdcode": "جاسازی نشانی",
|
"pad.share.emebdcode": "جاسازی نشانی",
|
||||||
"pad.chat": "گفتگو",
|
"pad.chat": "گفتگو",
|
||||||
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
|
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
|
||||||
"pad.chat.loadmessages": "بارگیری پیامهای بیشتر",
|
"pad.chat.loadmessages": "گݔشترݔں پیگامء چارگ",
|
||||||
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
|
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
|
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
|
||||||
"timeslider.toolbar.authors": "نویسوک:",
|
"timeslider.toolbar.authors": "نویسوک:",
|
||||||
|
|
|
@ -88,6 +88,8 @@
|
||||||
"pad.chat": "Чат",
|
"pad.chat": "Чат",
|
||||||
"pad.chat.title": "Адкрыць чат для гэтага дакумэнту.",
|
"pad.chat.title": "Адкрыць чат для гэтага дакумэнту.",
|
||||||
"pad.chat.loadmessages": "Загрузіць болей паведамленьняў",
|
"pad.chat.loadmessages": "Загрузіць болей паведамленьняў",
|
||||||
|
"pad.chat.stick.title": "Замацаваць чат на экране",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Напішыце вашае паведамленьне тут",
|
||||||
"timeslider.pageTitle": "Часавая шкала {{appTitle}}",
|
"timeslider.pageTitle": "Часавая шкала {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Вярнуцца да дакумэнту",
|
"timeslider.toolbar.returnbutton": "Вярнуцца да дакумэнту",
|
||||||
"timeslider.toolbar.authors": "Аўтары:",
|
"timeslider.toolbar.authors": "Аўтары:",
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Vodnokon4e",
|
"Vodnokon4e",
|
||||||
"StanProg"
|
"StanProg",
|
||||||
|
"Vlad5250"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Нов пад",
|
"index.newPad": "Нов пад",
|
||||||
|
@ -46,6 +47,8 @@
|
||||||
"pad.chat": "Чат",
|
"pad.chat": "Чат",
|
||||||
"pad.chat.title": "Отваряне на чат за този пад.",
|
"pad.chat.title": "Отваряне на чат за този пад.",
|
||||||
"pad.chat.loadmessages": "Зареждане на повече съобщения",
|
"pad.chat.loadmessages": "Зареждане на повече съобщения",
|
||||||
|
"pad.chat.stick.title": "Залепяне на разговора на екрана",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Тук напишете съобщение",
|
||||||
"timeslider.toolbar.returnbutton": "Връщане към пада",
|
"timeslider.toolbar.returnbutton": "Връщане към пада",
|
||||||
"timeslider.toolbar.authors": "Автори:",
|
"timeslider.toolbar.authors": "Автори:",
|
||||||
"timeslider.toolbar.authorsList": "Няма автори",
|
"timeslider.toolbar.authorsList": "Няма автори",
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"pad.chat": "চ্যাট",
|
"pad.chat": "চ্যাট",
|
||||||
"pad.chat.title": "এই প্যাডের জন্য চ্যাট চালু করুন।",
|
"pad.chat.title": "এই প্যাডের জন্য চ্যাট চালু করুন।",
|
||||||
"pad.chat.loadmessages": "আরও বার্তা লোড করুন",
|
"pad.chat.loadmessages": "আরও বার্তা লোড করুন",
|
||||||
|
"pad.chat.writeMessage.placeholder": "আপনার বার্তাটি এখানে লিখুন",
|
||||||
"timeslider.toolbar.returnbutton": "প্যাডে ফিরে যাও",
|
"timeslider.toolbar.returnbutton": "প্যাডে ফিরে যাও",
|
||||||
"timeslider.toolbar.authors": "লেখকগণ:",
|
"timeslider.toolbar.authors": "লেখকগণ:",
|
||||||
"timeslider.toolbar.authorsList": "কোনো লেখক নেই",
|
"timeslider.toolbar.authorsList": "কোনো লেখক নেই",
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
"pad.toolbar.ul.title": "Listenn en dizurzh (Ktrl+Pennlizherenn+L)",
|
"pad.toolbar.ul.title": "Listenn en dizurzh (Ktrl+Pennlizherenn+L)",
|
||||||
"pad.toolbar.indent.title": "Endantañ (TAB)",
|
"pad.toolbar.indent.title": "Endantañ (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Diendantañ (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Diendantañ (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Dizober (Ktrl-Z)",
|
"pad.toolbar.undo.title": "Dizober (Ktrl+Z)",
|
||||||
"pad.toolbar.redo.title": "Adober (Ktrl-Y)",
|
"pad.toolbar.redo.title": "Adober (Ktrl+Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Diverkañ al livioù oc'h anaout an aozerien (Ktrl+Pennlizherenn+C)",
|
"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.import_export.title": "Enporzhiañ/Ezporzhiañ eus/war-zu ur furmad restr disheñvel",
|
||||||
"pad.toolbar.timeslider.title": "Istor dinamek",
|
"pad.toolbar.timeslider.title": "Istor dinamek",
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
"pad.modals.cancel": "Nullañ",
|
"pad.modals.cancel": "Nullañ",
|
||||||
"pad.modals.userdup": "Digor en ur prenestr all",
|
"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.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": "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.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.",
|
"pad.modals.looping.explanation": "Kudennoù kehentiñ zo gant ar servijer sinkronelekaat.",
|
||||||
|
@ -89,6 +89,8 @@
|
||||||
"pad.chat": "Flap",
|
"pad.chat": "Flap",
|
||||||
"pad.chat.title": "Digeriñ ar flap kevelet gant ar pad-mañ.",
|
"pad.chat.title": "Digeriñ ar flap kevelet gant ar pad-mañ.",
|
||||||
"pad.chat.loadmessages": "Kargañ muioc'h a gemennadennoù",
|
"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.pageTitle": "Istor dinamek eus {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Distreiñ d'ar pad-mañ.",
|
"timeslider.toolbar.returnbutton": "Distreiñ d'ar pad-mañ.",
|
||||||
"timeslider.toolbar.authors": "Aozerien :",
|
"timeslider.toolbar.authors": "Aozerien :",
|
||||||
|
@ -126,7 +128,7 @@
|
||||||
"pad.impexp.importing": "Oc'h enporzhiañ...",
|
"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.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.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.uploadFailed": "C'hwitet eo bet an enporzhiañ. Klaskit en-dro.",
|
||||||
"pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn",
|
"pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn",
|
||||||
"pad.impexp.copypaste": "Eilit/pegit, mar plij",
|
"pad.impexp.copypaste": "Eilit/pegit, mar plij",
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"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.connected": "Connectat.",
|
||||||
"pad.modals.reconnecting": "S'està tornant a connectar al vostre pad…",
|
"pad.modals.reconnecting": "S'està tornant a connectar al vostre pad…",
|
||||||
"pad.modals.forcereconnect": "Força tornar a connectar",
|
"pad.modals.forcereconnect": "Força tornar a connectar",
|
||||||
|
@ -94,6 +94,8 @@
|
||||||
"pad.chat": "Xat",
|
"pad.chat": "Xat",
|
||||||
"pad.chat.title": "Obre el xat d'aquest pad.",
|
"pad.chat.title": "Obre el xat d'aquest pad.",
|
||||||
"pad.chat.loadmessages": "Carrega més missatges",
|
"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.pageTitle": "Línia temporal — {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Torna al pad",
|
"timeslider.toolbar.returnbutton": "Torna al pad",
|
||||||
"timeslider.toolbar.authors": "Autors:",
|
"timeslider.toolbar.authors": "Autors:",
|
||||||
|
|
|
@ -92,6 +92,8 @@
|
||||||
"pad.chat": "Unterhaltung",
|
"pad.chat": "Unterhaltung",
|
||||||
"pad.chat.title": "Den Chat für dieses Pad öffnen.",
|
"pad.chat.title": "Den Chat für dieses Pad öffnen.",
|
||||||
"pad.chat.loadmessages": "Weitere Nachrichten laden",
|
"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.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
|
||||||
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
|
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
|
||||||
"timeslider.toolbar.authors": "Autoren:",
|
"timeslider.toolbar.authors": "Autoren:",
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
"Mirzali",
|
"Mirzali",
|
||||||
"Kumkumuk",
|
"Kumkumuk",
|
||||||
"1917 Ekim Devrimi",
|
"1917 Ekim Devrimi",
|
||||||
"Gırd"
|
"Gırd",
|
||||||
|
"Orbot707"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Pedo newe",
|
"index.newPad": "Bloknoto newe",
|
||||||
"index.createOpenPad": "Yana eno bamaeya bloknot vıraz/ak:",
|
"index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:",
|
||||||
"pad.toolbar.bold.title": "Qalın (Ctrl-B)",
|
"pad.toolbar.bold.title": "Qalınd (Ctrl-B)",
|
||||||
"pad.toolbar.italic.title": "Namıte (Ctrl-I)",
|
"pad.toolbar.italic.title": "Namıte (Ctrl-I)",
|
||||||
"pad.toolbar.underline.title": "Bınxetın (Ctrl-U)",
|
"pad.toolbar.underline.title": "Bınxetın (Ctrl-U)",
|
||||||
"pad.toolbar.strikethrough.title": "Serxetın (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Serxetın (Ctrl+5)",
|
||||||
|
@ -20,12 +21,12 @@
|
||||||
"pad.toolbar.indent.title": "Serrêze (TAB)",
|
"pad.toolbar.indent.title": "Serrêze (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Teberdayış (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Teberdayış (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Meke (Ctrl-Z)",
|
"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.clearAuthorship.title": "Rengê Nuştoğiê Arıstey (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "Babaetna tewranê dosyaya azere/ateber ke",
|
"pad.toolbar.import_export.title": "Babaetna tewranê dosyaya azere/ateber ke",
|
||||||
"pad.toolbar.timeslider.title": "Ğızagê zemani",
|
"pad.toolbar.timeslider.title": "Ğızagê zemani",
|
||||||
"pad.toolbar.savedRevision.title": "Çımraviyarnayışi qeyd ke",
|
"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.embed.title": "Na bloknot degusn u bıhesrne",
|
||||||
"pad.toolbar.showusers.title": "Karbera ena bloknot dı bımotné",
|
"pad.toolbar.showusers.title": "Karbera ena bloknot dı bımotné",
|
||||||
"pad.colorpicker.save": "Qeyd ke",
|
"pad.colorpicker.save": "Qeyd ke",
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"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.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.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
|
||||||
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
|
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
|
||||||
"pad.modals.reconnecttimer": "Anciya gırê beno",
|
"pad.modals.reconnecttimer": "Anciya gırê beno",
|
||||||
|
@ -91,6 +92,8 @@
|
||||||
"pad.chat": "Mıhebet",
|
"pad.chat": "Mıhebet",
|
||||||
"pad.chat.title": "Qandê ena ped mıhebet ake.",
|
"pad.chat.title": "Qandê ena ped mıhebet ake.",
|
||||||
"pad.chat.loadmessages": "Dehana zaf mesaci bar keri",
|
"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.pageTitle": "Ğızagê zemani {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Peyser şo ped",
|
"timeslider.toolbar.returnbutton": "Peyser şo ped",
|
||||||
"timeslider.toolbar.authors": "Nuştoği:",
|
"timeslider.toolbar.authors": "Nuştoği:",
|
||||||
|
@ -104,7 +107,7 @@
|
||||||
"timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini",
|
"timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini",
|
||||||
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "Çele",
|
"timeslider.month.january": "Çele",
|
||||||
"timeslider.month.february": "Zemherı",
|
"timeslider.month.february": "Sıbate",
|
||||||
"timeslider.month.march": "Adar",
|
"timeslider.month.march": "Adar",
|
||||||
"timeslider.month.april": "Nisane",
|
"timeslider.month.april": "Nisane",
|
||||||
"timeslider.month.may": "Gulane",
|
"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.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.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.entername": "Namey xo cıkewe",
|
||||||
"pad.userlist.unnamed": "Name nébıyo",
|
"pad.userlist.unnamed": "bêname",
|
||||||
"pad.userlist.guest": "Meyman",
|
"pad.userlist.guest": "Meyman",
|
||||||
"pad.userlist.deny": "Red ke",
|
"pad.userlist.deny": "Red ke",
|
||||||
"pad.userlist.approve": "Tesdiq ke",
|
"pad.userlist.approve": "Tesdiq ke",
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Μορφή Open Document)",
|
"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.connected": "Συνδεμένοι.",
|
||||||
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας...",
|
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας...",
|
||||||
"pad.modals.forcereconnect": "Επιβολή επανασύνδεσης",
|
"pad.modals.forcereconnect": "Επιβολή επανασύνδεσης",
|
||||||
|
@ -90,6 +90,8 @@
|
||||||
"pad.chat": "Συνομιλία",
|
"pad.chat": "Συνομιλία",
|
||||||
"pad.chat.title": "Άνοιγμα της συνομιλίας για αυτό το pad.",
|
"pad.chat.title": "Άνοιγμα της συνομιλίας για αυτό το pad.",
|
||||||
"pad.chat.loadmessages": "Φόρτωση περισσότερων μηνυμάτων",
|
"pad.chat.loadmessages": "Φόρτωση περισσότερων μηνυμάτων",
|
||||||
|
"pad.chat.stick.title": "Κρατήστε τη συνομιλία στην οθόνη",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Γράψτε το μήνυμα σας εδώ",
|
||||||
"timeslider.pageTitle": "{{appTitle}} Χρονοδιάγραμμα",
|
"timeslider.pageTitle": "{{appTitle}} Χρονοδιάγραμμα",
|
||||||
"timeslider.toolbar.returnbutton": "Επιστροφή στο pad",
|
"timeslider.toolbar.returnbutton": "Επιστροφή στο pad",
|
||||||
"timeslider.toolbar.authors": "Συντάκτες:",
|
"timeslider.toolbar.authors": "Συντάκτες:",
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"Chase me ladies, I'm the Cavalry",
|
"Chase me ladies, I'm the Cavalry",
|
||||||
"Shirayuki",
|
"Shirayuki",
|
||||||
"Andibing",
|
"Andibing",
|
||||||
"HairyFotr"
|
"HairyFotr",
|
||||||
|
"Cblair91"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "New Pad",
|
"index.newPad": "New Pad",
|
||||||
|
@ -89,6 +90,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Open the chat for this pad.",
|
"pad.chat.title": "Open the chat for this pad.",
|
||||||
"pad.chat.loadmessages": "Load more messages",
|
"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.pageTitle": "{{appTitle}} Timeslider",
|
||||||
"timeslider.toolbar.returnbutton": "Return to pad",
|
"timeslider.toolbar.returnbutton": "Return to pad",
|
||||||
"timeslider.toolbar.authors": "Authors:",
|
"timeslider.toolbar.authors": "Authors:",
|
||||||
|
|
|
@ -116,6 +116,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Open the chat for this pad.",
|
"pad.chat.title": "Open the chat for this pad.",
|
||||||
"pad.chat.loadmessages": "Load more messages",
|
"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.pageTitle": "{{appTitle}} Timeslider",
|
||||||
"timeslider.toolbar.returnbutton": "Return to pad",
|
"timeslider.toolbar.returnbutton": "Return to pad",
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"Eliovir",
|
"Eliovir",
|
||||||
"Mschmitt",
|
"Mschmitt",
|
||||||
"Objectivesea",
|
"Objectivesea",
|
||||||
"Robin van der Vliet"
|
"Robin van der Vliet",
|
||||||
|
"Mirin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nova Teksto",
|
"index.newPad": "Nova Teksto",
|
||||||
|
@ -89,6 +90,8 @@
|
||||||
"pad.chat": "Babilejo",
|
"pad.chat": "Babilejo",
|
||||||
"pad.chat.title": "Malfermi la babilejon por ĉi tiu teksto.",
|
"pad.chat.title": "Malfermi la babilejon por ĉi tiu teksto.",
|
||||||
"pad.chat.loadmessages": "Ŝargi pliajn mesaĝojn",
|
"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.pageTitle": "{{appTitle}} Tempoŝovilo",
|
||||||
"timeslider.toolbar.returnbutton": "Reiri al teksto",
|
"timeslider.toolbar.returnbutton": "Reiri al teksto",
|
||||||
"timeslider.toolbar.authors": "Aŭtoroj:",
|
"timeslider.toolbar.authors": "Aŭtoroj:",
|
||||||
|
|
|
@ -99,6 +99,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Abrir el chat para este pad.",
|
"pad.chat.title": "Abrir el chat para este pad.",
|
||||||
"pad.chat.loadmessages": "Cargar más mensajes",
|
"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.pageTitle": "{{appTitle}} Línea de tiempo",
|
||||||
"timeslider.toolbar.returnbutton": "Volver al pad",
|
"timeslider.toolbar.returnbutton": "Volver al pad",
|
||||||
"timeslider.toolbar.authors": "Autores:",
|
"timeslider.toolbar.authors": "Autores:",
|
||||||
|
|
|
@ -91,6 +91,8 @@
|
||||||
"pad.chat": "Txata",
|
"pad.chat": "Txata",
|
||||||
"pad.chat.title": "Pad honetarako txata ireki.",
|
"pad.chat.title": "Pad honetarako txata ireki.",
|
||||||
"pad.chat.loadmessages": "Mezu gehiago kargatu",
|
"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.pageTitle": "{{appTitle}} denbora lerroa",
|
||||||
"timeslider.toolbar.returnbutton": "Padera itzuli",
|
"timeslider.toolbar.returnbutton": "Padera itzuli",
|
||||||
"timeslider.toolbar.authors": "Egileak:",
|
"timeslider.toolbar.authors": "Egileak:",
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"Reza1615",
|
"Reza1615",
|
||||||
"ZxxZxxZ",
|
"ZxxZxxZ",
|
||||||
"الناز",
|
"الناز",
|
||||||
"Omid.koli"
|
"Omid.koli",
|
||||||
|
"FarsiNevis"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "دفترچه یادداشت تازه",
|
"index.newPad": "دفترچه یادداشت تازه",
|
||||||
|
@ -92,6 +93,8 @@
|
||||||
"pad.chat": "گفتگو",
|
"pad.chat": "گفتگو",
|
||||||
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
|
"pad.chat.title": "بازکردن گفتگو برای این دفترچه یادداشت",
|
||||||
"pad.chat.loadmessages": "بارگیری پیامهای بیشتر",
|
"pad.chat.loadmessages": "بارگیری پیامهای بیشتر",
|
||||||
|
"pad.chat.stick.title": "چسباندن چت به صفحه",
|
||||||
|
"pad.chat.writeMessage.placeholder": "پیام خود را اینجا بنویسید",
|
||||||
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
|
"timeslider.pageTitle": "لغزندهٔ زمان {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
|
"timeslider.toolbar.returnbutton": "بازگشت به دفترچه یادداشت",
|
||||||
"timeslider.toolbar.authors": "نویسندگان:",
|
"timeslider.toolbar.authors": "نویسندگان:",
|
||||||
|
|
|
@ -24,11 +24,12 @@
|
||||||
"C13m3n7",
|
"C13m3n7",
|
||||||
"Wladek92",
|
"Wladek92",
|
||||||
"Urhixidur",
|
"Urhixidur",
|
||||||
"Envlh"
|
"Envlh",
|
||||||
|
"Verdy p"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nouveau pad",
|
"index.newPad": "Nouveau bloc-notes",
|
||||||
"index.createOpenPad": "ou créer/ouvrir un pad intitulé :",
|
"index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé :",
|
||||||
"pad.toolbar.bold.title": "Gras (Ctrl+B)",
|
"pad.toolbar.bold.title": "Gras (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "Italique (Ctrl+I)",
|
"pad.toolbar.italic.title": "Italique (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "Souligné (Ctrl+U)",
|
"pad.toolbar.underline.title": "Souligné (Ctrl+U)",
|
||||||
|
@ -39,87 +40,89 @@
|
||||||
"pad.toolbar.unindent.title": "Désindenter (Maj+TAB)",
|
"pad.toolbar.unindent.title": "Désindenter (Maj+TAB)",
|
||||||
"pad.toolbar.undo.title": "Annuler (Ctrl+Z)",
|
"pad.toolbar.undo.title": "Annuler (Ctrl+Z)",
|
||||||
"pad.toolbar.redo.title": "Rétablir (Ctrl+Y)",
|
"pad.toolbar.redo.title": "Rétablir (Ctrl+Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Effacer les couleurs identifiant les auteurs (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "Importer/Exporter de/vers un format de fichier différent",
|
"pad.toolbar.import_export.title": "Importer de/Exporter vers un format de fichier différent",
|
||||||
"pad.toolbar.timeslider.title": "Historique dynamique",
|
"pad.toolbar.timeslider.title": "Historique dynamique",
|
||||||
"pad.toolbar.savedRevision.title": "Enregistrer la révision",
|
"pad.toolbar.savedRevision.title": "Enregistrer la révision",
|
||||||
"pad.toolbar.settings.title": "Paramètres",
|
"pad.toolbar.settings.title": "Paramètres",
|
||||||
"pad.toolbar.embed.title": "Partager et intégrer ce pad",
|
"pad.toolbar.embed.title": "Partager et intégrer ce bloc-notes",
|
||||||
"pad.toolbar.showusers.title": "Afficher les utilisateurs du pad",
|
"pad.toolbar.showusers.title": "Afficher les utilisateurs du bloc-notes",
|
||||||
"pad.colorpicker.save": "Enregistrer",
|
"pad.colorpicker.save": "Enregistrer",
|
||||||
"pad.colorpicker.cancel": "Annuler",
|
"pad.colorpicker.cancel": "Annuler",
|
||||||
"pad.loading": "Chargement…",
|
"pad.loading": "Chargement...",
|
||||||
"pad.noCookie": "Le cookie n’a pas pu être trouvé. Veuillez autoriser les cookies dans votre navigateur !",
|
"pad.noCookie": "Un cookie n’a 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 pad",
|
"pad.passwordRequired": "Vous avez besoin d'un mot de passe pour accéder à ce bloc-note",
|
||||||
"pad.permissionDenied": "Vous n'avez pas la permission d’accéder à ce pad",
|
"pad.permissionDenied": "Vous n’êtes pas autorisé à accéder à ce bloc-notes",
|
||||||
"pad.wrongPassword": "Votre mot de passe est incorrect",
|
"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.myView": "Ma vue",
|
||||||
"pad.settings.stickychat": "Toujours afficher le clavardage",
|
"pad.settings.stickychat": "Toujours afficher le clavardage",
|
||||||
"pad.settings.chatandusers": "Afficher la discussion et les utilisateurs",
|
"pad.settings.chatandusers": "Afficher le clavardage et les utilisateurs",
|
||||||
"pad.settings.colorcheck": "Couleurs d’identification",
|
"pad.settings.colorcheck": "Surlignage par auteur",
|
||||||
"pad.settings.linenocheck": "Numéros de lignes",
|
"pad.settings.linenocheck": "Numéros de lignes",
|
||||||
"pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche ?",
|
"pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche ?",
|
||||||
"pad.settings.fontType": "Police :",
|
"pad.settings.fontType": "Police :",
|
||||||
"pad.settings.fontType.normal": "Normal",
|
"pad.settings.fontType.normal": "Normal",
|
||||||
"pad.settings.fontType.monospaced": "Monospace",
|
"pad.settings.fontType.monospaced": "Monospace",
|
||||||
"pad.settings.globalView": "Vue d’ensemble",
|
"pad.settings.globalView": "Vue d’ensemble",
|
||||||
"pad.settings.language": "Langue :",
|
"pad.settings.language": "Langue :",
|
||||||
"pad.importExport.import_export": "Importer/Exporter",
|
"pad.importExport.import_export": "Importer/Exporter",
|
||||||
"pad.importExport.import": "Charger un texte ou un document",
|
"pad.importExport.import": "Charger un texte ou un document",
|
||||||
"pad.importExport.importSuccessful": "Réussi !",
|
"pad.importExport.importSuccessful": "Réussi !",
|
||||||
"pad.importExport.export": "Exporter le pad actuel comme :",
|
"pad.importExport.export": "Exporter le bloc-notes actuel en :",
|
||||||
"pad.importExport.exportetherpad": "Etherpad",
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
"pad.importExport.exporthtml": "HTML",
|
"pad.importExport.exporthtml": "HTML",
|
||||||
"pad.importExport.exportplain": "Texte brut",
|
"pad.importExport.exportplain": "Texte brut",
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"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 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.modals.connected": "Connecté.",
|
"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.forcereconnect": "Forcer la reconnexion",
|
||||||
"pad.modals.reconnecttimer": "Essai de reconnexion",
|
"pad.modals.reconnecttimer": "Essai de reconnexion",
|
||||||
"pad.modals.cancel": "Annuler",
|
"pad.modals.cancel": "Annuler",
|
||||||
"pad.modals.userdup": "Ouvert dans une autre fenêtre",
|
"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.userdup.advice": "Se reconnecter en utilisant cette fenêtre.",
|
||||||
"pad.modals.unauth": "Non autorisé",
|
"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.unauth.explanation": "Vos autorisations 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.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.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": "Le serveur est introuvable.",
|
||||||
"pad.modals.initsocketfail.explanation": "Impossible de se connecter au serveur de synchronisation.",
|
"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.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.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.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.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 l’administrateur du service si vous pensez que c’est une erreur. Essayez de vous reconnecter pour continuer à modifier.",
|
"pad.modals.badChangeset.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter l’administrateur du service si vous pensez que c’est une erreur. Essayez de vous reconnecter pour continuer à modifier.",
|
||||||
"pad.modals.corruptPad.explanation": "Le pad auquel vous essayez d’accéder est corrompu.",
|
"pad.modals.corruptPad.explanation": "Le bloc-notes auquel vous essayez d’accéder est corrompu.",
|
||||||
"pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter l’administrateur du service.",
|
"pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter l’administrateur du service.",
|
||||||
"pad.modals.deleted": "Supprimé.",
|
"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": "Vous avez été déconnecté.",
|
||||||
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
|
"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 l’administrateur du service.",
|
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer l’administrateur du service.",
|
||||||
"pad.share": "Partager ce pad",
|
"pad.share": "Partager ce bloc-notes",
|
||||||
"pad.share.readonly": "Lecture seule",
|
"pad.share.readonly": "Lecture seule",
|
||||||
"pad.share.link": "Lien",
|
"pad.share.link": "Lien",
|
||||||
"pad.share.emebdcode": "Incorporer un lien",
|
"pad.share.emebdcode": "Incorporer un lien",
|
||||||
"pad.chat": "Clavardage",
|
"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.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.pageTitle": "Historique dynamique de {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Retourner au pad",
|
"timeslider.toolbar.returnbutton": "Retourner au bloc-notes",
|
||||||
"timeslider.toolbar.authors": "Auteurs :",
|
"timeslider.toolbar.authors": "Auteurs :",
|
||||||
"timeslider.toolbar.authorsList": "Aucun auteur",
|
"timeslider.toolbar.authorsList": "Aucun auteur",
|
||||||
"timeslider.toolbar.exportlink.title": "Exporter",
|
"timeslider.toolbar.exportlink.title": "Exporter",
|
||||||
"timeslider.exportCurrent": "Exporter la version actuelle sous :",
|
"timeslider.exportCurrent": "Exporter la version actuelle sous :",
|
||||||
"timeslider.version": "Version {{version}}",
|
"timeslider.version": "Version {{version}}",
|
||||||
"timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}",
|
"timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}",
|
||||||
"timeslider.playPause": "Lecture / Pause des contenus du pad",
|
"timeslider.playPause": "Lecture / Pause des contenus du bloc-notes",
|
||||||
"timeslider.backRevision": "Reculer d’une révision dans ce pad",
|
"timeslider.backRevision": "Reculer d’une révision dans ce bloc-notes",
|
||||||
"timeslider.forwardRevision": "Avancer d’une révision dans ce pad",
|
"timeslider.forwardRevision": "Avancer d’une révision dans ce bloc-notes",
|
||||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "janvier",
|
"timeslider.month.january": "janvier",
|
||||||
"timeslider.month.february": "février",
|
"timeslider.month.february": "février",
|
||||||
|
@ -135,20 +138,20 @@
|
||||||
"timeslider.month.december": "décembre",
|
"timeslider.month.december": "décembre",
|
||||||
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: auteur anonyme, other: auteurs anonymes ]}",
|
"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.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 l’historique",
|
||||||
"pad.userlist.entername": "Entrez votre nom",
|
"pad.userlist.entername": "Entrez votre nom",
|
||||||
"pad.userlist.unnamed": "anonyme",
|
"pad.userlist.unnamed": "anonyme",
|
||||||
"pad.userlist.guest": "Invité",
|
"pad.userlist.guest": "Invité",
|
||||||
"pad.userlist.deny": "Refuser",
|
"pad.userlist.deny": "Refuser",
|
||||||
"pad.userlist.approve": "Approuver",
|
"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.importbutton": "Importer maintenant",
|
||||||
"pad.impexp.importing": "Import en cours...",
|
"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.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 n’avons 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 n’avons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié ; veuillez l’importer vers un nouveau bloc-notes",
|
||||||
"pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer",
|
"pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer",
|
||||||
"pad.impexp.importfailed": "Échec de l'importation",
|
"pad.impexp.importfailed": "Échec de l’importation",
|
||||||
"pad.impexp.copypaste": "Veuillez copier/coller",
|
"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.exportdisabled": "L’exportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails."
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,8 @@
|
||||||
"pad.chat": "שיחה",
|
"pad.chat": "שיחה",
|
||||||
"pad.chat.title": "פתיחת השיחה של הפנקס הזה.",
|
"pad.chat.title": "פתיחת השיחה של הפנקס הזה.",
|
||||||
"pad.chat.loadmessages": "טעינת הודעות נוספות",
|
"pad.chat.loadmessages": "טעינת הודעות נוספות",
|
||||||
|
"pad.chat.stick.title": "הצמדת צ׳אט למסך",
|
||||||
|
"pad.chat.writeMessage.placeholder": "מקום לכתיבת ההודעה שלך",
|
||||||
"timeslider.pageTitle": "גולל זמן של {{appTitle}}",
|
"timeslider.pageTitle": "גולל זמן של {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "חזרה אל הפנקס",
|
"timeslider.toolbar.returnbutton": "חזרה אל הפנקס",
|
||||||
"timeslider.toolbar.authors": "כותבים:",
|
"timeslider.toolbar.authors": "כותבים:",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Bugoslav"
|
"Bugoslav",
|
||||||
|
"Hmxhmx"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Novi blokić",
|
"index.newPad": "Novi blokić",
|
||||||
|
@ -84,6 +85,8 @@
|
||||||
"pad.chat": "Čavrljanje",
|
"pad.chat": "Čavrljanje",
|
||||||
"pad.chat.title": "Otvori čavrljanje uz ovaj blokić.",
|
"pad.chat.title": "Otvori čavrljanje uz ovaj blokić.",
|
||||||
"pad.chat.loadmessages": "Učitaj više poruka",
|
"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.pageTitle": "{{appTitle}} Vremenska lenta",
|
||||||
"timeslider.toolbar.returnbutton": "Vrati se natrag na blokić",
|
"timeslider.toolbar.returnbutton": "Vrati se natrag na blokić",
|
||||||
"timeslider.toolbar.authors": "Autori:",
|
"timeslider.toolbar.authors": "Autori:",
|
||||||
|
|
|
@ -93,6 +93,8 @@
|
||||||
"pad.chat": "Csevegés",
|
"pad.chat": "Csevegés",
|
||||||
"pad.chat.title": "A noteszhez tartozó csevegés megnyitása.",
|
"pad.chat.title": "A noteszhez tartozó csevegés megnyitása.",
|
||||||
"pad.chat.loadmessages": "További üzenetek betöltése",
|
"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.pageTitle": "{{appTitle}} időcsúszka",
|
||||||
"timeslider.toolbar.returnbutton": "Vissza a noteszhez",
|
"timeslider.toolbar.returnbutton": "Vissza a noteszhez",
|
||||||
"timeslider.toolbar.authors": "Szerzők:",
|
"timeslider.toolbar.authors": "Szerzők:",
|
||||||
|
|
|
@ -87,6 +87,8 @@
|
||||||
"pad.chat": "Spjall",
|
"pad.chat": "Spjall",
|
||||||
"pad.chat.title": "Opna spjallið fyrir þessa skrifblokk.",
|
"pad.chat.title": "Opna spjallið fyrir þessa skrifblokk.",
|
||||||
"pad.chat.loadmessages": "Hlaða inn fleiri skeytum",
|
"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.pageTitle": "Tímalína {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Fara til baka í skrifblokk",
|
"timeslider.toolbar.returnbutton": "Fara til baka í skrifblokk",
|
||||||
"timeslider.toolbar.authors": "Höfundar:",
|
"timeslider.toolbar.authors": "Höfundar:",
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
"pad.toolbar.strikethrough.title": "Barrato (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Barrato (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "Elenco numerato (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "Elenco numerato (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "Elenco puntato (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "Elenco puntato (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "Rientro (TAB)",
|
"pad.toolbar.indent.title": "Indentazione (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Riduci rientro (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Riduci indentazione (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Annulla (Ctrl-Z)",
|
"pad.toolbar.undo.title": "Annulla (Ctrl-Z)",
|
||||||
"pad.toolbar.redo.title": "Ripeti (Ctrl-Y)",
|
"pad.toolbar.redo.title": "Ripeti (Ctrl-Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Elimina i colori che indicano gli autori (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "Elimina i colori che indicano gli autori (Ctrl+Shift+C)",
|
||||||
|
@ -91,6 +91,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Apri la chat per questo Pad.",
|
"pad.chat.title": "Apri la chat per questo Pad.",
|
||||||
"pad.chat.loadmessages": "Carica altri messaggi",
|
"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.pageTitle": "Cronologia {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Ritorna al Pad",
|
"timeslider.toolbar.returnbutton": "Ritorna al Pad",
|
||||||
"timeslider.toolbar.authors": "Autori:",
|
"timeslider.toolbar.authors": "Autori:",
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"Shirayuki",
|
"Shirayuki",
|
||||||
"Torinky",
|
"Torinky",
|
||||||
"Omotecho",
|
"Omotecho",
|
||||||
"Aefgh39622"
|
"Aefgh39622",
|
||||||
|
"Afaz"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "新規作成",
|
"index.newPad": "新規作成",
|
||||||
|
@ -89,6 +90,8 @@
|
||||||
"pad.chat": "チャット",
|
"pad.chat": "チャット",
|
||||||
"pad.chat.title": "このパッドのチャットを開きます。",
|
"pad.chat.title": "このパッドのチャットを開きます。",
|
||||||
"pad.chat.loadmessages": "その他のメッセージを読み込む",
|
"pad.chat.loadmessages": "その他のメッセージを読み込む",
|
||||||
|
"pad.chat.stick.title": "チャットを画面に貼り付ける",
|
||||||
|
"pad.chat.writeMessage.placeholder": "ここにメッセージを書き込んでください",
|
||||||
"timeslider.pageTitle": "{{appTitle}} タイムスライダー",
|
"timeslider.pageTitle": "{{appTitle}} タイムスライダー",
|
||||||
"timeslider.toolbar.returnbutton": "パッドに戻る",
|
"timeslider.toolbar.returnbutton": "パッドに戻る",
|
||||||
"timeslider.toolbar.authors": "作者:",
|
"timeslider.toolbar.authors": "作者:",
|
||||||
|
|
|
@ -84,6 +84,8 @@
|
||||||
"pad.chat": "Asqerdec",
|
"pad.chat": "Asqerdec",
|
||||||
"pad.chat.title": "Ldi asqerdec deg upad-agi.",
|
"pad.chat.title": "Ldi asqerdec deg upad-agi.",
|
||||||
"pad.chat.loadmessages": "Sali-d ugar n yiznan",
|
"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.pageTitle": "Amazray asmussan n {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Uqal ar upad",
|
"timeslider.toolbar.returnbutton": "Uqal ar upad",
|
||||||
"timeslider.toolbar.authors": "Imeskaren:",
|
"timeslider.toolbar.authors": "Imeskaren:",
|
||||||
|
|
|
@ -92,6 +92,8 @@
|
||||||
"pad.chat": "대화",
|
"pad.chat": "대화",
|
||||||
"pad.chat.title": "이 패드에 대화를 엽니다.",
|
"pad.chat.title": "이 패드에 대화를 엽니다.",
|
||||||
"pad.chat.loadmessages": "더 많은 메시지 불러오기",
|
"pad.chat.loadmessages": "더 많은 메시지 불러오기",
|
||||||
|
"pad.chat.stick.title": "채팅을 화면에 고정",
|
||||||
|
"pad.chat.writeMessage.placeholder": "여기에 메시지를 적으십시오",
|
||||||
"timeslider.pageTitle": "{{appTitle}} 시간슬라이더",
|
"timeslider.pageTitle": "{{appTitle}} 시간슬라이더",
|
||||||
"timeslider.toolbar.returnbutton": "패드로 돌아가기",
|
"timeslider.toolbar.returnbutton": "패드로 돌아가기",
|
||||||
"timeslider.toolbar.authors": "저자:",
|
"timeslider.toolbar.authors": "저자:",
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"George Animal",
|
"George Animal",
|
||||||
"Gomada",
|
"Gomada",
|
||||||
"Mehk63",
|
"Mehk63",
|
||||||
"Ghybu"
|
"Ghybu",
|
||||||
|
"MikaelF"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Bloknota nû",
|
"index.newPad": "Bloknota nû",
|
||||||
|
@ -60,18 +61,18 @@
|
||||||
"timeslider.version": "Guhertoya {{version}}",
|
"timeslider.version": "Guhertoya {{version}}",
|
||||||
"timeslider.saved": "Di dîroka {{day}} {{month}} {{year}} de hate tomarkirin",
|
"timeslider.saved": "Di dîroka {{day}} {{month}} {{year}} de hate tomarkirin",
|
||||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}",
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}.{{minutes}}.{{seconds}}",
|
||||||
"timeslider.month.january": "rêbendan",
|
"timeslider.month.january": "kanûna paşîn",
|
||||||
"timeslider.month.february": "reşemî",
|
"timeslider.month.february": "sibat",
|
||||||
"timeslider.month.march": "adar",
|
"timeslider.month.march": "adar",
|
||||||
"timeslider.month.april": "avrêl",
|
"timeslider.month.april": "nîsan",
|
||||||
"timeslider.month.may": "gulan",
|
"timeslider.month.may": "gulan",
|
||||||
"timeslider.month.june": "pûşper",
|
"timeslider.month.june": "hezîran",
|
||||||
"timeslider.month.july": "tîrmeh",
|
"timeslider.month.july": "tîrmeh",
|
||||||
"timeslider.month.august": "gelawêj",
|
"timeslider.month.august": "tebax",
|
||||||
"timeslider.month.september": "rezber",
|
"timeslider.month.september": "îlon",
|
||||||
"timeslider.month.october": "kewçêr",
|
"timeslider.month.october": "çiriya pêşîn",
|
||||||
"timeslider.month.november": "Mijdar",
|
"timeslider.month.november": "Çiriya paşîn",
|
||||||
"timeslider.month.december": "berfanbar",
|
"timeslider.month.december": "kanûna pêşîn",
|
||||||
"pad.userlist.entername": "Navê xwe têkeve",
|
"pad.userlist.entername": "Navê xwe têkeve",
|
||||||
"pad.userlist.unnamed": "nenavkirî",
|
"pad.userlist.unnamed": "nenavkirî",
|
||||||
"pad.userlist.guest": "Mêvan",
|
"pad.userlist.guest": "Mêvan",
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
"pad.toolbar.italic.title": "Schréi (Ctrl+I)",
|
"pad.toolbar.italic.title": "Schréi (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "Ënnerstrach (Ctrl+U)",
|
"pad.toolbar.underline.title": "Ënnerstrach (Ctrl+U)",
|
||||||
"pad.toolbar.strikethrough.title": "Duerchgestrach (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Duerchgestrach (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "Numeréiert Lëscht (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "Nummeréiert Lëscht (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "Net-numeréiert Lëscht (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "Net-nummeréiert Lëscht (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "Aréckelen (TAB)",
|
"pad.toolbar.indent.title": "Aréckelen (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Erausréckelen (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Erausréckelen (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Réckgängeg (Ctrl-Z)",
|
"pad.toolbar.undo.title": "Réckgängeg (Ctrl-Z)",
|
||||||
|
@ -59,6 +59,7 @@
|
||||||
"pad.share.link": "Link",
|
"pad.share.link": "Link",
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.loadmessages": "Méi Message lueden",
|
"pad.chat.loadmessages": "Méi Message lueden",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Schreift Äre Message hei",
|
||||||
"timeslider.toolbar.authors": "Auteuren:",
|
"timeslider.toolbar.authors": "Auteuren:",
|
||||||
"timeslider.toolbar.authorsList": "Keng Auteuren",
|
"timeslider.toolbar.authorsList": "Keng Auteuren",
|
||||||
"timeslider.toolbar.exportlink.title": "Exportéieren",
|
"timeslider.toolbar.exportlink.title": "Exportéieren",
|
||||||
|
|
|
@ -1,78 +1,80 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Mogoeilor"
|
"Mogoeilor",
|
||||||
|
"Lorestani"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "دشته تازه",
|
"index.newPad": "دٱفتٱرچٱ تازٱ",
|
||||||
"pad.toolbar.bold.title": "توپر",
|
"pad.toolbar.bold.title": "تۊپور",
|
||||||
"pad.toolbar.italic.title": "کج کوله(ctrl-l)",
|
"pad.toolbar.italic.title": "هٱلٛ هار(ctrl-l)",
|
||||||
"pad.toolbar.underline.title": "زیر خط دار بین (Ctrl-U)",
|
"pad.toolbar.underline.title": "زؽر خٱت (Ctrl-U)",
|
||||||
"pad.toolbar.ol.title": "نوم گه منظم",
|
"pad.toolbar.ol.title": "نومگٱ مورٱتٱب بیٱ",
|
||||||
"pad.toolbar.ul.title": "نوم گه بی نظم",
|
"pad.toolbar.ul.title": "نومگٱ مورٱتٱب ناٛیٱ",
|
||||||
"pad.toolbar.indent.title": "مئن رئته(TAB)",
|
"pad.toolbar.indent.title": "قوپساٛیی(TAB)",
|
||||||
"pad.toolbar.unindent.title": "وه در رئته (Shift+TAB)",
|
"pad.toolbar.unindent.title": "ڤ دٱر رٱتاٛیی (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "رد انجوم دئین (Ctrl-Z)",
|
"pad.toolbar.undo.title": "رٱد ٱنجوم داٛئن (Ctrl-Z)",
|
||||||
"pad.toolbar.redo.title": "د نو انجوم دئین(Ctrl-Y)",
|
"pad.toolbar.redo.title": "د نۊ ٱنجوم داٛئن(Ctrl-Y)",
|
||||||
|
"pad.toolbar.savedRevision.title": "ڤانری بٱلگٱ",
|
||||||
"pad.toolbar.settings.title": "میزوکاری",
|
"pad.toolbar.settings.title": "میزوکاری",
|
||||||
"pad.colorpicker.save": "ذخيره كردن",
|
"pad.colorpicker.save": "زٱخیرٱ كردن",
|
||||||
"pad.colorpicker.cancel": "انجوم شیو كردن",
|
"pad.colorpicker.cancel": "ٱنجوم شؽڤ كردن",
|
||||||
"pad.loading": "د حالت سوار كرد",
|
"pad.loading": "د هالٱت سڤار كرد...",
|
||||||
"pad.wrongPassword": "پاسوردتو اشتوائه",
|
"pad.wrongPassword": "پٱسڤردتو اْشتبائٱ",
|
||||||
"pad.settings.padSettings": "میزوکاری دشته",
|
"pad.settings.padSettings": "میزوکاری دٱفتٱرچٱ",
|
||||||
"pad.settings.myView": "نظرگه مه",
|
"pad.settings.myView": "نٱزٱرگٱ ماْ",
|
||||||
"pad.settings.stickychat": "همیشه د بلگه چک چنه بکید",
|
"pad.settings.stickychat": "همیشٱ د بٱلگٱ چٱک چنٱ بٱکؽت",
|
||||||
"pad.settings.linenocheck": "شماره خطیا",
|
"pad.settings.linenocheck": "شمارٱ خٱتؽا",
|
||||||
"pad.settings.fontType": "نوع فونت:",
|
"pad.settings.fontType": "نوع فونت:",
|
||||||
"pad.settings.fontType.normal": "عادی",
|
"pad.settings.fontType.normal": "عادی",
|
||||||
"pad.settings.fontType.monospaced": "تک جاگه",
|
"pad.settings.fontType.monospaced": "تک جاگه",
|
||||||
"pad.settings.globalView": "دیئن جهونی",
|
"pad.settings.globalView": "دیئن جهونی",
|
||||||
"pad.settings.language": "زون:",
|
"pad.settings.language": "زڤون:",
|
||||||
"pad.importExport.import_export": "وامین آوردن/د در دئن",
|
"pad.importExport.import_export": "ڤامین آوئردن/ڤ دٱر داٛئن",
|
||||||
"pad.importExport.importSuccessful": "موفق بی!",
|
"pad.importExport.importSuccessful": "موئٱفٱق بی!",
|
||||||
"pad.importExport.export": "دشته تازه چی وه در بیه:",
|
"pad.importExport.export": "دٱفتٱرچٱ تازٱ چی ڤ دٱر بیٱ:",
|
||||||
"pad.importExport.exporthtml": "اچ تی ام ال",
|
"pad.importExport.exporthtml": "اْچ تی اْم اْل",
|
||||||
"pad.importExport.exportplain": "نیسسه ساده",
|
"pad.importExport.exportplain": "نیسسٱ سادٱ",
|
||||||
"pad.importExport.exportword": "واجه پالایشتگر مایکروسافت",
|
"pad.importExport.exportword": "ڤاژٱ پالایشگٱر مایکروسافت",
|
||||||
"pad.importExport.exportpdf": "پی دی اف",
|
"pad.importExport.exportpdf": "پی دی اْف",
|
||||||
"pad.importExport.exportopen": "او دی اف(قالو سند وا بیه)",
|
"pad.importExport.exportopen": "او دی اْف(قالب سٱنٱد ڤاز)",
|
||||||
"pad.modals.connected": "وصل بیه",
|
"pad.modals.connected": "ڤٱسل بیٱ",
|
||||||
"pad.modals.forcereconnect": "سی وصل بین مژبور کو",
|
"pad.modals.forcereconnect": "سی ڤٱسل بیئن دوئارٱ مٱجبۊر کو",
|
||||||
"pad.modals.userdup": "د نیمدری هنی واز بیه",
|
"pad.modals.userdup": "د نیمدری هنی ڤاز بیٱ",
|
||||||
"pad.modals.initsocketfail": "سرور د دسرسی نئ.",
|
"pad.modals.initsocketfail": "سرور د دٱسرسی نؽ.",
|
||||||
"pad.modals.deleted": "پاک بیه",
|
"pad.modals.deleted": "پاک بیٱ",
|
||||||
"pad.modals.deleted.explanation": "ای دشته جا وه جا بیه",
|
"pad.modals.deleted.explanation": "اؽ دٱفتٱرچٱ جا ڤ جا بیٱ",
|
||||||
"pad.modals.disconnected": "ارتواطتو قطع بیه",
|
"pad.modals.disconnected": "اْرتبات تو قٱت بیٱ.",
|
||||||
"pad.share": "ای دشته نه بهر کو",
|
"pad.share": "اؽ دٱفتٱرچٱ ناْ بٱئر کو",
|
||||||
"pad.share.readonly": "فقط بحون",
|
"pad.share.readonly": "فقٱت ڤٱننی",
|
||||||
"pad.share.link": "هوم پیوند",
|
"pad.share.link": "هوم پاٛڤٱن",
|
||||||
"pad.chat": "گپ زئن",
|
"pad.chat": "سالفٱ",
|
||||||
"pad.chat.title": "گپ چنه نه سی دشته وا کو.",
|
"pad.chat.title": "سالفٱ ناْ سی دٱفتٱرچٱ ڤاز کو.",
|
||||||
"pad.chat.loadmessages": "پیغومیا بیشتر نه سوار کو",
|
"pad.chat.loadmessages": "پاٛغومؽا ؽشتر ناْ سڤار کو",
|
||||||
"timeslider.toolbar.returnbutton": "ورگرد د دشته",
|
"timeslider.toolbar.returnbutton": "ڤرگٱشتن ڤ دٱفتٱرچٱ",
|
||||||
"timeslider.toolbar.authors": "نیسنه یا:",
|
"timeslider.toolbar.authors": "نیسٱنٱ یا:",
|
||||||
"timeslider.toolbar.authorsList": "بی نیسنه",
|
"timeslider.toolbar.authorsList": "بؽ نیسٱنٱ",
|
||||||
"timeslider.toolbar.exportlink.title": "وه در ديئن",
|
"timeslider.toolbar.exportlink.title": "ڤ دٱر داٛئن",
|
||||||
"timeslider.version": "نسقه{{نسقه}}",
|
"timeslider.version": "نۏسخٱ{{نۏسخٱ}}",
|
||||||
"timeslider.month.january": "جانويه",
|
"timeslider.month.january": "ژانڤیٱ",
|
||||||
"timeslider.month.february": "فوريه",
|
"timeslider.month.february": "فڤریٱ",
|
||||||
"timeslider.month.march": "مارس",
|
"timeslider.month.march": "مارس",
|
||||||
"timeslider.month.april": "آوريل",
|
"timeslider.month.april": "آڤريل",
|
||||||
"timeslider.month.may": "ما",
|
"timeslider.month.may": "ماٛی",
|
||||||
"timeslider.month.june": "جوئن",
|
"timeslider.month.june": "ژوئٱن",
|
||||||
"timeslider.month.july": "جولای",
|
"timeslider.month.july": "جۊلای",
|
||||||
"timeslider.month.august": "اگوست",
|
"timeslider.month.august": "آگوست",
|
||||||
"timeslider.month.september": "سپتامر",
|
"timeslider.month.september": "سپتامر",
|
||||||
"timeslider.month.october": "اكتور",
|
"timeslider.month.october": "اوكتوبر",
|
||||||
"timeslider.month.november": "نوامر",
|
"timeslider.month.november": "نوڤامر",
|
||||||
"timeslider.month.december": "دسامر",
|
"timeslider.month.december": "دسامر",
|
||||||
"pad.userlist.entername": "نوم تونه وارد بکید",
|
"pad.userlist.entername": "نوم توناْ ڤارد بٱکؽت",
|
||||||
"pad.userlist.unnamed": "نوم نهشته",
|
"pad.userlist.unnamed": "بؽ نوم",
|
||||||
"pad.userlist.guest": "میزوان",
|
"pad.userlist.guest": "ماٛموݩ",
|
||||||
"pad.userlist.deny": "پرو کردن",
|
"pad.userlist.deny": "رٱد کردن",
|
||||||
"pad.userlist.approve": "اصلا کردن",
|
"pad.userlist.approve": "قبۊل کردن",
|
||||||
"pad.impexp.importbutton": "ایسه وارد کو",
|
"pad.impexp.importbutton": "ایساْ ڤارد کو",
|
||||||
"pad.impexp.importing": "د حالت وارد کردن",
|
"pad.impexp.importing": "د هالٱت ڤارد کردن...",
|
||||||
"pad.impexp.importfailed": "وامین آوردن شکست حرد",
|
"pad.impexp.importfailed": "ڤامؽن آوئردن شکٱست هٱرد",
|
||||||
"pad.impexp.copypaste": "خواهشن وردار بدیسن"
|
"pad.impexp.copypaste": "خاهشٱن ڤردار بٱدیسن"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
"Mantak111",
|
"Mantak111",
|
||||||
"I-svetaines",
|
"I-svetaines",
|
||||||
"Zygimantus",
|
"Zygimantus",
|
||||||
"Vogone"
|
"Vogone",
|
||||||
|
"Naktis"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Naujas bloknotas",
|
"index.newPad": "Naujas bloknotas",
|
||||||
|
@ -59,6 +60,8 @@
|
||||||
"pad.modals.connected": "Prisijungta.",
|
"pad.modals.connected": "Prisijungta.",
|
||||||
"pad.modals.reconnecting": "Iš naujo prisijungiama prie Jūsų bloknoto",
|
"pad.modals.reconnecting": "Iš naujo prisijungiama prie Jūsų bloknoto",
|
||||||
"pad.modals.forcereconnect": "Priversti prisijungti iš naujo",
|
"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": "Atidaryta kitame lange",
|
||||||
"pad.modals.userdup.explanation": "Šis bloknotas, atrodo yra atidarytas daugiau nei viename šio kompiuterio naršyklės 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ą.",
|
"pad.modals.userdup.advice": "Prisijunkite iš naujo, kad vietoj to naudotumėte šį langą.",
|
||||||
|
@ -87,6 +90,8 @@
|
||||||
"pad.chat": "Pokalbiai",
|
"pad.chat": "Pokalbiai",
|
||||||
"pad.chat.title": "Atverti šio bloknoto pokalbį.",
|
"pad.chat.title": "Atverti šio bloknoto pokalbį.",
|
||||||
"pad.chat.loadmessages": "Įkrauti daugiau pranešimų",
|
"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.pageTitle": "{{appTitle}} Laiko slinkiklis",
|
||||||
"timeslider.toolbar.returnbutton": "Grįžti į bloknotą",
|
"timeslider.toolbar.returnbutton": "Grįžti į bloknotą",
|
||||||
"timeslider.toolbar.authors": "Autoriai:",
|
"timeslider.toolbar.authors": "Autoriai:",
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Bjankuloski06",
|
"Bjankuloski06",
|
||||||
"Brest"
|
"Brest",
|
||||||
|
"Vlad5250"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Нова тетратка",
|
"index.newPad": "Нова тетратка",
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
"pad.toolbar.savedRevision.title": "Зачувај преработка",
|
"pad.toolbar.savedRevision.title": "Зачувај преработка",
|
||||||
"pad.toolbar.settings.title": "Поставки",
|
"pad.toolbar.settings.title": "Поставки",
|
||||||
"pad.toolbar.embed.title": "Споделете и вметнете ја тетраткава",
|
"pad.toolbar.embed.title": "Споделете и вметнете ја тетраткава",
|
||||||
"pad.toolbar.showusers.title": "Прикаж. корисниците на тетраткава",
|
"pad.toolbar.showusers.title": "Прикажи корисниците на тетраткава",
|
||||||
"pad.colorpicker.save": "Зачувај",
|
"pad.colorpicker.save": "Зачувај",
|
||||||
"pad.colorpicker.cancel": "Откажи",
|
"pad.colorpicker.cancel": "Откажи",
|
||||||
"pad.loading": "Вчитувам...",
|
"pad.loading": "Вчитувам...",
|
||||||
|
@ -87,6 +88,8 @@
|
||||||
"pad.chat": "Разговор",
|
"pad.chat": "Разговор",
|
||||||
"pad.chat.title": "Отвори го разговорот за оваа тетратка.",
|
"pad.chat.title": "Отвори го разговорот за оваа тетратка.",
|
||||||
"pad.chat.loadmessages": "Вчитај уште пораки",
|
"pad.chat.loadmessages": "Вчитај уште пораки",
|
||||||
|
"pad.chat.stick.title": "Залепи го разговорот на екранот",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Тука напишете порака",
|
||||||
"timeslider.pageTitle": "{{appTitle}} Историски преглед",
|
"timeslider.pageTitle": "{{appTitle}} Историски преглед",
|
||||||
"timeslider.toolbar.returnbutton": "Назад на тетратката",
|
"timeslider.toolbar.returnbutton": "Назад на тетратката",
|
||||||
"timeslider.toolbar.authors": "Автори:",
|
"timeslider.toolbar.authors": "Автори:",
|
||||||
|
@ -122,7 +125,7 @@
|
||||||
"pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ?",
|
"pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ?",
|
||||||
"pad.impexp.importbutton": "Увези сега",
|
"pad.impexp.importbutton": "Увези сега",
|
||||||
"pad.impexp.importing": "Увезувам...",
|
"pad.impexp.importing": "Увезувам...",
|
||||||
"pad.impexp.confirmimport": "Увезувајќи ја податотеката ќе го замените целиот досегашен текст во тетратката. Дали сте сигурни дека сакате да продолжите?",
|
"pad.impexp.confirmimport": "Увезувањето на податотека ќе го презапише тековниот текст на тетратката. Дали сте сигурни дека сакате да продолжите?",
|
||||||
"pad.impexp.convertFailed": "Не можев да ја увезам податотеката. Послужете се со поинаков формат или прекопирајте го текстот рачно.",
|
"pad.impexp.convertFailed": "Не можев да ја увезам податотеката. Послужете се со поинаков формат или прекопирајте го текстот рачно.",
|
||||||
"pad.impexp.padHasData": "Не можевме да ја увеземе оваа податотека бидејќи оваа тетратка веќе има промени. Увезете ја во нова тетратка.",
|
"pad.impexp.padHasData": "Не можевме да ја увеземе оваа податотека бидејќи оваа тетратка веќе има промени. Увезете ја во нова тетратка.",
|
||||||
"pad.impexp.uploadFailed": "Подигањето не успеа. Обидете се повторно.",
|
"pad.impexp.uploadFailed": "Подигањето не успеа. Обидете се повторно.",
|
||||||
|
|
|
@ -90,6 +90,8 @@
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Åpne chatten for denne blokken.",
|
"pad.chat.title": "Åpne chatten for denne blokken.",
|
||||||
"pad.chat.loadmessages": "Last flere beskjeder",
|
"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.pageTitle": "{{appTitle}} Tidslinje",
|
||||||
"timeslider.toolbar.returnbutton": "Gå tilbake til blokk",
|
"timeslider.toolbar.returnbutton": "Gå tilbake til blokk",
|
||||||
"timeslider.toolbar.authors": "Forfattere:",
|
"timeslider.toolbar.authors": "Forfattere:",
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"Robin van der Vliet",
|
"Robin van der Vliet",
|
||||||
"Mainframe98",
|
"Mainframe98",
|
||||||
"KlaasZ4usV",
|
"KlaasZ4usV",
|
||||||
"Rickvl"
|
"Rickvl",
|
||||||
|
"Marcelhospers"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nieuw pad",
|
"index.newPad": "Nieuw pad",
|
||||||
|
@ -92,6 +93,8 @@
|
||||||
"pad.chat": "Chatten",
|
"pad.chat": "Chatten",
|
||||||
"pad.chat.title": "Chat voor dit pad opnenen",
|
"pad.chat.title": "Chat voor dit pad opnenen",
|
||||||
"pad.chat.loadmessages": "Meer berichten laden",
|
"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.pageTitle": "Tijdlijn voor {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Terug naar pad",
|
"timeslider.toolbar.returnbutton": "Terug naar pad",
|
||||||
"timeslider.toolbar.authors": "Auteurs:",
|
"timeslider.toolbar.authors": "Auteurs:",
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"Aalam",
|
"Aalam",
|
||||||
"Babanwalia",
|
"Babanwalia",
|
||||||
"ਪ੍ਰਚਾਰਕ",
|
"ਪ੍ਰਚਾਰਕ",
|
||||||
"Tow"
|
"Tow",
|
||||||
|
"ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
"pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ",
|
"pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ",
|
||||||
"pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ",
|
"pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ",
|
||||||
"pad.toolbar.savedRevision.title": "ਰੀਵਿਜ਼ਨ ਸੰਭਾਲੋ",
|
"pad.toolbar.savedRevision.title": "ਦੁਹਰਾਅ ਸਾਂਭੋ",
|
||||||
"pad.toolbar.settings.title": "ਸੈਟਿੰਗ",
|
"pad.toolbar.settings.title": "ਸੈਟਿੰਗ",
|
||||||
"pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ",
|
"pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ",
|
||||||
"pad.toolbar.showusers.title": "ਇਹ ਪੈਡ ਉੱਤੇ ਯੂਜ਼ਰ ਵੇਖਾਓ",
|
"pad.toolbar.showusers.title": "ਇਹ ਪੈਡ ਉੱਤੇ ਯੂਜ਼ਰ ਵੇਖਾਓ",
|
||||||
|
@ -30,9 +31,9 @@
|
||||||
"pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ",
|
"pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ",
|
||||||
"pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
|
"pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
|
||||||
"pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।",
|
"pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।",
|
||||||
"pad.passwordRequired": "ਇਹ ਪੈਡ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ",
|
"pad.passwordRequired": "ਇਸ ਪੈਡ ਤੱਕ ਅਪੜਨ ਲਈ ਤੁਹਾਨੂੰ ਇੱਕ ਲੰਘ-ਸ਼ਬਦ ਦੀ ਲੋੜ ਹੈ",
|
||||||
"pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ",
|
"pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ",
|
||||||
"pad.wrongPassword": "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਗਲਤੀ ਸੀ",
|
"pad.wrongPassword": "ਤੁਹਾਡਾ ਲੰਘ-ਸ਼ਬਦ ਗਲਤ ਸੀ",
|
||||||
"pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ",
|
"pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ",
|
||||||
"pad.settings.myView": "ਮੇਰੀ ਝਲਕ",
|
"pad.settings.myView": "ਮੇਰੀ ਝਲਕ",
|
||||||
"pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ",
|
"pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ",
|
||||||
|
@ -97,7 +98,7 @@
|
||||||
"timeslider.saved": "{{day}} {{month}} {{year}} ਨੂੰ ਸੰਭਾਲਿਆ",
|
"timeslider.saved": "{{day}} {{month}} {{year}} ਨੂੰ ਸੰਭਾਲਿਆ",
|
||||||
"timeslider.playPause": "ਪੈਡ ਸਮੱਗਰੀ ਚਲਾਓ / ਵਿਰਾਮ ਕਰੋ",
|
"timeslider.playPause": "ਪੈਡ ਸਮੱਗਰੀ ਚਲਾਓ / ਵਿਰਾਮ ਕਰੋ",
|
||||||
"timeslider.backRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਪਿਛਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ",
|
"timeslider.backRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਪਿਛਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ",
|
||||||
"timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ",
|
"timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਦੁਹਰਾਅ ਤੇ ਜਾਓ",
|
||||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "ਜਨਵਰੀ",
|
"timeslider.month.january": "ਜਨਵਰੀ",
|
||||||
"timeslider.month.february": "ਫ਼ਰਵਰੀ",
|
"timeslider.month.february": "ਫ਼ਰਵਰੀ",
|
||||||
|
|
|
@ -84,6 +84,8 @@
|
||||||
"pad.chat": "Ciaciarada",
|
"pad.chat": "Ciaciarada",
|
||||||
"pad.chat.title": "Duverté la ciaciarada për cost feuj.",
|
"pad.chat.title": "Duverté la ciaciarada për cost feuj.",
|
||||||
"pad.chat.loadmessages": "Carié pi 'd mëssagi",
|
"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.pageTitle": "Stòria dinàmica ëd {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Torné al feuj",
|
"timeslider.toolbar.returnbutton": "Torné al feuj",
|
||||||
"timeslider.toolbar.authors": "Autor:",
|
"timeslider.toolbar.authors": "Autor:",
|
||||||
|
|
|
@ -101,6 +101,8 @@
|
||||||
"pad.chat": "Bate-papo",
|
"pad.chat": "Bate-papo",
|
||||||
"pad.chat.title": "Abrir o bate-papo desta nota.",
|
"pad.chat.title": "Abrir o bate-papo desta nota.",
|
||||||
"pad.chat.loadmessages": "Carregar mais mensagens",
|
"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.pageTitle": "Linha do tempo de {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Retornar para a nota",
|
"timeslider.toolbar.returnbutton": "Retornar para a nota",
|
||||||
"timeslider.toolbar.authors": "Autores:",
|
"timeslider.toolbar.authors": "Autores:",
|
||||||
|
|
|
@ -9,38 +9,39 @@
|
||||||
"Macofe",
|
"Macofe",
|
||||||
"Ti4goc",
|
"Ti4goc",
|
||||||
"Cainamarques",
|
"Cainamarques",
|
||||||
"Athena in Wonderland"
|
"Athena in Wonderland",
|
||||||
|
"Waldyrious"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Nova Nota",
|
"index.newPad": "Nova Nota",
|
||||||
"index.createOpenPad": "ou crie/abra uma Nota com o nome:",
|
"index.createOpenPad": "ou crie/abra uma nota com o nome:",
|
||||||
"pad.toolbar.bold.title": "Negrito (Ctrl-B)",
|
"pad.toolbar.bold.title": "Negrito (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "Itálico (Ctrl-I)",
|
"pad.toolbar.italic.title": "Itálico (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "Sublinhado (Ctrl-U)",
|
"pad.toolbar.underline.title": "Sublinhado (Ctrl+U)",
|
||||||
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "Avançar (TAB)",
|
"pad.toolbar.indent.title": "Indentar (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Recuar (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Desfazer (Ctrl-Z)",
|
"pad.toolbar.undo.title": "Desfazer (Ctrl+Z)",
|
||||||
"pad.toolbar.redo.title": "Refazer (Ctrl-Y)",
|
"pad.toolbar.redo.title": "Refazer (Ctrl+Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)",
|
"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.import_export.title": "Importar/exportar de/para diferentes formatos de ficheiro",
|
||||||
"pad.toolbar.timeslider.title": "Linha de tempo",
|
"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.settings.title": "Configurações",
|
||||||
"pad.toolbar.embed.title": "Compartilhar e incorporar este pad",
|
"pad.toolbar.embed.title": "Partilhar e incorporar esta nota",
|
||||||
"pad.toolbar.showusers.title": "Mostrar os utilizadores nesta Nota",
|
"pad.toolbar.showusers.title": "Mostrar os utilizadores nesta nota",
|
||||||
"pad.colorpicker.save": "Gravar",
|
"pad.colorpicker.save": "Gravar",
|
||||||
"pad.colorpicker.cancel": "Cancelar",
|
"pad.colorpicker.cancel": "Cancelar",
|
||||||
"pad.loading": "A carregar…",
|
"pad.loading": "A carregar…",
|
||||||
"pad.noCookie": "O cookie não foi encontrado. Por favor, ative os cookies no seu navegador!",
|
"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.passwordRequired": "Precisa de uma palavra-passe para aceder a esta nota",
|
||||||
"pad.permissionDenied": "Não tem permissão para aceder a este pad.",
|
"pad.permissionDenied": "Não tem permissão para aceder a esta nota",
|
||||||
"pad.wrongPassword": "A palavra-chave está errada",
|
"pad.wrongPassword": "A sua palavra-passe estava errada",
|
||||||
"pad.settings.padSettings": "Configurações da Nota",
|
"pad.settings.padSettings": "Configurações da nota",
|
||||||
"pad.settings.myView": "Minha vista",
|
"pad.settings.myView": "A minha vista",
|
||||||
"pad.settings.stickychat": "Bate-papo sempre no ecrã",
|
"pad.settings.stickychat": "Conversação sempre no ecrã",
|
||||||
"pad.settings.chatandusers": "Mostrar a conversação e os utilizadores",
|
"pad.settings.chatandusers": "Mostrar a conversação e os utilizadores",
|
||||||
"pad.settings.colorcheck": "Cores de autoria",
|
"pad.settings.colorcheck": "Cores de autoria",
|
||||||
"pad.settings.linenocheck": "Números de linha",
|
"pad.settings.linenocheck": "Números de linha",
|
||||||
|
@ -52,8 +53,8 @@
|
||||||
"pad.settings.language": "Língua:",
|
"pad.settings.language": "Língua:",
|
||||||
"pad.importExport.import_export": "Importar/Exportar",
|
"pad.importExport.import_export": "Importar/Exportar",
|
||||||
"pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento",
|
"pad.importExport.import": "Carregar qualquer ficheiro de texto ou documento",
|
||||||
"pad.importExport.importSuccessful": "Bem sucedido!",
|
"pad.importExport.importSuccessful": "Completo!",
|
||||||
"pad.importExport.export": "Exportar a Nota atual como:",
|
"pad.importExport.export": "Exportar a nota atual como:",
|
||||||
"pad.importExport.exportetherpad": "Etherpad",
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
"pad.importExport.exporthtml": "HTML",
|
"pad.importExport.exporthtml": "HTML",
|
||||||
"pad.importExport.exportplain": "Texto simples",
|
"pad.importExport.exportplain": "Texto simples",
|
||||||
|
@ -62,19 +63,19 @@
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"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.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.connected": "Ligado.",
|
||||||
"pad.modals.reconnecting": "Reconectando-se ao seu bloco…",
|
"pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…",
|
||||||
"pad.modals.forcereconnect": "Forçar reconexão",
|
"pad.modals.forcereconnect": "Forçar restabelecimento de ligação",
|
||||||
"pad.modals.reconnecttimer": "A tentar religar",
|
"pad.modals.reconnecttimer": "A tentar restabelecer ligação",
|
||||||
"pad.modals.cancel": "Cancelar",
|
"pad.modals.cancel": "Cancelar",
|
||||||
"pad.modals.userdup": "Aberto noutra janela",
|
"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.userdup.advice": "Religar para utilizar esta janela.",
|
||||||
"pad.modals.unauth": "Não autorizado",
|
"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.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.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.looping.cause": "Talvez tenha ligado por um firewall ou proxy incompatível.",
|
||||||
"pad.modals.initsocketfail": "O servidor está inacessí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.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.explanation": "O servidor não está a responder.",
|
||||||
"pad.modals.slowcommit.cause": "Isto pode ser por problemas com a ligação de rede.",
|
"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.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.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": "Eliminado.",
|
||||||
"pad.modals.deleted.explanation": "Este pad foi removido.",
|
"pad.modals.deleted.explanation": "Esta nota foi removida.",
|
||||||
"pad.modals.disconnected": "Você foi desconectado.",
|
"pad.modals.disconnected": "Você foi desligado.",
|
||||||
"pad.modals.disconnected.explanation": "A conexão com o servidor foi perdida",
|
"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.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.readonly": "Somente para leitura",
|
||||||
"pad.share.link": "Ligação",
|
"pad.share.link": "Hiperligação",
|
||||||
"pad.share.emebdcode": "Incorporar o URL",
|
"pad.share.emebdcode": "Incorporar o URL",
|
||||||
"pad.chat": "Bate-papo",
|
"pad.chat": "Conversação",
|
||||||
"pad.chat.title": "Abrir o bate-papo para este pad.",
|
"pad.chat.title": "Abrir a conversação para esta nota.",
|
||||||
"pad.chat.loadmessages": "Carregar mais mensagens",
|
"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.pageTitle": "Linha do tempo de {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Voltar ao pad",
|
"timeslider.toolbar.returnbutton": "Voltar à nota",
|
||||||
"timeslider.toolbar.authors": "Autores:",
|
"timeslider.toolbar.authors": "Autores:",
|
||||||
"timeslider.toolbar.authorsList": "Sem Autores",
|
"timeslider.toolbar.authorsList": "Sem Autores",
|
||||||
"timeslider.toolbar.exportlink.title": "Exportar",
|
"timeslider.toolbar.exportlink.title": "Exportar",
|
||||||
"timeslider.exportCurrent": "Exportar versão atual como:",
|
"timeslider.exportCurrent": "Exportar versão atual como:",
|
||||||
"timeslider.version": "Versão {{version}}",
|
"timeslider.version": "Versão {{version}}",
|
||||||
"timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
|
"timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
|
||||||
"timeslider.playPause": "Reproduzir / Pausar conteúdo do Pad",
|
"timeslider.playPause": "Reproduzir / pausar conteúdo da nota",
|
||||||
"timeslider.backRevision": "Voltar a uma revisão anterior neste Pad",
|
"timeslider.backRevision": "Voltar a uma revisão anterior desta nota",
|
||||||
"timeslider.forwardRevision": "Ir a uma revisão posterior neste Pad",
|
"timeslider.forwardRevision": "Avançar para uma revisão posterior desta nota",
|
||||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "Janeiro",
|
"timeslider.month.january": "Janeiro",
|
||||||
"timeslider.month.february": "Fevereiro",
|
"timeslider.month.february": "Fevereiro",
|
||||||
|
@ -129,11 +132,11 @@
|
||||||
"pad.editbar.clearcolors": "Deseja limpar as cores de autoria em todo o documento?",
|
"pad.editbar.clearcolors": "Deseja limpar as cores de autoria em todo o documento?",
|
||||||
"pad.impexp.importbutton": "Importar agora",
|
"pad.impexp.importbutton": "Importar agora",
|
||||||
"pad.impexp.importing": "Importando...",
|
"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.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.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 upload falhou. Por favor, tente novamente",
|
"pad.impexp.uploadFailed": "O carregamento falhou; tente novamente, por favor",
|
||||||
"pad.impexp.importfailed": "A importação falhou",
|
"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."
|
"pad.impexp.exportdisabled": "A exportação no formato {{type}} está desativada. Por favor, contacte o administrador do sistema para mais informações."
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,8 @@
|
||||||
"pad.chat": "Used as button text and as title of Chat window.\n{{Identical|Chat}}",
|
"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.title": "Used as tooltip for the Chat button",
|
||||||
"pad.chat.loadmessages": "chat messages",
|
"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.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.returnbutton": "Used as link title",
|
||||||
"timeslider.toolbar.authors": "A list of Authors follows after the colon.\n{{Identical|Author}}",
|
"timeslider.toolbar.authors": "A list of Authors follows after the colon.\n{{Identical|Author}}",
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"Nzeemin",
|
"Nzeemin",
|
||||||
"Facenapalm",
|
"Facenapalm",
|
||||||
"Patrick Star",
|
"Patrick Star",
|
||||||
"Movses"
|
"Movses",
|
||||||
|
"Diralik"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Создать",
|
"index.newPad": "Создать",
|
||||||
|
@ -82,8 +83,8 @@
|
||||||
"pad.modals.badChangeset.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы, если вы считаете, что это ошибка. Попробуйте переподключиться для того, чтобы продолжить редактирование.",
|
"pad.modals.badChangeset.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы, если вы считаете, что это ошибка. Попробуйте переподключиться для того, чтобы продолжить редактирование.",
|
||||||
"pad.modals.corruptPad.explanation": "Документ, к которому вы пытаетесь получить доступ, повреждён.",
|
"pad.modals.corruptPad.explanation": "Документ, к которому вы пытаетесь получить доступ, повреждён.",
|
||||||
"pad.modals.corruptPad.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы.",
|
"pad.modals.corruptPad.cause": "Это может быть из-за неправильной конфигурации сервера или некоторых других неожиданных действий. Пожалуйста, свяжитесь с администратором службы.",
|
||||||
"pad.modals.deleted": "Удален.",
|
"pad.modals.deleted": "Удалён.",
|
||||||
"pad.modals.deleted.explanation": "Этот документ был удален.",
|
"pad.modals.deleted.explanation": "Этот документ был удалён.",
|
||||||
"pad.modals.disconnected": "Соединение разорвано.",
|
"pad.modals.disconnected": "Соединение разорвано.",
|
||||||
"pad.modals.disconnected.explanation": "Подключение к серверу потеряно",
|
"pad.modals.disconnected.explanation": "Подключение к серверу потеряно",
|
||||||
"pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.",
|
"pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.",
|
||||||
|
@ -93,7 +94,9 @@
|
||||||
"pad.share.emebdcode": "Вставить URL",
|
"pad.share.emebdcode": "Вставить URL",
|
||||||
"pad.chat": "Чат",
|
"pad.chat": "Чат",
|
||||||
"pad.chat.title": "Открыть чат для этого документа.",
|
"pad.chat.title": "Открыть чат для этого документа.",
|
||||||
"pad.chat.loadmessages": "Еще сообщения",
|
"pad.chat.loadmessages": "Ещё сообщения",
|
||||||
|
"pad.chat.stick.title": "Закрепить чат на экране",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Напишите своё сообщение сюда",
|
||||||
"timeslider.pageTitle": "Временная шкала {{appTitle}}",
|
"timeslider.pageTitle": "Временная шкала {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "К документу",
|
"timeslider.toolbar.returnbutton": "К документу",
|
||||||
"timeslider.toolbar.authors": "Авторы:",
|
"timeslider.toolbar.authors": "Авторы:",
|
||||||
|
@ -135,5 +138,5 @@
|
||||||
"pad.impexp.uploadFailed": "Загрузка не удалась, пожалуйста, попробуйте ещё раз",
|
"pad.impexp.uploadFailed": "Загрузка не удалась, пожалуйста, попробуйте ещё раз",
|
||||||
"pad.impexp.importfailed": "Ошибка при импорте",
|
"pad.impexp.importfailed": "Ошибка при импорте",
|
||||||
"pad.impexp.copypaste": "Пожалуйста, скопируйте",
|
"pad.impexp.copypaste": "Пожалуйста, скопируйте",
|
||||||
"pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключен. Для подробной информации обратитесь к системному администратору."
|
"pad.impexp.exportdisabled": "Экспорт в формате {{type}} отключён. Для подробной информации обратитесь к системному администратору."
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
"timeslider.toolbar.authorsList": "ڪوبه ليکڪ ناهي",
|
"timeslider.toolbar.authorsList": "ڪوبه ليکڪ ناهي",
|
||||||
"timeslider.toolbar.exportlink.title": "برآمد ڪريو",
|
"timeslider.toolbar.exportlink.title": "برآمد ڪريو",
|
||||||
"timeslider.version": "ورزن {{version}}",
|
"timeslider.version": "ورزن {{version}}",
|
||||||
"timeslider.saved": "شانڍيل {{مهينو}} {{ڏينهن}}, {{سال}}",
|
"timeslider.saved": "سانڍيل {{month}} {{day}}، {{year}}",
|
||||||
"timeslider.dateformat": "{{مهينو}}/{{ڏينهن}}/{{سال}} {{ڪلاڪ}}:{{منٽ}}:{{سيڪنڊ}}",
|
"timeslider.dateformat": "{{مهينو}}/{{ڏينهن}}/{{سال}} {{ڪلاڪ}}:{{منٽ}}:{{سيڪنڊ}}",
|
||||||
"timeslider.month.january": "جنوري",
|
"timeslider.month.january": "جنوري",
|
||||||
"timeslider.month.february": "فيبروري",
|
"timeslider.month.february": "فيبروري",
|
||||||
|
|
132
src/locales/sh.json
Normal file
132
src/locales/sh.json
Normal 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."
|
||||||
|
}
|
|
@ -38,6 +38,7 @@
|
||||||
"pad.share.emebdcode": "امنیڈ یو آر ایل",
|
"pad.share.emebdcode": "امنیڈ یو آر ایل",
|
||||||
"pad.chat": "چیٹ",
|
"pad.chat": "چیٹ",
|
||||||
"pad.chat.loadmessages": "ٻئے سنیہے لوڈ کرو",
|
"pad.chat.loadmessages": "ٻئے سنیہے لوڈ کرو",
|
||||||
|
"pad.chat.writeMessage.placeholder": "آپݨاں سنیہا اتھ لکھو",
|
||||||
"timeslider.toolbar.returnbutton": "واپس پیڈ تے ونڄو",
|
"timeslider.toolbar.returnbutton": "واپس پیڈ تے ونڄو",
|
||||||
"timeslider.toolbar.authors": "مصنف:",
|
"timeslider.toolbar.authors": "مصنف:",
|
||||||
"timeslider.toolbar.authorsList": "کوئی مصنف کائنی",
|
"timeslider.toolbar.authorsList": "کوئی مصنف کائنی",
|
||||||
|
|
|
@ -90,6 +90,8 @@
|
||||||
"pad.chat": "Klepet",
|
"pad.chat": "Klepet",
|
||||||
"pad.chat.title": "Odpri klepetalno okno dokumenta.",
|
"pad.chat.title": "Odpri klepetalno okno dokumenta.",
|
||||||
"pad.chat.loadmessages": "Naloži več sporočil",
|
"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.pageTitle": "Časovni trak {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Vrni se na dokument",
|
"timeslider.toolbar.returnbutton": "Vrni se na dokument",
|
||||||
"timeslider.toolbar.authors": "Avtorji:",
|
"timeslider.toolbar.authors": "Avtorji:",
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": [
|
||||||
"Besnik b",
|
"Besnik b",
|
||||||
"Kosovastar"
|
"Kosovastar",
|
||||||
|
"Liridon"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Bllok i ri",
|
"index.newPad": "Bllok i ri",
|
||||||
|
@ -87,6 +88,8 @@
|
||||||
"pad.chat": "Fjalosje",
|
"pad.chat": "Fjalosje",
|
||||||
"pad.chat.title": "Hapni fjalosjen për këtë bllok.",
|
"pad.chat.title": "Hapni fjalosjen për këtë bllok.",
|
||||||
"pad.chat.loadmessages": "Ngarko më tepër mesazhe",
|
"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.pageTitle": "Rrjedhë kohore e {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Rikthehuni te blloku",
|
"timeslider.toolbar.returnbutton": "Rikthehuni te blloku",
|
||||||
"timeslider.toolbar.authors": "Autorë:",
|
"timeslider.toolbar.authors": "Autorë:",
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"Милан Јелисавчић",
|
"Милан Јелисавчић",
|
||||||
"Srdjan m",
|
"Srdjan m",
|
||||||
"Obsuser",
|
"Obsuser",
|
||||||
"Acamicamacaraca"
|
"Acamicamacaraca",
|
||||||
|
"BadDog"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Нови Пад",
|
"index.newPad": "Нови Пад",
|
||||||
|
@ -91,6 +92,8 @@
|
||||||
"pad.chat": "Ћаскање",
|
"pad.chat": "Ћаскање",
|
||||||
"pad.chat.title": "Отворите ћаскање за овај пад.",
|
"pad.chat.title": "Отворите ћаскање за овај пад.",
|
||||||
"pad.chat.loadmessages": "Учитај више порука",
|
"pad.chat.loadmessages": "Учитај више порука",
|
||||||
|
"pad.chat.stick.title": "Залепите ћаскање на екран",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Напишите поруку овде",
|
||||||
"timeslider.pageTitle": "{{appTitle}} временска линија",
|
"timeslider.pageTitle": "{{appTitle}} временска линија",
|
||||||
"timeslider.toolbar.returnbutton": "Врати се на пад",
|
"timeslider.toolbar.returnbutton": "Врати се на пад",
|
||||||
"timeslider.toolbar.authors": "Аутори:",
|
"timeslider.toolbar.authors": "Аутори:",
|
||||||
|
|
129
src/locales/sr-el.json
Normal file
129
src/locales/sr-el.json
Normal 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."
|
||||||
|
}
|
|
@ -88,6 +88,8 @@
|
||||||
"pad.chat": "Chatt",
|
"pad.chat": "Chatt",
|
||||||
"pad.chat.title": "Öppna chatten för detta block.",
|
"pad.chat.title": "Öppna chatten för detta block.",
|
||||||
"pad.chat.loadmessages": "Läs in fler meddelanden",
|
"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.pageTitle": "{{appTitle}} tidsreglage",
|
||||||
"timeslider.toolbar.returnbutton": "Återvänd till blocket",
|
"timeslider.toolbar.returnbutton": "Återvänd till blocket",
|
||||||
"timeslider.toolbar.authors": "Författare:",
|
"timeslider.toolbar.authors": "Författare:",
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
"Meelo",
|
"Meelo",
|
||||||
"Trockya",
|
"Trockya",
|
||||||
"McAang",
|
"McAang",
|
||||||
"Vito Genovese"
|
"Vito Genovese",
|
||||||
|
"Hedda",
|
||||||
|
"Grkn gll"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "Yeni Bloknot",
|
"index.newPad": "Yeni Bloknot",
|
||||||
|
@ -48,7 +50,7 @@
|
||||||
"pad.settings.fontType.monospaced": "Tek aralıklı",
|
"pad.settings.fontType.monospaced": "Tek aralıklı",
|
||||||
"pad.settings.globalView": "Genel Görünüm",
|
"pad.settings.globalView": "Genel Görünüm",
|
||||||
"pad.settings.language": "Dil:",
|
"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.import": "Herhangi bir metin dosyası ya da belgesi yükle",
|
||||||
"pad.importExport.importSuccessful": "Başarılı!",
|
"pad.importExport.importSuccessful": "Başarılı!",
|
||||||
"pad.importExport.export": "Mevcut bloknotu şu olarak dışa aktar:",
|
"pad.importExport.export": "Mevcut bloknotu şu olarak dışa aktar:",
|
||||||
|
@ -92,6 +94,8 @@
|
||||||
"pad.chat": "Sohbet",
|
"pad.chat": "Sohbet",
|
||||||
"pad.chat.title": "Bu bloknot için sohbeti açın.",
|
"pad.chat.title": "Bu bloknot için sohbeti açın.",
|
||||||
"pad.chat.loadmessages": "Daha fazla mesaj yükle",
|
"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.pageTitle": "{{appTitle}} Zaman Çizelgesi",
|
||||||
"timeslider.toolbar.returnbutton": "Bloknota geri dön",
|
"timeslider.toolbar.returnbutton": "Bloknota geri dön",
|
||||||
"timeslider.toolbar.authors": "Yazarlar:",
|
"timeslider.toolbar.authors": "Yazarlar:",
|
||||||
|
|
|
@ -95,6 +95,8 @@
|
||||||
"pad.chat": "Чат",
|
"pad.chat": "Чат",
|
||||||
"pad.chat.title": "Відкрити чат для цього документа.",
|
"pad.chat.title": "Відкрити чат для цього документа.",
|
||||||
"pad.chat.loadmessages": "Завантажити більше повідомлень",
|
"pad.chat.loadmessages": "Завантажити більше повідомлень",
|
||||||
|
"pad.chat.stick.title": "Закріпити чат на екрані",
|
||||||
|
"pad.chat.writeMessage.placeholder": "Напишіть своє повідомлення сюди",
|
||||||
"timeslider.pageTitle": "Часова шкала {{appTitle}}",
|
"timeslider.pageTitle": "Часова шкала {{appTitle}}",
|
||||||
"timeslider.toolbar.returnbutton": "Повернутись до документа",
|
"timeslider.toolbar.returnbutton": "Повернутись до документа",
|
||||||
"timeslider.toolbar.authors": "Автори:",
|
"timeslider.toolbar.authors": "Автори:",
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
"Yfdyh000",
|
"Yfdyh000",
|
||||||
"乌拉跨氪",
|
"乌拉跨氪",
|
||||||
"燃玉",
|
"燃玉",
|
||||||
"JuneAugust"
|
"JuneAugust",
|
||||||
|
"94rain",
|
||||||
|
"VulpesVulpes825"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.newPad": "新记事本",
|
"index.newPad": "新记事本",
|
||||||
|
@ -96,6 +98,8 @@
|
||||||
"pad.chat": "聊天",
|
"pad.chat": "聊天",
|
||||||
"pad.chat.title": "打开此记事本的聊天窗口。",
|
"pad.chat.title": "打开此记事本的聊天窗口。",
|
||||||
"pad.chat.loadmessages": "加载更多信息",
|
"pad.chat.loadmessages": "加载更多信息",
|
||||||
|
"pad.chat.stick.title": "在屏幕上固定聊天界面",
|
||||||
|
"pad.chat.writeMessage.placeholder": "在此写下您的消息",
|
||||||
"timeslider.pageTitle": "{{appTitle}} 时间轴",
|
"timeslider.pageTitle": "{{appTitle}} 时间轴",
|
||||||
"timeslider.toolbar.returnbutton": "返回记事本",
|
"timeslider.toolbar.returnbutton": "返回记事本",
|
||||||
"timeslider.toolbar.authors": "作者:",
|
"timeslider.toolbar.authors": "作者:",
|
||||||
|
|
|
@ -93,6 +93,8 @@
|
||||||
"pad.chat": "聊天功能",
|
"pad.chat": "聊天功能",
|
||||||
"pad.chat.title": "打開記事本聊天功能",
|
"pad.chat.title": "打開記事本聊天功能",
|
||||||
"pad.chat.loadmessages": "載入更多訊息",
|
"pad.chat.loadmessages": "載入更多訊息",
|
||||||
|
"pad.chat.stick.title": "釘住聊天在螢幕上",
|
||||||
|
"pad.chat.writeMessage.placeholder": "在此編寫您的訊息",
|
||||||
"timeslider.pageTitle": "{{appTitle}}時間軸",
|
"timeslider.pageTitle": "{{appTitle}}時間軸",
|
||||||
"timeslider.toolbar.returnbutton": "返回到記事本",
|
"timeslider.toolbar.returnbutton": "返回到記事本",
|
||||||
"timeslider.toolbar.authors": "協作者:",
|
"timeslider.toolbar.authors": "協作者:",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# About the folder structure
|
# About the folder structure
|
||||||
|
|
||||||
* **db** - all modules that are accessing the data structure and are communicating directly to the database
|
* **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
|
* **utils** - helper modules
|
||||||
|
|
||||||
# Module name conventions
|
# Module name conventions
|
||||||
|
|
1011
src/node/db/API.js
1011
src/node/db/API.js
File diff suppressed because it is too large
Load diff
|
@ -18,211 +18,189 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var db = require("./DB");
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var customError = require("../utils/customError");
|
var customError = require("../utils/customError");
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
||||||
|
|
||||||
exports.getColorPalette = function(){
|
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"];
|
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
|
* Checks if the author exists
|
||||||
*/
|
*/
|
||||||
exports.doesAuthorExists = function (authorID, callback)
|
exports.doesAuthorExist = async function(authorID)
|
||||||
{
|
{
|
||||||
//check if the database entry of this author exists
|
let author = await db.get("globalAuthor:" + authorID);
|
||||||
db.get("globalAuthor:" + authorID, function (err, author)
|
|
||||||
{
|
return author !== null;
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, author != null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* exported for backwards compatibility */
|
||||||
|
exports.doesAuthorExists = exports.doesAuthorExist;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the AuthorID for a token.
|
* Returns the AuthorID for a token.
|
||||||
* @param {String} token The 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)
|
let author = await mapAuthorWithDBKey("token2author", token);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
// return only the sub value authorID
|
||||||
//return only the sub value authorID
|
return author ? author.authorID : author;
|
||||||
callback(null, author ? author.authorID : author);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the AuthorID for a mapper.
|
* Returns the AuthorID for a mapper.
|
||||||
* @param {String} token The mapper
|
* @param {String} token The mapper
|
||||||
* @param {String} name The name of the author (optional)
|
* @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)
|
let author = await mapAuthorWithDBKey("mapper2author", authorMapper);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//set the name of this author
|
if (name) {
|
||||||
if(name)
|
// set the name of this author
|
||||||
exports.setAuthorName(author.authorID, name);
|
await exports.setAuthorName(author.authorID, name);
|
||||||
|
}
|
||||||
|
|
||||||
//return the authorID
|
return author;
|
||||||
callback(null, author);
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
||||||
* so far this is token2author and mapper2author
|
* so far this is token2author and mapper2author
|
||||||
* @param {String} mapperkey The database key name for this mapper
|
* @param {String} mapperkey The database key name for this mapper
|
||||||
* @param {String} mapper The 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
|
// try to map to an author
|
||||||
db.get(mapperkey + ":" + mapper, function (err, author)
|
let author = await db.get(mapperkey + ":" + mapper);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//there is no author with this mapper, so create one
|
if (author === null) {
|
||||||
if(author == null)
|
// there is no author with this mapper, so create one
|
||||||
{
|
let author = await exports.createAuthor(null);
|
||||||
exports.createAuthor(null, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//create the token2author relation
|
// create the token2author relation
|
||||||
db.set(mapperkey + ":" + mapper, author.authorID);
|
await db.set(mapperkey + ":" + mapper, author.authorID);
|
||||||
|
|
||||||
//return the author
|
// return the author
|
||||||
callback(null, author);
|
return author;
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//there is a author with this mapper
|
// there is an author with this mapper
|
||||||
//update the timestamp of this author
|
// update the timestamp of this author
|
||||||
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
|
await db.setSub("globalAuthor:" + author, ["timestamp"], Date.now());
|
||||||
|
|
||||||
//return the author
|
// return the author
|
||||||
callback(null, {authorID: author});
|
return { authorID: author};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal function that creates the database entry for an author
|
* Internal function that creates the database entry for an author
|
||||||
* @param {String} name The name of the author
|
* @param {String} name The name of the author
|
||||||
*/
|
*/
|
||||||
exports.createAuthor = function(name, callback)
|
exports.createAuthor = function(name)
|
||||||
{
|
{
|
||||||
//create the new author name
|
// create the new author name
|
||||||
var author = "a." + randomString(16);
|
let author = "a." + randomString(16);
|
||||||
|
|
||||||
//create the globalAuthors db entry
|
// create the globalAuthors db entry
|
||||||
var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": new Date().getTime()};
|
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);
|
db.set("globalAuthor:" + author, authorObj);
|
||||||
|
|
||||||
callback(null, {authorID: author});
|
return { authorID: author };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Author Obj of the author
|
* Returns the Author Obj of the author
|
||||||
* @param {String} author The id 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
|
* Returns the color Id of the author
|
||||||
* @param {String} author The 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
|
* Sets the color Id of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} colorId The color 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
|
* Returns the name of the author
|
||||||
* @param {String} author The id 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
|
* Sets the name of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} name The name 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
|
* Returns an array of all pads this author contributed to
|
||||||
* @param {String} author The id of the author
|
* @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:
|
/* 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
|
* (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
|
* (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
|
// get the globalAuthor
|
||||||
if(author == null)
|
let author = await db.get("globalAuthor:" + authorID);
|
||||||
{
|
|
||||||
callback(new customError("authorID does not exist","apierror"))
|
if (author === null) {
|
||||||
return;
|
// author does not exist
|
||||||
|
throw new customError("authorID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
//everything is fine, return the pad IDs
|
// everything is fine, return the pad IDs
|
||||||
var pads = [];
|
let padIDs = Object.keys(author.padIDs || {});
|
||||||
if(author.padIDs != null)
|
|
||||||
{
|
return { padIDs };
|
||||||
for (var padId in author.padIDs)
|
|
||||||
{
|
|
||||||
pads.push(padId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback(null, {padIDs: pads});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -230,26 +208,27 @@ exports.listPadsOfAuthor = function (authorID, callback)
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} padID The id of the pad the author contributes to
|
* @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
|
// get the entry
|
||||||
db.get("globalAuthor:" + authorID, function(err, author)
|
let author = await db.get("globalAuthor:" + authorID);
|
||||||
{
|
|
||||||
if(ERR(err)) return;
|
|
||||||
if(author == null) return;
|
|
||||||
|
|
||||||
//the entry doesn't exist so far, let's create it
|
if (author === null) return;
|
||||||
if(author.padIDs == null)
|
|
||||||
{
|
/*
|
||||||
|
* 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 = {};
|
author.padIDs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
//add the entry for this pad
|
// add the entry for this pad
|
||||||
author.padIDs[padID] = 1;// anything, because value is not used
|
author.padIDs[padID] = 1; // anything, because value is not used
|
||||||
|
|
||||||
//save the new element back
|
// save the new element back
|
||||||
db.set("globalAuthor:" + authorID, author);
|
db.set("globalAuthor:" + authorID, author);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -257,18 +236,15 @@ exports.addPad = function (authorID, padID)
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} padID The id of the pad the author contributes to
|
* @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)
|
let author = await db.get("globalAuthor:" + authorID);
|
||||||
{
|
|
||||||
if(ERR(err)) return;
|
|
||||||
if(author == null) return;
|
|
||||||
|
|
||||||
if(author.padIDs != null)
|
if (author === null) return;
|
||||||
{
|
|
||||||
//remove pad from author
|
if (author.padIDs !== null) {
|
||||||
|
// remove pad from author
|
||||||
delete author.padIDs[padID];
|
delete author.padIDs[padID];
|
||||||
db.set("globalAuthor:" + authorID, author);
|
db.set("globalAuthor:" + authorID, author);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,10 @@
|
||||||
var ueberDB = require("ueberdb2");
|
var ueberDB = require("ueberdb2");
|
||||||
var settings = require("../utils/Settings");
|
var settings = require("../utils/Settings");
|
||||||
var log4js = require('log4js');
|
var log4js = require('log4js');
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
//set database settings
|
// set database settings
|
||||||
var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
|
let db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UeberDB Object that provides the database functions
|
* The UeberDB Object that provides the database functions
|
||||||
|
@ -35,23 +36,38 @@ exports.db = null;
|
||||||
* Initalizes the database with the settings provided by the settings module
|
* Initalizes the database with the settings provided by the settings module
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
*/
|
*/
|
||||||
exports.init = function(callback)
|
exports.init = function() {
|
||||||
{
|
// initalize the database async
|
||||||
//initalize the database async
|
return new Promise((resolve, reject) => {
|
||||||
db.init(function(err)
|
db.init(function(err) {
|
||||||
{
|
if (err) {
|
||||||
//there was an error while initializing the database, output it and stop
|
// there was an error while initializing the database, output it and stop
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
console.error("ERROR: Problem while initalizing the database");
|
console.error("ERROR: Problem while initalizing the database");
|
||||||
console.error(err.stack ? err.stack : err);
|
console.error(err.stack ? err.stack : err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
} else {
|
||||||
//everything ok
|
// everything ok, set up Promise-based methods
|
||||||
else
|
['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;
|
exports.db = db;
|
||||||
callback(null);
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,318 +18,166 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
var customError = require("../utils/customError");
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
||||||
var db = require("./DB").db;
|
var db = require("./DB");
|
||||||
var async = require("async");
|
|
||||||
var padManager = require("./PadManager");
|
var padManager = require("./PadManager");
|
||||||
var sessionManager = require("./SessionManager");
|
var sessionManager = require("./SessionManager");
|
||||||
|
|
||||||
exports.listAllGroups = function(callback) {
|
exports.listAllGroups = async function()
|
||||||
db.get("groups", function (err, groups) {
|
{
|
||||||
if(ERR(err, callback)) return;
|
let groups = await db.get("groups");
|
||||||
|
groups = groups || {};
|
||||||
|
|
||||||
// there are no groups
|
let groupIDs = Object.keys(groups);
|
||||||
if(groups == null) {
|
return { groupIDs };
|
||||||
callback(null, {groupIDs: []});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupIDs = [];
|
|
||||||
for ( var groupID in groups ) {
|
|
||||||
groupIDs.push(groupID);
|
|
||||||
}
|
|
||||||
callback(null, {groupIDs: groupIDs});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.deleteGroup = function(groupID, callback)
|
exports.deleteGroup = async function(groupID)
|
||||||
{
|
{
|
||||||
var group;
|
let group = await db.get("group:" + groupID);
|
||||||
|
|
||||||
async.series([
|
// ensure group exists
|
||||||
//ensure group exists
|
if (group == null) {
|
||||||
function (callback)
|
// group does not exist
|
||||||
{
|
throw new customError("groupID does not exist", "apierror");
|
||||||
//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
|
// iterate through all pads of this group and delete them (in parallel)
|
||||||
group = _group;
|
await Promise.all(Object.keys(group.pads).map(padID => {
|
||||||
callback();
|
return padManager.getPad(padID).then(pad => pad.remove());
|
||||||
});
|
}));
|
||||||
},
|
|
||||||
//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
|
// iterate through group2sessions and delete all sessions
|
||||||
async.forEach(padIDs, function(padID, callback)
|
let group2sessions = await db.get("group2sessions:" + groupID);
|
||||||
{
|
let sessions = group2sessions ? group2sessions.sessionsIDs : {};
|
||||||
padManager.getPad(padID, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
pad.remove(callback);
|
// loop through all sessions and delete them (in parallel)
|
||||||
});
|
await Promise.all(Object.keys(sessions).map(session => {
|
||||||
}, callback);
|
return sessionManager.deleteSession(session);
|
||||||
},
|
}));
|
||||||
//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
|
// remove group and group2sessions entry
|
||||||
if(group2sessions == null) {callback(); return}
|
await db.remove("group2sessions:" + groupID);
|
||||||
|
await db.remove("group:" + groupID);
|
||||||
|
|
||||||
//collect all sessions in an array, that allows us to use async.forEach
|
// unlist the group
|
||||||
var sessions = [];
|
let groups = await exports.listAllGroups();
|
||||||
for(var i in group2sessions.sessionsIDs)
|
groups = groups ? groups.groupIDs : [];
|
||||||
{
|
|
||||||
sessions.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop trough all sessions and delete them
|
let index = groups.indexOf(groupID);
|
||||||
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 : [];
|
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
// it's not listed
|
// it's not listed
|
||||||
if(groups.indexOf(groupID) == -1) {
|
|
||||||
callback();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.splice(groups.indexOf(groupID), 1);
|
// remove from the list
|
||||||
|
groups.splice(index, 1);
|
||||||
// store empty groupe list
|
|
||||||
if(groups.length == 0) {
|
|
||||||
db.set("groups", {});
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// regenerate group list
|
// regenerate group list
|
||||||
var newGroups = {};
|
var newGroups = {};
|
||||||
async.forEach(groups, function(group, cb) {
|
groups.forEach(group => newGroups[group] = 1);
|
||||||
newGroups[group] = 1;
|
await db.set("groups", newGroups);
|
||||||
cb();
|
|
||||||
},function() {
|
|
||||||
db.set("groups", newGroups);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesGroupExist = function(groupID, callback)
|
exports.doesGroupExist = async function(groupID)
|
||||||
{
|
{
|
||||||
//try to get the group entry
|
// try to get the group entry
|
||||||
db.get("group:" + groupID, function (err, group)
|
let group = await db.get("group:" + groupID);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
return (group != null);
|
||||||
callback(null, group != null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createGroup = function(callback)
|
exports.createGroup = async function()
|
||||||
{
|
{
|
||||||
//search for non existing groupID
|
// search for non existing groupID
|
||||||
var groupID = "g." + randomString(16);
|
var groupID = "g." + randomString(16);
|
||||||
|
|
||||||
//create the group
|
// create the group
|
||||||
db.set("group:" + groupID, {pads: {}});
|
await db.set("group:" + groupID, {pads: {}});
|
||||||
|
|
||||||
//list the group
|
// list the group
|
||||||
exports.listAllGroups(function(err, groups) {
|
let groups = await exports.listAllGroups();
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
groups = groups? groups.groupIDs : [];
|
groups = groups? groups.groupIDs : [];
|
||||||
|
|
||||||
groups.push(groupID);
|
groups.push(groupID);
|
||||||
|
|
||||||
// regenerate group list
|
// regenerate group list
|
||||||
var newGroups = {};
|
var newGroups = {};
|
||||||
async.forEach(groups, function(group, cb) {
|
groups.forEach(group => newGroups[group] = 1);
|
||||||
newGroups[group] = 1;
|
await db.set("groups", newGroups);
|
||||||
cb();
|
|
||||||
},function() {
|
return { groupID };
|
||||||
db.set("groups", newGroups);
|
|
||||||
callback(null, {groupID: groupID});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
|
exports.createGroupIfNotExistsFor = async function(groupMapper)
|
||||||
{
|
{
|
||||||
//ensure mapper is optional
|
// ensure mapper is optional
|
||||||
if(typeof groupMapper != "string")
|
if (typeof groupMapper !== "string") {
|
||||||
{
|
throw new customError("groupMapper is not a string", "apierror");
|
||||||
callback(new customError("groupMapper is no string","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//try to get a group for this mapper
|
// try to get a group for this mapper
|
||||||
db.get("mapper2group:"+groupMapper, function(err, groupID)
|
let groupID = await db.get("mapper2group:" + groupMapper);
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
if (groupID) {
|
||||||
// there is a group for this mapper
|
// there is a group for this mapper
|
||||||
if(groupID) {
|
let exists = await exports.doesGroupExist(groupID);
|
||||||
exports.doesGroupExist(groupID, function(err, exists) {
|
|
||||||
if(ERR(err, callback)) return;
|
if (exists) return { groupID };
|
||||||
if(exists) return callback(null, {groupID: groupID});
|
}
|
||||||
|
|
||||||
// hah, the returned group doesn't exist, let's create one
|
// hah, the returned group doesn't exist, let's create one
|
||||||
createGroupForMapper(callback)
|
let result = await exports.createGroup();
|
||||||
})
|
|
||||||
|
|
||||||
return;
|
// create the mapper entry for this group
|
||||||
}
|
await db.set("mapper2group:" + groupMapper, result.groupID);
|
||||||
|
|
||||||
//there is no group for this mapper, let's create a group
|
return result;
|
||||||
createGroupForMapper(callback)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createGroupPad = function(groupID, padName, text, callback)
|
exports.createGroupPad = async function(groupID, padName, text)
|
||||||
{
|
{
|
||||||
//create the padID
|
// create the padID
|
||||||
var padID = groupID + "$" + padName;
|
let padID = groupID + "$" + padName;
|
||||||
|
|
||||||
async.series([
|
// ensure group exists
|
||||||
//ensure group exists
|
let groupExists = await exports.doesGroupExist(groupID);
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
exports.doesGroupExist(groupID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
if (!groupExists) {
|
||||||
if(exists == false)
|
throw new customError("groupID does not exist", "apierror");
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//group exists, everything is fine
|
// ensure pad doesn't exist already
|
||||||
callback();
|
let padExists = await padManager.doesPadExists(padID);
|
||||||
});
|
|
||||||
},
|
|
||||||
//ensure pad does not exists
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//pad exists already
|
if (padExists) {
|
||||||
if(exists == true)
|
// pad exists already
|
||||||
{
|
throw new customError("padName does already exist", "apierror");
|
||||||
callback(new customError("padName does already exist","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//pad does not exist, everything is fine
|
// create the pad
|
||||||
callback();
|
await padManager.getPad(padID, text);
|
||||||
});
|
|
||||||
},
|
|
||||||
//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
|
//create an entry in the group for this pad
|
||||||
function (callback)
|
await db.setSub("group:" + groupID, ["pads", padID], 1);
|
||||||
{
|
|
||||||
db.setSub("group:" + groupID, ["pads", padID], 1);
|
return { padID };
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, {padID: padID});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.listPads = function(groupID, callback)
|
exports.listPads = async function(groupID)
|
||||||
{
|
{
|
||||||
exports.doesGroupExist(groupID, function(err, exists)
|
let exists = await exports.doesGroupExist(groupID);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
// ensure the group exists
|
||||||
if(exists == false)
|
if (!exists) {
|
||||||
{
|
throw new customError("groupID does not exist", "apierror");
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//group exists, let's get the pads
|
// group exists, let's get the pads
|
||||||
db.getSub("group:" + groupID, ["pads"], function(err, result)
|
let result = await db.getSub("group:" + groupID, ["pads"]);
|
||||||
{
|
let padIDs = Object.keys(result);
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
var pads = [];
|
return { padIDs };
|
||||||
for ( var padId in result ) {
|
|
||||||
pads.push(padId);
|
|
||||||
}
|
|
||||||
callback(null, {padIDs: pads});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
||||||
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
||||||
var db = require("./DB").db;
|
var db = require("./DB");
|
||||||
var async = require("async");
|
|
||||||
var settings = require('../utils/Settings');
|
var settings = require('../utils/Settings');
|
||||||
var authorManager = require("./AuthorManager");
|
var authorManager = require("./AuthorManager");
|
||||||
var padManager = require("./PadManager");
|
var padManager = require("./PadManager");
|
||||||
|
@ -19,7 +17,7 @@ var crypto = require("crypto");
|
||||||
var randomString = require("../utils/randomstring");
|
var randomString = require("../utils/randomstring");
|
||||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||||
|
|
||||||
//serialization/deserialization attributes
|
// serialization/deserialization attributes
|
||||||
var attributeBlackList = ["id"];
|
var attributeBlackList = ["id"];
|
||||||
var jsonableList = ["pool"];
|
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.atext = Changeset.makeAText("\n");
|
||||||
this.pool = new AttributePool();
|
this.pool = new AttributePool();
|
||||||
this.head = -1;
|
this.head = -1;
|
||||||
|
@ -60,7 +57,7 @@ Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() {
|
||||||
|
|
||||||
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
|
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
|
||||||
var savedRev = new Array();
|
var savedRev = new Array();
|
||||||
for(var rev in this.savedRevisions){
|
for (var rev in this.savedRevisions) {
|
||||||
savedRev.push(this.savedRevisions[rev].revNum);
|
savedRev.push(this.savedRevisions[rev].revNum);
|
||||||
}
|
}
|
||||||
savedRev.sort(function(a, b) {
|
savedRev.sort(function(a, b) {
|
||||||
|
@ -74,8 +71,9 @@ Pad.prototype.getPublicStatus = function getPublicStatus() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
|
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
|
||||||
if(!author)
|
if (!author) {
|
||||||
author = '';
|
author = '';
|
||||||
|
}
|
||||||
|
|
||||||
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||||
Changeset.copyAText(newAText, this.atext);
|
Changeset.copyAText(newAText, this.atext);
|
||||||
|
@ -86,23 +84,24 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
|
||||||
newRevData.changeset = aChangeset;
|
newRevData.changeset = aChangeset;
|
||||||
newRevData.meta = {};
|
newRevData.meta = {};
|
||||||
newRevData.meta.author = author;
|
newRevData.meta.author = author;
|
||||||
newRevData.meta.timestamp = new Date().getTime();
|
newRevData.meta.timestamp = Date.now();
|
||||||
|
|
||||||
//ex. getNumForAuthor
|
// ex. getNumForAuthor
|
||||||
if(author != '')
|
if (author != '') {
|
||||||
this.pool.putAttrib(['author', author || '']);
|
this.pool.putAttrib(['author', author || '']);
|
||||||
|
}
|
||||||
|
|
||||||
if(newRev % 100 == 0)
|
if (newRev % 100 == 0) {
|
||||||
{
|
|
||||||
newRevData.meta.atext = this.atext;
|
newRevData.meta.atext = this.atext;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
|
db.set("pad:" + this.id + ":revs:" + newRev, newRevData);
|
||||||
this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
|
|
||||||
// set the author to pad
|
// set the author to pad
|
||||||
if(author)
|
if (author) {
|
||||||
authorManager.addPad(author, this.id);
|
authorManager.addPad(author, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.head == 0) {
|
if (this.head == 0) {
|
||||||
hooks.callAll("padCreate", {'pad':this, 'author': author});
|
hooks.callAll("padCreate", {'pad':this, 'author': author});
|
||||||
|
@ -111,49 +110,47 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//save all attributes to the database
|
// save all attributes to the database
|
||||||
Pad.prototype.saveToDatabase = function saveToDatabase(){
|
Pad.prototype.saveToDatabase = function saveToDatabase() {
|
||||||
var dbObject = {};
|
var dbObject = {};
|
||||||
|
|
||||||
for(var attr in this){
|
for (var attr in this) {
|
||||||
if(typeof this[attr] === "function") continue;
|
if (typeof this[attr] === "function") continue;
|
||||||
if(attributeBlackList.indexOf(attr) !== -1) continue;
|
if (attributeBlackList.indexOf(attr) !== -1) continue;
|
||||||
|
|
||||||
dbObject[attr] = this[attr];
|
dbObject[attr] = this[attr];
|
||||||
|
|
||||||
if(jsonableList.indexOf(attr) !== -1){
|
if (jsonableList.indexOf(attr) !== -1) {
|
||||||
dbObject[attr] = dbObject[attr].toJsonable();
|
dbObject[attr] = dbObject[attr].toJsonable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.set("pad:"+this.id, dbObject);
|
db.set("pad:" + this.id, dbObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get time of last edit (changeset application)
|
// get time of last edit (changeset application)
|
||||||
Pad.prototype.getLastEdit = function getLastEdit(callback){
|
Pad.prototype.getLastEdit = function getLastEdit() {
|
||||||
var revNum = this.getHeadRevisionNumber();
|
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) {
|
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum) {
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
|
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["changeset"]);
|
||||||
};
|
}
|
||||||
|
|
||||||
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) {
|
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum) {
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
|
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "author"]);
|
||||||
};
|
}
|
||||||
|
|
||||||
Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) {
|
Pad.prototype.getRevisionDate = function getRevisionDate(revNum) {
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
|
return db.getSub("pad:" + this.id + ":revs:" + revNum, ["meta", "timestamp"]);
|
||||||
};
|
}
|
||||||
|
|
||||||
Pad.prototype.getAllAuthors = function getAllAuthors() {
|
Pad.prototype.getAllAuthors = function getAllAuthors() {
|
||||||
var authors = [];
|
var authors = [];
|
||||||
|
|
||||||
for(var key in this.pool.numToAttrib)
|
for(var key in this.pool.numToAttrib) {
|
||||||
{
|
if (this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") {
|
||||||
if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
|
|
||||||
{
|
|
||||||
authors.push(this.pool.numToAttrib[key][1]);
|
authors.push(this.pool.numToAttrib[key][1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,120 +158,77 @@ Pad.prototype.getAllAuthors = function getAllAuthors() {
|
||||||
return authors;
|
return authors;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) {
|
Pad.prototype.getInternalRevisionAText = async function getInternalRevisionAText(targetRev) {
|
||||||
var _this = this;
|
let keyRev = this.getKeyRevisionNumber(targetRev);
|
||||||
|
|
||||||
var keyRev = this.getKeyRevisionNumber(targetRev);
|
// find out which changesets are needed
|
||||||
var atext;
|
let neededChangesets = [];
|
||||||
var changesets = [];
|
for (let curRev = keyRev; curRev < targetRev; ) {
|
||||||
|
neededChangesets.push(++curRev);
|
||||||
//find out which changesets are needed
|
|
||||||
var neededChangesets = [];
|
|
||||||
var curRev = keyRev;
|
|
||||||
while (curRev < targetRev)
|
|
||||||
{
|
|
||||||
curRev++;
|
|
||||||
neededChangesets.push(curRev);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async.series([
|
// get all needed data out of the database
|
||||||
//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);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
// start to get the atext of the key revision
|
||||||
});
|
let p_atext = db.getSub("pad:" + this.id + ":revs:" + keyRev, ["meta", "atext"]);
|
||||||
},
|
|
||||||
//get all needed changesets
|
// get all needed changesets
|
||||||
function (callback)
|
let changesets = [];
|
||||||
{
|
await Promise.all(neededChangesets.map(item => {
|
||||||
async.forEach(neededChangesets, function(item, callback)
|
return this.getRevisionChangeset(item).then(changeset => {
|
||||||
{
|
|
||||||
_this.getRevisionChangeset(item, function(err, changeset)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
changesets[item] = changeset;
|
changesets[item] = changeset;
|
||||||
callback();
|
|
||||||
});
|
});
|
||||||
}, callback);
|
}));
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//apply all changesets to the key changeset
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var apool = _this.apool();
|
|
||||||
var curRev = keyRev;
|
|
||||||
|
|
||||||
while (curRev < targetRev)
|
// we should have the atext by now
|
||||||
{
|
let atext = await p_atext;
|
||||||
curRev++;
|
atext = Changeset.cloneAText(atext);
|
||||||
var cs = changesets[curRev];
|
|
||||||
try{
|
// 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);
|
atext = Changeset.applyToAText(cs, atext, apool);
|
||||||
}catch(e) {
|
|
||||||
return callback(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null);
|
return atext;
|
||||||
}
|
}
|
||||||
], function(err)
|
|
||||||
{
|
Pad.prototype.getRevision = function getRevisionChangeset(revNum) {
|
||||||
if(ERR(err, callback)) return;
|
return db.get("pad:" + this.id + ":revs:" + revNum);
|
||||||
callback(null, atext);
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
};
|
}));
|
||||||
|
|
||||||
Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) {
|
return returnTable;
|
||||||
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();
|
|
||||||
});
|
|
||||||
}, function(err){
|
|
||||||
callback(err, returnTable);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) {
|
Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) {
|
||||||
startRev = parseInt(startRev, 10);
|
startRev = parseInt(startRev, 10);
|
||||||
var head = this.getHeadRevisionNumber();
|
var head = this.getHeadRevisionNumber();
|
||||||
endRev = endRev ? parseInt(endRev, 10) : head;
|
endRev = endRev ? parseInt(endRev, 10) : head;
|
||||||
if(isNaN(startRev) || startRev < 0 || startRev > head) {
|
|
||||||
|
if (isNaN(startRev) || startRev < 0 || startRev > head) {
|
||||||
startRev = null;
|
startRev = null;
|
||||||
}
|
}
|
||||||
if(isNaN(endRev) || endRev < startRev) {
|
|
||||||
|
if (isNaN(endRev) || endRev < startRev) {
|
||||||
endRev = null;
|
endRev = null;
|
||||||
} else if(endRev > head) {
|
} else if (endRev > head) {
|
||||||
endRev = head;
|
endRev = head;
|
||||||
}
|
}
|
||||||
if(startRev !== null && endRev !== null) {
|
|
||||||
|
if (startRev !== null && endRev !== null) {
|
||||||
return { startRev: startRev , endRev: endRev }
|
return { startRev: startRev , endRev: endRev }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -289,12 +243,12 @@ Pad.prototype.text = function text() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.setText = function setText(newText) {
|
Pad.prototype.setText = function setText(newText) {
|
||||||
//clean the new text
|
// clean the new text
|
||||||
newText = exports.cleanText(newText);
|
newText = exports.cleanText(newText);
|
||||||
|
|
||||||
var oldText = this.text();
|
var oldText = this.text();
|
||||||
|
|
||||||
//create the changeset
|
// create the changeset
|
||||||
// We want to ensure the pad still ends with a \n, but otherwise keep
|
// We want to ensure the pad still ends with a \n, but otherwise keep
|
||||||
// getText() and setText() consistent.
|
// getText() and setText() consistent.
|
||||||
var changeset;
|
var changeset;
|
||||||
|
@ -304,165 +258,112 @@ Pad.prototype.setText = function setText(newText) {
|
||||||
changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
|
changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
|
||||||
}
|
}
|
||||||
|
|
||||||
//append the changeset
|
// append the changeset
|
||||||
this.appendRevision(changeset);
|
this.appendRevision(changeset);
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.appendText = function appendText(newText) {
|
Pad.prototype.appendText = function appendText(newText) {
|
||||||
//clean the new text
|
// clean the new text
|
||||||
newText = exports.cleanText(newText);
|
newText = exports.cleanText(newText);
|
||||||
|
|
||||||
var oldText = this.text();
|
var oldText = this.text();
|
||||||
|
|
||||||
//create the changeset
|
// create the changeset
|
||||||
var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText);
|
var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText);
|
||||||
|
|
||||||
//append the changeset
|
// append the changeset
|
||||||
this.appendRevision(changeset);
|
this.appendRevision(changeset);
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
|
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
|
||||||
this.chatHead++;
|
this.chatHead++;
|
||||||
//save the chat entry in the database
|
// save the chat entry in the database
|
||||||
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
|
db.set("pad:" + this.id + ":chat:" + this.chatHead, { "text": text, "userId": userId, "time": time });
|
||||||
this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
|
Pad.prototype.getChatMessage = async function getChatMessage(entryNum) {
|
||||||
var _this = this;
|
// get the chat entry
|
||||||
var entry;
|
let entry = await db.get("pad:" + this.id + ":chat:" + entryNum);
|
||||||
|
|
||||||
async.series([
|
// get the authorName if the entry exists
|
||||||
//get the chat entry
|
if (entry != null) {
|
||||||
function(callback)
|
entry.userName = await authorManager.getAuthorName(entry.userId);
|
||||||
{
|
|
||||||
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
|
return entry;
|
||||||
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) {
|
Pad.prototype.getChatMessages = async function getChatMessages(start, end) {
|
||||||
//collect the numbers of chat entries and in which order we need them
|
|
||||||
var neededEntries = [];
|
// collect the numbers of chat entries and in which order we need them
|
||||||
var order = 0;
|
let neededEntries = [];
|
||||||
for(var i=start;i<=end; i++)
|
for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) {
|
||||||
{
|
neededEntries.push({ entryNum, order });
|
||||||
neededEntries.push({entryNum:i, order: order});
|
|
||||||
order++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _this = this;
|
// get all entries out of the database
|
||||||
|
let entries = [];
|
||||||
//get all entries out of the database
|
await Promise.all(neededEntries.map(entryObject => {
|
||||||
var entries = [];
|
return this.getChatMessage(entryObject.entryNum).then(entry => {
|
||||||
async.forEach(neededEntries, function(entryObject, callback)
|
|
||||||
{
|
|
||||||
_this.getChatMessage(entryObject.entryNum, function(err, entry)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
entries[entryObject.order] = entry;
|
entries[entryObject.order] = entry;
|
||||||
callback();
|
|
||||||
});
|
});
|
||||||
}, function(err)
|
}));
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//sort out broken chat entries
|
// sort out broken chat entries
|
||||||
//it looks like in happend in the past that the chat head was
|
// it looks like in happened in the past that the chat head was
|
||||||
//incremented, but the chat message wasn't added
|
// incremented, but the chat message wasn't added
|
||||||
var cleanedEntries = [];
|
let cleanedEntries = entries.filter(entry => {
|
||||||
for(var i=0;i<entries.length;i++)
|
let pass = (entry != null);
|
||||||
{
|
if (!pass) {
|
||||||
if(entries[i]!=null)
|
console.warn("WARNING: Found broken chat entry in pad " + this.id);
|
||||||
cleanedEntries.push(entries[i]);
|
|
||||||
else
|
|
||||||
console.warn("WARNING: Found broken chat entry in pad " + _this.id);
|
|
||||||
}
|
}
|
||||||
|
return pass;
|
||||||
callback(null, cleanedEntries);
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.init = function init(text, callback) {
|
return cleanedEntries;
|
||||||
var _this = this;
|
}
|
||||||
|
|
||||||
//replace text with default text if text isn't set
|
Pad.prototype.init = async function init(text) {
|
||||||
if(text == null)
|
|
||||||
{
|
// replace text with default text if text isn't set
|
||||||
|
if (text == null) {
|
||||||
text = settings.defaultPadText;
|
text = settings.defaultPadText;
|
||||||
}
|
}
|
||||||
|
|
||||||
//try to load the pad
|
// try to load the pad
|
||||||
db.get("pad:"+this.id, function(err, value)
|
let value = await db.get("pad:" + this.id);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//if this pad exists, load it
|
// if this pad exists, load it
|
||||||
if(value != null)
|
if (value != null) {
|
||||||
{
|
// copy all attr. To a transfrom via fromJsonable if necassary
|
||||||
//copy all attr. To a transfrom via fromJsonable if necassary
|
for (var attr in value) {
|
||||||
for(var attr in value){
|
if (jsonableList.indexOf(attr) !== -1) {
|
||||||
if(jsonableList.indexOf(attr) !== -1){
|
this[attr] = this[attr].fromJsonable(value[attr]);
|
||||||
_this[attr] = _this[attr].fromJsonable(value[attr]);
|
|
||||||
} else {
|
} else {
|
||||||
_this[attr] = value[attr];
|
this[attr] = value[attr];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
//this pad doesn't exist, so create it
|
// this pad doesn't exist, so create it
|
||||||
else
|
let firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
|
||||||
{
|
|
||||||
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
|
|
||||||
|
|
||||||
_this.appendRevision(firstChangeset, '');
|
this.appendRevision(firstChangeset, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
hooks.callAll("padLoad", {'pad':_this});
|
hooks.callAll("padLoad", { 'pad': this });
|
||||||
callback(null);
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.copy = function copy(destinationID, force, callback) {
|
Pad.prototype.copy = async function copy(destinationID, force) {
|
||||||
var sourceID = this.id;
|
|
||||||
var _this = this;
|
|
||||||
var destGroupID;
|
|
||||||
|
|
||||||
// make force optional
|
let sourceID = this.id;
|
||||||
if (typeof force == "function") {
|
|
||||||
callback = force;
|
// allow force to be a string
|
||||||
force = false;
|
if (typeof force === "string") {
|
||||||
|
force = (force.toLowerCase() === "true");
|
||||||
|
} else {
|
||||||
|
force = !!force;
|
||||||
}
|
}
|
||||||
else if (force == undefined || force.toLowerCase() != "true") {
|
|
||||||
force = false;
|
|
||||||
}
|
|
||||||
else force = true;
|
|
||||||
|
|
||||||
// Kick everyone from this pad.
|
// Kick everyone from this pad.
|
||||||
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
|
// 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);
|
// padMessageHandler.kickSessionsFromPad(sourceID);
|
||||||
|
|
||||||
// flush the source pad:
|
// flush the source pad:
|
||||||
_this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
|
|
||||||
async.series([
|
|
||||||
// if it's a group pad, let's make sure the group exists.
|
// if it's a group pad, let's make sure the group exists.
|
||||||
function(callback)
|
let destGroupID;
|
||||||
{
|
if (destinationID.indexOf("$") >= 0) {
|
||||||
if (destinationID.indexOf("$") === -1)
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
destGroupID = destinationID.split("$")[0]
|
destGroupID = destinationID.split("$")[0]
|
||||||
groupManager.doesGroupExist(destGroupID, function (err, exists)
|
let groupExists = await groupManager.doesGroupExist(destGroupID);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
// group does not exist
|
||||||
if(exists == false)
|
if (!groupExists) {
|
||||||
{
|
throw new customError("groupID does not exist for destinationID", "apierror");
|
||||||
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.
|
// if the pad exists, we should abort, unless forced.
|
||||||
function(callback)
|
let exists = await padManager.doesPadExist(destinationID);
|
||||||
{
|
|
||||||
padManager.doesPadExists(destinationID, function (err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
/*
|
if (exists) {
|
||||||
* this is the negation of a truthy comparison. Has been left in this
|
if (!force) {
|
||||||
* wonky state to keep the old (possibly buggy) behaviour
|
|
||||||
*/
|
|
||||||
if (!(exists == true))
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!force)
|
|
||||||
{
|
|
||||||
console.error("erroring out without force");
|
console.error("erroring out without force");
|
||||||
callback(new customError("destinationID already exists","apierror"));
|
throw new customError("destinationID already exists", "apierror");
|
||||||
console.error("erroring out without force - after");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// exists and forcing
|
// exists and forcing
|
||||||
padManager.getPad(destinationID, function(err, pad) {
|
let pad = await padManager.getPad(destinationID);
|
||||||
if (ERR(err, callback)) return;
|
await pad.remove();
|
||||||
pad.remove(callback);
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// copy the 'pad' entry
|
// copy the 'pad' entry
|
||||||
function(callback)
|
let pad = await db.get("pad:" + sourceID);
|
||||||
{
|
db.set("pad:" + destinationID, pad);
|
||||||
db.get("pad:"+sourceID, function(err, pad) {
|
|
||||||
db.set("pad:"+destinationID, pad);
|
|
||||||
});
|
|
||||||
|
|
||||||
callback();
|
// copy all relations in parallel
|
||||||
},
|
let promises = [];
|
||||||
//copy all relations
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
//copy all chat messages
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var chatHead = _this.chatHead;
|
|
||||||
|
|
||||||
for(var i=0;i<=chatHead;i++)
|
// copy all chat messages
|
||||||
{
|
let chatHead = this.chatHead;
|
||||||
db.get("pad:"+sourceID+":chat:"+i, function (err, chat) {
|
for (let i = 0; i <= chatHead; ++i) {
|
||||||
if (ERR(err, callback)) return;
|
let p = db.get("pad:" + sourceID + ":chat:" + i).then(chat => {
|
||||||
db.set("pad:"+destinationID+":chat:"+i, chat);
|
return db.set("pad:" + destinationID + ":chat:" + i, chat);
|
||||||
});
|
});
|
||||||
|
promises.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
// copy all revisions
|
||||||
},
|
let revHead = this.head;
|
||||||
//copy all revisions
|
for (let i = 0; i <= revHead; ++i) {
|
||||||
function(callback)
|
let p = db.get("pad:" + sourceID + ":revs:" + i).then(rev => {
|
||||||
{
|
return db.set("pad:" + destinationID + ":revs:" + i, rev);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
promises.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
// add the new pad to all authors who contributed to the old one
|
||||||
},
|
this.getAllAuthors().forEach(authorID => {
|
||||||
//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);
|
authorManager.addPad(authorID, destinationID);
|
||||||
});
|
});
|
||||||
|
|
||||||
callback();
|
// wait for the above to complete
|
||||||
},
|
await Promise.all(promises);
|
||||||
// parallel
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
function(callback) {
|
|
||||||
// Group pad? Add it to the group's list
|
// Group pad? Add it to the group's list
|
||||||
if(destGroupID) db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
|
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)
|
// Initialize the new pad (will update the listAllPads cache)
|
||||||
setTimeout(function(){
|
await padManager.getPad(destinationID, null); // this runs too early.
|
||||||
padManager.getPad(destinationID, null, callback) // this runs too early.
|
|
||||||
},10);
|
|
||||||
},
|
|
||||||
// let the plugins know the pad was copied
|
// let the plugins know the pad was copied
|
||||||
function(callback) {
|
hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID });
|
||||||
hooks.callAll('padCopy', { 'originalPad': _this, 'destinationID': destinationID });
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
// series
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, {padID: destinationID});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.remove = function remove(callback) {
|
return { padID: destinationID };
|
||||||
|
}
|
||||||
|
|
||||||
|
Pad.prototype.remove = async function remove() {
|
||||||
var padID = this.id;
|
var padID = this.id;
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
//kick everyone from this pad
|
// kick everyone from this pad
|
||||||
padMessageHandler.kickSessionsFromPad(padID);
|
padMessageHandler.kickSessionsFromPad(padID);
|
||||||
|
|
||||||
async.series([
|
// delete all relations - the original code used async.parallel but
|
||||||
//delete all relations
|
// none of the operations except getting the group depended on callbacks
|
||||||
function(callback)
|
// so the database operations here are just started and then left to
|
||||||
{
|
// run to completion
|
||||||
async.parallel([
|
|
||||||
//is it a group pad? -> delete the entry of this pad in the group
|
// is it a group pad? -> delete the entry of this pad in the group
|
||||||
function(callback)
|
if (padID.indexOf("$") >= 0) {
|
||||||
{
|
|
||||||
if(padID.indexOf("$") === -1)
|
|
||||||
{
|
|
||||||
// it isn't a group pad, nothing to do here
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it is a group pad
|
// it is a group pad
|
||||||
var groupID = padID.substring(0,padID.indexOf("$"));
|
let groupID = padID.substring(0, padID.indexOf("$"));
|
||||||
|
let group = await db.get("group:" + groupID);
|
||||||
|
|
||||||
db.get("group:" + groupID, function (err, group)
|
// remove the pad entry
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//remove the pad entry
|
|
||||||
delete group.pads[padID];
|
delete group.pads[padID];
|
||||||
|
|
||||||
//set the new value
|
// set the new value
|
||||||
db.set("group:" + groupID, group);
|
db.set("group:" + groupID, group);
|
||||||
|
}
|
||||||
|
|
||||||
callback();
|
// remove the readonly entries
|
||||||
});
|
let readonlyID = readOnlyManager.getReadOnlyId(padID);
|
||||||
},
|
|
||||||
//remove the readonly entries
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
readOnlyManager.getReadOnlyId(padID, function(err, readonlyID)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
db.remove("pad2readonly:" + padID);
|
db.remove("pad2readonly:" + padID);
|
||||||
db.remove("readonly2pad:" + readonlyID);
|
db.remove("readonly2pad:" + readonlyID);
|
||||||
|
|
||||||
callback();
|
// delete all chat messages
|
||||||
});
|
for (let i = 0, n = this.chatHead; i <= n; ++i) {
|
||||||
},
|
db.remove("pad:" + padID + ":chat:" + i);
|
||||||
//delete all chat messages
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var chatHead = _this.chatHead;
|
|
||||||
|
|
||||||
for(var i=0;i<=chatHead;i++)
|
|
||||||
{
|
|
||||||
db.remove("pad:"+padID+":chat:"+i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
// delete all revisions
|
||||||
},
|
for (let i = 0, n = this.head; i <= n; ++i) {
|
||||||
//delete all revisions
|
db.remove("pad:" + padID + ":revs:" + i);
|
||||||
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
|
||||||
},
|
this.getAllAuthors().forEach(authorID => {
|
||||||
//remove pad from all authors who contributed
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var authorIDs = _this.getAllAuthors();
|
|
||||||
|
|
||||||
authorIDs.forEach(function (authorID)
|
|
||||||
{
|
|
||||||
authorManager.removePad(authorID, padID);
|
authorManager.removePad(authorID, padID);
|
||||||
});
|
});
|
||||||
|
|
||||||
callback();
|
// delete the pad entry and delete pad from padManager
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//delete the pad entry and delete pad from padManager
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.removePad(padID);
|
padManager.removePad(padID);
|
||||||
hooks.callAll("padRemove", {'padID':padID});
|
hooks.callAll("padRemove", { padID });
|
||||||
callback();
|
}
|
||||||
}
|
|
||||||
], function(err)
|
// set in db
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
//set in db
|
|
||||||
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
|
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
|
||||||
this.publicStatus = publicStatus;
|
this.publicStatus = publicStatus;
|
||||||
this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
|
@ -730,22 +521,22 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
|
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
|
||||||
//if this revision is already saved, return silently
|
// if this revision is already saved, return silently
|
||||||
for(var i in this.savedRevisions){
|
for (var i in this.savedRevisions) {
|
||||||
if(this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum){
|
if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//build the saved revision object
|
// build the saved revision object
|
||||||
var savedRevision = {};
|
var savedRevision = {};
|
||||||
savedRevision.revNum = revNum;
|
savedRevision.revNum = revNum;
|
||||||
savedRevision.savedById = savedById;
|
savedRevision.savedById = savedById;
|
||||||
savedRevision.label = label || "Revision " + revNum;
|
savedRevision.label = label || "Revision " + revNum;
|
||||||
savedRevision.timestamp = new Date().getTime();
|
savedRevision.timestamp = Date.now();
|
||||||
savedRevision.id = randomString(10);
|
savedRevision.id = randomString(10);
|
||||||
|
|
||||||
//save this new saved revision
|
// save this new saved revision
|
||||||
this.savedRevisions.push(savedRevision);
|
this.savedRevisions.push(savedRevision);
|
||||||
this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
};
|
};
|
||||||
|
@ -756,19 +547,17 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() {
|
||||||
|
|
||||||
/* Crypto helper methods */
|
/* Crypto helper methods */
|
||||||
|
|
||||||
function hash(password, salt)
|
function hash(password, salt) {
|
||||||
{
|
|
||||||
var shasum = crypto.createHash('sha512');
|
var shasum = crypto.createHash('sha512');
|
||||||
shasum.update(password + salt);
|
shasum.update(password + salt);
|
||||||
|
|
||||||
return shasum.digest("hex") + "$" + salt;
|
return shasum.digest("hex") + "$" + salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSalt()
|
function generateSalt() {
|
||||||
{
|
|
||||||
return randomString(86);
|
return randomString(86);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compare(hashStr, password)
|
function compare(hashStr, password) {
|
||||||
{
|
|
||||||
return hash(password, hashStr.split("$")[1]) === hashStr;
|
return hash(password, hashStr.split("$")[1]) === hashStr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
var customError = require("../utils/customError");
|
||||||
var Pad = require("../db/Pad").Pad;
|
var Pad = require("../db/Pad").Pad;
|
||||||
var db = require("./DB").db;
|
var db = require("./DB");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache of all loaded Pads.
|
* A cache of all loaded Pads.
|
||||||
|
@ -35,12 +34,11 @@ var db = require("./DB").db;
|
||||||
* that's defined somewhere more sensible.
|
* that's defined somewhere more sensible.
|
||||||
*/
|
*/
|
||||||
var globalPads = {
|
var globalPads = {
|
||||||
get: function (name) { return this[':'+name]; },
|
get: function(name) { return this[':'+name]; },
|
||||||
set: function (name, value)
|
set: function(name, value) {
|
||||||
{
|
|
||||||
this[':'+name] = value;
|
this[':'+name] = value;
|
||||||
},
|
},
|
||||||
remove: function (name) {
|
remove: function(name) {
|
||||||
delete this[':'+name];
|
delete this[':'+name];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -50,183 +48,151 @@ var globalPads = {
|
||||||
*
|
*
|
||||||
* Updated without db access as new pads are created/old ones removed.
|
* Updated without db access as new pads are created/old ones removed.
|
||||||
*/
|
*/
|
||||||
var padList = {
|
let padList = {
|
||||||
list: [],
|
list: [],
|
||||||
sorted : false,
|
sorted : false,
|
||||||
initiated: false,
|
initiated: false,
|
||||||
init: function(cb)
|
init: async function() {
|
||||||
{
|
let dbData = await db.findKeys("pad:*", "*:*:*");
|
||||||
db.findKeys("pad:*", "*:*:*", function(err, dbData)
|
|
||||||
{
|
if (dbData != null) {
|
||||||
if(ERR(err, cb)) return;
|
this.initiated = true;
|
||||||
if(dbData != null){
|
|
||||||
padList.initiated = true
|
for (let val of dbData) {
|
||||||
dbData.forEach(function(val){
|
this.addPad(val.replace(/pad:/,""), false);
|
||||||
padList.addPad(val.replace(/pad:/,""),false);
|
|
||||||
});
|
|
||||||
cb && cb()
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
load: function(cb) {
|
load: async function() {
|
||||||
if(this.initiated) cb && cb()
|
if (!this.initiated) {
|
||||||
else this.init(cb)
|
return this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Returns all pads in alphabetical order as array.
|
* Returns all pads in alphabetical order as array.
|
||||||
*/
|
*/
|
||||||
getPads: function(cb){
|
getPads: async function() {
|
||||||
this.load(function() {
|
await this.load();
|
||||||
if(!padList.sorted){
|
|
||||||
padList.list = padList.list.sort();
|
if (!this.sorted) {
|
||||||
padList.sorted = true;
|
this.list.sort();
|
||||||
|
this.sorted = true;
|
||||||
}
|
}
|
||||||
cb && cb(padList.list);
|
|
||||||
})
|
return this.list;
|
||||||
},
|
},
|
||||||
addPad: function(name)
|
addPad: function(name) {
|
||||||
{
|
if (!this.initiated) return;
|
||||||
if(!this.initiated) return;
|
|
||||||
if(this.list.indexOf(name) == -1){
|
if (this.list.indexOf(name) == -1) {
|
||||||
this.list.push(name);
|
this.list.push(name);
|
||||||
this.sorted=false;
|
this.sorted = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removePad: function(name)
|
removePad: function(name) {
|
||||||
{
|
if (!this.initiated) return;
|
||||||
if(!this.initiated) return;
|
|
||||||
var index = this.list.indexOf(name);
|
var index = this.list.indexOf(name);
|
||||||
if(index>-1){
|
|
||||||
this.list.splice(index,1);
|
if (index > -1) {
|
||||||
this.sorted=false;
|
this.list.splice(index, 1);
|
||||||
|
this.sorted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//initialises the allknowing data structure
|
|
||||||
|
|
||||||
/**
|
// initialises the all-knowing data structure
|
||||||
* 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 = [
|
|
||||||
[/\s+/g, '_'],
|
|
||||||
[/:+/g, '_']
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Pad Object with the callback
|
* Returns a Pad Object with the callback
|
||||||
* @param id A String with the id of the pad
|
* @param id A String with the id of the pad
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
*/
|
*/
|
||||||
exports.getPad = function(id, text, callback)
|
exports.getPad = async function(id, text)
|
||||||
{
|
{
|
||||||
//check if this is a valid padId
|
// check if this is a valid padId
|
||||||
if(!exports.isValidPadId(id))
|
if (!exports.isValidPadId(id)) {
|
||||||
{
|
throw new customError(id + " is not a valid padId", "apierror");
|
||||||
callback(new customError(id + " is not a valid padId","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//make text an optional parameter
|
// check if this is a valid text
|
||||||
if(typeof text == "function")
|
if (text != null) {
|
||||||
{
|
// check if text is a string
|
||||||
callback = text;
|
if (typeof text != "string") {
|
||||||
text = null;
|
throw new customError("text is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if this is a valid text
|
// check if text is less than 100k chars
|
||||||
if(text != null)
|
if (text.length > 100000) {
|
||||||
{
|
throw new customError("text must be less than 100k chars", "apierror");
|
||||||
//check if text is a string
|
|
||||||
if(typeof text != "string")
|
|
||||||
{
|
|
||||||
callback(new customError("text is not a string","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//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);
|
let pad = globalPads.get(id);
|
||||||
|
|
||||||
//return pad if its already loaded
|
// return pad if it's already loaded
|
||||||
if(pad != null)
|
if (pad != null) {
|
||||||
{
|
return pad;
|
||||||
callback(null, pad);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//try to load pad
|
// try to load pad
|
||||||
pad = new Pad(id);
|
pad = new Pad(id);
|
||||||
|
|
||||||
//initalize the pad
|
// initalize the pad
|
||||||
pad.init(text, function(err)
|
await pad.init(text);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
globalPads.set(id, pad);
|
globalPads.set(id, pad);
|
||||||
padList.addPad(id);
|
padList.addPad(id);
|
||||||
callback(null, pad);
|
|
||||||
});
|
return pad;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.listAllPads = function(cb)
|
exports.listAllPads = async function()
|
||||||
{
|
{
|
||||||
padList.getPads(function(list) {
|
let padIDs = await padList.getPads();
|
||||||
cb && cb(null, {padIDs: list});
|
|
||||||
});
|
return { padIDs };
|
||||||
}
|
}
|
||||||
|
|
||||||
//checks if a pad exists
|
// checks if a pad exists
|
||||||
exports.doesPadExists = function(padId, callback)
|
exports.doesPadExist = async function(padId)
|
||||||
{
|
{
|
||||||
db.get("pad:"+padId, function(err, value)
|
let value = await db.get("pad:" + padId);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
return (value != null && value.atext);
|
||||||
if(value != null && value.atext){
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns a sanitized padId, respecting legacy pad id formats
|
// alias for backwards compatibility
|
||||||
exports.sanitizePadId = function(padId, callback) {
|
exports.doesPadExists = exports.doesPadExist;
|
||||||
var transform_index = arguments[2] || 0;
|
|
||||||
//we're out of possible transformations, so just return it
|
/**
|
||||||
if(transform_index >= padIdTransforms.length)
|
* 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.
|
||||||
callback(padId);
|
*/
|
||||||
return;
|
const padIdTransforms = [
|
||||||
|
[/\s+/g, '_'],
|
||||||
|
[/:+/g, '_']
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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 padId exists
|
let [from, to] = padIdTransforms[i];
|
||||||
exports.doesPadExists(padId, function(junk, exists)
|
|
||||||
{
|
padId = padId.replace(from, to);
|
||||||
if(exists)
|
|
||||||
{
|
|
||||||
callback(padId);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//get the next transformation *that's different*
|
// we're out of possible transformations, so just return it
|
||||||
var transformedPadId = padId;
|
return 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isValidPadId = function(padId)
|
exports.isValidPadId = function(padId)
|
||||||
|
@ -237,13 +203,13 @@ exports.isValidPadId = function(padId)
|
||||||
/**
|
/**
|
||||||
* Removes the pad from database and unloads it.
|
* Removes the pad from database and unloads it.
|
||||||
*/
|
*/
|
||||||
exports.removePad = function(padId){
|
exports.removePad = function(padId) {
|
||||||
db.remove("pad:"+padId);
|
db.remove("pad:" + padId);
|
||||||
exports.unloadPad(padId);
|
exports.unloadPad(padId);
|
||||||
padList.removePad(padId);
|
padList.removePad(padId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//removes a pad from the cache
|
// removes a pad from the cache
|
||||||
exports.unloadPad = function(padId)
|
exports.unloadPad = function(padId)
|
||||||
{
|
{
|
||||||
globalPads.remove(padId);
|
globalPads.remove(padId);
|
||||||
|
|
|
@ -19,80 +19,47 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
var db = require("./DB");
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var randomString = require("../utils/randomstring");
|
var randomString = require("../utils/randomstring");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a read only id for a pad
|
* returns a read only id for a pad
|
||||||
* @param {String} padId the id of the pad
|
* @param {String} padId the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getReadOnlyId = function (padId, callback)
|
exports.getReadOnlyId = async function (padId)
|
||||||
{
|
{
|
||||||
var readOnlyId;
|
// check if there is a pad2readonly entry
|
||||||
|
let readOnlyId = await db.get("pad2readonly:" + padId);
|
||||||
|
|
||||||
async.waterfall([
|
// there is no readOnly Entry in the database, let's create one
|
||||||
//check if there is a pad2readonly entry
|
if (readOnlyId == null) {
|
||||||
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);
|
readOnlyId = "r." + randomString(16);
|
||||||
|
|
||||||
db.set("pad2readonly:" + padId, readOnlyId);
|
db.set("pad2readonly:" + padId, readOnlyId);
|
||||||
db.set("readonly2pad:" + readOnlyId, padId);
|
db.set("readonly2pad:" + readOnlyId, padId);
|
||||||
}
|
}
|
||||||
//there is a readOnly Entry in the database, let's take this one
|
|
||||||
else
|
|
||||||
{
|
|
||||||
readOnlyId = dbReadOnlyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
return readOnlyId;
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
//return the results
|
|
||||||
callback(null, readOnlyId);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a the padId for a read only id
|
* returns the padId for a read only id
|
||||||
* @param {String} readOnlyId 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
|
* @param {String} padIdOrReadonlyPadId read only id or real pad id
|
||||||
*/
|
*/
|
||||||
exports.getIds = function(id, callback) {
|
exports.getIds = async function(id) {
|
||||||
if (id.indexOf("r.") == 0)
|
let readonly = (id.indexOf("r.") === 0);
|
||||||
exports.getPadId(id, function (err, value) {
|
|
||||||
if(ERR(err, callback)) return;
|
// Might be null, if this is an unknown read-only id
|
||||||
callback(null, {
|
let readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
|
||||||
readOnlyPadId: id,
|
let padId = readonly ? await exports.getPadId(id) : id;
|
||||||
padId: value, // Might be null, if this is an unknown read-only id
|
|
||||||
readonly: true
|
return { readOnlyPadId, padId, readonly };
|
||||||
});
|
|
||||||
});
|
|
||||||
else
|
|
||||||
exports.getReadOnlyId(id, function (err, value) {
|
|
||||||
callback(null, {
|
|
||||||
readOnlyPadId: value,
|
|
||||||
padId: id,
|
|
||||||
readonly: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var async = require("async");
|
|
||||||
var authorManager = require("./AuthorManager");
|
var authorManager = require("./AuthorManager");
|
||||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
|
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
|
||||||
var padManager = require("./PadManager");
|
var padManager = require("./PadManager");
|
||||||
|
@ -35,295 +32,230 @@ var authLogger = log4js.getLogger("auth");
|
||||||
* @param sessionCookie the session the user has (set via api)
|
* @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 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 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})
|
* @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
|
||||||
*/
|
*/
|
||||||
exports.checkAccess = function (padID, sessionCookie, token, password, callback)
|
exports.checkAccess = async function(padID, sessionCookie, token, password)
|
||||||
{
|
{
|
||||||
var statusObject;
|
// immutable object
|
||||||
|
let deny = Object.freeze({ accessStatus: "deny" });
|
||||||
|
|
||||||
if(!padID) {
|
if (!padID) {
|
||||||
callback(null, {accessStatus: "deny"});
|
return deny;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow plugins to deny access
|
// allow plugins to deny access
|
||||||
var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1;
|
var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1;
|
||||||
if(deniedByHook)
|
if (deniedByHook) {
|
||||||
{
|
return deny;
|
||||||
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)
|
// a valid session is required (api-only mode)
|
||||||
if(settings.requireSession)
|
if (!sessionCookie) {
|
||||||
{
|
|
||||||
// without sessionCookie, access is denied
|
// without sessionCookie, access is denied
|
||||||
if(!sessionCookie)
|
return deny;
|
||||||
{
|
|
||||||
callback(null, {accessStatus: "deny"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// a session is not required, so we'll check if it's a public pad
|
// a session is not required, so we'll check if it's a public pad
|
||||||
else
|
if (padID.indexOf("$") === -1) {
|
||||||
{
|
|
||||||
// it's not a group pad, means we can grant access
|
// 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
|
// assume user has access
|
||||||
statusObject = {accessStatus: "grant", authorID: author};
|
let authorID = await p_tokenAuthor;
|
||||||
|
let statusObject = { accessStatus: "grant", authorID };
|
||||||
|
|
||||||
|
if (settings.editOnly) {
|
||||||
// user can't create pads
|
// user can't create pads
|
||||||
if(settings.editOnly)
|
|
||||||
{
|
|
||||||
// check if pad exists
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
|
let padExists = await p_padExists;
|
||||||
|
|
||||||
|
if (!padExists) {
|
||||||
// pad doesn't exist - user can't have access
|
// pad doesn't exist - user can't have access
|
||||||
if(!exists) statusObject.accessStatus = "deny";
|
statusObject.accessStatus = "deny";
|
||||||
// grant or deny access, with author of token
|
}
|
||||||
callback(null, statusObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// user may create new pads - no need to check anything
|
// user may create new pads - no need to check anything
|
||||||
// grant access, with author of token
|
// grant access, with author of token
|
||||||
callback(null, statusObject);
|
return statusObject;
|
||||||
});
|
|
||||||
|
|
||||||
//don't continue
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupID = padID.split("$")[0];
|
let validSession = false;
|
||||||
var padExists = false;
|
let sessionAuthor;
|
||||||
var validSession = false;
|
let isPublic;
|
||||||
var sessionAuthor;
|
let isPasswordProtected;
|
||||||
var tokenAuthor;
|
let passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
|
||||||
var isPublic;
|
|
||||||
var isPasswordProtected;
|
|
||||||
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
|
|
||||||
|
|
||||||
async.series([
|
// get information about all sessions contained in this cookie
|
||||||
//get basic informations from the database
|
if (sessionCookie) {
|
||||||
function(callback)
|
let groupID = padID.split("$")[0];
|
||||||
{
|
let sessionIDs = sessionCookie.split(',');
|
||||||
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(',');
|
// was previously iterated in parallel using async.forEach
|
||||||
async.forEach(sessionIDs, function(sessionID, callback)
|
let sessionInfos = await Promise.all(sessionIDs.map(sessionID => {
|
||||||
{
|
return sessionManager.getSessionInfo(sessionID);
|
||||||
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;
|
// seperated out the iteration of sessioninfos from the (parallel) fetches from the DB
|
||||||
|
for (let sessionInfo of sessionInfos) {
|
||||||
var now = Math.floor(new Date().getTime()/1000);
|
try {
|
||||||
|
// is it for this group?
|
||||||
//is it for this group?
|
if (sessionInfo.groupID != groupID) {
|
||||||
if(sessionInfo.groupID != groupID)
|
|
||||||
{
|
|
||||||
authLogger.debug("Auth failed: wrong group");
|
authLogger.debug("Auth failed: wrong group");
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//is validUntil still ok?
|
// is validUntil still ok?
|
||||||
if(sessionInfo.validUntil <= now)
|
let now = Math.floor(Date.now() / 1000);
|
||||||
{
|
if (sessionInfo.validUntil <= now) {
|
||||||
authLogger.debug("Auth failed: validUntil");
|
authLogger.debug("Auth failed: validUntil");
|
||||||
callback();
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is a valid session
|
// fall-through - there is a valid session
|
||||||
validSession = true;
|
validSession = true;
|
||||||
sessionAuthor = sessionInfo.authorID;
|
sessionAuthor = sessionInfo.authorID;
|
||||||
|
break;
|
||||||
callback();
|
} catch (err) {
|
||||||
});
|
// skip session if it doesn't exist
|
||||||
}, callback);
|
if (err.message == "sessionID does not exist") {
|
||||||
},
|
authLogger.debug("Auth failed: unknown session");
|
||||||
//get author for token
|
} else {
|
||||||
function(callback)
|
throw err;
|
||||||
{
|
}
|
||||||
//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)
|
let padExists = await p_padExists;
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//is it a public pad?
|
if (padExists) {
|
||||||
|
let pad = await padManager.getPad(padID);
|
||||||
|
|
||||||
|
// is it a public pad?
|
||||||
isPublic = pad.getPublicStatus();
|
isPublic = pad.getPublicStatus();
|
||||||
|
|
||||||
//is it password protected?
|
// is it password protected?
|
||||||
isPasswordProtected = pad.isPasswordProtected();
|
isPasswordProtected = pad.isPasswordProtected();
|
||||||
|
|
||||||
//is password correct?
|
// is password correct?
|
||||||
if(isPasswordProtected && password && pad.isCorrectPassword(password))
|
if (isPasswordProtected && password && pad.isCorrectPassword(password)) {
|
||||||
{
|
|
||||||
passwordStatus = "correct";
|
passwordStatus = "correct";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
callback();
|
// - a valid session for this group is avaible AND pad exists
|
||||||
});
|
if (validSession && padExists) {
|
||||||
},
|
let authorID = sessionAuthor;
|
||||||
function(callback)
|
let grant = Object.freeze({ accessStatus: "grant", authorID });
|
||||||
{
|
|
||||||
//- a valid session for this group is avaible AND pad exists
|
if (!isPasswordProtected) {
|
||||||
if(validSession && padExists)
|
// - the pad is not password protected
|
||||||
{
|
|
||||||
//- the pad is not password protected
|
// --> grant access
|
||||||
if(!isPasswordProtected)
|
return grant;
|
||||||
{
|
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
|
||||||
}
|
}
|
||||||
//- the setting to bypass password validation is set
|
|
||||||
else if(settings.sessionNoPassword)
|
if (settings.sessionNoPassword) {
|
||||||
{
|
// - the setting to bypass password validation is set
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
// --> grant access
|
||||||
|
return grant;
|
||||||
}
|
}
|
||||||
//- the pad is password protected and password is correct
|
|
||||||
else if(isPasswordProtected && passwordStatus == "correct")
|
if (isPasswordProtected && passwordStatus === "correct") {
|
||||||
{
|
// - the pad is password protected and password is correct
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
// --> grant access
|
||||||
|
return grant;
|
||||||
}
|
}
|
||||||
//- the pad is password protected but wrong password given
|
|
||||||
else if(isPasswordProtected && passwordStatus == "wrong")
|
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
|
|
||||||
statusObject = {accessStatus: "wrongPassword"};
|
// --> deny access, ask for new password and tell them that the password is wrong
|
||||||
|
return { accessStatus: "wrongPassword" };
|
||||||
}
|
}
|
||||||
//- the pad is password protected but no password given
|
|
||||||
else if(isPasswordProtected && passwordStatus == "notGiven")
|
if (isPasswordProtected && passwordStatus === "notGiven") {
|
||||||
{
|
// - the pad is password protected but no password given
|
||||||
//--> ask for password
|
|
||||||
statusObject = {accessStatus: "needPassword"};
|
// --> ask for password
|
||||||
|
return { accessStatus: "needPassword" };
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
throw new Error("Oops, something wrong happend");
|
||||||
throw new Error("Ops, something wrong happend");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//- a valid session for this group avaible but pad doesn't exists
|
if (validSession && !padExists) {
|
||||||
else if(validSession && !padExists)
|
// - a valid session for this group avaible but pad doesn't exist
|
||||||
{
|
|
||||||
//--> grant access
|
// --> grant access by default
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
let accessStatus = "grant";
|
||||||
//--> deny access if user isn't allowed to create the pad
|
let authorID = sessionAuthor;
|
||||||
if(settings.editOnly)
|
|
||||||
{
|
// --> deny access if user isn't allowed to create the pad
|
||||||
|
if (settings.editOnly) {
|
||||||
authLogger.debug("Auth failed: valid session & pad does not exist");
|
authLogger.debug("Auth failed: valid session & pad does not exist");
|
||||||
statusObject.accessStatus = "deny";
|
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();
|
return { accessStatus, authorID };
|
||||||
}
|
}
|
||||||
], function(err)
|
|
||||||
{
|
if (!validSession && padExists) {
|
||||||
if(ERR(err, callback)) return;
|
// there is no valid session avaiable AND pad exists
|
||||||
callback(null, statusObject);
|
|
||||||
});
|
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" };
|
||||||
|
}
|
||||||
|
|
|
@ -18,360 +18,207 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
var customError = require("../utils/customError");
|
||||||
var randomString = require("../utils/randomstring");
|
var randomString = require("../utils/randomstring");
|
||||||
var db = require("./DB").db;
|
var db = require("./DB");
|
||||||
var async = require("async");
|
var groupManager = require("./GroupManager");
|
||||||
var groupMangager = require("./GroupManager");
|
var authorManager = require("./AuthorManager");
|
||||||
var authorMangager = require("./AuthorManager");
|
|
||||||
|
|
||||||
exports.doesSessionExist = function(sessionID, callback)
|
exports.doesSessionExist = async function(sessionID)
|
||||||
{
|
{
|
||||||
//check if the database entry of this session exists
|
//check if the database entry of this session exists
|
||||||
db.get("session:" + sessionID, function (err, session)
|
let session = await db.get("session:" + sessionID);
|
||||||
{
|
return (session !== null);
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, session != null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new session between an author and a group
|
* 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);
|
||||||
async.series([
|
if (!groupExists) {
|
||||||
//check if group exists
|
throw new customError("groupID does not exist", "apierror");
|
||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to parse validUntil if it's not a number
|
||||||
|
if (typeof validUntil !== "number") {
|
||||||
validUntil = parseInt(validUntil);
|
validUntil = parseInt(validUntil);
|
||||||
}
|
}
|
||||||
|
|
||||||
//ensure this is not a negativ number
|
// check it's a valid number
|
||||||
if(validUntil < 0)
|
if (isNaN(validUntil)) {
|
||||||
{
|
throw new customError("validUntil is not a number", "apierror");
|
||||||
callback(new customError("validUntil is a negativ number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//ensure this is not a float value
|
// ensure this is not a negative number
|
||||||
if(!is_int(validUntil))
|
if (validUntil < 0) {
|
||||||
{
|
throw new customError("validUntil is a negative number", "apierror");
|
||||||
callback(new customError("validUntil is a float value","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if validUntil is in the future
|
// ensure this is not a float value
|
||||||
if(Math.floor(new Date().getTime()/1000) > validUntil)
|
if (!is_int(validUntil)) {
|
||||||
{
|
throw new customError("validUntil is a float value", "apierror");
|
||||||
callback(new customError("validUntil is in the past","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//generate sessionID
|
// check if validUntil is in the future
|
||||||
sessionID = "s." + randomString(16);
|
if (validUntil < Math.floor(Date.now() / 1000)) {
|
||||||
|
throw new customError("validUntil is in the past", "apierror");
|
||||||
|
}
|
||||||
|
|
||||||
//set the session into the database
|
// generate sessionID
|
||||||
db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
|
let sessionID = "s." + randomString(16);
|
||||||
|
|
||||||
callback();
|
// set the session into the database
|
||||||
},
|
await db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
|
||||||
//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
|
// get the entry
|
||||||
if(group2sessions == null || group2sessions.sessionIDs == null)
|
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 : {}};
|
group2sessions = {sessionIDs : {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
//add the entry for this session
|
// add the entry for this session
|
||||||
group2sessions.sessionIDs[sessionID] = 1;
|
group2sessions.sessionIDs[sessionID] = 1;
|
||||||
|
|
||||||
//save the new element back
|
// save the new element back
|
||||||
db.set("group2sessions:" + groupID, group2sessions);
|
await db.set("group2sessions:" + groupID, group2sessions);
|
||||||
|
|
||||||
callback();
|
// get the author2sessions entry
|
||||||
});
|
let author2sessions = await db.get("author2sessions:" + authorID);
|
||||||
},
|
|
||||||
//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) {
|
||||||
if(author2sessions == null || author2sessions.sessionIDs == null)
|
// the entry doesn't exist so far, let's create it
|
||||||
{
|
|
||||||
author2sessions = {sessionIDs : {}};
|
author2sessions = {sessionIDs : {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
//add the entry for this session
|
// add the entry for this session
|
||||||
author2sessions.sessionIDs[sessionID] = 1;
|
author2sessions.sessionIDs[sessionID] = 1;
|
||||||
|
|
||||||
//save the new element back
|
//save the new element back
|
||||||
db.set("author2sessions:" + authorID, author2sessions);
|
await db.set("author2sessions:" + authorID, author2sessions);
|
||||||
|
|
||||||
callback();
|
return { sessionID };
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//return error and sessionID
|
|
||||||
callback(null, {sessionID: sessionID});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getSessionInfo = function(sessionID, callback)
|
exports.getSessionInfo = async function(sessionID)
|
||||||
{
|
{
|
||||||
//check if the database entry of this session exists
|
// check if the database entry of this session exists
|
||||||
db.get("session:" + sessionID, function (err, session)
|
let session = await db.get("session:" + sessionID);
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//session does not exists
|
if (session == null) {
|
||||||
if(session == null)
|
// session does not exist
|
||||||
{
|
throw new customError("sessionID does not exist", "apierror");
|
||||||
callback(new customError("sessionID does not exist","apierror"))
|
|
||||||
}
|
}
|
||||||
//everything is fine, return the sessioninfos
|
|
||||||
else
|
// everything is fine, return the sessioninfos
|
||||||
{
|
return session;
|
||||||
callback(null, session);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a session
|
* Deletes a session
|
||||||
*/
|
*/
|
||||||
exports.deleteSession = function(sessionID, callback)
|
exports.deleteSession = async function(sessionID)
|
||||||
{
|
{
|
||||||
var authorID, groupID;
|
// ensure that the session exists
|
||||||
var group2sessions, author2sessions;
|
let session = await db.get("session:" + sessionID);
|
||||||
|
if (session == null) {
|
||||||
async.series([
|
throw new customError("sessionID does not exist", "apierror");
|
||||||
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();
|
// everything is fine, use the sessioninfos
|
||||||
}
|
let groupID = session.groupID;
|
||||||
});
|
let authorID = session.authorID;
|
||||||
},
|
|
||||||
//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
|
// get the group2sessions and author2sessions entries
|
||||||
if(group2sessions != null) { // Maybe the group was already deleted
|
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];
|
delete group2sessions.sessionIDs[sessionID];
|
||||||
db.set("group2sessions:" + groupID, group2sessions);
|
await db.set("group2sessions:" + groupID, group2sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
//remove session from author2sessions
|
// remove session from author2sessions
|
||||||
if(author2sessions != null) { // Maybe the author was already deleted
|
if (author2sessions != null) { // Maybe the author was already deleted
|
||||||
delete author2sessions.sessionIDs[sessionID];
|
delete author2sessions.sessionIDs[sessionID];
|
||||||
db.set("author2sessions:" + authorID, author2sessions);
|
await db.set("author2sessions:" + authorID, author2sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.listSessionsOfGroup = function(groupID, callback)
|
exports.listSessionsOfGroup = async function(groupID)
|
||||||
{
|
{
|
||||||
groupMangager.doesGroupExist(groupID, function(err, exists)
|
// check that the group exists
|
||||||
{
|
let exists = await groupManager.doesGroupExist(groupID);
|
||||||
if(ERR(err, callback)) return;
|
if (!exists) {
|
||||||
|
throw new customError("groupID does not exist", "apierror");
|
||||||
|
}
|
||||||
|
|
||||||
//group does not exist
|
let sessions = await listSessionsWithDBKey("group2sessions:" + groupID);
|
||||||
if(exists == false)
|
return sessions;
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//everything is fine, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
listSessionsWithDBKey("group2sessions:" + groupID, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.listSessionsOfAuthor = function(authorID, callback)
|
exports.listSessionsOfAuthor = async function(authorID)
|
||||||
{
|
{
|
||||||
authorMangager.doesAuthorExists(authorID, function(err, exists)
|
// check that the author exists
|
||||||
{
|
let exists = await authorManager.doesAuthorExist(authorID)
|
||||||
if(ERR(err, callback)) return;
|
if (!exists) {
|
||||||
|
throw new customError("authorID does not exist", "apierror");
|
||||||
|
}
|
||||||
|
|
||||||
//group does not exist
|
let sessions = await listSessionsWithDBKey("author2sessions:" + authorID);
|
||||||
if(exists == false)
|
return sessions;
|
||||||
{
|
|
||||||
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
|
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
||||||
function listSessionsWithDBKey (dbkey, callback)
|
// required to return null rather than an empty object if there are none
|
||||||
|
async function listSessionsWithDBKey(dbkey)
|
||||||
{
|
{
|
||||||
var sessions;
|
// get the group2sessions entry
|
||||||
|
let sessionObject = await db.get(dbkey);
|
||||||
async.series([
|
let sessions = sessionObject ? sessionObject.sessionIDs : null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// iterate through the sessions and get the sessioninfos
|
||||||
|
for (let sessionID in sessions) {
|
||||||
|
try {
|
||||||
|
let sessionInfo = await exports.getSessionInfo(sessionID);
|
||||||
sessions[sessionID] = sessionInfo;
|
sessions[sessionID] = sessionInfo;
|
||||||
callback();
|
} catch (err) {
|
||||||
});
|
if (err == "apierror: sessionID does not exist") {
|
||||||
}, callback);
|
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
||||||
|
sessions[sessionID] = null;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
], function(err)
|
}
|
||||||
{
|
}
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, sessions);
|
return sessions;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//checks if a number is an int
|
// checks if a number is an int
|
||||||
function is_int(value)
|
function is_int(value)
|
||||||
{
|
{
|
||||||
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
|
return (parseFloat(value) == parseInt(value)) && !isNaN(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* Stores session data in the database
|
* Stores session data in the database
|
||||||
* Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js
|
* 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
|
* 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,
|
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.__proto__ = Store.prototype;
|
||||||
|
|
||||||
SessionStore.prototype.get = function(sid, fn){
|
SessionStore.prototype.get = function(sid, fn) {
|
||||||
messageLogger.debug('GET ' + sid);
|
messageLogger.debug('GET ' + sid);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
db.get("sessionstorage:" + sid, function (err, sess)
|
|
||||||
{
|
db.get("sessionstorage:" + sid, function(err, sess) {
|
||||||
if (sess) {
|
if (sess) {
|
||||||
sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires;
|
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) {
|
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);
|
messageLogger.debug('SET ' + sid);
|
||||||
|
|
||||||
db.set("sessionstorage:" + sid, sess);
|
db.set("sessionstorage:" + sid, sess);
|
||||||
process.nextTick(function(){
|
if (fn) {
|
||||||
if(fn) fn();
|
process.nextTick(fn);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SessionStore.prototype.destroy = function(sid, fn){
|
SessionStore.prototype.destroy = function(sid, fn) {
|
||||||
messageLogger.debug('DESTROY ' + sid);
|
messageLogger.debug('DESTROY ' + sid);
|
||||||
|
|
||||||
db.remove("sessionstorage:" + sid);
|
db.remove("sessionstorage:" + sid);
|
||||||
process.nextTick(function(){
|
if (fn) {
|
||||||
if(fn) fn();
|
process.nextTick(fn);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SessionStore.prototype.all = function(fn){
|
/*
|
||||||
|
* 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');
|
messageLogger.debug('ALL');
|
||||||
|
|
||||||
var sessions = [];
|
var sessions = [];
|
||||||
db.forEach(function(key, value){
|
|
||||||
|
db.forEach(function(key, value) {
|
||||||
if (key.substr(0,15) === "sessionstorage:") {
|
if (key.substr(0,15) === "sessionstorage:") {
|
||||||
sessions.push(value);
|
sessions.push(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
fn(null, sessions);
|
fn(null, sessions);
|
||||||
};
|
};
|
||||||
|
|
||||||
SessionStore.prototype.clear = function(fn){
|
SessionStore.prototype.clear = function(fn) {
|
||||||
messageLogger.debug('CLEAR');
|
messageLogger.debug('CLEAR');
|
||||||
db.forEach(function(key, value){
|
|
||||||
|
db.forEach(function(key, value) {
|
||||||
if (key.substr(0,15) === "sessionstorage:") {
|
if (key.substr(0,15) === "sessionstorage:") {
|
||||||
db.db.remove("session:" + key);
|
db.remove("session:" + key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(fn) fn();
|
if (fn) fn();
|
||||||
};
|
};
|
||||||
|
|
||||||
SessionStore.prototype.length = function(fn){
|
SessionStore.prototype.length = function(fn) {
|
||||||
messageLogger.debug('LENGTH');
|
messageLogger.debug('LENGTH');
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
db.forEach(function(key, value){
|
|
||||||
|
db.forEach(function(key, value) {
|
||||||
if (key.substr(0,15) === "sessionstorage:") {
|
if (key.substr(0,15) === "sessionstorage:") {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
fn(null, i);
|
fn(null, i);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var absolutePaths = require('../utils/AbsolutePaths');
|
var absolutePaths = require('../utils/AbsolutePaths');
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var api = require("../db/API");
|
var api = require("../db/API");
|
||||||
var log4js = require('log4js');
|
var log4js = require('log4js');
|
||||||
|
@ -32,19 +31,17 @@ var apiHandlerLogger = log4js.getLogger('APIHandler');
|
||||||
//ensure we have an apikey
|
//ensure we have an apikey
|
||||||
var apikey = null;
|
var apikey = null;
|
||||||
var apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || "./APIKEY.txt");
|
var apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || "./APIKEY.txt");
|
||||||
try
|
|
||||||
{
|
try {
|
||||||
apikey = fs.readFileSync(apikeyFilename,"utf8");
|
apikey = fs.readFileSync(apikeyFilename,"utf8");
|
||||||
apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`);
|
apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`);
|
||||||
}
|
} catch(e) {
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`);
|
apiHandlerLogger.info(`Api key file "${apikeyFilename}" not found. Creating with random contents.`);
|
||||||
apikey = randomString(32);
|
apikey = randomString(32);
|
||||||
fs.writeFileSync(apikeyFilename,apikey,"utf8");
|
fs.writeFileSync(apikeyFilename,apikey,"utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
//a list of all functions
|
// a list of all functions
|
||||||
var version = {};
|
var version = {};
|
||||||
|
|
||||||
version["1"] = Object.assign({},
|
version["1"] = Object.assign({},
|
||||||
|
@ -152,110 +149,73 @@ exports.version = version;
|
||||||
* @req express request object
|
* @req express request object
|
||||||
* @res express response 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
|
// say goodbye if this is an unknown API version
|
||||||
var isKnownApiVersion = false;
|
if (!(apiVersion in version)) {
|
||||||
for(var knownApiVersion in version)
|
|
||||||
{
|
|
||||||
if(knownApiVersion == apiVersion)
|
|
||||||
{
|
|
||||||
isKnownApiVersion = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//say goodbye if this is an unknown API version
|
|
||||||
if(!isKnownApiVersion)
|
|
||||||
{
|
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.send({code: 3, message: "no such api version", data: null});
|
res.send({code: 3, message: "no such api version", data: null});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//check if this is a valid function name
|
// say goodbye if this is an unknown function
|
||||||
var isKnownFunctionname = false;
|
if (!(functionName in version[apiVersion])) {
|
||||||
for(var knownFunctionname in version[apiVersion])
|
// no status code?!
|
||||||
{
|
|
||||||
if(knownFunctionname == functionName)
|
|
||||||
{
|
|
||||||
isKnownFunctionname = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//say goodbye if this is a unknown function
|
|
||||||
if(!isKnownFunctionname)
|
|
||||||
{
|
|
||||||
res.send({code: 3, message: "no such function", data: null});
|
res.send({code: 3, message: "no such function", data: null});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//check the api key!
|
// check the api key!
|
||||||
fields["apikey"] = fields["apikey"] || fields["api_key"];
|
fields["apikey"] = fields["apikey"] || fields["api_key"];
|
||||||
|
|
||||||
if(fields["apikey"] != apikey.trim())
|
if (fields["apikey"] !== apikey.trim()) {
|
||||||
{
|
|
||||||
res.statusCode = 401;
|
res.statusCode = 401;
|
||||||
res.send({code: 4, message: "no or wrong API Key", data: null});
|
res.send({code: 4, message: "no or wrong API Key", data: null});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//sanitize any pad id's before continuing
|
// sanitize any padIDs before continuing
|
||||||
if(fields["padID"])
|
if (fields["padID"]) {
|
||||||
{
|
fields["padID"] = await padManager.sanitizePadId(fields["padID"]);
|
||||||
padManager.sanitizePadId(fields["padID"], function(padId)
|
|
||||||
{
|
|
||||||
fields["padID"] = padId;
|
|
||||||
callAPI(apiVersion, functionName, fields, req, res);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if(fields["padName"])
|
// there was an 'else' here before - removed it to ensure
|
||||||
{
|
// that this sanitize step can't be circumvented by forcing
|
||||||
padManager.sanitizePadId(fields["padName"], function(padId)
|
// the first branch to be taken
|
||||||
{
|
if (fields["padName"]) {
|
||||||
fields["padName"] = padId;
|
fields["padName"] = await padManager.sanitizePadId(fields["padName"]);
|
||||||
callAPI(apiVersion, functionName, fields, req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callAPI(apiVersion, functionName, fields, req, res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// no need to await - callAPI returns a promise
|
||||||
|
return callAPI(apiVersion, functionName, fields, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
//calls the api function
|
// calls the api function
|
||||||
function callAPI(apiVersion, functionName, fields, req, res)
|
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) {
|
var functionParams = version[apiVersion][functionName].map(function (field) {
|
||||||
return fields[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
|
try {
|
||||||
api[functionName].apply(this, functionParams);
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,169 +19,122 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var exporthtml = require("../utils/ExportHtml");
|
var exporthtml = require("../utils/ExportHtml");
|
||||||
var exporttxt = require("../utils/ExportTxt");
|
var exporttxt = require("../utils/ExportTxt");
|
||||||
var exportEtherpad = require("../utils/ExportEtherpad");
|
var exportEtherpad = require("../utils/ExportEtherpad");
|
||||||
var async = require("async");
|
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
var settings = require('../utils/Settings');
|
var settings = require('../utils/Settings');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||||
var TidyHtml = require('../utils/TidyHtml');
|
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
|
let convertor = null;
|
||||||
if(settings.abiword != null)
|
|
||||||
|
// load abiword only if it is enabled
|
||||||
|
if (settings.abiword != null) {
|
||||||
convertor = require("../utils/Abiword");
|
convertor = require("../utils/Abiword");
|
||||||
|
}
|
||||||
|
|
||||||
// Use LibreOffice if an executable has been defined in the settings
|
// Use LibreOffice if an executable has been defined in the settings
|
||||||
if(settings.soffice != null)
|
if (settings.soffice != null) {
|
||||||
convertor = require("../utils/LibreOffice");
|
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
|
* do a requested export
|
||||||
*/
|
*/
|
||||||
exports.doExport = function(req, res, padId, type)
|
async function doExport(req, res, padId, type)
|
||||||
{
|
{
|
||||||
var fileName = padId;
|
var fileName = padId;
|
||||||
|
|
||||||
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
|
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
|
||||||
hooks.aCallFirst("exportFileName", padId,
|
let hookFileName = await 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;
|
|
||||||
|
|
||||||
//tell the browser that this is a downloadable file
|
// if fileName is set then set it to the padId, note that fileName is returned as an array.
|
||||||
|
if (hookFileName.length) {
|
||||||
|
fileName = hookFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell the browser that this is a downloadable file
|
||||||
res.attachment(fileName + "." + type);
|
res.attachment(fileName + "." + type);
|
||||||
|
|
||||||
//if this is a plain text export, we can do this directly
|
// 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
|
// We have to over engineer this because tabs are stored as attributes and not plain text
|
||||||
if(type == "etherpad"){
|
if (type === "etherpad") {
|
||||||
exportEtherpad.getPadRaw(padId, function(err, pad){
|
let pad = await exportEtherpad.getPadRaw(padId);
|
||||||
if(!err){
|
|
||||||
res.send(pad);
|
res.send(pad);
|
||||||
// return;
|
} else if (type === "txt") {
|
||||||
}
|
let txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
|
||||||
});
|
|
||||||
}
|
|
||||||
else if(type == "txt")
|
|
||||||
{
|
|
||||||
exporttxt.getPadTXTDocument(padId, req.params.rev, function(err, txt)
|
|
||||||
{
|
|
||||||
if(!err) {
|
|
||||||
res.send(txt);
|
res.send(txt);
|
||||||
}
|
} else {
|
||||||
});
|
// render the html document
|
||||||
}
|
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev);
|
||||||
else
|
|
||||||
{
|
|
||||||
var html;
|
|
||||||
var randNum;
|
|
||||||
var srcFile, destFile;
|
|
||||||
|
|
||||||
async.series([
|
// decide what to do with the html export
|
||||||
//render the html document
|
|
||||||
function(callback)
|
// if this is a html export, we can send this from here directly
|
||||||
{
|
if (type === "html") {
|
||||||
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
|
// do any final changes the plugin might want to make
|
||||||
hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML){
|
let newHTML = await hooks.aCallFirst("exportHTMLSend", html);
|
||||||
if(newHTML.length) html = newHTML;
|
if (newHTML.length) html = newHTML;
|
||||||
res.send(html);
|
res.send(html);
|
||||||
callback("stop");
|
throw "stop";
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else //write the html export to a file
|
|
||||||
{
|
// else write the html export to a file
|
||||||
randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
let randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
||||||
srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html";
|
let srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html";
|
||||||
fs.writeFile(srcFile, html, callback);
|
await fsp_writeFile(srcFile, html);
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tidy up the exported HTML
|
// Tidy up the exported HTML
|
||||||
function(callback)
|
// ensure html can be collected by the garbage collector
|
||||||
{
|
|
||||||
//ensure html can be collected by the garbage collector
|
|
||||||
html = null;
|
html = null;
|
||||||
|
await TidyHtml.tidy(srcFile);
|
||||||
|
|
||||||
TidyHtml.tidy(srcFile, callback);
|
// send the convert job to the convertor (abiword, libreoffice, ..)
|
||||||
},
|
let destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type;
|
||||||
|
|
||||||
//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
|
// Allow plugins to overwrite the convert in export process
|
||||||
hooks.aCallAll("exportConvert", {srcFile: srcFile, destFile: destFile, req: req, res: res}, function(err, result){
|
let result = await hooks.aCallAll("exportConvert", { srcFile, destFile, req, res });
|
||||||
if(!err && result.length > 0){
|
if (result.length > 0) {
|
||||||
// console.log("export handled by plugin", destFile);
|
// console.log("export handled by plugin", destFile);
|
||||||
handledByPlugin = true;
|
handledByPlugin = true;
|
||||||
callback();
|
} else {
|
||||||
}else{
|
// @TODO no Promise interface for convertors (yet)
|
||||||
convertor.convertFile(srcFile, destFile, type, callback);
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
|
||||||
//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);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -20,10 +20,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ERR = require("async-stacktrace")
|
var padManager = require("../db/PadManager")
|
||||||
, padManager = require("../db/PadManager")
|
|
||||||
, padMessageHandler = require("./PadMessageHandler")
|
, padMessageHandler = require("./PadMessageHandler")
|
||||||
, async = require("async")
|
|
||||||
, fs = require("fs")
|
, fs = require("fs")
|
||||||
, path = require("path")
|
, path = require("path")
|
||||||
, settings = require('../utils/Settings')
|
, settings = require('../utils/Settings')
|
||||||
|
@ -32,301 +30,241 @@ var ERR = require("async-stacktrace")
|
||||||
, importHtml = require("../utils/ImportHtml")
|
, importHtml = require("../utils/ImportHtml")
|
||||||
, importEtherpad = require("../utils/ImportEtherpad")
|
, importEtherpad = require("../utils/ImportEtherpad")
|
||||||
, log4js = require("log4js")
|
, 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;
|
let fsp_exists = util.promisify(fs.exists);
|
||||||
var exportExtension = "htm";
|
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
|
let convertor = null;
|
||||||
if(settings.abiword != null && settings.soffice === 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");
|
convertor = require("../utils/Abiword");
|
||||||
|
}
|
||||||
|
|
||||||
//load soffice only if its enabled
|
// load soffice only if it is enabled
|
||||||
if(settings.soffice != null) {
|
if (settings.soffice != null) {
|
||||||
convertor = require("../utils/LibreOffice");
|
convertor = require("../utils/LibreOffice");
|
||||||
exportExtension = "html";
|
exportExtension = "html";
|
||||||
}
|
}
|
||||||
|
|
||||||
//for node 0.6 compatibily, os.tmpDir() only works from 0.8
|
const tmpDirectory = os.tmpdir();
|
||||||
var tmpDirectory = process.env.TEMP || process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* do a requested import
|
* do a requested import
|
||||||
*/
|
*/
|
||||||
exports.doImport = function(req, res, padId)
|
async function doImport(req, res, padId)
|
||||||
{
|
{
|
||||||
var apiLogger = log4js.getLogger("ImportHandler");
|
var apiLogger = log4js.getLogger("ImportHandler");
|
||||||
|
|
||||||
//pipe to a file
|
// pipe to a file
|
||||||
//convert file to html via abiword or soffice
|
// convert file to html via abiword or soffice
|
||||||
//set html in the pad
|
// set html in the pad
|
||||||
|
|
||||||
var srcFile, destFile
|
|
||||||
, pad
|
|
||||||
, text
|
|
||||||
, importHandledByPlugin
|
|
||||||
, directDatabaseAccess
|
|
||||||
, useConvertor;
|
|
||||||
|
|
||||||
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
||||||
|
|
||||||
// setting flag for whether to use convertor or not
|
// setting flag for whether to use convertor or not
|
||||||
useConvertor = (convertor != null);
|
let useConvertor = (convertor != null);
|
||||||
|
|
||||||
async.series([
|
let form = new formidable.IncomingForm();
|
||||||
//save the uploaded file to /tmp
|
|
||||||
function(callback) {
|
|
||||||
var form = new formidable.IncomingForm();
|
|
||||||
form.keepExtensions = true;
|
form.keepExtensions = true;
|
||||||
form.uploadDir = tmpDirectory;
|
form.uploadDir = tmpDirectory;
|
||||||
|
|
||||||
|
// locally wrapped Promise, since form.parse requires a callback
|
||||||
|
let srcFile = await new Promise((resolve, reject) => {
|
||||||
form.parse(req, function(err, fields, files) {
|
form.parse(req, function(err, fields, files) {
|
||||||
//the upload failed, stop at this point
|
if (err || files.file === undefined) {
|
||||||
if(err || files.file === undefined) {
|
// the upload failed, stop at this point
|
||||||
if(err) console.warn("Uploading Error: " + err.stack);
|
if (err) {
|
||||||
callback("uploadFailed");
|
console.warn("Uploading Error: " + err.stack);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
reject("uploadFailed");
|
||||||
//everything ok, continue
|
}
|
||||||
//save the path of the uploaded file
|
resolve(files.file.path);
|
||||||
srcFile = files.file.path;
|
});
|
||||||
callback();
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
//ensure this is a file ending we know, else we change the file ending to .txt
|
// 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
|
// this allows us to accept source code files like .c or .java
|
||||||
function(callback) {
|
let fileEnding = path.extname(srcFile).toLowerCase()
|
||||||
var fileEnding = path.extname(srcFile).toLowerCase()
|
|
||||||
, knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"]
|
, knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad", ".rtf"]
|
||||||
, fileEndingKnown = (knownFileEndings.indexOf(fileEnding) > -1);
|
, fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
|
||||||
|
|
||||||
//if the file ending is known, continue as normal
|
if (fileEndingUnknown) {
|
||||||
if(fileEndingKnown) {
|
// the file ending is not known
|
||||||
callback();
|
|
||||||
|
|
||||||
return;
|
if (settings.allowUnknownFileEnds === true) {
|
||||||
}
|
// we need to rename this file with a .txt ending
|
||||||
|
let oldSrcFile = srcFile;
|
||||||
|
|
||||||
//we need to rename this file with a .txt ending
|
srcFile = path.join(path.dirname(srcFile), path.basename(srcFile, fileEnding) + ".txt");
|
||||||
if(settings.allowUnknownFileEnds === true){
|
await fs.rename(oldSrcFile, srcFile);
|
||||||
var oldSrcFile = srcFile;
|
} else {
|
||||||
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);
|
console.warn("Not allowing unknown file type to be imported", fileEnding);
|
||||||
callback("uploadFailed");
|
throw "uploadFailed";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
function(callback){
|
|
||||||
destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension);
|
let destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + "." + exportExtension);
|
||||||
|
|
||||||
// Logic for allowing external Import Plugins
|
// Logic for allowing external Import Plugins
|
||||||
hooks.aCallAll("import", {srcFile: srcFile, destFile: destFile}, function(err, result){
|
let result = await hooks.aCallAll("import", { srcFile, destFile });
|
||||||
if(ERR(err, callback)) return callback();
|
let importHandledByPlugin = (result.length > 0); // This feels hacky and wrong..
|
||||||
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 (fileIsNotEtherpad) {
|
let fileIsEtherpad = (fileEnding === ".etherpad");
|
||||||
callback();
|
let fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm");
|
||||||
|
let fileIsTXT = (fileEnding === ".txt");
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
if (headCount >= 10) {
|
||||||
|
apiLogger.warn("Direct database Import attempt of a pad that already has content, we won't be doing this");
|
||||||
|
throw "padHasData";
|
||||||
}
|
}
|
||||||
|
|
||||||
// we do this here so we can see if the pad has quit ea few edits
|
const fsp_readFile = util.promisify(fs.readFile);
|
||||||
padManager.getPad(padId, function(err, _pad){
|
let _text = await fsp_readFile(srcFile, "utf8");
|
||||||
var headCount = _pad.head;
|
req.directDatabaseAccess = true;
|
||||||
if(headCount >= 10){
|
await importEtherpad.setPadRaw(padId, _text);
|
||||||
apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this")
|
|
||||||
return callback("padHasData");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.readFile(srcFile, "utf8", function(err, _text){
|
// convert file to html if necessary
|
||||||
directDatabaseAccess = true;
|
if (!importHandledByPlugin && !req.directDatabaseAccess) {
|
||||||
importEtherpad.setPadRaw(padId, _text, function(err){
|
if (fileIsTXT) {
|
||||||
callback();
|
// Don't use convertor for text files
|
||||||
});
|
useConvertor = false;
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//convert file to html
|
|
||||||
function(callback) {
|
|
||||||
if (importHandledByPlugin || directDatabaseAccess) {
|
|
||||||
callback();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// See https://github.com/ether/etherpad-lite/issues/2572
|
||||||
if (fileIsHTML || (useConvertor === false)) {
|
if (fileIsHTML || !useConvertor) {
|
||||||
// if no convertor only rename
|
// if no convertor only rename
|
||||||
fs.rename(srcFile, destFile, callback);
|
fs.renameSync(srcFile, destFile);
|
||||||
|
} else {
|
||||||
return;
|
// @TODO - no Promise interface for convertors (yet)
|
||||||
}
|
await new Promise((resolve, reject) => {
|
||||||
|
|
||||||
convertor.convertFile(srcFile, destFile, exportExtension, function(err) {
|
convertor.convertFile(srcFile, destFile, exportExtension, function(err) {
|
||||||
//catch convert errors
|
// catch convert errors
|
||||||
if(err) {
|
if (err) {
|
||||||
console.warn("Converting Error:", err);
|
console.warn("Converting Error:", err);
|
||||||
return callback("convertFailed");
|
reject("convertFailed");
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
callback();
|
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
|
}
|
||||||
function(callback) {
|
|
||||||
if (useConvertor || directDatabaseAccess) {
|
|
||||||
callback();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useConvertor && !req.directDatabaseAccess) {
|
||||||
// Read the file with no encoding for raw buffer access.
|
// Read the file with no encoding for raw buffer access.
|
||||||
fs.readFile(destFile, function(err, buf) {
|
let buf = await fsp_readFile(destFile);
|
||||||
if (err) throw err;
|
|
||||||
var isAscii = true;
|
|
||||||
// Check if there are only ascii chars in the uploaded file
|
// Check if there are only ascii chars in the uploaded file
|
||||||
for (var i=0, len=buf.length; i<len; i++) {
|
let isAscii = ! Array.prototype.some.call(buf, c => (c > 240));
|
||||||
if (buf[i] > 240) {
|
|
||||||
isAscii=false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAscii) {
|
if (!isAscii) {
|
||||||
callback("uploadFailed");
|
throw "uploadFailed";
|
||||||
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callback();
|
// get the pad object
|
||||||
});
|
let pad = await padManager.getPad(padId);
|
||||||
},
|
|
||||||
|
|
||||||
//get the pad object
|
// read the text
|
||||||
function(callback) {
|
let text;
|
||||||
padManager.getPad(padId, function(err, _pad){
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//read the text
|
if (!req.directDatabaseAccess) {
|
||||||
function(callback) {
|
text = await fsp_readFile(destFile, "utf8");
|
||||||
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..
|
// Title needs to be stripped out else it appends it to the pad..
|
||||||
text = text.replace("<title>", "<!-- <title>");
|
text = text.replace("<title>", "<!-- <title>");
|
||||||
text = text.replace("</title>","</title>-->");
|
text = text.replace("</title>","</title>-->");
|
||||||
|
|
||||||
//node on windows has a delay on releasing of the file lock.
|
// node on windows has a delay on releasing of the file lock.
|
||||||
//We add a 100ms delay to work around this
|
// We add a 100ms delay to work around this
|
||||||
if(os.type().indexOf("Windows") > -1){
|
if (os.type().indexOf("Windows") > -1){
|
||||||
setTimeout(function() {callback();}, 100);
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
} else {
|
}
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//change text of the pad and broadcast the changeset
|
// change text of the pad and broadcast the changeset
|
||||||
function(callback) {
|
if (!req.directDatabaseAccess) {
|
||||||
if(!directDatabaseAccess){
|
if (importHandledByPlugin || useConvertor || fileIsHTML) {
|
||||||
var fileEnding = path.extname(srcFile).toLowerCase();
|
try {
|
||||||
if (importHandledByPlugin || useConvertor || fileEnding == ".htm" || fileEnding == ".html") {
|
importHtml.setPadHTML(pad, text);
|
||||||
importHtml.setPadHTML(pad, text, function(e){
|
} catch (e) {
|
||||||
if(e) apiLogger.warn("Error importing, possibly caused by malformed HTML");
|
apiLogger.warn("Error importing, possibly caused by malformed HTML");
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
pad.setText(text);
|
pad.setText(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the Pad into memory then brodcast updates to all clients
|
// Load the Pad into memory then broadcast updates to all clients
|
||||||
padManager.unloadPad(padId);
|
padManager.unloadPad(padId);
|
||||||
padManager.getPad(padId, function(err, _pad){
|
pad = await padManager.getPad(padId);
|
||||||
var pad = _pad;
|
|
||||||
padManager.unloadPad(padId);
|
padManager.unloadPad(padId);
|
||||||
|
|
||||||
// direct Database Access means a pad user should perform a switchToPad
|
// direct Database Access means a pad user should perform a switchToPad
|
||||||
// and not attempt to recieve updated pad data..
|
// and not attempt to receive updated pad data
|
||||||
if (directDatabaseAccess) {
|
if (req.directDatabaseAccess) {
|
||||||
callback();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
padMessageHandler.updatePadClients(pad, function(){
|
// tell clients to update
|
||||||
callback();
|
await padMessageHandler.updatePadClients(pad);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
// clean up temporary files
|
||||||
|
|
||||||
//clean up temporary files
|
/*
|
||||||
function(callback) {
|
* TODO: directly delete the file and handle the eventual error. Checking
|
||||||
if (directDatabaseAccess) {
|
* before for existence is prone to race conditions, and does not handle any
|
||||||
callback();
|
* errors anyway.
|
||||||
|
*/
|
||||||
return;
|
if (await fsp_exists(srcFile)) {
|
||||||
|
fsp_unlink(srcFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
//for node < 0.7 compatible
|
if (await fsp_exists(destFile)) {
|
||||||
var fileExists = fs.exists || path.exists;
|
fsp_unlink(destFile);
|
||||||
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);
|
}
|
||||||
}
|
|
||||||
], function(err) {
|
|
||||||
var status = "ok";
|
|
||||||
|
|
||||||
//check for known errors and replace the status
|
exports.doImport = function (req, res, padId)
|
||||||
if(err == "uploadFailed" || err == "convertFailed" || err == "padHasData")
|
{
|
||||||
{
|
/**
|
||||||
|
* 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;
|
status = err;
|
||||||
err = null;
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
}).then(() => {
|
||||||
ERR(err);
|
// close the connection
|
||||||
|
|
||||||
//close the connection
|
|
||||||
res.send(
|
res.send(
|
||||||
"<head> \
|
"<head> \
|
||||||
<script type='text/javascript' src='../../static/js/jquery.js'></script> \
|
<script type='text/javascript' src='../../static/js/jquery.js'></script> \
|
||||||
</head> \
|
</head> \
|
||||||
<script> \
|
<script> \
|
||||||
$(window).load(function(){ \
|
$(window).load(function(){ \
|
||||||
var impexp = window.parent.padimpexp.handleFrameCall('" + directDatabaseAccess +"', '" + status + "'); \
|
var impexp = window.parent.padimpexp.handleFrameCall('" + req.directDatabaseAccess +"', '" + status + "'); \
|
||||||
}) \
|
}) \
|
||||||
</script>"
|
</script>"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,7 +19,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var log4js = require('log4js');
|
var log4js = require('log4js');
|
||||||
var messageLogger = log4js.getLogger("message");
|
var messageLogger = log4js.getLogger("message");
|
||||||
var securityManager = require("../db/SecurityManager");
|
var securityManager = require("../db/SecurityManager");
|
||||||
|
@ -41,10 +40,10 @@ var socket;
|
||||||
*/
|
*/
|
||||||
exports.addComponent = function(moduleName, module)
|
exports.addComponent = function(moduleName, module)
|
||||||
{
|
{
|
||||||
//save the component
|
// save the component
|
||||||
components[moduleName] = module;
|
components[moduleName] = module;
|
||||||
|
|
||||||
//give the module the socket
|
// give the module the socket
|
||||||
module.setSocketIO(socket);
|
module.setSocketIO(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,94 +51,85 @@ exports.addComponent = function(moduleName, module)
|
||||||
* sets the socket.io and adds event functions for routing
|
* sets the socket.io and adds event functions for routing
|
||||||
*/
|
*/
|
||||||
exports.setSocketIO = function(_socket) {
|
exports.setSocketIO = function(_socket) {
|
||||||
//save this socket internaly
|
// save this socket internaly
|
||||||
socket = _socket;
|
socket = _socket;
|
||||||
|
|
||||||
socket.sockets.on('connection', function(client)
|
socket.sockets.on('connection', function(client)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Broken: See http://stackoverflow.com/questions/4647348/send-message-to-specific-client-with-socket-io-and-node-js
|
// 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
|
// Fixed by having a persistant object, ideally this would actually be in the database layer
|
||||||
// TODO move to 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'];
|
remoteAddress[client.id] = client.handshake.headers['x-forwarded-for'];
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
remoteAddress[client.id] = client.handshake.address;
|
remoteAddress[client.id] = client.handshake.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientAuthorized = false;
|
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 = client.send;
|
||||||
client.send = function(message) {
|
client.send = function(message) {
|
||||||
messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message));
|
messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message));
|
||||||
client._send(message);
|
client._send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
//tell all components about this connect
|
// tell all components about this connect
|
||||||
for(var i in components) {
|
for (let i in components) {
|
||||||
components[i].handleConnect(client);
|
components[i].handleConnect(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on('message', function(message)
|
client.on('message', async function(message) {
|
||||||
{
|
if (message.protocolVersion && message.protocolVersion != 2) {
|
||||||
if(message.protocolVersion && message.protocolVersion != 2) {
|
|
||||||
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
|
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//client is authorized, everything ok
|
if (clientAuthorized) {
|
||||||
if(clientAuthorized) {
|
// client is authorized, everything ok
|
||||||
handleMessage(client, message);
|
handleMessage(client, message);
|
||||||
} else { //try to authorize the client
|
} else {
|
||||||
if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
|
// try to authorize the client
|
||||||
var checkAccessCallback = function(err, statusObject) {
|
if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
|
||||||
ERR(err);
|
// check for read-only pads
|
||||||
|
let padId = message.padId;
|
||||||
|
if (padId.indexOf("r.") === 0) {
|
||||||
|
padId = await readOnlyManager.getPadId(message.padId);
|
||||||
|
}
|
||||||
|
|
||||||
//access was granted, mark the client as authorized and handle the message
|
let { accessStatus } = await securityManager.checkAccess(padId, message.sessionID, message.token, message.password);
|
||||||
if(statusObject.accessStatus == "grant") {
|
|
||||||
|
if (accessStatus === "grant") {
|
||||||
|
// access was granted, mark the client as authorized and handle the message
|
||||||
clientAuthorized = true;
|
clientAuthorized = true;
|
||||||
handleMessage(client, message);
|
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 {
|
} else {
|
||||||
//this message has everything to try an authorization
|
// no access, send the client a message that tells him why
|
||||||
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback);
|
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
|
||||||
|
client.json.send({ accessStatus });
|
||||||
}
|
}
|
||||||
} else { //drop message
|
} else {
|
||||||
messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
|
// drop message
|
||||||
|
messageLogger.warn("Dropped message because of bad permissions:" + stringifyWithoutPassword(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('disconnect', function()
|
client.on('disconnect', function() {
|
||||||
{
|
// tell all components about this disconnect
|
||||||
//tell all components about this disconnect
|
for (let i in components) {
|
||||||
for(var i in components)
|
|
||||||
{
|
|
||||||
components[i].handleDisconnect(client);
|
components[i].handleDisconnect(client);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//try to handle the message of this client
|
// try to handle the message of this client
|
||||||
function handleMessage(client, message)
|
function handleMessage(client, message)
|
||||||
{
|
{
|
||||||
|
if (message.component && components[message.component]) {
|
||||||
if(message.component && components[message.component]) {
|
// check if component is registered in the components array
|
||||||
//check if component is registered in the components array
|
if (components[message.component]) {
|
||||||
if(components[message.component]) {
|
|
||||||
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
|
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
|
||||||
components[message.component].handleMessage(client, message);
|
components[message.component].handleMessage(client, message);
|
||||||
}
|
}
|
||||||
|
@ -148,18 +138,14 @@ function handleMessage(client, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns a stringified representation of a message, removes the password
|
// returns a stringified representation of a message, removes the password
|
||||||
//this ensures there are no passwords in the log
|
// this ensures there are no passwords in the log
|
||||||
function stringifyWithoutPassword(message)
|
function stringifyWithoutPassword(message)
|
||||||
{
|
{
|
||||||
var newMessage = {};
|
let newMessage = Object.assign({}, message);
|
||||||
|
|
||||||
for(var i in message)
|
if (newMessage.password != null) {
|
||||||
{
|
newMessage.password = "xxx";
|
||||||
if(i == "password" && message[i] != null)
|
|
||||||
newMessage["password"] = "xxx";
|
|
||||||
else
|
|
||||||
newMessage[i]=message[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(newMessage);
|
return JSON.stringify(newMessage);
|
||||||
|
|
|
@ -12,27 +12,27 @@ var serverName;
|
||||||
exports.createServer = function () {
|
exports.createServer = function () {
|
||||||
console.log("Report bugs at https://github.com/ether/etherpad-lite/issues")
|
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()})`);
|
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
|
||||||
|
|
||||||
exports.restartServer();
|
exports.restartServer();
|
||||||
|
|
||||||
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
|
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`);
|
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");
|
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';
|
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");
|
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 () {
|
exports.restartServer = function () {
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
console.log("Restarting express server");
|
console.log("Restarting express server");
|
||||||
server.close();
|
server.close();
|
||||||
|
@ -41,7 +41,6 @@ exports.restartServer = function () {
|
||||||
var app = express(); // New syntax for express v3
|
var app = express(); // New syntax for express v3
|
||||||
|
|
||||||
if (settings.ssl) {
|
if (settings.ssl) {
|
||||||
|
|
||||||
console.log("SSL -- enabled");
|
console.log("SSL -- enabled");
|
||||||
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
||||||
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
|
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
|
||||||
|
@ -50,9 +49,10 @@ exports.restartServer = function () {
|
||||||
key: fs.readFileSync( settings.ssl.key ),
|
key: fs.readFileSync( settings.ssl.key ),
|
||||||
cert: fs.readFileSync( settings.ssl.cert )
|
cert: fs.readFileSync( settings.ssl.cert )
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settings.ssl.ca) {
|
if (settings.ssl.ca) {
|
||||||
options.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];
|
var caFileName = settings.ssl.ca[i];
|
||||||
options.ca.push(fs.readFileSync(caFileName));
|
options.ca.push(fs.readFileSync(caFileName));
|
||||||
}
|
}
|
||||||
|
@ -60,27 +60,31 @@ exports.restartServer = function () {
|
||||||
|
|
||||||
var https = require('https');
|
var https = require('https');
|
||||||
server = https.createServer(options, app);
|
server = https.createServer(options, app);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
server = http.createServer(app);
|
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
|
// 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");
|
res.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop IE going into compatability mode
|
// Stop IE going into compatability mode
|
||||||
// https://github.com/ether/etherpad-lite/issues/2547
|
// https://github.com/ether/etherpad-lite/issues/2547
|
||||||
res.header("X-UA-Compatible", "IE=Edge,chrome=1");
|
res.header("X-UA-Compatible", "IE=Edge,chrome=1");
|
||||||
|
|
||||||
|
// send git version in the Server response header if exposeVersion is true.
|
||||||
|
if (settings.exposeVersion) {
|
||||||
res.header("Server", serverName);
|
res.header("Server", serverName);
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
if(settings.trustProxy){
|
if (settings.trustProxy) {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||||
var _ = require('underscore');
|
var _ = require('underscore');
|
||||||
var semver = require('semver');
|
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) {
|
args.app.get('/admin/plugins', function(req, res) {
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||||
var render_args = {
|
var render_args = {
|
||||||
|
@ -13,91 +13,99 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
search_results: {},
|
search_results: {},
|
||||||
errors: [],
|
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) {
|
args.app.get('/admin/plugins/info', function(req, res) {
|
||||||
var gitCommit = settings.getGitCommit();
|
var gitCommit = settings.getGitCommit();
|
||||||
var epVersion = settings.getEpVersion();
|
var epVersion = settings.getEpVersion();
|
||||||
res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html",
|
|
||||||
{
|
res.send(eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {
|
||||||
gitCommit: gitCommit,
|
gitCommit: gitCommit,
|
||||||
epVersion: epVersion
|
epVersion: epVersion
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.socketio = function (hook_name, args, cb) {
|
exports.socketio = function(hook_name, args, cb) {
|
||||||
var io = args.io.of("/pluginfw/installer");
|
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;
|
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
|
// send currently installed plugins
|
||||||
var installed = Object.keys(plugins.plugins).map(function(plugin) {
|
var installed = Object.keys(plugins.plugins).map(function(plugin) {
|
||||||
return plugins.plugins[plugin].package
|
return plugins.plugins[plugin].package
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.emit("results:installed", {installed: installed});
|
socket.emit("results:installed", {installed: installed});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("checkUpdates", function() {
|
socket.on("checkUpdates", async function() {
|
||||||
// Check plugins for updates
|
// Check plugins for updates
|
||||||
installer.getAvailablePlugins(/*maxCacheAge:*/60*10, function(er, results) {
|
try {
|
||||||
if(er) {
|
let results = await installer.getAvailablePlugins(/*maxCacheAge:*/ 60 * 10);
|
||||||
console.warn(er);
|
|
||||||
socket.emit("results:updatable", {updatable: {}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var updatable = _(plugins.plugins).keys().filter(function(plugin) {
|
var updatable = _(plugins.plugins).keys().filter(function(plugin) {
|
||||||
if(!results[plugin]) return false;
|
if (!results[plugin]) return false;
|
||||||
var latestVersion = results[plugin].version
|
|
||||||
var currentVersion = plugins.plugins[plugin].package.version
|
var latestVersion = results[plugin].version;
|
||||||
return semver.gt(latestVersion, currentVersion)
|
var currentVersion = plugins.plugins[plugin].package.version;
|
||||||
|
|
||||||
|
return semver.gt(latestVersion, currentVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("results:updatable", {updatable: updatable});
|
socket.emit("results:updatable", {updatable: updatable});
|
||||||
});
|
} catch (er) {
|
||||||
})
|
console.warn(er);
|
||||||
|
|
||||||
socket.on("getAvailable", function (query) {
|
socket.emit("results:updatable", {updatable: {}});
|
||||||
installer.getAvailablePlugins(/*maxCacheAge:*/false, 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);
|
socket.emit("results:available", results);
|
||||||
});
|
} catch (er) {
|
||||||
|
console.error(er);
|
||||||
|
socket.emit("results:available", {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("search", function (query) {
|
socket.on("search", async function(query) {
|
||||||
installer.search(query.searchTerm, /*maxCacheAge:*/60*10, function (er, results) {
|
try {
|
||||||
if(er) {
|
let results = await installer.search(query.searchTerm, /*maxCacheAge:*/ 60 * 10);
|
||||||
console.error(er)
|
|
||||||
results = {}
|
|
||||||
}
|
|
||||||
var res = Object.keys(results)
|
var res = Object.keys(results)
|
||||||
.map(function(pluginName) {
|
.map(function(pluginName) {
|
||||||
return results[pluginName]
|
return results[pluginName];
|
||||||
})
|
})
|
||||||
.filter(function(plugin) {
|
.filter(function(plugin) {
|
||||||
return !plugins.plugins[plugin.name]
|
return !plugins.plugins[plugin.name];
|
||||||
});
|
});
|
||||||
res = sortPluginList(res, query.sortBy, query.sortDir)
|
res = sortPluginList(res, query.sortBy, query.sortDir)
|
||||||
.slice(query.offset, query.offset+query.limit);
|
.slice(query.offset, query.offset+query.limit);
|
||||||
socket.emit("results:search", {results: res, query: query});
|
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) {
|
socket.on("install", function(plugin_name) {
|
||||||
installer.install(plugin_name, function (er) {
|
installer.install(plugin_name, function(er) {
|
||||||
if(er) console.warn(er)
|
if (er) console.warn(er);
|
||||||
|
|
||||||
socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null});
|
socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("uninstall", function (plugin_name) {
|
socket.on("uninstall", function(plugin_name) {
|
||||||
installer.uninstall(plugin_name, function (er) {
|
installer.uninstall(plugin_name, function(er) {
|
||||||
if(er) console.warn(er)
|
if (er) console.warn(er);
|
||||||
|
|
||||||
socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null});
|
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) {
|
function sortPluginList(plugins, property, /*ASC?*/dir) {
|
||||||
return plugins.sort(function(a, b) {
|
return plugins.sort(function(a, b) {
|
||||||
if (a[property] < b[property])
|
if (a[property] < b[property]) {
|
||||||
return dir? -1 : 1;
|
return dir? -1 : 1;
|
||||||
if (a[property] > b[property])
|
}
|
||||||
|
|
||||||
|
if (a[property] > b[property]) {
|
||||||
return dir? 1 : -1;
|
return dir? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
// a must be equal to b
|
// a must be equal to b
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
//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) {
|
args.app.post('/api/:version/:func', function(req, res) {
|
||||||
new formidable.IncomingForm().parse(req, function (err, fields, files) {
|
new formidable.IncomingForm().parse(req, function (err, fields, files) {
|
||||||
apiCaller(req, res, fields)
|
apiCaller(req, res, Object.assign(req.query, fields))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,20 +11,23 @@ exports.gracefulShutdown = function(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
//ensure there is only one graceful shutdown running
|
// ensure there is only one graceful shutdown running
|
||||||
if(exports.onShutdown) return;
|
if (exports.onShutdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
exports.onShutdown = true;
|
exports.onShutdown = true;
|
||||||
|
|
||||||
console.log("graceful shutdown...");
|
console.log("graceful shutdown...");
|
||||||
|
|
||||||
//do the db shutdown
|
// do the db shutdown
|
||||||
db.db.doShutdown(function() {
|
db.doShutdown().then(function() {
|
||||||
console.log("db sucessfully closed.");
|
console.log("db sucessfully closed.");
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(function(){
|
setTimeout(function() {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
@ -35,22 +38,36 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
exports.app = args.app;
|
exports.app = args.app;
|
||||||
|
|
||||||
// Handle errors
|
// 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
|
// if an error occurs Connect will pass it down
|
||||||
// through these "error-handling" middleware
|
// through these "error-handling" middleware
|
||||||
// allowing you to respond however you like
|
// allowing you to respond however you like
|
||||||
res.status(500).send({ error: 'Sorry, something bad happened!' });
|
res.status(500).send({ error: 'Sorry, something bad happened!' });
|
||||||
console.error(err.stack? err.stack : err.toString());
|
console.error(err.stack? err.stack : err.toString());
|
||||||
stats.meter('http500').mark()
|
stats.meter('http500').mark()
|
||||||
})
|
});
|
||||||
|
|
||||||
//connect graceful shutdown with sigint and uncaughtexception
|
/*
|
||||||
if(os.type().indexOf("Windows") == -1) {
|
* Connect graceful shutdown with sigint and uncaught exception
|
||||||
//sigint is so far not working on windows
|
*
|
||||||
//https://github.com/joyent/node/issues/1553
|
* 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);
|
process.on('SIGINT', exports.gracefulShutdown);
|
||||||
|
|
||||||
// when running as PID1 (e.g. in docker container)
|
// when running as PID1 (e.g. in docker container)
|
||||||
// allow graceful shutdown on SIGTERM c.f. #3265
|
// allow graceful shutdown on SIGTERM c.f. #3265
|
||||||
process.on('SIGTERM', exports.gracefulShutdown);
|
process.on('SIGTERM', exports.gracefulShutdown);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,14 @@ var importHandler = require('../../handler/ImportHandler');
|
||||||
var padManager = require("../../db/PadManager");
|
var padManager = require("../../db/PadManager");
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
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"];
|
var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
|
||||||
//send a 404 if we don't support this filetype
|
//send a 404 if we don't support this filetype
|
||||||
if (types.indexOf(req.params.type) == -1) {
|
if (types.indexOf(req.params.type) == -1) {
|
||||||
next();
|
return next();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//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" &&
|
if (settings.exportAvailable() == "no" &&
|
||||||
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
|
["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");
|
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", "*");
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
hasPadAccess(req, res, function() {
|
if (await hasPadAccess(req, res)) {
|
||||||
console.log('req.params.pad', req.params.pad);
|
console.log('req.params.pad', req.params.pad);
|
||||||
padManager.doesPadExists(req.params.pad, function(err, exists)
|
let exists = await padManager.doesPadExists(req.params.pad);
|
||||||
{
|
if (!exists) {
|
||||||
if(!exists) {
|
|
||||||
return next();
|
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
|
// handle import requests
|
||||||
args.app.post('/p/:pad/import', function(req, res, next) {
|
args.app.post('/p/:pad/import', async function(req, res, next) {
|
||||||
hasPadAccess(req, res, function() {
|
if (await hasPadAccess(req, res)) {
|
||||||
padManager.doesPadExists(req.params.pad, function(err, exists)
|
let exists = await padManager.doesPadExists(req.params.pad);
|
||||||
{
|
if (!exists) {
|
||||||
if(!exists) {
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
importHandler.doImport(req, res, req.params.pad);
|
importHandler.doImport(req, res, req.params.pad);
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +1,26 @@
|
||||||
var async = require('async');
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var readOnlyManager = require("../../db/ReadOnlyManager");
|
var readOnlyManager = require("../../db/ReadOnlyManager");
|
||||||
var hasPadAccess = require("../../padaccess");
|
var hasPadAccess = require("../../padaccess");
|
||||||
var exporthtml = require("../../utils/ExportHtml");
|
var exporthtml = require("../../utils/ExportHtml");
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
//serve read only pad
|
// serve read only pad
|
||||||
args.app.get('/ro/:id', function(req, res)
|
args.app.get('/ro/:id', async function(req, res) {
|
||||||
{
|
|
||||||
var html;
|
|
||||||
var padId;
|
|
||||||
|
|
||||||
async.series([
|
// translate the read only pad to a padId
|
||||||
//translate the read only pad to a padId
|
let padId = await readOnlyManager.getPadId(req.params.id);
|
||||||
function(callback)
|
if (padId == null) {
|
||||||
{
|
res.status(404).send('404 - Not Found');
|
||||||
readOnlyManager.getPadId(req.params.id, function(err, _padId)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
padId = _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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPadAccess(req, res, function()
|
// we need that to tell hasPadAcess about the pad
|
||||||
{
|
req.params.pad = padId;
|
||||||
//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")
|
if (await hasPadAccess(req, res)) {
|
||||||
res.status(404).send('404 - Not Found');
|
// render the html document
|
||||||
else
|
let html = await exporthtml.getPadHTMLDocument(padId, null);
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,22 @@ var padManager = require('../../db/PadManager');
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
|
||||||
args.app.param('pad', function (req, res, next, padId) {
|
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
||||||
//ensure the padname is valid and the url doesn't end with a /
|
args.app.param('pad', async function (req, res, next, padId) {
|
||||||
if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
|
// ensure the padname is valid and the url doesn't end with a /
|
||||||
{
|
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
||||||
res.status(404).send('Such a padname is forbidden');
|
res.status(404).send('Such a padname is forbidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
padManager.sanitizePadId(padId, function(sanitizedPadId) {
|
let sanitizedPadId = await padManager.sanitizePadId(padId);
|
||||||
//the pad id was sanitized, so we redirect to the sanitized version
|
|
||||||
if(sanitizedPadId != padId)
|
if (sanitizedPadId === padId) {
|
||||||
{
|
// the pad id was fine, so just render it
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
// the pad id was sanitized, so we redirect to the sanitized version
|
||||||
var real_url = sanitizedPadId;
|
var real_url = sanitizedPadId;
|
||||||
real_url = encodeURIComponent(real_url);
|
real_url = encodeURIComponent(real_url);
|
||||||
var query = url.parse(req.url).query;
|
var query = url.parse(req.url).query;
|
||||||
|
@ -22,11 +25,5 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
res.header('Location', real_url);
|
res.header('Location', real_url);
|
||||||
res.status(302).send('You should be redirected to <a href="' + real_url + '">' + real_url + '</a>');
|
res.status(302).send('You should be redirected to <a href="' + real_url + '">' + real_url + '</a>');
|
||||||
}
|
}
|
||||||
//the pad id was fine, so just render it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,33 @@
|
||||||
var path = require("path")
|
var path = require("path")
|
||||||
, npm = require("npm")
|
, npm = require("npm")
|
||||||
, fs = require("fs")
|
, fs = require("fs")
|
||||||
, async = require("async");
|
, util = require("util");
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
args.app.get('/tests/frontend/specs_list.js', function(req, res){
|
args.app.get('/tests/frontend/specs_list.js', async function(req, res) {
|
||||||
|
let [coreTests, pluginTests] = await Promise.all([
|
||||||
|
exports.getCoreTests(),
|
||||||
|
exports.getPluginTests()
|
||||||
|
]);
|
||||||
|
|
||||||
async.parallel({
|
// merge the two sets of results
|
||||||
coreSpecs: function(callback){
|
let files = [].concat(coreTests, pluginTests).sort();
|
||||||
exports.getCoreTests(callback);
|
console.debug("Sent browser the following test specs:", files);
|
||||||
},
|
res.send("var specs_list = " + JSON.stringify(files) + ";\n");
|
||||||
pluginSpecs: function(callback){
|
|
||||||
exports.getPluginTests(callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(err, results){
|
|
||||||
var files = results.coreSpecs; // push the core specs to a file object
|
|
||||||
files = files.concat(results.pluginSpecs); // add the plugin Specs to the core specs
|
|
||||||
console.debug("Sent browser the following test specs:", files.sort());
|
|
||||||
res.send("var specs_list = " + JSON.stringify(files.sort()) + ";\n");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// path.join seems to normalize by default, but we'll just be explicit
|
// path.join seems to normalize by default, but we'll just be explicit
|
||||||
var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/"));
|
var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/"));
|
||||||
|
|
||||||
var url2FilePath = function(url){
|
var url2FilePath = function(url) {
|
||||||
var subPath = url.substr("/tests/frontend".length);
|
var subPath = url.substr("/tests/frontend".length);
|
||||||
if (subPath == ""){
|
if (subPath == "") {
|
||||||
subPath = "index.html"
|
subPath = "index.html"
|
||||||
}
|
}
|
||||||
subPath = subPath.split("?")[0];
|
subPath = subPath.split("?")[0];
|
||||||
|
|
||||||
var filePath = path.normalize(path.join(rootTestFolder, subPath));
|
var filePath = path.normalize(path.join(rootTestFolder, subPath));
|
||||||
|
|
||||||
// make sure we jail the paths to the test folder, otherwise serve index
|
// make sure we jail the paths to the test folder, otherwise serve index
|
||||||
if (filePath.indexOf(rootTestFolder) !== 0) {
|
if (filePath.indexOf(rootTestFolder) !== 0) {
|
||||||
filePath = path.join(rootTestFolder, "index.html");
|
filePath = path.join(rootTestFolder, "index.html");
|
||||||
|
@ -46,8 +39,8 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
var specFilePath = url2FilePath(req.url);
|
var specFilePath = url2FilePath(req.url);
|
||||||
var specFileName = path.basename(specFilePath);
|
var specFileName = path.basename(specFilePath);
|
||||||
|
|
||||||
fs.readFile(specFilePath, function(err, content){
|
fs.readFile(specFilePath, function(err, content) {
|
||||||
if(err){ return res.send(500); }
|
if (err) { return res.send(500); }
|
||||||
|
|
||||||
content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });";
|
content = "describe(" + JSON.stringify(specFileName) + ", function(){ " + content + " });";
|
||||||
|
|
||||||
|
@ -65,27 +58,30 @@ exports.expressCreateServer = function (hook_name, args, cb) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getPluginTests = function(callback){
|
const readdir = util.promisify(fs.readdir);
|
||||||
var pluginSpecs = [];
|
|
||||||
var plugins = fs.readdirSync('node_modules');
|
exports.getPluginTests = async function(callback) {
|
||||||
plugins.forEach(function(plugin){
|
const moduleDir = "node_modules/";
|
||||||
if(fs.existsSync("node_modules/"+plugin+"/static/tests/frontend/specs")){ // if plugins exists
|
const specPath = "/static/tests/frontend/specs/";
|
||||||
var specFiles = fs.readdirSync("node_modules/"+plugin+"/static/tests/frontend/specs/");
|
const staticDir = "/static/plugins/";
|
||||||
async.forEach(specFiles, function(spec){ // for each specFile push it to pluginSpecs
|
|
||||||
pluginSpecs.push("/static/plugins/"+plugin+"/static/tests/frontend/specs/" + spec);
|
let pluginSpecs = [];
|
||||||
},
|
|
||||||
function(err){
|
let plugins = await readdir(moduleDir);
|
||||||
// blow up if something bad happens!
|
let promises = plugins
|
||||||
|
.map(plugin => [ plugin, moduleDir + plugin + specPath] )
|
||||||
|
.filter(([plugin, specDir]) => fs.existsSync(specDir)) // check plugin exists
|
||||||
|
.map(([plugin, specDir]) => {
|
||||||
|
return readdir(specDir)
|
||||||
|
.then(specFiles => specFiles.map(spec => {
|
||||||
|
pluginSpecs.push(staticDir + plugin + specPath + spec);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
return Promise.all(promises).then(() => pluginSpecs);
|
||||||
callback(null, pluginSpecs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCoreTests = function(callback){
|
exports.getCoreTests = function() {
|
||||||
fs.readdir('tests/frontend/specs', function(err, coreSpecs){ // get the core test specs
|
// get the core test specs
|
||||||
if(err){ return res.send(500); }
|
return readdir('tests/frontend/specs');
|
||||||
callback(null, coreSpecs);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var securityManager = require('./db/SecurityManager');
|
var securityManager = require('./db/SecurityManager');
|
||||||
|
|
||||||
//checks for padAccess
|
// checks for padAccess
|
||||||
module.exports = function (req, res, callback) {
|
module.exports = async function (req, res) {
|
||||||
securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, function(err, accessObj) {
|
try {
|
||||||
if(ERR(err, callback)) return;
|
let accessObj = await securityManager.checkAccess(req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password);
|
||||||
|
|
||||||
//there is access, continue
|
if (accessObj.accessStatus === "grant") {
|
||||||
if(accessObj.accessStatus == "grant") {
|
// there is access, continue
|
||||||
callback();
|
return true;
|
||||||
//no access
|
|
||||||
} else {
|
} else {
|
||||||
|
// no access
|
||||||
res.status(403).send("403 - Can't touch this");
|
res.status(403).send("403 - Can't touch this");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// @TODO - send internal server error here?
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,75 +22,61 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var log4js = require('log4js')
|
var log4js = require('log4js')
|
||||||
, async = require('async')
|
|
||||||
, stats = require('./stats')
|
|
||||||
, NodeVersion = require('./utils/NodeVersion')
|
, NodeVersion = require('./utils/NodeVersion')
|
||||||
;
|
;
|
||||||
|
|
||||||
log4js.replaceConsole();
|
log4js.replaceConsole();
|
||||||
|
|
||||||
stats.gauge('memoryUsage', function() {
|
/*
|
||||||
return process.memoryUsage().rss
|
* early check for version compatibility before calling
|
||||||
})
|
* any modules that require newer versions of NodeJS
|
||||||
|
*/
|
||||||
|
NodeVersion.enforceMinNodeVersion('8.9.0');
|
||||||
|
|
||||||
var settings
|
/*
|
||||||
, db
|
* Etherpad 1.8.3 will require at least nodejs 10.13.0.
|
||||||
, plugins
|
*/
|
||||||
, hooks;
|
NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* start up stats counting system
|
||||||
|
*/
|
||||||
|
var stats = require('./stats');
|
||||||
|
stats.gauge('memoryUsage', function() {
|
||||||
|
return process.memoryUsage().rss;
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* no use of let or await here because it would cause startup
|
||||||
|
* to fail completely on very early versions of NodeJS
|
||||||
|
*/
|
||||||
var npm = require("npm/lib/npm.js");
|
var npm = require("npm/lib/npm.js");
|
||||||
|
|
||||||
async.waterfall([
|
npm.load({}, function() {
|
||||||
function(callback)
|
var settings = require('./utils/Settings');
|
||||||
{
|
var db = require('./db/DB');
|
||||||
NodeVersion.enforceMinNodeVersion('6.9.0', callback);
|
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
||||||
},
|
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
||||||
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
NodeVersion.checkDeprecationStatus('8.9.0', '1.8.0', callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
// load npm
|
|
||||||
function(callback) {
|
|
||||||
npm.load({}, function(er) {
|
|
||||||
callback(er)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// load everything
|
|
||||||
function(callback) {
|
|
||||||
settings = require('./utils/Settings');
|
|
||||||
db = require('./db/DB');
|
|
||||||
plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
|
||||||
hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
|
||||||
hooks.plugins = plugins;
|
hooks.plugins = plugins;
|
||||||
callback();
|
|
||||||
},
|
|
||||||
|
|
||||||
//initalize the database
|
db.init()
|
||||||
function (callback)
|
.then(plugins.update)
|
||||||
{
|
.then(function() {
|
||||||
db.init(callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
function(callback) {
|
|
||||||
plugins.update(callback)
|
|
||||||
},
|
|
||||||
|
|
||||||
function (callback) {
|
|
||||||
console.info("Installed plugins: " + plugins.formatPluginsWithVersion());
|
console.info("Installed plugins: " + plugins.formatPluginsWithVersion());
|
||||||
console.debug("Installed parts:\n" + plugins.formatParts());
|
console.debug("Installed parts:\n" + plugins.formatParts());
|
||||||
console.debug("Installed hooks:\n" + plugins.formatHooks());
|
console.debug("Installed hooks:\n" + plugins.formatHooks());
|
||||||
|
|
||||||
// Call loadSettings hook
|
// Call loadSettings hook
|
||||||
hooks.aCallAll("loadSettings", { settings: settings });
|
hooks.aCallAll("loadSettings", { settings: settings });
|
||||||
callback();
|
|
||||||
},
|
|
||||||
|
|
||||||
//initalize the http server
|
// initalize the http server
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
hooks.callAll("createServer", {});
|
hooks.callAll("createServer", {});
|
||||||
callback(null);
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
console.error("exception thrown: " + e.message);
|
||||||
|
if (e.stack) {
|
||||||
|
console.log(e.stack);
|
||||||
}
|
}
|
||||||
]);
|
process.exit(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
/*
|
|
||||||
* TODO: this polyfill is needed for Node 6.9 support.
|
|
||||||
*
|
|
||||||
* Once minimum supported Node version is raised to 8.9.0, it will be removed.
|
|
||||||
*/
|
|
||||||
if (!Object.values) {
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var statsLogger = log4js.getLogger("stats");
|
|
||||||
|
|
||||||
statsLogger.warn(`Enabling a polyfill to run on this Node version (${process.version}). Next Etherpad version will remove support for Node version < 8.9.0. Please update your runtime.`);
|
|
||||||
|
|
||||||
var values = require('object.values');
|
|
||||||
|
|
||||||
values.shim();
|
|
||||||
}
|
|
||||||
|
|
||||||
var measured = require('measured-core')
|
var measured = require('measured-core')
|
||||||
|
|
||||||
module.exports = measured.createCollection();
|
module.exports = measured.createCollection();
|
||||||
|
|
|
@ -61,8 +61,8 @@ var popIfEndsWith = function(stringArray, lastDesiredElements) {
|
||||||
* Heuristically computes the directory in which Etherpad is installed.
|
* Heuristically computes the directory in which Etherpad is installed.
|
||||||
*
|
*
|
||||||
* All the relative paths have to be interpreted against this absolute base
|
* All the relative paths have to be interpreted against this absolute base
|
||||||
* path. Since the Unix and Windows install have a different layout on disk,
|
* path. Since the Windows package install has a different layout on disk, it is
|
||||||
* they are treated as two special cases.
|
* dealt with as a special case.
|
||||||
*
|
*
|
||||||
* The path is computed only on first invocation. Subsequent invocations return
|
* The path is computed only on first invocation. Subsequent invocations return
|
||||||
* a cached value.
|
* a cached value.
|
||||||
|
@ -79,26 +79,27 @@ exports.findEtherpadRoot = function() {
|
||||||
|
|
||||||
const findRoot = require('find-root');
|
const findRoot = require('find-root');
|
||||||
const foundRoot = findRoot(__dirname);
|
const foundRoot = findRoot(__dirname);
|
||||||
|
const splitFoundRoot = foundRoot.split(path.sep);
|
||||||
|
|
||||||
var directoriesToStrip;
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
/*
|
/*
|
||||||
* Given the structure of our Windows package, foundRoot's value
|
* On Unix platforms and on Windows manual installs, foundRoot's value will
|
||||||
* will be the following on win32:
|
* be:
|
||||||
*
|
|
||||||
* <BASE_DIR>\node_modules\ep_etherpad-lite
|
|
||||||
*/
|
|
||||||
directoriesToStrip = ['node_modules', 'ep_etherpad-lite'];
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* On Unix platforms, foundRoot's value will be:
|
|
||||||
*
|
*
|
||||||
* <BASE_DIR>\src
|
* <BASE_DIR>\src
|
||||||
*/
|
*/
|
||||||
directoriesToStrip = ['src'];
|
var maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']);
|
||||||
|
|
||||||
|
if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) {
|
||||||
|
/*
|
||||||
|
* If we did not find the path we are expecting, and we are running under
|
||||||
|
* Windows, we may still be running from a prebuilt package, whose directory
|
||||||
|
* structure is different:
|
||||||
|
*
|
||||||
|
* <BASE_DIR>\node_modules\ep_etherpad-lite
|
||||||
|
*/
|
||||||
|
maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['node_modules', 'ep_etherpad-lite']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeEtherpadRoot = popIfEndsWith(foundRoot.split(path.sep), directoriesToStrip);
|
|
||||||
if (maybeEtherpadRoot === false) {
|
if (maybeEtherpadRoot === false) {
|
||||||
absPathLogger.error(`Could not identity Etherpad base path in this ${process.platform} installation in "${foundRoot}"`);
|
absPathLogger.error(`Could not identity Etherpad base path in this ${process.platform} installation in "${foundRoot}"`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -15,58 +15,48 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
var async = require("async");
|
let db = require("../db/DB");
|
||||||
var db = require("../db/DB").db;
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
|
|
||||||
exports.getPadRaw = function(padId, callback){
|
exports.getPadRaw = async function(padId) {
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
let padKey = "pad:" + padId;
|
||||||
db.get("pad:"+padId, cb);
|
let padcontent = await db.get(padKey);
|
||||||
},
|
|
||||||
function(padcontent,cb){
|
let records = [ padKey ];
|
||||||
var records = ["pad:"+padId];
|
for (let i = 0; i <= padcontent.head; i++) {
|
||||||
for (var i = 0; i <= padcontent.head; i++) {
|
records.push(padKey + ":revs:" + i);
|
||||||
records.push("pad:"+padId+":revs:" + i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i <= padcontent.chatHead; i++) {
|
for (let i = 0; i <= padcontent.chatHead; i++) {
|
||||||
records.push("pad:"+padId+":chat:" + i);
|
records.push(padKey + ":chat:" + i);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = {};
|
let data = {};
|
||||||
|
for (let key of records) {
|
||||||
async.forEachSeries(Object.keys(records), function(key, r){
|
|
||||||
|
|
||||||
// For each piece of info about a pad.
|
// For each piece of info about a pad.
|
||||||
db.get(records[key], function(err, entry){
|
let entry = data[key] = await db.get(key);
|
||||||
data[records[key]] = entry;
|
|
||||||
|
|
||||||
// Get the Pad Authors
|
// Get the Pad Authors
|
||||||
if(entry.pool && entry.pool.numToAttrib){
|
if (entry.pool && entry.pool.numToAttrib) {
|
||||||
var authors = entry.pool.numToAttrib;
|
let authors = entry.pool.numToAttrib;
|
||||||
async.forEachSeries(Object.keys(authors), function(k, c){
|
|
||||||
if(authors[k][0] === "author"){
|
for (let k of Object.keys(authors)) {
|
||||||
var authorId = authors[k][1];
|
if (authors[k][0] === "author") {
|
||||||
|
let authorId = authors[k][1];
|
||||||
|
|
||||||
// Get the author info
|
// Get the author info
|
||||||
db.get("globalAuthor:"+authorId, function(e, authorEntry){
|
let authorEntry = await db.get("globalAuthor:" + authorId);
|
||||||
if(authorEntry && authorEntry.padIDs) authorEntry.padIDs = padId;
|
if (authorEntry) {
|
||||||
if(!e) data["globalAuthor:"+authorId] = authorEntry;
|
data["globalAuthor:" + authorId] = authorEntry;
|
||||||
});
|
if (authorEntry.padIDs) {
|
||||||
|
authorEntry.padIDs = padId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
return data;
|
||||||
// console.log("authorsK", authors[k]);
|
|
||||||
c(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
r(null); // callback;
|
|
||||||
});
|
|
||||||
}, function(err){
|
|
||||||
cb(err, data);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
], function(err, data){
|
|
||||||
callback(null, data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ exports._analyzeLine = function(text, aline, apool){
|
||||||
|
|
||||||
|
|
||||||
exports._encodeWhitespace = function(s){
|
exports._encodeWhitespace = function(s){
|
||||||
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c){
|
return s.replace(/[^\x21-\x7E\s\t\n\r]/gu, function(c){
|
||||||
return "&#" +c.charCodeAt(0) + ";";
|
return "&#" +c.codePointAt(0) + ";";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue