Merge branch 'develop' into master

pull/4613/head 1.8.7
Richard Hansen 2020-12-23 16:21:00 -05:00
commit 020f5ff730
367 changed files with 47951 additions and 23347 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: ether

60
.github/workflows/backend-tests.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: "Backend tests"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
withoutplugins:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: without plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install libreoffice
run: |
sudo add-apt-repository -y ppa:libreoffice/ppa
sudo apt update
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
# configures some settings and runs npm run test
- name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh
withplugins:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: with Plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install libreoffice
run: |
sudo add-apt-repository -y ppa:libreoffice/ppa
sudo apt update
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents
# configures some settings and runs npm run test
- name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh

26
.github/workflows/dockerfile.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: "Dockerfile"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
dockerfile:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: build image and run connectivity test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: docker build
run: |
docker build -t etherpad:test .
docker run -d -p 9001:9001 etherpad:test
./bin/installDeps.sh
sleep 3 # delay for startup?
cd src && npm run test-container

83
.github/workflows/frontend-tests.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: "Frontend tests"
on: [push]
jobs:
withoutplugins:
name: without plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: tests/frontend/travis/sauce_tunnel.sh
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: export GIT_HASH to env
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Run the frontend tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
tests/frontend/travis/runner.sh
withplugins:
name: with plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: tests/frontend/travis/sauce_tunnel.sh
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents ep_set_title_on_pad
- name: export GIT_HASH to env
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
# XXX we should probably run all tests, because plugins could effect their results
- name: Remove standard frontend test files, so only plugin tests are run
run: rm tests/frontend/specs/*
- name: Run the frontend tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
tests/frontend/travis/runner.sh

24
.github/workflows/lint-package-lock.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: "Lint"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
lint-package-lock:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: package-lock.json
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install lockfile-lint
run: npm install lockfile-lint
- name: Run lockfile-lint on package-lock.json
run: npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm

53
.github/workflows/load-test.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: "Loadtest"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
withoutplugins:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: without plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install etherpad-load-test
run: sudo npm install -g etherpad-load-test
- name: Run load test
run: tests/frontend/travis/runnerLoadTest.sh
withplugins:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: with Plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install etherpad-load-test
run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents
# configures some settings and runs npm run test
- name: Run load test
run: tests/frontend/travis/runnerLoadTest.sh

39
.github/workflows/rate-limit.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: "rate limit"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
ratelimit:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: docker network
run: docker network create --subnet=172.23.42.0/16 ep_net
- name: build docker image
run: |
docker build -f Dockerfile -t epl-debian-slim .
docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest .
docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip .
- name: run docker images
run: |
docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &
docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest
docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip
- name: install dependencies and create symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: run rate limit test
run: |
cd tests/ratelimit
./testlimits.sh

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ var/dirty.db
bin/convertSettings.json
*~
*.patch
src/static/js/jquery.js
npm-debug.log
*.DS_Store
.ep_initialized

View File

@ -8,59 +8,127 @@ services:
cache: false
before_install:
- sudo add-apt-repository -y ppa:libreoffice/ppa
- sudo apt-get update
- sudo apt-get -y install libreoffice
- sudo apt-get -y install libreoffice-pdfimport
install:
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "tests/frontend/travis/runner.sh"
env:
global:
- secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec="
- secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g="
_set_loglevel_warn: &set_loglevel_warn |
sed -e 's/"loglevel":[^,]*/"loglevel": "WARN"/' \
settings.json.template >settings.json.template.new &&
mv settings.json.template.new settings.json.template
_install_libreoffice: &install_libreoffice >-
sudo add-apt-repository -y ppa:libreoffice/ppa &&
sudo apt-get update &&
sudo apt-get -y install libreoffice libreoffice-pdfimport
_install_plugins: &install_plugins >-
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_markdown
ep_readonly_guest
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
ep_set_title_on_pad
jobs:
include:
# we can only frontend tests from the ether/ organization and not from forks.
# To request tests to be run ask a maintainer to fork your repo to ether/
- if: fork = false
name: "Test the Frontend"
name: "Test the Frontend without Plugins"
install:
#FIXME
- "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/g' settings.json.template > settings.json"
- *set_loglevel_warn
- "tests/frontend/travis/sauce_tunnel.sh"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "tests/frontend/travis/runner.sh"
- name: "Run the Backend tests"
- "./tests/frontend/travis/runner.sh"
- name: "Run the Backend tests without Plugins"
install:
- *install_libreoffice
- *set_loglevel_warn
- "bin/installDeps.sh"
- "cd src && npm install && cd -"
script:
- "tests/frontend/travis/runnerBackend.sh"
## Temporarily commented out the Dockerfile tests
# - name: "Test the Dockerfile"
# install:
# - "cd src && npm install && cd -"
# script:
# - "docker build -t etherpad:test ."
# - "docker run -d -p 9001:9001 etherpad:test && sleep 3"
# - "cd src && npm run test-container"
- name: "Load test Etherpad"
- name: "Test the Dockerfile"
install:
- "cd src && npm install && cd -"
script:
- "docker build -t etherpad:test ."
- "docker run -d -p 9001:9001 etherpad:test && sleep 3"
- "cd src && npm run test-container"
- name: "Load test Etherpad without Plugins"
install:
- *set_loglevel_warn
- "bin/installDeps.sh"
- "cd src && npm install && cd -"
- "npm install -g etherpad-load-test"
script:
- "tests/frontend/travis/runnerLoadTest.sh"
# we can only frontend tests from the ether/ organization and not from forks.
# To request tests to be run ask a maintainer to fork your repo to ether/
- if: fork = false
name: "Test the Frontend Plugins only"
install:
- *set_loglevel_warn
- "tests/frontend/travis/sauce_tunnel.sh"
- "bin/installDeps.sh"
- "rm tests/frontend/specs/*"
- *install_plugins
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "./tests/frontend/travis/runner.sh"
- name: "Lint test package-lock.json"
install:
- "npm install lockfile-lint"
script:
- npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm
- name: "Run the Backend tests with Plugins"
install:
- *install_libreoffice
- *set_loglevel_warn
- "bin/installDeps.sh"
- *install_plugins
- "cd src && npm install && cd -"
script:
- "tests/frontend/travis/runnerBackend.sh"
- name: "Test the Dockerfile"
install:
- "cd src && npm install && cd -"
script:
- "docker build -t etherpad:test ."
- "docker run -d -p 9001:9001 etherpad:test && sleep 3"
- "cd src && npm run test-container"
- name: "Load test Etherpad with Plugins"
install:
- *set_loglevel_warn
- "bin/installDeps.sh"
- *install_plugins
- "cd src && npm install && cd -"
- "npm install -g etherpad-load-test"
script:
- "tests/frontend/travis/runnerLoadTest.sh"
- name: "Test rate limit"
install:
- "docker network create --subnet=172.23.42.0/16 ep_net"
- "docker build -f Dockerfile -t epl-debian-slim ."
- "docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest ."
- "docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip ."
- "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest"
- "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &"
- "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip"
- "./bin/installDeps.sh"
script:
- "cd tests/ratelimit && bash testlimits.sh"
notifications:
irc:

View File

@ -1,5 +1,88 @@
# Develop -- TODO Change to 1.8.x.
* ...
# 1.8.7
### Compatibility-breaking changes
* **IMPORTANT:** It is no longer possible to protect a group pad with a
password. All API calls to `setPassword` or `isPasswordProtected` will fail.
Existing group pads that were previously password protected will no longer be
password protected. If you need fine-grained access control, you can restrict
API session creation in your frontend service, or you can use plugins.
* All workarounds for Microsoft Internet Explorer have been removed. IE might
still work, but it is untested.
* Plugin hook functions are now subject to new sanity checks. Buggy hook
functions will cause an error message to be logged
* Authorization failures now return 403 by default instead of 401
* The `authorize` hook is now only called after successful authentication. Use
the new `preAuthorize` hook if you need to bypass authentication
* The `authFailure` hook is deprecated; use the new `authnFailure` and
`authzFailure` hooks instead
* The `indexCustomInlineScripts` hook was removed
* The `client` context property for the `handleMessage` and
`handleMessageSecurity` hooks has been renamed to `socket` (the old name is
still usable but deprecated)
* The `aceAttribClasses` hook functions are now called synchronously
* The format of `ENTER`, `CREATE`, and `LEAVE` log messages has changed
* Strings passed to `$.gritter.add()` are now expected to be plain text, not
HTML. Use jQuery or DOM objects if you need formatting
### Notable new features
* Users can now import without creating and editing the pad first
* Added a new `readOnly` user setting that makes it possible to create users in
`settings.json` that can read pads but not create or modify them
* Added a new `canCreate` user setting that makes it possible to create users in
`settings.json` that can modify pads but not create them
* The `authorize` hook now accepts `readOnly` to grant read-only access to a pad
* The `authorize` hook now accepts `modify` to grant modify-only (creation
prohibited) access to a pad
* All authentication successes and failures are now logged
* Added a new `cookie.sameSite` setting that makes it possible to enable
authentication when Etherpad is embedded in an iframe from another site
* New `exportHTMLAdditionalContent` hook to include additional HTML content
* New `exportEtherpadAdditionalContent` hook to include additional database
content in `.etherpad` exports
* New `expressCloseServer` hook to close Express when required
* The `padUpdate` hook context now includes `revs` and `changeset`
* `checkPlugins.js` has various improvements to help plugin developers
* The HTTP request object (and therefore the express-session state) is now
accessible from within most `eejsBlock_*` hooks
* Users without a `password` or `hash` property in `settings.json` are no longer
ignored, so they can now be used by authentication plugins
* New permission denied modal and block ``permissionDenied``
* Plugins are now updated to the latest version instead of minor or patches
### Notable fixes
* Fixed rate limit accounting when Etherpad is behind a reverse proxy
* Fixed typos that prevented access to pads via an HTTP API session
* Fixed authorization failures for pad URLs containing a percent-encoded
character
* Fixed exporting of read-only pads
* Passwords are no longer written to connection state database entries or logged
in debug logs
* When using the keyboard to navigate through the toolbar buttons the button
with the focus is now highlighted
* Fixed support for Node.js 10 by passing the `--experimental-worker` flag
* Fixed export of HTML attributes within a line
* Fixed occasional "Cannot read property 'offsetTop' of undefined" error in
timeslider when "follow pad contents" is checked
* socket.io errors are now displayed instead of silently ignored
* Pasting while the caret is in a link now works (except for middle-click paste
on X11 systems)
* Removal of Microsoft Internet Explorer specific code
* Import better handles line breaks and white space
* Fix issue with ``createDiffHTML`` incorrect call of ``getInternalRevisionAText``
* Allow additional characters in URLs
* MySQL engine fix and various other UeberDB updates (See UeberDB changelog).
* Admin UI improvements on search results (to remove duplicate items)
* Removal of unused cruft from ``clientVars`` (``ip`` and ``userAgent``)
### Minor changes
* Temporary disconnections no longer force a full page refresh
* Toolbar layout for narrow screens is improved
* Fixed `SameSite` cookie attribute for the `language`, `token`, and `pref`
cookies
* Fixed superfluous database accesses when deleting a pad
* Expanded test coverage.
* `package-lock.json` is now lint checked on commit
* Various lint fixes/modernization of code
# 1.8.6
* IMPORTANT: This fixes a severe problem with postgresql in 1.8.5

View File

@ -51,4 +51,4 @@ COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json
RUN chmod -R g=u .
EXPOSE 9001
CMD ["node", "node_modules/ep_etherpad-lite/node/server.js"]
CMD ["node", "--experimental-worker", "node_modules/ep_etherpad-lite/node/server.js"]

View File

@ -1,12 +1,12 @@
# A real-time collaborative editor for the web
<a href="https://hub.docker.com/r/etherpad/etherpad"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/etherpad/etherpad"></a>
[![Travis (.org)](https://api.travis-ci.org/ether/etherpad-lite.svg?branch=develop)](https://travis-ci.org/github/ether/etherpad-lite)
[![Travis (.com)](https://api.travis-ci.com/ether/etherpad-lite.svg?branch=develop)](https://travis-ci.com/github/ether/etherpad-lite)
![Demo Etherpad Animated Jif](doc/images/etherpad_demo.gif "Etherpad in action")
# About
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.
Etherpad is a real-time collaborative editor [scalable to thousands of simultaneous real time users](http://scale.etherpad.org/). It provides [full data export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) capabilities, and runs on _your_ server, under _your_ control.
**[Try it out](https://video.etherpad.com)**
@ -19,7 +19,7 @@ Etherpad is a real-time collaborative editor scalable to thousands of simultaneo
### Quick install on Debian/Ubuntu
```
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt install -y nodejs
git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh
```
@ -127,7 +127,8 @@ Read our [**Developer Guidelines**](https://github.com/ether/etherpad-lite/blob/
# Get in touch
The official channel for contacting the development team is via the [Github issues](https://github.com/ether/etherpad-lite/issues).
For **responsible disclosure of vulnerabilities**, please write a mail to the maintainer (a.mux@inwind.it).
For **responsible disclosure of vulnerabilities**, please write a mail to the maintainers (a.mux@inwind.it and contact@etherpad.org).
Join the official [Etherpad Discord Channel](https://discord.com/invite/daEjfhw)
# HTTP API
Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API)

View File

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

View File

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

View File

@ -1,120 +1,111 @@
/*
* This is a debug tool. It checks all revisions for data corruption
*/
if (process.argv.length != 3) {
console.error("Use: node bin/checkPadDeltas.js $PADID");
process.exit(1);
}
// get the padID
const padId = process.argv[2];
// load and initialize NPM;
var expect = require('expect.js')
var diff = require('diff')
var async = require('async')
let npm = require('../src/node_modules/npm');
var async = require("ep_etherpad-lite/node_modules/async");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
npm.load({}, async function() {
try {
// initialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
await db.init();
// load modules
let Changeset = require('ep_etherpad-lite/static/js/Changeset');
let padManager = require('../src/node/db/PadManager');
let exists = await padManager.doesPadExists(padId);
if (!exists) {
console.error("Pad does not exist");
process.exit(1);
}
// get the pad
let pad = await padManager.getPad(padId);
//create an array with key revisions
//key revisions always save the full pad atext
var head = pad.getHeadRevisionNumber();
var keyRevisions = [];
for(var i=0;i<head;i+=100)
{
keyRevisions.push(i);
}
//create an array with all revisions
var revisions = [];
for(var i=0;i<=head;i++)
{
revisions.push(i);
}
var atext = Changeset.makeAText("\n")
//run trough all revisions
async.forEachSeries(revisions, function(revNum, callback)
{
//console.log('Fetching', revNum)
db.db.get("pad:"+padId+":revs:" + revNum, function(err, revision)
{
if(err) return callback(err);
//check if there is a atext in the keyRevisions
if(~keyRevisions.indexOf(revNum) && (revision === undefined || revision.meta === undefined || revision.meta.atext === undefined)) {
console.error("No atext in key revision " + revNum);
callback();
return;
}
try {
//console.log("check revision ", revNum);
var cs = revision.changeset;
atext = Changeset.applyToAText(cs, atext, pad.pool);
}
catch(e) {
console.error("Bad changeset at revision " + revNum + " - " + e.message);
callback();
return;
}
if(~keyRevisions.indexOf(revNum)) {
try {
expect(revision.meta.atext.text).to.eql(atext.text)
expect(revision.meta.atext.attribs).to.eql(atext.attribs)
}catch(e) {
console.error("Atext in key revision "+revNum+" doesn't match computed one.")
console.log(diff.diffChars(atext.text, revision.meta.atext.text).map(function(op) {if(!op.added && !op.removed) op.value = op.value.length; return op}))
//console.error(e)
//console.log('KeyRev. :', revision.meta.atext)
//console.log('Computed:', atext)
callback()
return
}
}
setImmediate(callback)
});
}, function(er) {
if(pad.atext.text == atext.text) console.log('ok')
else {
console.error('Pad AText doesn\'t match computed one! (Computed ',atext.text.length, ', db', pad.atext.text.length,')')
console.log(diff.diffChars(atext.text, pad.atext.text).map(function(op) {if(!op.added && !op.removed) op.value = op.value.length; return op}))
}
callback(er)
});
process.exit(0);
} catch (e) {
console.trace(e);
process.exit(1);
}
});
/*
* This is a debug tool. It checks all revisions for data corruption
*/
if (process.argv.length != 3) {
console.error('Use: node bin/checkPadDeltas.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
// load and initialize NPM;
const expect = require('expect.js');
const diff = require('diff');
var async = require('async');
const npm = require('../src/node_modules/npm');
var async = require('ep_etherpad-lite/node_modules/async');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
npm.load({}, async () => {
try {
// initialize database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load modules
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const padManager = require('../src/node/db/PadManager');
const exists = await padManager.doesPadExists(padId);
if (!exists) {
console.error('Pad does not exist');
process.exit(1);
}
// get the pad
const pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (var i = 0; i < head; i += 100) {
keyRevisions.push(i);
}
// create an array with all revisions
const revisions = [];
for (var i = 0; i <= head; i++) {
revisions.push(i);
}
let atext = Changeset.makeAText('\n');
// run trough all revisions
async.forEachSeries(revisions, (revNum, callback) => {
// console.log('Fetching', revNum)
db.db.get(`pad:${padId}:revs:${revNum}`, (err, revision) => {
if (err) return callback(err);
// check if there is a atext in the keyRevisions
if (~keyRevisions.indexOf(revNum) && (revision === undefined || revision.meta === undefined || revision.meta.atext === undefined)) {
console.error(`No atext in key revision ${revNum}`);
callback();
return;
}
try {
// console.log("check revision ", revNum);
const cs = revision.changeset;
atext = Changeset.applyToAText(cs, atext, pad.pool);
} catch (e) {
console.error(`Bad changeset at revision ${revNum} - ${e.message}`);
callback();
return;
}
if (~keyRevisions.indexOf(revNum)) {
try {
expect(revision.meta.atext.text).to.eql(atext.text);
expect(revision.meta.atext.attribs).to.eql(atext.attribs);
} catch (e) {
console.error(`Atext in key revision ${revNum} doesn't match computed one.`);
console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; }));
// console.error(e)
// console.log('KeyRev. :', revision.meta.atext)
// console.log('Computed:', atext)
callback();
return;
}
}
setImmediate(callback);
});
}, (er) => {
if (pad.atext.text == atext.text) { console.log('ok'); } else {
console.error('Pad AText doesn\'t match computed one! (Computed ', atext.text.length, ', db', pad.atext.text.length, ')');
console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; }));
}
callback(er);
});
process.exit(0);
} catch (e) {
console.trace(e);
process.exit(1);
}
});

View File

@ -1,7 +1,10 @@
#!/bin/sh
#Move to the folder where ep-lite is installed
cd $(dirname $0)
# Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/..
# Source constants and usefull functions
. bin/functions.sh
#Was this script started in the bin folder? if yes move out
if [ -d "../bin" ]; then
@ -38,4 +41,4 @@ bin/installDeps.sh "$@" || exit 1
echo "Started Etherpad..."
SCRIPTPATH=$(pwd -P)
node "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@"
node $(compute_node_args) "${SCRIPTPATH}/node_modules/ep_etherpad-lite/node/server.js" "$@"

View File

@ -1,128 +1,116 @@
var startTime = Date.now();
var fs = require("fs");
var ueberDB = require("../src/node_modules/ueberdb2");
var mysql = require("../src/node_modules/ueberdb2/node_modules/mysql");
var async = require("../src/node_modules/async");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
const startTime = Date.now();
const fs = require('fs');
const ueberDB = require('../src/node_modules/ueberdb2');
const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql');
const async = require('../src/node_modules/async');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
var settingsFile = process.argv[2];
var sqlOutputFile = process.argv[3];
const settingsFile = process.argv[2];
const sqlOutputFile = process.argv[3];
//stop if the settings file is not set
if(!settingsFile || !sqlOutputFile)
{
console.error("Use: node convert.js $SETTINGSFILE $SQLOUTPUT");
// stop if the settings file is not set
if (!settingsFile || !sqlOutputFile) {
console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT');
process.exit(1);
}
log("read settings file...");
//read the settings file and parse the json
var settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
log("done");
log('read settings file...');
// read the settings file and parse the json
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
log('done');
log("open output file...");
var sqlOutput = fs.openSync(sqlOutputFile, "w");
var sql = "SET CHARACTER SET UTF8;\n" +
"CREATE TABLE IF NOT EXISTS `store` ( \n" +
"`key` VARCHAR( 100 ) NOT NULL , \n" +
"`value` LONGTEXT NOT NULL , \n" +
"PRIMARY KEY ( `key` ) \n" +
") ENGINE = INNODB;\n" +
"START TRANSACTION;\n\n";
log('open output file...');
const sqlOutput = fs.openSync(sqlOutputFile, 'w');
const sql = 'SET CHARACTER SET UTF8;\n' +
'CREATE TABLE IF NOT EXISTS `store` ( \n' +
'`key` VARCHAR( 100 ) NOT NULL , \n' +
'`value` LONGTEXT NOT NULL , \n' +
'PRIMARY KEY ( `key` ) \n' +
') ENGINE = INNODB;\n' +
'START TRANSACTION;\n\n';
fs.writeSync(sqlOutput, sql);
log("done");
log('done');
var etherpadDB = mysql.createConnection({
host : settings.etherpadDB.host,
user : settings.etherpadDB.user,
password : settings.etherpadDB.password,
database : settings.etherpadDB.database,
port : settings.etherpadDB.port
const etherpadDB = mysql.createConnection({
host: settings.etherpadDB.host,
user: settings.etherpadDB.user,
password: settings.etherpadDB.password,
database: settings.etherpadDB.database,
port: settings.etherpadDB.port,
});
//get the timestamp once
var timestamp = Date.now();
// get the timestamp once
const timestamp = Date.now();
var padIDs;
let padIDs;
async.series([
//get all padids out of the database...
function(callback)
{
log("get all padIds out of the database...");
// get all padids out of the database...
function (callback) {
log('get all padIds out of the database...');
etherpadDB.query("SELECT ID FROM PAD_META", [], function(err, _padIDs)
{
etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => {
padIDs = _padIDs;
callback(err);
});
},
function(callback)
{
log("done");
function (callback) {
log('done');
//create a queue with a concurrency 100
var queue = async.queue(function (padId, callback)
{
convertPad(padId, function(err)
{
// create a queue with a concurrency 100
const queue = async.queue((padId, callback) => {
convertPad(padId, (err) => {
incrementPadStats();
callback(err);
});
}, 100);
//set the step callback as the queue callback
// set the step callback as the queue callback
queue.drain = callback;
//add the padids to the worker queue
for(var i=0,length=padIDs.length;i<length;i++)
{
// add the padids to the worker queue
for (let i = 0, length = padIDs.length; i < length; i++) {
queue.push(padIDs[i].ID);
}
}
], function(err)
{
if(err) throw err;
},
], (err) => {
if (err) throw err;
//write the groups
var sql = "";
for(var proID in proID2groupID)
{
var groupID = proID2groupID[proID];
var subdomain = proID2subdomain[proID];
// write the groups
let sql = '';
for (const proID in proID2groupID) {
const groupID = proID2groupID[proID];
const subdomain = proID2subdomain[proID];
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("group:" + groupID) + ", " + etherpadDB.escape(JSON.stringify(groups[groupID]))+ ");\n";
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("mapper2group:subdomain:" + subdomain) + ", " + etherpadDB.escape(groupID)+ ");\n";
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`;
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`;
}
//close transaction
sql+="COMMIT;";
// close transaction
sql += 'COMMIT;';
//end the sql file
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
// end the sql file
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
fs.closeSync(sqlOutput);
log("finished.");
log('finished.');
process.exit(0);
});
function log(str)
{
console.log((Date.now() - startTime)/1000 + "\t" + str);
function log(str) {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
}
var padsDone = 0;
let padsDone = 0;
function incrementPadStats()
{
function incrementPadStats() {
padsDone++;
if(padsDone%100 == 0)
{
var averageTime = Math.round(padsDone/((Date.now() - startTime)/1000));
log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s")
if (padsDone % 100 == 0) {
const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000));
log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`);
}
}
@ -130,293 +118,246 @@ var proID2groupID = {};
var proID2subdomain = {};
var groups = {};
function convertPad(padId, callback)
{
var changesets = [];
var changesetsMeta = [];
var chatMessages = [];
var authors = [];
var apool;
var subdomain;
var padmeta;
function convertPad(padId, callback) {
const changesets = [];
const changesetsMeta = [];
const chatMessages = [];
const authors = [];
let apool;
let subdomain;
let padmeta;
async.series([
//get all needed db values
function(callback)
{
// get all needed db values
function (callback) {
async.parallel([
//get the pad revisions
function(callback)
{
var sql = "SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)";
// get the pad revisions
function (callback) {
const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
//parse the pages
for(var i=0,length=results.length;i<length;i++)
{
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} catch (e) { err = e; }
}
callback(err);
});
},
//get the chat entries
function(callback)
{
var sql = "SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)";
// get the chat entries
function (callback) {
const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
//parse the pages
for(var i=0,length=results.length;i<length;i++)
{
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false);
}
}catch(e) {err = e}
} catch (e) { err = e; }
}
callback(err);
});
},
//get the pad revisions meta data
function(callback)
{
var sql = "SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)";
// get the pad revisions meta data
function (callback) {
const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
//parse the pages
for(var i=0,length=results.length;i<length;i++)
{
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} catch (e) { err = e; }
}
callback(err);
});
},
//get the attribute pool of this pad
function(callback)
{
var sql = "SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?";
// get the attribute pool of this pad
function (callback) {
const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
apool=JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
apool = JSON.parse(results[0].JSON).x;
} catch (e) { err = e; }
}
callback(err);
});
},
//get the authors informations
function(callback)
{
var sql = "SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)";
// get the authors informations
function (callback) {
const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
//parse the pages
for(var i=0, length=results.length;i<length;i++)
{
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
}catch(e) {err = e}
} catch (e) { err = e; }
}
callback(err);
});
},
//get the pad information
function(callback)
{
var sql = "SELECT JSON FROM `PAD_META` WHERE ID=?";
// get the pad information
function (callback) {
const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?';
etherpadDB.query(sql, [padId], function(err, results)
{
if(!err)
{
try
{
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
padmeta = JSON.parse(results[0].JSON).x;
}catch(e) {err = e}
} catch (e) { err = e; }
}
callback(err);
});
},
//get the subdomain
function(callback)
{
//skip if this is no proPad
if(padId.indexOf("$") == -1)
{
// get the subdomain
function (callback) {
// skip if this is no proPad
if (padId.indexOf('$') == -1) {
callback();
return;
}
//get the proID out of this padID
var proID = padId.split("$")[0];
// get the proID out of this padID
const proID = padId.split('$')[0];
var sql = "SELECT subDomain FROM pro_domains WHERE ID = ?";
const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?';
etherpadDB.query(sql, [proID], function(err, results)
{
if(!err)
{
etherpadDB.query(sql, [proID], (err, results) => {
if (!err) {
subdomain = results[0].subDomain;
}
callback(err);
});
}
},
], callback);
},
function(callback)
{
//saves all values that should be written to the database
var values = {};
function (callback) {
// saves all values that should be written to the database
const values = {};
//this is a pro pad, let's convert it to a group pad
if(padId.indexOf("$") != -1)
{
var padIdParts = padId.split("$");
var proID = padIdParts[0];
var padName = padIdParts[1];
// this is a pro pad, let's convert it to a group pad
if (padId.indexOf('$') != -1) {
const padIdParts = padId.split('$');
const proID = padIdParts[0];
const padName = padIdParts[1];
var groupID
let groupID;
//this proID is not converted so far, do it
if(proID2groupID[proID] == null)
{
groupID = "g." + randomString(16);
// this proID is not converted so far, do it
if (proID2groupID[proID] == null) {
groupID = `g.${randomString(16)}`;
//create the mappers for this new group
// create the mappers for this new group
proID2groupID[proID] = groupID;
proID2subdomain[proID] = subdomain;
groups[groupID] = {pads: {}};
}
//use the generated groupID;
// use the generated groupID;
groupID = proID2groupID[proID];
//rename the pad
padId = groupID + "$" + padName;
// rename the pad
padId = `${groupID}$${padName}`;
//set the value for this pad in the group
// set the value for this pad in the group
groups[groupID].pads[padId] = 1;
}
try
{
var newAuthorIDs = {};
var oldName2newName = {};
try {
const newAuthorIDs = {};
const oldName2newName = {};
//replace the authors with generated authors
// replace the authors with generated authors
// we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global
for(var i in apool.numToAttrib)
{
for (var i in apool.numToAttrib) {
var key = apool.numToAttrib[i][0];
var value = apool.numToAttrib[i][1];
const value = apool.numToAttrib[i][1];
//skip non authors and anonymous authors
if(key != "author" || value == "")
continue;
// skip non authors and anonymous authors
if (key != 'author' || value == '') continue;
//generate new author values
var authorID = "a." + randomString(16);
var authorColorID = authors[i].colorId || Math.floor(Math.random()*(exports.getColorPalette().length));
var authorName = authors[i].name || null;
// generate new author values
const authorID = `a.${randomString(16)}`;
const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length));
const authorName = authors[i].name || null;
//overwrite the authorID of the attribute pool
// overwrite the authorID of the attribute pool
apool.numToAttrib[i][1] = authorID;
//write the author to the database
values["globalAuthor:" + authorID] = {"colorId" : authorColorID, "name": authorName, "timestamp": timestamp};
// write the author to the database
values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp};
//save in mappers
// save in mappers
newAuthorIDs[i] = authorID;
oldName2newName[value] = authorID;
}
//save all revisions
for(var i=0;i<changesets.length;i++)
{
values["pad:" + padId + ":revs:" + i] = {changeset: changesets[i],
meta : {
author: newAuthorIDs[changesetsMeta[i].a],
timestamp: changesetsMeta[i].t,
atext: changesetsMeta[i].atext || undefined
}};
// save all revisions
for (var i = 0; i < changesets.length; i++) {
values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i],
meta: {
author: newAuthorIDs[changesetsMeta[i].a],
timestamp: changesetsMeta[i].t,
atext: changesetsMeta[i].atext || undefined,
}};
}
//save all chat messages
for(var i=0;i<chatMessages.length;i++)
{
values["pad:" + padId + ":chat:" + i] = {"text": chatMessages[i].lineText,
"userId": oldName2newName[chatMessages[i].userId],
"time": chatMessages[i].time}
// save all chat messages
for (var i = 0; i < chatMessages.length; i++) {
values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText,
userId: oldName2newName[chatMessages[i].userId],
time: chatMessages[i].time};
}
//generate the latest atext
var fullAPool = (new AttributePool()).fromJsonable(apool);
var keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
var atext = changesetsMeta[keyRev].atext;
var curRev = keyRev;
while (curRev < padmeta.head)
{
// generate the latest atext
const fullAPool = (new AttributePool()).fromJsonable(apool);
const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
let atext = changesetsMeta[keyRev].atext;
let curRev = keyRev;
while (curRev < padmeta.head) {
curRev++;
var changeset = changesets[curRev];
const changeset = changesets[curRev];
atext = Changeset.applyToAText(changeset, atext, fullAPool);
}
values["pad:" + padId] = {atext: atext,
pool: apool,
head: padmeta.head,
chatHead: padmeta.numChatMessages }
}
catch(e)
{
console.error("Error while converting pad " + padId + ", pad skipped");
values[`pad:${padId}`] = {atext,
pool: apool,
head: padmeta.head,
chatHead: padmeta.numChatMessages};
} catch (e) {
console.error(`Error while converting pad ${padId}, pad skipped`);
console.error(e.stack ? e.stack : JSON.stringify(e));
callback();
return;
}
var sql = "";
for(var key in values)
{
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape(key) + ", " + etherpadDB.escape(JSON.stringify(values[key]))+ ");\n";
let sql = '';
for (var key in values) {
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`;
}
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
callback();
}
},
], callback);
}
@ -425,29 +366,26 @@ function convertPad(padId, callback)
* The offsets describes the length of a unit in the page, the data are
* all values behind each other
*/
function parsePage(array, pageStart, offsets, data, json)
{
var start = 0;
var lengths = offsets.split(",");
function parsePage(array, pageStart, offsets, data, json) {
let start = 0;
const lengths = offsets.split(',');
for(var i=0;i<lengths.length;i++)
{
var unitLength = lengths[i];
for (let i = 0; i < lengths.length; i++) {
let unitLength = lengths[i];
//skip empty units
if(unitLength == "")
continue;
// skip empty units
if (unitLength == '') continue;
//parse the number
// parse the number
unitLength = Number(unitLength);
//cut the unit out of data
var unit = data.substr(start, unitLength);
// cut the unit out of data
const unit = data.substr(start, unitLength);
//put it into the array
// put it into the array
array[pageStart + i] = json ? JSON.parse(unit) : unit;
//update start
start+=unitLength;
// update start
start += unitLength;
}
}

View File

@ -2,7 +2,7 @@
* A tool for generating a test user session which can be used for debugging configs
* that require sessions.
*/
const m = (f) => __dirname + '/../' + f;
const m = (f) => `${__dirname}/../${f}`;
const fs = require('fs');
const path = require('path');
@ -12,10 +12,10 @@ const settings = require(m('src/node/utils/Settings'));
const supertest = require(m('src/node_modules/supertest'));
(async () => {
const api = supertest('http://'+settings.ip+':'+settings.port);
const api = supertest(`http://${settings.ip}:${settings.port}`);
const filePath = path.join(__dirname, '../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
let res;
@ -43,5 +43,5 @@ const supertest = require(m('src/node_modules/supertest'));
res = await api.post(uri('createSession', {apikey, groupID, authorID, validUntil}));
if (res.body.code === 1) throw new Error(`Error creating session: ${res.body}`);
console.log('Session made: ====> create a cookie named sessionID and set the value to',
res.body.data.sessionID);
res.body.data.sessionID);
})();

View File

@ -3,6 +3,9 @@
# Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/..
# Source constants and usefull functions
. bin/functions.sh
# Prepare the environment
bin/installDeps.sh || exit 1
@ -12,4 +15,4 @@ echo "Open 'chrome://inspect' on Chrome to start debugging."
# Use 0.0.0.0 to allow external connections to the debugger
# (ex: running Etherpad on a docker container). Use default port # (9229)
node --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@"
node $(compute_node_args) --inspect=0.0.0.0:9229 node_modules/ep_etherpad-lite/node/server.js "$@"

View File

@ -4,48 +4,48 @@
*/
const request = require('../src/node_modules/request');
const settings = require(__dirname+'/../tests/container/loadSettings').loadSettings();
const supertest = require(__dirname+'/../src/node_modules/supertest');
const api = supertest('http://'+settings.ip+":"+settings.port);
const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings();
const supertest = require(`${__dirname}/../src/node_modules/supertest`);
const api = supertest(`http://${settings.ip}:${settings.port}`);
const path = require('path');
const fs = require('fs');
// get the API Key
var filePath = path.join(__dirname, '../APIKEY.txt');
var apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
const filePath = path.join(__dirname, '../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
// Set apiVersion to base value, we change this later.
var apiVersion = 1;
var guids;
let apiVersion = 1;
let guids;
// Update the apiVersion
api.get('/api/')
.expect(function(res){
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error("No version set in API");
return;
})
.then(function(){
let guri = '/api/'+apiVersion+'/listAllGroups?apikey='+apikey;
api.get(guri)
.then(function(res){
guids = res.body.data.groupIDs;
guids.forEach(function(groupID){
let luri = '/api/'+apiVersion+'/listSessionsOfGroup?apikey='+apikey + "&groupID="+groupID;
api.get(luri)
.then(function(res){
if(res.body.data){
Object.keys(res.body.data).forEach(function(sessionID){
if(sessionID){
console.log("Deleting", sessionID);
let duri = '/api/'+apiVersion+'/deleteSession?apikey='+apikey + "&sessionID="+sessionID;
api.post(duri); // deletes
}
})
}else{
// no session in this group.
}
})
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
return;
})
})
})
.then(() => {
const guri = `/api/${apiVersion}/listAllGroups?apikey=${apikey}`;
api.get(guri)
.then((res) => {
guids = res.body.data.groupIDs;
guids.forEach((groupID) => {
const luri = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`;
api.get(luri)
.then((res) => {
if (res.body.data) {
Object.keys(res.body.data).forEach((sessionID) => {
if (sessionID) {
console.log('Deleting', sessionID);
const duri = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`;
api.post(duri); // deletes
}
});
} else {
// no session in this group.
}
});
});
});
});

View File

@ -4,47 +4,45 @@
*/
const request = require('../src/node_modules/request');
const settings = require(__dirname+'/../tests/container/loadSettings').loadSettings();
const supertest = require(__dirname+'/../src/node_modules/supertest');
const api = supertest('http://'+settings.ip+":"+settings.port);
const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings();
const supertest = require(`${__dirname}/../src/node_modules/supertest`);
const api = supertest(`http://${settings.ip}:${settings.port}`);
const path = require('path');
const fs = require('fs');
if (process.argv.length != 3) {
console.error("Use: node deletePad.js $PADID");
console.error('Use: node deletePad.js $PADID');
process.exit(1);
}
// get the padID
let padId = process.argv[2];
const padId = process.argv[2];
// get the API Key
var filePath = path.join(__dirname, '../APIKEY.txt');
var apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
const filePath = path.join(__dirname, '../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
// Set apiVersion to base value, we change this later.
var apiVersion = 1;
let apiVersion = 1;
// Update the apiVersion
api.get('/api/')
.expect(function(res){
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error("No version set in API");
return;
})
.end(function(err, res){
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
return;
})
.end((err, res) => {
// Now we know the latest API version, let's delete pad
var uri = '/api/'+apiVersion+'/deletePad?apikey='+apikey+'&padID='+padId;
api.post(uri)
.expect(function(res){
if (res.body.code === 1){
console.error("Error deleting pad", res.body);
}else{
console.log("Deleted pad", res.body);
}
return;
})
.end(function(){})
});
const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`;
api.post(uri)
.expect((res) => {
if (res.body.code === 1) {
console.error('Error deleting pad', res.body);
} else {
console.log('Deleted pad', res.body);
}
return;
})
.end(() => {});
});
// end

View File

@ -20,19 +20,19 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var marked = require('marked');
var fs = require('fs');
var path = require('path');
const marked = require('marked');
const fs = require('fs');
const path = require('path');
// parse the args.
// Don't use nopt or whatever for this. It's simple enough.
var args = process.argv.slice(2);
var format = 'json';
var template = null;
var inputFile = null;
const args = process.argv.slice(2);
let format = 'json';
let template = null;
let inputFile = null;
args.forEach(function (arg) {
args.forEach((arg) => {
if (!arg.match(/^\-\-/)) {
inputFile = arg;
} else if (arg.match(/^\-\-format=/)) {
@ -40,7 +40,7 @@ args.forEach(function (arg) {
} else if (arg.match(/^\-\-template=/)) {
template = arg.replace(/^\-\-template=/, '');
}
})
});
if (!inputFile) {
@ -49,25 +49,25 @@ if (!inputFile) {
console.error('Input file = %s', inputFile);
fs.readFile(inputFile, 'utf8', function(er, input) {
fs.readFile(inputFile, 'utf8', (er, input) => {
if (er) throw er;
// process the input for @include lines
processIncludes(inputFile, input, next);
});
var includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi;
var includeData = {};
const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi;
const includeData = {};
function processIncludes(inputFile, input, cb) {
var includes = input.match(includeExpr);
const includes = input.match(includeExpr);
if (includes === null) return cb(null, input);
var errState = null;
let errState = null;
console.error(includes);
var incCount = includes.length;
let incCount = includes.length;
if (incCount === 0) cb(null, input);
includes.forEach(function(include) {
var fname = include.replace(/^@include\s+/, '');
includes.forEach((include) => {
let fname = include.replace(/^@include\s+/, '');
if (!fname.match(/\.md$/)) fname += '.md';
if (includeData.hasOwnProperty(fname)) {
@ -78,11 +78,11 @@ function processIncludes(inputFile, input, cb) {
}
}
var fullFname = path.resolve(path.dirname(inputFile), fname);
fs.readFile(fullFname, 'utf8', function(er, inc) {
const fullFname = path.resolve(path.dirname(inputFile), fname);
fs.readFile(fullFname, 'utf8', (er, inc) => {
if (errState) return;
if (er) return cb(errState = er);
processIncludes(fullFname, inc, function(er, inc) {
processIncludes(fullFname, inc, (er, inc) => {
if (errState) return;
if (er) return cb(errState = er);
incCount--;
@ -101,20 +101,20 @@ function next(er, input) {
if (er) throw er;
switch (format) {
case 'json':
require('./json.js')(input, inputFile, function(er, obj) {
require('./json.js')(input, inputFile, (er, obj) => {
console.log(JSON.stringify(obj, null, 2));
if (er) throw er;
});
break;
case 'html':
require('./html.js')(input, inputFile, template, function(er, html) {
require('./html.js')(input, inputFile, template, (er, html) => {
if (er) throw er;
console.log(html);
});
break;
default:
throw new Error('Invalid format: ' + format);
throw new Error(`Invalid format: ${format}`);
}
}

View File

@ -19,15 +19,15 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var fs = require('fs');
var marked = require('marked');
var path = require('path');
const fs = require('fs');
const marked = require('marked');
const path = require('path');
module.exports = toHTML;
function toHTML(input, filename, template, cb) {
var lexed = marked.lexer(input);
fs.readFile(template, 'utf8', function(er, template) {
const lexed = marked.lexer(input);
fs.readFile(template, 'utf8', (er, template) => {
if (er) return cb(er);
render(lexed, filename, template, cb);
});
@ -35,7 +35,7 @@ function toHTML(input, filename, template, cb) {
function render(lexed, filename, template, cb) {
// get the section
var section = getSection(lexed);
const section = getSection(lexed);
filename = path.basename(filename, '.md');
@ -43,7 +43,7 @@ function render(lexed, filename, template, cb) {
// generate the table of contents.
// this mutates the lexed contents in-place.
buildToc(lexed, filename, function(er, toc) {
buildToc(lexed, filename, (er, toc) => {
if (er) return cb(er);
template = template.replace(/__FILENAME__/g, filename);
@ -63,11 +63,11 @@ function render(lexed, filename, template, cb) {
// just update the list item text in-place.
// lists that come right after a heading are what we're after.
function parseLists(input) {
var state = null;
var depth = 0;
var output = [];
let state = null;
let depth = 0;
const output = [];
output.links = input.links;
input.forEach(function(tok) {
input.forEach((tok) => {
if (state === null) {
if (tok.type === 'heading') {
state = 'AFTERHEADING';
@ -79,7 +79,7 @@ function parseLists(input) {
if (tok.type === 'list_start') {
state = 'LIST';
if (depth === 0) {
output.push({ type:'html', text: '<div class="signature">' });
output.push({type: 'html', text: '<div class="signature">'});
}
depth++;
output.push(tok);
@ -99,7 +99,7 @@ function parseLists(input) {
depth--;
if (depth === 0) {
state = null;
output.push({ type:'html', text: '</div>' });
output.push({type: 'html', text: '</div>'});
}
output.push(tok);
return;
@ -117,16 +117,16 @@ function parseLists(input) {
function parseListItem(text) {
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>');
//XXX maybe put more stuff here?
// XXX maybe put more stuff here?
return text;
}
// section is just the first heading
function getSection(lexed) {
var section = '';
for (var i = 0, l = lexed.length; i < l; i++) {
var tok = lexed[i];
const section = '';
for (let i = 0, l = lexed.length; i < l; i++) {
const tok = lexed[i];
if (tok.type === 'heading') return tok.text;
}
return '';
@ -134,40 +134,39 @@ function getSection(lexed) {
function buildToc(lexed, filename, cb) {
var indent = 0;
var toc = [];
var depth = 0;
lexed.forEach(function(tok) {
const indent = 0;
let toc = [];
let depth = 0;
lexed.forEach((tok) => {
if (tok.type !== 'heading') return;
if (tok.depth - depth > 1) {
return cb(new Error('Inappropriate heading level\n' +
JSON.stringify(tok)));
return cb(new Error(`Inappropriate heading level\n${
JSON.stringify(tok)}`));
}
depth = tok.depth;
var id = getId(filename + '_' + tok.text.trim());
toc.push(new Array((depth - 1) * 2 + 1).join(' ') +
'* <a href="#' + id + '">' +
tok.text + '</a>');
tok.text += '<span><a class="mark" href="#' + id + '" ' +
'id="' + id + '">#</a></span>';
const id = getId(`${filename}_${tok.text.trim()}`);
toc.push(`${new Array((depth - 1) * 2 + 1).join(' ')
}* <a href="#${id}">${
tok.text}</a>`);
tok.text += `<span><a class="mark" href="#${id}" ` +
`id="${id}">#</a></span>`;
});
toc = marked.parse(toc.join('\n'));
cb(null, toc);
}
var idCounters = {};
const idCounters = {};
function getId(text) {
text = text.toLowerCase();
text = text.replace(/[^a-z0-9]+/g, '_');
text = text.replace(/^_+|_+$/, '');
text = text.replace(/^([^a-z])/, '_$1');
if (idCounters.hasOwnProperty(text)) {
text += '_' + (++idCounters[text]);
text += `_${++idCounters[text]}`;
} else {
idCounters[text] = 0;
}
return text;
}

View File

@ -24,24 +24,24 @@ module.exports = doJSON;
// Take the lexed input, and return a JSON-encoded object
// A module looks like this: https://gist.github.com/1777387
var marked = require('marked');
const marked = require('marked');
function doJSON(input, filename, cb) {
var root = {source: filename};
var stack = [root];
var depth = 0;
var current = root;
var state = null;
var lexed = marked.lexer(input);
lexed.forEach(function (tok) {
var type = tok.type;
var text = tok.text;
const root = {source: filename};
const stack = [root];
let depth = 0;
let current = root;
let state = null;
const lexed = marked.lexer(input);
lexed.forEach((tok) => {
const type = tok.type;
let text = tok.text;
// <!-- type = module -->
// This is for cases where the markdown semantic structure is lacking.
if (type === 'paragraph' || type === 'html') {
var metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g;
text = text.replace(metaExpr, function(_0, k, v) {
const metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g;
text = text.replace(metaExpr, (_0, k, v) => {
current[k.trim()] = v.trim();
return '';
});
@ -52,8 +52,8 @@ function doJSON(input, filename, cb) {
if (type === 'heading' &&
!text.trim().match(/^example/i)) {
if (tok.depth - depth > 1) {
return cb(new Error('Inappropriate heading level\n'+
JSON.stringify(tok)));
return cb(new Error(`Inappropriate heading level\n${
JSON.stringify(tok)}`));
}
// Sometimes we have two headings with a single
@ -61,7 +61,7 @@ function doJSON(input, filename, cb) {
if (current &&
state === 'AFTERHEADING' &&
depth === tok.depth) {
var clone = current;
const clone = current;
current = newSection(tok);
current.clone = clone;
// don't keep it around on the stack.
@ -75,7 +75,7 @@ function doJSON(input, filename, cb) {
// root is always considered the level=0 section,
// and the lowest heading is 1, so this should always
// result in having a valid parent node.
var d = tok.depth;
let d = tok.depth;
while (d <= depth) {
finishSection(stack.pop(), stack[stack.length - 1]);
d++;
@ -98,7 +98,7 @@ function doJSON(input, filename, cb) {
//
// If one of these isn't found, then anything that comes between
// here and the next heading should be parsed as the desc.
var stability
let stability;
if (state === 'AFTERHEADING') {
if (type === 'code' &&
(stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) {
@ -138,7 +138,6 @@ function doJSON(input, filename, cb) {
current.desc = current.desc || [];
current.desc.push(tok);
});
// finish any sections left open
@ -146,7 +145,7 @@ function doJSON(input, filename, cb) {
finishSection(current, stack[stack.length - 1]);
}
return cb(null, root)
return cb(null, root);
}
@ -193,14 +192,14 @@ function doJSON(input, filename, cb) {
// default: 'false' } ] } ]
function processList(section) {
var list = section.list;
var values = [];
var current;
var stack = [];
const list = section.list;
const values = [];
let current;
const stack = [];
// for now, *just* build the hierarchical list
list.forEach(function(tok) {
var type = tok.type;
list.forEach((tok) => {
const type = tok.type;
if (type === 'space') return;
if (type === 'list_item_start') {
if (!current) {
@ -217,26 +216,26 @@ function processList(section) {
return;
} else if (type === 'list_item_end') {
if (!current) {
throw new Error('invalid list - end without current item\n' +
JSON.stringify(tok) + '\n' +
JSON.stringify(list));
throw new Error(`invalid list - end without current item\n${
JSON.stringify(tok)}\n${
JSON.stringify(list)}`);
}
current = stack.pop();
} else if (type === 'text') {
if (!current) {
throw new Error('invalid list - text without current item\n' +
JSON.stringify(tok) + '\n' +
JSON.stringify(list));
throw new Error(`invalid list - text without current item\n${
JSON.stringify(tok)}\n${
JSON.stringify(list)}`);
}
current.textRaw = current.textRaw || '';
current.textRaw += tok.text + ' ';
current.textRaw += `${tok.text} `;
}
});
// shove the name in there for properties, since they are always
// just going to be the value etc.
if (section.type === 'property' && values[0]) {
values[0].textRaw = '`' + section.name + '` ' + values[0].textRaw;
values[0].textRaw = `\`${section.name}\` ${values[0].textRaw}`;
}
// now pull the actual values out of the text bits.
@ -252,9 +251,9 @@ function processList(section) {
// each item is an argument, unless the name is 'return',
// in which case it's the return value.
section.signatures = section.signatures || [];
var sig = {}
var sig = {};
section.signatures.push(sig);
sig.params = values.filter(function(v) {
sig.params = values.filter((v) => {
if (v.name === 'return') {
sig.return = v;
return false;
@ -271,7 +270,7 @@ function processList(section) {
delete value.name;
section.typeof = value.type;
delete value.type;
Object.keys(value).forEach(function(k) {
Object.keys(value).forEach((k) => {
section[k] = value[k];
});
break;
@ -289,36 +288,36 @@ function processList(section) {
// textRaw = "someobject.someMethod(a, [b=100], [c])"
function parseSignature(text, sig) {
var params = text.match(paramExpr);
let params = text.match(paramExpr);
if (!params) return;
params = params[1];
// the ] is irrelevant. [ indicates optionalness.
params = params.replace(/\]/g, '');
params = params.split(/,/)
params.forEach(function(p, i, _) {
params = params.split(/,/);
params.forEach((p, i, _) => {
p = p.trim();
if (!p) return;
var param = sig.params[i];
var optional = false;
var def;
let param = sig.params[i];
let optional = false;
let def;
// [foo] -> optional
if (p.charAt(0) === '[') {
optional = true;
p = p.substr(1);
}
var eq = p.indexOf('=');
const eq = p.indexOf('=');
if (eq !== -1) {
def = p.substr(eq + 1);
p = p.substr(0, eq);
}
if (!param) {
param = sig.params[i] = { name: p };
param = sig.params[i] = {name: p};
}
// at this point, the name should match.
if (p !== param.name) {
console.error('Warning: invalid param "%s"', p);
console.error(' > ' + JSON.stringify(param));
console.error(' > ' + text);
console.error(` > ${JSON.stringify(param)}`);
console.error(` > ${text}`);
}
if (optional) param.optional = true;
if (def !== undefined) param.default = def;
@ -332,18 +331,18 @@ function parseListItem(item) {
// the goal here is to find the name, type, default, and optional.
// anything left over is 'desc'
var text = item.textRaw.trim();
let text = item.textRaw.trim();
// text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, '');
text = text.replace(/^, /, '').trim();
var retExpr = /^returns?\s*:?\s*/i;
var ret = text.match(retExpr);
const retExpr = /^returns?\s*:?\s*/i;
const ret = text.match(retExpr);
if (ret) {
item.name = 'return';
text = text.replace(retExpr, '');
} else {
var nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
var name = text.match(nameExpr);
const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/;
const name = text.match(nameExpr);
if (name) {
item.name = name[1];
text = text.replace(nameExpr, '');
@ -351,24 +350,24 @@ function parseListItem(item) {
}
text = text.trim();
var defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i;
var def = text.match(defaultExpr);
const defaultExpr = /\(default\s*[:=]?\s*['"`]?([^, '"`]*)['"`]?\)/i;
const def = text.match(defaultExpr);
if (def) {
item.default = def[1];
text = text.replace(defaultExpr, '');
}
text = text.trim();
var typeExpr = /^\{([^\}]+)\}/;
var type = text.match(typeExpr);
const typeExpr = /^\{([^\}]+)\}/;
const type = text.match(typeExpr);
if (type) {
item.type = type[1];
text = text.replace(typeExpr, '');
}
text = text.trim();
var optExpr = /^Optional\.|(?:, )?Optional$/;
var optional = text.match(optExpr);
const optExpr = /^Optional\.|(?:, )?Optional$/;
const optional = text.match(optExpr);
if (optional) {
item.optional = true;
text = text.replace(optExpr, '');
@ -382,9 +381,9 @@ function parseListItem(item) {
function finishSection(section, parent) {
if (!section || !parent) {
throw new Error('Invalid finishSection call\n'+
JSON.stringify(section) + '\n' +
JSON.stringify(parent));
throw new Error(`Invalid finishSection call\n${
JSON.stringify(section)}\n${
JSON.stringify(parent)}`);
}
if (!section.type) {
@ -394,7 +393,7 @@ function finishSection(section, parent) {
}
section.displayName = section.name;
section.name = section.name.toLowerCase()
.trim().replace(/\s+/g, '_');
.trim().replace(/\s+/g, '_');
}
if (section.desc && Array.isArray(section.desc)) {
@ -411,10 +410,10 @@ function finishSection(section, parent) {
// Merge them into the parent.
if (section.type === 'class' && section.ctors) {
section.signatures = section.signatures || [];
var sigs = section.signatures;
section.ctors.forEach(function(ctor) {
const sigs = section.signatures;
section.ctors.forEach((ctor) => {
ctor.signatures = ctor.signatures || [{}];
ctor.signatures.forEach(function(sig) {
ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc;
});
sigs.push.apply(sigs, ctor.signatures);
@ -425,7 +424,7 @@ function finishSection(section, parent) {
// properties are a bit special.
// their "type" is the type of object, not "property"
if (section.properties) {
section.properties.forEach(function (p) {
section.properties.forEach((p) => {
if (p.typeof) p.type = p.typeof;
else delete p.type;
delete p.typeof;
@ -434,27 +433,27 @@ function finishSection(section, parent) {
// handle clones
if (section.clone) {
var clone = section.clone;
const clone = section.clone;
delete section.clone;
delete clone.clone;
deepCopy(section, clone);
finishSection(clone, parent);
}
var plur;
let plur;
if (section.type.slice(-1) === 's') {
plur = section.type + 'es';
plur = `${section.type}es`;
} else if (section.type.slice(-1) === 'y') {
plur = section.type.replace(/y$/, 'ies');
} else {
plur = section.type + 's';
plur = `${section.type}s`;
}
// if the parent's type is 'misc', then it's just a random
// collection of stuff, like the "globals" section.
// Make the children top-level items.
if (section.type === 'misc') {
Object.keys(section).forEach(function(k) {
Object.keys(section).forEach((k) => {
switch (k) {
case 'textRaw':
case 'name':
@ -486,9 +485,7 @@ function finishSection(section, parent) {
// Not a general purpose deep copy.
// But sufficient for these basic things.
function deepCopy(src, dest) {
Object.keys(src).filter(function(k) {
return !dest.hasOwnProperty(k);
}).forEach(function(k) {
Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => {
dest[k] = deepCopy_(src[k]);
});
}
@ -497,14 +494,14 @@ function deepCopy_(src) {
if (!src) return src;
if (Array.isArray(src)) {
var c = new Array(src.length);
src.forEach(function(v, i) {
src.forEach((v, i) => {
c[i] = deepCopy_(v);
});
return c;
}
if (typeof src === 'object') {
var c = {};
Object.keys(src).forEach(function(k) {
Object.keys(src).forEach((k) => {
c[k] = deepCopy_(src[k]);
});
return c;
@ -514,21 +511,21 @@ function deepCopy_(src) {
// these parse out the contents of an H# tag
var eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
var classExpr = /^Class:\s*([^ ]+).*?$/i;
var propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i;
var braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i;
var classMethExpr =
const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
const classExpr = /^Class:\s*([^ ]+).*?$/i;
const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i;
const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i;
const classMethExpr =
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
var methExpr =
const methExpr =
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i;
var newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/;
var paramExpr = /\((.*)\);?$/;
function newSection(tok) {
var section = {};
const section = {};
// infer the type from the text.
var text = section.textRaw = tok.text;
const text = section.textRaw = tok.text;
if (text.match(eventExpr)) {
section.type = 'event';
section.name = text.replace(eventExpr, '$1');

View File

@ -5,60 +5,60 @@
*/
if (process.argv.length != 3) {
console.error("Use: node extractPadData.js $PADID");
console.error('Use: node extractPadData.js $PADID');
process.exit(1);
}
// get the padID
let padId = process.argv[2];
const padId = process.argv[2];
let npm = require('../src/node_modules/npm');
const npm = require('../src/node_modules/npm');
npm.load({}, async function(er) {
npm.load({}, async (er) => {
if (er) {
console.error("Could not load NPM: " + er)
console.error(`Could not load NPM: ${er}`);
process.exit(1);
}
try {
// initialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load extra modules
let dirtyDB = require('../src/node_modules/dirty');
let padManager = require('../src/node/db/PadManager');
let util = require('util');
const dirtyDB = require('../src/node_modules/dirty');
const padManager = require('../src/node/db/PadManager');
const util = require('util');
// initialize output database
let dirty = dirtyDB(padId + '.db');
const 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));
const wrapped = db.db.db.wrappedDB;
const get = util.promisify(wrapped.get.bind(wrapped));
const set = util.promisify(dirty.set.bind(dirty));
// array in which required key values will be accumulated
let neededDBValues = ['pad:' + padId];
const neededDBValues = [`pad:${padId}`];
// get the actual pad object
let pad = await padManager.getPad(padId);
const pad = await padManager.getPad(padId);
// add all authors
neededDBValues.push(...pad.getAllAuthors().map(author => 'globalAuthor:' + author));
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);
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push('pad:' + padId + ':chat:' + chat);
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
for (let dbkey of neededDBValues) {
for (const dbkey of neededDBValues) {
let dbvalue = await get(dbkey);
if (dbvalue && typeof dbvalue !== 'object') {
dbvalue = JSON.parse(dbvalue);

View File

@ -12,6 +12,9 @@ set -eu
# source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source constants and usefull functions
. ${DIR}/../bin/functions.sh
echo "Running directly, without checking/installing dependencies"
# move to the base Etherpad directory. This will be necessary until Etherpad
@ -19,4 +22,4 @@ echo "Running directly, without checking/installing dependencies"
cd "${DIR}/.."
# run Etherpad main class
node "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "${@}"
node $(compute_node_args) "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "$@"

74
bin/functions.sh Normal file
View File

@ -0,0 +1,74 @@
# minimum required node version
REQUIRED_NODE_MAJOR=10
REQUIRED_NODE_MINOR=13
# minimum required npm version
REQUIRED_NPM_MAJOR=5
REQUIRED_NPM_MINOR=5
pecho() { printf %s\\n "$*"; }
log() { pecho "$@"; }
error() { log "ERROR: $@" >&2; }
fatal() { error "$@"; exit 1; }
is_cmd() { command -v "$@" >/dev/null 2>&1; }
get_program_version() {
PROGRAM="$1"
KIND="${2:-full}"
PROGRAM_VERSION_STRING=$($PROGRAM --version)
PROGRAM_VERSION_STRING=${PROGRAM_VERSION_STRING#"v"}
DETECTED_MAJOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 1)
[ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM major version from version string \"$PROGRAM_VERSION_STRING\""
case "$DETECTED_MAJOR" in
''|*[!0-9]*)
fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\""
;;
esac
DETECTED_MINOR=$(pecho "$PROGRAM_VERSION_STRING" | cut -s -d "." -f 2)
[ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM minor version from version string \"$PROGRAM_VERSION_STRING\""
case "$DETECTED_MINOR" in
''|*[!0-9]*)
fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\""
esac
case $KIND in
major)
echo $DETECTED_MAJOR
exit;;
minor)
echo $DETECTED_MINOR
exit;;
*)
echo $DETECTED_MAJOR.$DETECTED_MINOR
exit;;
esac
echo $VERSION
}
compute_node_args() {
ARGS=""
NODE_MAJOR=$(get_program_version "node" "major")
[ "$NODE_MAJOR" -eq "10" ] && ARGS="$ARGS --experimental-worker"
echo $ARGS
}
require_minimal_version() {
PROGRAM_LABEL="$1"
VERSION="$2"
REQUIRED_MAJOR="$3"
REQUIRED_MINOR="$4"
VERSION_MAJOR=$(pecho "$VERSION" | cut -s -d "." -f 1)
VERSION_MINOR=$(pecho "$VERSION" | cut -s -d "." -f 2)
[ "$VERSION_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$VERSION_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$VERSION_MINOR" -ge "$REQUIRED_MINOR" ]) \
|| fatal "Your $PROGRAM_LABEL version \"$VERSION_MAJOR.$VERSION_MINOR\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required."
}

View File

@ -1,94 +1,87 @@
var startTime = Date.now();
const startTime = Date.now();
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
const fs = require('fs');
var fs = require("fs");
const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2');
const settings = require('ep_etherpad-lite/node/utils/Settings');
const log4js = require('ep_etherpad-lite/node_modules/log4js');
var ueberDB = require("ep_etherpad-lite/node_modules/ueberdb2");
var settings = require("ep_etherpad-lite/node/utils/Settings");
var log4js = require('ep_etherpad-lite/node_modules/log4js');
var dbWrapperSettings = {
const dbWrapperSettings = {
cache: 0,
writeInterval: 100,
json: false // data is already json encoded
json: false, // data is already json encoded
};
var db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger("ueberDB"));
const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB'));
var sqlFile = process.argv[2];
const sqlFile = process.argv[2];
//stop if the settings file is not set
if(!sqlFile)
{
console.error("Use: node importSqlFile.js $SQLFILE");
// stop if the settings file is not set
if (!sqlFile) {
console.error('Use: node importSqlFile.js $SQLFILE');
process.exit(1);
}
log("initializing db");
db.init(function(err)
{
//there was an error while initializing the database, output it and stop
if(err)
{
console.error("ERROR: Problem while initializing the database");
log('initializing db');
db.init((err) => {
// there was an error while initializing the database, output it and stop
if (err) {
console.error('ERROR: Problem while initializing the database');
console.error(err.stack ? err.stack : err);
process.exit(1);
}
else
{
log("done");
} else {
log('done');
log("open output file...");
var lines = fs.readFileSync(sqlFile, 'utf8').split("\n");
log('open output file...');
const lines = fs.readFileSync(sqlFile, 'utf8').split('\n');
var count = lines.length;
var keyNo = 0;
const count = lines.length;
let keyNo = 0;
process.stdout.write("Start importing " + count + " keys...\n");
lines.forEach(function(l) {
if (l.substr(0, 27) == "REPLACE INTO store VALUES (") {
var pos = l.indexOf("', '");
var key = l.substr(28, pos - 28);
var value = l.substr(pos + 3);
process.stdout.write(`Start importing ${count} keys...\n`);
lines.forEach((l) => {
if (l.substr(0, 27) == 'REPLACE INTO store VALUES (') {
const pos = l.indexOf("', '");
const key = l.substr(28, pos - 28);
let value = l.substr(pos + 3);
value = value.substr(0, value.length - 2);
console.log("key: " + key + " val: " + value);
console.log("unval: " + unescape(value));
console.log(`key: ${key} val: ${value}`);
console.log(`unval: ${unescape(value)}`);
db.set(key, unescape(value), null);
keyNo++;
if (keyNo % 1000 == 0) {
process.stdout.write(" " + keyNo + "/" + count + "\n");
process.stdout.write(` ${keyNo}/${count}\n`);
}
}
});
process.stdout.write("\n");
process.stdout.write("done. waiting for db to finish transaction. depended on dbms this may take some time...\n");
process.stdout.write('\n');
process.stdout.write('done. waiting for db to finish transaction. depended on dbms this may take some time...\n');
db.doShutdown(function() {
log("finished, imported " + keyNo + " keys.");
db.doShutdown(() => {
log(`finished, imported ${keyNo} keys.`);
process.exit(0);
});
}
});
});
function log(str)
{
console.log((Date.now() - startTime)/1000 + "\t" + str);
function log(str) {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
}
unescape = function(val) {
unescape = function (val) {
// value is a string
if (val.substr(0, 1) == "'") {
val = val.substr(0, val.length - 1).substr(1);
return val.replace(/\\[0nrbtZ\\'"]/g, function(s) {
switch(s) {
case "\\0": return "\0";
case "\\n": return "\n";
case "\\r": return "\r";
case "\\b": return "\b";
case "\\t": return "\t";
case "\\Z": return "\x1a";
return val.replace(/\\[0nrbtZ\\'"]/g, (s) => {
switch (s) {
case '\\0': return '\0';
case '\\n': return '\n';
case '\\r': return '\r';
case '\\b': return '\b';
case '\\t': return '\t';
case '\\Z': return '\x1a';
default: return s.substr(1);
}
});

View File

@ -1,52 +1,11 @@
#!/bin/sh
# minimum required node version
REQUIRED_NODE_MAJOR=10
REQUIRED_NODE_MINOR=13
# minimum required npm version
REQUIRED_NPM_MAJOR=5
REQUIRED_NPM_MINOR=5
pecho() { printf %s\\n "$*"; }
log() { pecho "$@"; }
error() { log "ERROR: $@" >&2; }
fatal() { error "$@"; exit 1; }
is_cmd() { command -v "$@" >/dev/null 2>&1; }
require_minimal_version() {
PROGRAM_LABEL="$1"
VERSION_STRING="$2"
REQUIRED_MAJOR="$3"
REQUIRED_MINOR="$4"
# Flag -s (--only-delimited on GNU cut) ensures no string is returned
# when there is no match
DETECTED_MAJOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 1)
DETECTED_MINOR=$(pecho "$VERSION_STRING" | cut -s -d "." -f 2)
[ -n "$DETECTED_MAJOR" ] || fatal "Cannot extract $PROGRAM_LABEL major version from version string \"$VERSION_STRING\""
[ -n "$DETECTED_MINOR" ] || fatal "Cannot extract $PROGRAM_LABEL minor version from version string \"$VERSION_STRING\""
case "$DETECTED_MAJOR" in
''|*[!0-9]*)
fatal "$PROGRAM_LABEL major version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MAJOR\""
;;
esac
case "$DETECTED_MINOR" in
''|*[!0-9]*)
fatal "$PROGRAM_LABEL minor version from \"$VERSION_STRING\" is not a number. Detected: \"$DETECTED_MINOR\""
esac
[ "$DETECTED_MAJOR" -gt "$REQUIRED_MAJOR" ] || ([ "$DETECTED_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$DETECTED_MINOR" -ge "$REQUIRED_MINOR" ]) \
|| fatal "Your $PROGRAM_LABEL version \"$VERSION_STRING\" is too old. $PROGRAM_LABEL $REQUIRED_MAJOR.$REQUIRED_MINOR.x or higher is required."
}
# Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/..
# Source constants and usefull functions
. bin/functions.sh
# Is node installed?
# Not checking io.js, default installation creates a symbolic link to node
is_cmd node || fatal "Please install node.js ( https://nodejs.org )"
@ -55,15 +14,10 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )"
is_cmd npm || fatal "Please install npm ( https://npmjs.org )"
# Check npm version
NPM_VERSION_STRING=$(npm --version)
require_minimal_version "npm" "$NPM_VERSION_STRING" "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR"
require_minimal_version "npm" $(get_program_version "npm") "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR"
# Check node version
NODE_VERSION_STRING=$(node --version)
NODE_VERSION_STRING=${NODE_VERSION_STRING#"v"}
require_minimal_version "nodejs" "$NODE_VERSION_STRING" "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR"
require_minimal_version "nodejs" $(get_program_version "node") "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR"
# Get the name of the settings file
settings="settings.json"

View File

@ -1,6 +1,5 @@
require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
process.chdir(npm.root+'/..')
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
process.chdir(`${npm.root}/..`);
// This script requires that you have modified your settings.json file
// to work with a real database. Please make a backup of your dirty.db
@ -10,40 +9,40 @@ require("ep_etherpad-lite/node_modules/npm").load({}, function(er,npm) {
// `node --max-old-space-size=4096 bin/migrateDirtyDBtoRealDB.js`
var settings = require("ep_etherpad-lite/node/utils/Settings");
var dirty = require("../src/node_modules/dirty");
var ueberDB = require("../src/node_modules/ueberdb2");
var log4js = require("../src/node_modules/log4js");
var dbWrapperSettings = {
"cache": "0", // The cache slows things down when you're mostly writing.
"writeInterval": 0 // Write directly to the database, don't buffer
const settings = require('ep_etherpad-lite/node/utils/Settings');
let dirty = require('../src/node_modules/dirty');
const ueberDB = require('../src/node_modules/ueberdb2');
const log4js = require('../src/node_modules/log4js');
const dbWrapperSettings = {
cache: '0', // The cache slows things down when you're mostly writing.
writeInterval: 0, // Write directly to the database, don't buffer
};
var db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger("ueberDB"));
var i = 0;
var length = 0;
const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB'));
let i = 0;
let length = 0;
db.init(function() {
console.log("Waiting for dirtyDB to parse its file.");
dirty = dirty('var/dirty.db').on("load", function() {
dirty.forEach(function(){
db.init(() => {
console.log('Waiting for dirtyDB to parse its file.');
dirty = dirty('var/dirty.db').on('load', () => {
dirty.forEach(() => {
length++;
});
console.log(`Found ${length} records, processing now.`);
dirty.forEach(async function(key, value) {
let error = await db.set(key, value);
dirty.forEach(async (key, value) => {
const error = await db.set(key, value);
console.log(`Wrote record ${i}`);
i++;
if (i === length) {
console.log("finished, just clearing up for a bit...");
setTimeout(function() {
console.log('finished, just clearing up for a bit...');
setTimeout(() => {
process.exit(0);
}, 5000);
}
});
console.log("Please wait for all records to flush to database, then kill this process.");
console.log('Please wait for all records to flush to database, then kill this process.');
});
console.log("done?")
console.log('done?');
});
});

View File

@ -1,46 +1,52 @@
The files in this folder are for Plugin developers.
# Get suggestions to improve your Plugin
This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project.
```
node bin/plugins/checkPlugin.js $PLUGIN_NAME$
```
# Basic Example:
```
node bin/plugins/checkPlugin.js ep_webrtc
```
## Autofixing - will autofix any issues it can
```
node bin/plugins/checkPlugins.js ep_whatever autofix
```
## Autocommitting, push, npm minor patch and npm publish (highly dangerous)
```
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit
```
# All the plugins
Replace johnmclear with your github username
```
# Clones
cd node_modules
GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone
cd ..
# autofixes and autocommits /pushes & npm publishes
for dir in `ls node_modules`;
do
# echo $0
if [[ $dir == *"ep_"* ]]; then
if [[ $dir != "ep_etherpad-lite" ]]; then
node bin/plugins/checkPlugin.js $dir autofix autocommit
fi
fi
# echo $dir
done
```
The files in this folder are for Plugin developers.
# Get suggestions to improve your Plugin
This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project.
```
node bin/plugins/checkPlugin.js $PLUGIN_NAME$
```
# Basic Example:
```
node bin/plugins/checkPlugin.js ep_webrtc
```
## Autofixing - will autofix any issues it can
```
node bin/plugins/checkPlugins.js ep_whatever autofix
```
## Autocommitting, push, npm minor patch and npm publish (highly dangerous)
```
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit
```
# All the plugins
Replace johnmclear with your github username
```
# Clones
cd node_modules
GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone
cd ..
# autofixes and autocommits /pushes & npm publishes
for dir in `ls node_modules`;
do
# echo $0
if [[ $dir == *"ep_"* ]]; then
if [[ $dir != "ep_etherpad-lite" ]]; then
node bin/plugins/checkPlugin.js $dir autofix autocommit
fi
fi
# echo $dir
done
```
# Automating update of ether organization plugins
```
getCorePlugins.sh
updateCorePlugins.sh
```

View File

@ -1,246 +1,469 @@
// pro usage for all your plugins, replace johnmclear with your github username
/*
cd node_modules
GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=1000" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone
cd ..
for dir in `ls node_modules`;
do
# echo $0
if [[ $dir == *"ep_"* ]]; then
if [[ $dir != "ep_etherpad-lite" ]]; then
node bin/plugins/checkPlugin.js $dir autofix autocommit
fi
fi
# echo $dir
done
*/
/*
*
* Usage
*
* Normal usage: node bin/plugins/checkPlugins.js ep_whatever
* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix
* Auto commit, push and publish(to npm) * highly dangerous:
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit
*/
const fs = require("fs");
const { exec } = require("child_process");
// get plugin name & path from user input
const pluginName = process.argv[2];
const pluginPath = "node_modules/"+pluginName;
console.log("Checking the plugin: "+ pluginName)
// Should we autofix?
if (process.argv[3] && process.argv[3] === "autofix") var autoFix = true;
// Should we update files where possible?
if (process.argv[5] && process.argv[5] === "autoupdate") var autoUpdate = true;
// Should we automcommit and npm publish?!
if (process.argv[4] && process.argv[4] === "autocommit") var autoCommit = true;
if(autoCommit){
console.warn("Auto commit is enabled, I hope you know what you are doing...")
}
fs.readdir(pluginPath, function (err, rootFiles) {
//handling error
if (err) {
return console.log('Unable to scan directory: ' + err);
}
// rewriting files to lower case
var files = [];
// some files we need to know the actual file name. Not compulsory but might help in the future.
var readMeFileName;
var repository;
var hasAutofixed = false;
for (var i = 0; i < rootFiles.length; i++) {
if(rootFiles[i].toLowerCase().indexOf("readme") !== -1) readMeFileName = rootFiles[i];
files.push(rootFiles[i].toLowerCase());
}
if(files.indexOf("package.json") === -1){
console.warn("no package.json, please create");
}
if(files.indexOf("package.json") !== -1){
let packageJSON = fs.readFileSync(pluginPath+"/package.json", {encoding:'utf8', flag:'r'});
if(packageJSON.toLowerCase().indexOf("repository") === -1){
console.warn("No repository in package.json");
if(autoFix){
console.warn("Repository not detected in package.json. Please add repository section manually.")
}
}else{
// useful for creating README later.
repository = JSON.parse(packageJSON).repository.url;
}
}
if(files.indexOf("readme") === -1 && files.indexOf("readme.md") === -1){
console.warn("README.md file not found, please create");
if(autoFix){
console.log("Autofixing missing README.md file, please edit the README.md file further to include plugin specific details.");
let readme = fs.readFileSync("bin/plugins/lib/README.md", {encoding:'utf8', flag:'r'})
readme = readme.replace(/\[plugin_name\]/g, pluginName);
if(repository){
let org = repository.split("/")[3];
let name = repository.split("/")[4];
readme = readme.replace(/\[org_name\]/g, org);
readme = readme.replace(/\[repo_url\]/g, name);
fs.writeFileSync(pluginPath+"/README.md", readme);
}else{
console.warn("Unable to find repository in package.json, aborting.")
}
}
}
if(files.indexOf("readme") !== -1 && files.indexOf("readme.md") !== -1){
let readme = fs.readFileSync(pluginPath+"/"+readMeFileName, {encoding:'utf8', flag:'r'});
if(readme.toLowerCase().indexOf("license") === -1){
console.warn("No license section in README");
if(autoFix){
console.warn("Please add License section to README manually.")
}
}
}
if(files.indexOf("license") === -1 && files.indexOf("license.md") === -1){
console.warn("LICENSE.md file not found, please create");
if(autoFix){
hasAutofixed = true;
console.log("Autofixing missing LICENSE.md file, including Apache 2 license.");
exec("git config user.name", (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
let license = fs.readFileSync("bin/plugins/lib/LICENSE.md", {encoding:'utf8', flag:'r'});
license = license.replace("[yyyy]", new Date().getFullYear());
license = license.replace("[name of copyright owner]", name)
fs.writeFileSync(pluginPath+"/LICENSE.md", license);
});
}
}
var travisConfig = fs.readFileSync("bin/plugins/lib/travis.yml", {encoding:'utf8', flag:'r'});
travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName);
if(files.indexOf(".travis.yml") === -1){
console.warn(".travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.")
// TODO: Make it check version of the .travis file to see if it needs an update.
if(autoFix){
hasAutofixed = true;
console.log("Autofixing missing .travis.yml file");
fs.writeFileSync(pluginPath+"/.travis.yml", travisConfig);
console.log("Travis file created, please sign into travis and enable this repository")
}
}
if(autoFix && autoUpdate){
// checks the file versioning of .travis and updates it to the latest.
let existingConfig = fs.readFileSync(pluginPath + "/.travis.yml", {encoding:'utf8', flag:'r'});
let existingConfigLocation = existingConfig.indexOf("##ETHERPAD_TRAVIS_V=");
let existingValue = existingConfig.substr(existingConfigLocation+20, existingConfig.length);
let newConfigLocation = travisConfig.indexOf("##ETHERPAD_TRAVIS_V=");
let newValue = travisConfig.substr(newConfigLocation+20, travisConfig.length);
if(existingConfigLocation === -1){
console.warn("no previous .travis.yml version found so writing new.")
// we will write the newTravisConfig to the location.
fs.writeFileSync(pluginPath + "/.travis.yml", travisConfig);
}else{
if(newValue > existingValue){
console.log("updating .travis.yml");
fs.writeFileSync(pluginPath + "/.travis.yml", travisConfig);
hasAutofixed = true;
}
}
}
if(files.indexOf(".gitignore") === -1){
console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository.")
if(autoFix){
hasAutofixed = true;
console.log("Autofixing missing .gitignore file");
let gitignore = fs.readFileSync("bin/plugins/lib/gitignore", {encoding:'utf8', flag:'r'});
fs.writeFileSync(pluginPath+"/.gitignore", gitignore);
}
}
if(files.indexOf("locales") === -1){
console.warn("Translations not found, please create. Translation files help with Etherpad accessibility.");
}
if(files.indexOf(".ep_initialized") !== -1){
console.warn(".ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.")
if(autoFix){
hasAutofixed = true;
console.log("Autofixing incorrectly existing .ep_initialized file");
fs.unlinkSync(pluginPath+"/.ep_initialized");
}
}
if(files.indexOf("npm-debug.log") !== -1){
console.warn("npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.")
if(autoFix){
hasAutofixed = true;
console.log("Autofixing incorrectly existing npm-debug.log file");
fs.unlinkSync(pluginPath+"/npm-debug.log");
}
}
if(files.indexOf("static") !== -1){
fs.readdir(pluginPath+"/static", function (errRead, staticFiles) {
if(staticFiles.indexOf("tests") === -1){
console.warn("Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin")
}
})
}else{
console.warn("Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin")
}
if(hasAutofixed){
console.log("Fixes applied, please check git diff then run the following command:\n\n")
// bump npm Version
if(autoCommit){
// holy shit you brave.
console.log("Attempting autocommit and auto publish to npm")
exec("cd node_modules/"+ pluginName + " && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..", (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log("I think she's got it! By George she's got it!")
process.exit(0)
});
}else{
console.log("cd node_modules/"+ pluginName + " && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..")
}
}
//listing all files using forEach
files.forEach(function (file) {
// Do whatever you want to do with the file
// console.log(file.toLowerCase());
});
});
/*
*
* Usage -- see README.md
*
* Normal usage: node bin/plugins/checkPlugins.js ep_whatever
* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix
* Auto commit, push and publish(to npm) * highly dangerous:
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit
*/
const fs = require('fs');
const {exec} = require('child_process');
// get plugin name & path from user input
const pluginName = process.argv[2];
if (!pluginName) {
console.error('no plugin name specified');
process.exit(1);
}
const pluginPath = `node_modules/${pluginName}`;
console.log(`Checking the plugin: ${pluginName}`);
// Should we autofix?
if (process.argv[3] && process.argv[3] === 'autofix') var autoFix = true;
// Should we update files where possible?
if (process.argv[5] && process.argv[5] === 'autoupdate') var autoUpdate = true;
// Should we automcommit and npm publish?!
if (process.argv[4] && process.argv[4] === 'autocommit') var autoCommit = true;
if (autoCommit) {
console.warn('Auto commit is enabled, I hope you know what you are doing...');
}
fs.readdir(pluginPath, (err, rootFiles) => {
// handling error
if (err) {
return console.log(`Unable to scan directory: ${err}`);
}
// rewriting files to lower case
const files = [];
// some files we need to know the actual file name. Not compulsory but might help in the future.
let readMeFileName;
let repository;
let hasAutoFixed = false;
for (let i = 0; i < rootFiles.length; i++) {
if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i];
files.push(rootFiles[i].toLowerCase());
}
if (files.indexOf('.git') === -1) {
console.error('No .git folder, aborting');
process.exit(1);
}
// do a git pull...
var child_process = require('child_process');
try {
child_process.execSync('git pull ', {cwd: `${pluginPath}/`});
} catch (e) {
console.error('Error git pull', e);
}
try {
const path = `${pluginPath}/.github/workflows/npmpublish.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/npmpublish.yml, create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
hasAutoFixed = true;
console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo");
} else {
console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo');
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile = fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
hasAutoFixed = true;
}
}
} catch (err) {
console.error(err);
}
try {
const path = `${pluginPath}/.github/workflows/backend-tests.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/backend-tests.yml, create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
hasAutoFixed = true;
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile = fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
hasAutoFixed = true;
}
}
} catch (err) {
console.error(err);
}
if (files.indexOf('package.json') === -1) {
console.warn('no package.json, please create');
}
if (files.indexOf('package.json') !== -1) {
const packageJSON = fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'});
const parsedPackageJSON = JSON.parse(packageJSON);
if (autoFix) {
let updatedPackageJSON = false;
if (!parsedPackageJSON.funding) {
updatedPackageJSON = true;
parsedPackageJSON.funding = {
type: 'individual',
url: 'https://etherpad.org/',
};
}
if (updatedPackageJSON) {
hasAutoFixed = true;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if (packageJSON.toLowerCase().indexOf('repository') === -1) {
console.warn('No repository in package.json');
if (autoFix) {
console.warn('Repository not detected in package.json. Please add repository section manually.');
}
} else {
// useful for creating README later.
repository = parsedPackageJSON.repository.url;
}
// include lint config
if (packageJSON.toLowerCase().indexOf('devdependencies') === -1 || !parsedPackageJSON.devDependencies.eslint) {
console.warn('Missing eslint reference in devDependencies');
if (autoFix) {
const devDependencies = {
'eslint': '^7.14.0',
'eslint-config-etherpad': '^1.0.13',
'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.2',
'eslint-plugin-promise': '^4.2.1',
};
hasAutoFixed = true;
parsedPackageJSON.devDependencies = devDependencies;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
const child_process = require('child_process');
try {
child_process.execSync('npm install', {cwd: `${pluginPath}/`});
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
// include peer deps config
if (packageJSON.toLowerCase().indexOf('peerdependencies') === -1 || !parsedPackageJSON.peerDependencies) {
console.warn('Missing peer deps reference in package.json');
if (autoFix) {
const peerDependencies = {
'ep_etherpad-lite': '>=1.8.6',
};
hasAutoFixed = true;
parsedPackageJSON.peerDependencies = peerDependencies;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
const child_process = require('child_process');
try {
child_process.execSync('npm install --no-save ep_etherpad-lite@file:../../src', {cwd: `${pluginPath}/`});
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) {
console.warn('No esLintConfig in package.json');
if (autoFix) {
const eslintConfig = {
root: true,
extends: 'etherpad/plugin',
};
hasAutoFixed = true;
parsedPackageJSON.eslintConfig = eslintConfig;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if (packageJSON.toLowerCase().indexOf('scripts') === -1) {
console.warn('No scripts in package.json');
if (autoFix) {
const scripts = {
'lint': 'eslint .',
'lint:fix': 'eslint --fix .',
};
hasAutoFixed = true;
parsedPackageJSON.scripts = scripts;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) {
console.warn('No engines or node engine in package.json');
if (autoFix) {
const engines = {
node: '>=10.13.0',
};
hasAutoFixed = true;
parsedPackageJSON.engines = engines;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
}
if (files.indexOf('package-lock.json') === -1) {
console.warn('package-lock.json file not found. Please run npm install in the plugin folder and commit the package-lock.json file.');
if (autoFix) {
var child_process = require('child_process');
try {
child_process.execSync('npm install', {cwd: `${pluginPath}/`});
console.log('Making package-lock.json');
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) {
console.warn('README.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing README.md file, please edit the README.md file further to include plugin specific details.');
let readme = fs.readFileSync('bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'});
readme = readme.replace(/\[plugin_name\]/g, pluginName);
if (repository) {
const org = repository.split('/')[3];
const name = repository.split('/')[4];
readme = readme.replace(/\[org_name\]/g, org);
readme = readme.replace(/\[repo_url\]/g, name);
fs.writeFileSync(`${pluginPath}/README.md`, readme);
} else {
console.warn('Unable to find repository in package.json, aborting.');
}
}
}
if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) {
console.warn('CONTRIBUTING.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md file further to include plugin specific details.');
let contributing = fs.readFileSync('bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'});
contributing = contributing.replace(/\[plugin_name\]/g, pluginName);
fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing);
}
}
if (files.indexOf('readme') !== -1 && files.indexOf('readme.md') !== -1) {
const readme = fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'});
if (readme.toLowerCase().indexOf('license') === -1) {
console.warn('No license section in README');
if (autoFix) {
console.warn('Please add License section to README manually.');
}
}
}
if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) {
console.warn('LICENSE.md file not found, please create');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing LICENSE.md file, including Apache 2 license.');
exec('git config user.name', (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
let license = fs.readFileSync('bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'});
license = license.replace('[yyyy]', new Date().getFullYear());
license = license.replace('[name of copyright owner]', name);
fs.writeFileSync(`${pluginPath}/LICENSE.md`, license);
});
}
}
let travisConfig = fs.readFileSync('bin/plugins/lib/travis.yml', {encoding: 'utf8', flag: 'r'});
travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName);
if (files.indexOf('.travis.yml') === -1) {
console.warn('.travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.');
// TODO: Make it check version of the .travis file to see if it needs an update.
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing .travis.yml file');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
console.log('Travis file created, please sign into travis and enable this repository');
}
}
if (autoFix && autoUpdate) {
// checks the file versioning of .travis and updates it to the latest.
const existingConfig = fs.readFileSync(`${pluginPath}/.travis.yml`, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = existingConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const existingValue = parseInt(existingConfig.substr(existingConfigLocation + 20, existingConfig.length));
const newConfigLocation = travisConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const newValue = parseInt(travisConfig.substr(newConfigLocation + 20, travisConfig.length));
if (existingConfigLocation === -1) {
console.warn('no previous .travis.yml version found so writing new.');
// we will write the newTravisConfig to the location.
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
} else if (newValue > existingValue) {
console.log('updating .travis.yml');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
hasAutoFixed = true;
}//
}
if (files.indexOf('.gitignore') === -1) {
console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository.");
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing .gitignore file');
const gitignore = fs.readFileSync('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'});
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
} else {
let gitignore =
fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'});
if (gitignore.indexOf('node_modules/') === -1) {
console.warn('node_modules/ missing from .gitignore');
if (autoFix) {
gitignore += 'node_modules/';
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
hasAutoFixed = true;
}
}
}
// if we include templates but don't have translations...
if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) {
console.warn('Translations not found, please create. Translation files help with Etherpad accessibility.');
}
if (files.indexOf('.ep_initialized') !== -1) {
console.warn('.ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing incorrectly existing .ep_initialized file');
fs.unlinkSync(`${pluginPath}/.ep_initialized`);
}
}
if (files.indexOf('npm-debug.log') !== -1) {
console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing incorrectly existing npm-debug.log file');
fs.unlinkSync(`${pluginPath}/npm-debug.log`);
}
}
if (files.indexOf('static') !== -1) {
fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => {
if (staticFiles.indexOf('tests') === -1) {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
});
} else {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
// linting begins
if (autoFix) {
var lintCmd = 'npm run lint:fix';
} else {
var lintCmd = 'npm run lint';
}
try {
child_process.execSync(lintCmd, {cwd: `${pluginPath}/`});
console.log('Linting...');
if (autoFix) {
// todo: if npm run lint doesn't do anything no need for...
hasAutoFixed = true;
}
} catch (e) {
// it is gonna throw an error anyway
console.log('Manual linting probably required, check with: npm run lint');
}
// linting ends.
if (hasAutoFixed) {
console.log('Fixes applied, please check git diff then run the following command:\n\n');
// bump npm Version
if (autoCommit) {
// holy shit you brave.
console.log('Attempting autocommit and auto publish to npm');
// github should push to npm for us :)
exec(`cd node_modules/${pluginName} && git rm -rf node_modules --ignore-unmatch && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && git push && cd ../..`, (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log("I think she's got it! By George she's got it!");
process.exit(0);
});
} else {
console.log(`cd node_modules/${pluginName} && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..`);
}
}
console.log('Finished');
});

4
bin/plugins/getCorePlugins.sh Executable file
View File

@ -0,0 +1,4 @@
cd node_modules/
GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone
GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100&page=2&" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone
GHUSER=ether; curl "https://api.github.com/users/$GHUSER/repos?per_page=100&page=3&" | grep -o 'git@[^"]*' | grep /ep_ | xargs -L1 git clone

View File

@ -0,0 +1,133 @@
# Contributor Guidelines
(Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch))
## Pull requests
* the commit series in the PR should be _linear_ (it **should not contain merge commits**). This is necessary because we want to be able to [bisect](https://en.wikipedia.org/wiki/Bisection_(software_engineering)) bugs easily. Rewrite history/perform a rebase if necessary
* PRs should be issued against the **develop** branch: we never pull directly into **master**
* PRs **should not have conflicts** with develop. If there are, please resolve them rebasing and force-pushing
* when preparing your PR, please make sure that you have included the relevant **changes to the documentation** (preferably with usage examples)
* contain meaningful and detailed **commit messages** in the form:
```
submodule: description
longer description of the change you have made, eventually mentioning the
number of the issue that is being fixed, in the form: Fixes #someIssueNumber
```
* if the PR is a **bug fix**:
* the first commit in the series must be a test that shows the failure
* subsequent commits will fix the bug and make the test pass
* the final commit message should include the text `Fixes: #xxx` to link it to its bug report
* think about stability: code has to be backwards compatible as much as possible. Always **assume your code will be run with an older version of the DB/config file**
* if you want to remove a feature, **deprecate it instead**:
* write an issue with your deprecation plan
* output a `WARN` in the log informing that the feature is going to be removed
* remove the feature in the next version
* if you want to add a new feature, put it under a **feature flag**:
* once the new feature has reached a minimal level of stability, do a PR for it, so it can be integrated early
* expose a mechanism for enabling/disabling the feature
* the new feature should be **disabled** by default. With the feature disabled, the code path should be exactly the same as before your contribution. This is a __necessary condition__ for early integration
* think of the PR not as something that __you wrote__, but as something that __someone else is going to read__. The commit series in the PR should tell a novice developer the story of your thoughts when developing it
## How to write a bug report
* Please be polite, we all are humans and problems can occur.
* Please add as much information as possible, for example
* client os(s) and version(s)
* browser(s) and version(s), is the problem reproducible on different clients
* special environments like firewalls or antivirus
* host os and version
* npm and nodejs version
* Logfiles if available
* steps to reproduce
* what you expected to happen
* what actually happened
* Please format logfiles and code examples with markdown see github Markdown help below the issue textarea for more information.
If you send logfiles, please set the loglevel switch DEBUG in your settings.json file:
```
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
"loglevel": "DEBUG",
```
The logfile location is defined in startup script or the log is directly shown in the commandline after you have started etherpad.
## General goals of Etherpad
To make sure everybody is going in the same direction:
* easy to install for admins and easy to use for people
* easy to integrate into other apps, but also usable as standalone
* lightweight and scalable
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core.
Also, keep it maintainable. We don't wanna end up as the monster Etherpad was!
## How to work with git?
* Don't work in your master branch.
* Make a new branch for every feature you're working on. (This ensures that you can work you can do lots of small, independent pull requests instead of one big one with complete different features)
* Don't use the online edit function of github (this only creates ugly and not working commits!)
* Try to make clean commits that are easy readable (including descriptive commit messages!)
* Test before you push. Sounds easy, it isn't!
* Don't check in stuff that gets generated during build or runtime
* Make small pull requests that are easy to review but make sure they do add value by themselves / individually
## Coding style
* Do write comments. (You don't have to comment every line, but if you come up with something that's a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless!)
* Never ever use tabs
* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces
* Don't overengineer. Don't try to solve any possible problem in one step, but try to solve problems as easy as possible and improve the solution over time!
* Do generalize sooner or later! (if an old solution, quickly hacked together, poses more problems than it solves today, refactor it!)
* Keep it compatible. Do not introduce changes to the public API, db schema or configurations too lightly. Don't make incompatible changes without good reasons!
* If you do make changes, document them! (see below)
* Use protocol independent urls "//"
## Branching model / git workflow
see git flow http://nvie.com/posts/a-successful-git-branching-model/
### `master` branch
* the stable
* This is the branch everyone should use for production stuff
### `develop`branch
* everything that is READY to go into master at some point in time
* This stuff is tested and ready to go out
### release branches
* stuff that should go into master very soon
* only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
* we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
### hotfix branches
* fixes for bugs in master
### feature branches (in your own repos)
* these are the branches where you develop your features in
* If it's ready to go out, it will be merged into develop
Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop
## Documentation
The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision.
Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request.
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
## Testing
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Back-end tests can be run from the `src` directory, via `npm test`.
## Things you can help with
Etherpad is much more than software. So if you aren't a developer then worry not, there is still a LOT you can do! A big part of what we do is community engagement. You can help in the following ways
* Triage bugs (applying labels) and confirming their existence
* Testing fixes (simply applying them and seeing if it fixes your issue or not) - Some git experience required
* Notifying large site admins of new releases
* Writing Changelogs for releases
* Creating Windows packages
* Creating releases
* Bumping dependencies periodically and checking they don't break anything
* Write proposals for grants
* Co-Author and Publish CVEs
* Work with SFC to maintain legal side of project
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS

View File

@ -1,13 +1,13 @@
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,28 +1,29 @@
[![Travis (.org)](https://api.travis-ci.org/[org_name]/[repo_url].svg?branch=develop)](https://travis-ci.org/github/[org_name]/[repo_url])
# My awesome plugin README example
Explain what your plugin does and who it's useful for.
## Example animated gif of usage if appropriate
## Installing
npm install [plugin_name]
or Use the Etherpad ``/admin`` interface.
## Settings
Document settings if any
## Testing
Document how to run backend / frontend tests.
### Frontend
Visit http://whatever/tests/frontend/ to run the frontend tests.
### backend
Type ``cd src && npm run test`` to run the backend tests.
## LICENSE
Apache 2.0
[![Travis (.com)](https://api.travis-ci.com/[org_name]/[repo_url].svg?branch=develop)](https://travis-ci.com/github/[org_name]/[repo_url])
# My awesome plugin README example
Explain what your plugin does and who it's useful for.
## Example animated gif of usage if appropriate
![screenshot](https://user-images.githubusercontent.com/220864/99979953-97841d80-2d9f-11eb-9782-5f65817c58f4.PNG)
## Installing
npm install [plugin_name]
or Use the Etherpad ``/admin`` interface.
## Settings
Document settings if any
## Testing
Document how to run backend / frontend tests.
### Frontend
Visit http://whatever/tests/frontend/ to run the frontend tests.
### backend
Type ``cd src && npm run test`` to run the backend tests.
## LICENSE
Apache 2.0

View File

@ -0,0 +1,51 @@
# You need to change lines 38 and 46 in case the plugin's name on npmjs.com is different
# from the repository name
name: "Backend tests"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
jobs:
withplugins:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: with Plugins
runs-on: ubuntu-latest
steps:
- name: Install libreoffice
run: |
sudo add-apt-repository -y ppa:libreoffice/ppa
sudo apt update
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
# clone etherpad-lite
- name: Install etherpad core
uses: actions/checkout@v2
with:
repository: ether/etherpad-lite
- name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
# clone this repository into node_modules/ep_plugin-name
- name: Checkout plugin repository
uses: actions/checkout@v2
with:
path: ./node_modules/${{github.event.repository.name}}
- name: Install plugin dependencies
run: |
cd node_modules/${{github.event.repository.name}}
npm ci
# configures some settings and runs npm run test
- name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh
##ETHERPAD_NPM_V=1
## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh

View File

@ -1,5 +1,5 @@
.ep_initialized
.DS_Store
node_modules/
node_modules
npm-debug.log
.ep_initialized
.DS_Store
node_modules/
node_modules
npm-debug.log

View File

@ -0,0 +1,73 @@
# This workflow will run tests using node and then publish a package to the npm registry when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Node.js Package
on:
pull_request:
push:
branches:
- main
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
# Clone ether/etherpad-lite to ../etherpad-lite so that ep_etherpad-lite
# can be "installed" in this plugin's node_modules. The checkout v2 action
# doesn't support cloning outside of $GITHUB_WORKSPACE (see
# https://github.com/actions/checkout/issues/197), so the repo is first
# cloned to etherpad-lite then moved to ../etherpad-lite. To avoid
# conflicts with this plugin's clone, etherpad-lite must be cloned and
# moved out before this plugin's repo is cloned to $GITHUB_WORKSPACE.
- uses: actions/checkout@v2
with:
repository: ether/etherpad-lite
path: etherpad-lite
- run: mv etherpad-lite ..
# etherpad-lite has been moved outside of $GITHUB_WORKSPACE, so it is now
# safe to clone this plugin's repo to $GITHUB_WORKSPACE.
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
# All of ep_etherpad-lite's devDependencies are installed because the
# plugin might do `require('ep_etherpad-lite/node_modules/${devDep}')`.
# Eventually it would be nice to create an ESLint plugin that prohibits
# Etherpad plugins from piggybacking off of ep_etherpad-lite's
# devDependencies. If we had that, we could change this line to only
# install production dependencies.
- run: cd ../etherpad-lite/src && npm ci
- run: npm ci
# This runs some sanity checks and creates a symlink at
# node_modules/ep_etherpad-lite that points to ../../etherpad-lite/src.
# This step must be done after `npm ci` installs the plugin's dependencies
# because npm "helpfully" cleans up such symlinks. :( Installing
# ep_etherpad-lite in the plugin's node_modules prevents lint errors and
# unit test failures if the plugin does `require('ep_etherpad-lite/foo')`.
- run: npm install --no-save ep_etherpad-lite@file:../etherpad-lite/src
- run: npm test
- run: npm run lint
publish-npm:
if: github.event_name == 'push'
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: git config user.name 'github-actions[bot]'
- run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
- run: npm ci
- run: npm version patch
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- run: git push --follow-tags
##ETHERPAD_NPM_V=1
## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh

138
bin/plugins/lib/travis.yml Executable file → Normal file
View File

@ -1,68 +1,70 @@
language: node_js
node_js:
- "lts/*"
cache: false
before_install:
- sudo add-apt-repository -y ppa:libreoffice/ppa
- sudo apt-get update
- sudo apt-get -y install libreoffice
- sudo apt-get -y install libreoffice-pdfimport
services:
- docker
install:
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
before_script:
- "tests/frontend/travis/sauce_tunnel.sh"
script:
- "tests/frontend/travis/runner.sh"
env:
global:
- secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec="
- secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g="
jobs:
include:
- name: "Run the Backend tests"
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
- "cd src && npm install && cd -"
script:
- "tests/frontend/travis/runnerBackend.sh"
- name: "Test the Frontend"
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "tests/frontend/travis/runner.sh"
notifications:
irc:
channels:
- "irc.freenode.org#etherpad-lite-dev"
##ETHERPAD_TRAVIS_V=3
## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh
language: node_js
node_js:
- "lts/*"
cache: false
services:
- docker
install:
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
#script:
# - "tests/frontend/travis/runner.sh"
env:
global:
- secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec="
- secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g="
jobs:
include:
- name: "Lint test package-lock"
install:
- "npm install lockfile-lint"
script:
- npx lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm
- name: "Run the Backend tests"
before_install:
- sudo add-apt-repository -y ppa:libreoffice/ppa
- sudo apt-get update
- sudo apt-get -y install libreoffice
- sudo apt-get -y install libreoffice-pdfimport
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir -p node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
- "cd src && npm install && cd -"
script:
- "tests/frontend/travis/runnerBackend.sh"
- name: "Test the Frontend"
before_script:
- "tests/frontend/travis/sauce_tunnel.sh"
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir -p node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "tests/frontend/travis/runner.sh"
notifications:
irc:
channels:
- "irc.freenode.org#etherpad-lite-dev"
##ETHERPAD_TRAVIS_V=9
## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -e
for dir in node_modules/ep_*; do
dir=${dir#node_modules/}
[ "$dir" != ep_etherpad-lite ] || continue
node bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate
done

View File

@ -3,121 +3,124 @@
known "good" revision.
*/
if(process.argv.length != 4 && process.argv.length != 5) {
console.error("Use: node bin/repairPad.js $PADID $REV [$NEWPADID]");
if (process.argv.length != 4 && process.argv.length != 5) {
console.error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]');
process.exit(1);
}
var npm = require("../src/node_modules/npm");
var async = require("../src/node_modules/async");
var ueberDB = require("../src/node_modules/ueberdb2");
const npm = require('../src/node_modules/npm');
const async = require('../src/node_modules/async');
const ueberDB = require('../src/node_modules/ueberdb2');
var padId = process.argv[2];
var newRevHead = process.argv[3];
var newPadId = process.argv[4] || padId + "-rebuilt";
const padId = process.argv[2];
const newRevHead = process.argv[3];
const newPadId = process.argv[4] || `${padId}-rebuilt`;
var db, oldPad, newPad, settings;
var AuthorManager, ChangeSet, Pad, PadManager;
let db, oldPad, newPad, settings;
let AuthorManager, ChangeSet, Pad, PadManager;
async.series([
function(callback) {
npm.load({}, function(err) {
if(err) {
console.error("Could not load NPM: " + err)
function (callback) {
npm.load({}, (err) => {
if (err) {
console.error(`Could not load NPM: ${err}`);
process.exit(1);
} else {
callback();
}
})
});
},
function(callback) {
function (callback) {
// Get a handle into the database
db = require('../src/node/db/DB');
db.init(callback);
}, function(callback) {
PadManager = require('../src/node/db/PadManager');
Pad = require('../src/node/db/Pad').Pad;
// Get references to the original pad and to a newly created pad
// HACK: This is a standalone script, so we want to write everything
// out to the database immediately. The only problem with this is
// that a driver (like the mysql driver) can hardcode these values.
db.db.db.settings = {cache: 0, writeInterval: 0, json: true};
// Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) {
console.error("Cannot create a pad with that id as it is invalid");
process.exit(1);
}
PadManager.doesPadExists(newPadId, function(err, exists) {
if (exists) {
console.error("Cannot create a pad with that id as it already exists");
process.exit(1);
}
});
PadManager.getPad(padId, function(err, pad) {
oldPad = pad;
newPad = new Pad(newPadId);
callback();
});
}, function(callback) {
},
function (callback) {
PadManager = require('../src/node/db/PadManager');
Pad = require('../src/node/db/Pad').Pad;
// Get references to the original pad and to a newly created pad
// HACK: This is a standalone script, so we want to write everything
// out to the database immediately. The only problem with this is
// that a driver (like the mysql driver) can hardcode these values.
db.db.db.settings = {cache: 0, writeInterval: 0, json: true};
// Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) {
console.error('Cannot create a pad with that id as it is invalid');
process.exit(1);
}
PadManager.doesPadExists(newPadId, (err, exists) => {
if (exists) {
console.error('Cannot create a pad with that id as it already exists');
process.exit(1);
}
});
PadManager.getPad(padId, (err, pad) => {
oldPad = pad;
newPad = new Pad(newPadId);
callback();
});
},
function (callback) {
// Clone all Chat revisions
var chatHead = oldPad.chatHead;
for(var i = 0, curHeadNum = 0; i <= chatHead; i++) {
db.db.get("pad:" + padId + ":chat:" + i, function (err, chat) {
db.db.set("pad:" + newPadId + ":chat:" + curHeadNum++, chat);
console.log("Created: Chat Revision: pad:" + newPadId + ":chat:" + curHeadNum);
const chatHead = oldPad.chatHead;
for (var i = 0, curHeadNum = 0; i <= chatHead; i++) {
db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => {
db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat);
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`);
});
}
callback();
}, function(callback) {
},
function (callback) {
// Rebuild Pad from revisions up to and including the new revision head
AuthorManager = require("../src/node/db/AuthorManager");
Changeset = require("ep_etherpad-lite/static/js/Changeset");
AuthorManager = require('../src/node/db/AuthorManager');
Changeset = require('ep_etherpad-lite/static/js/Changeset');
// Author attributes are derived from changesets, but there can also be
// non-author attributes with specific mappings that changesets depend on
// and, AFAICT, cannot be recreated any other way
newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
for(var curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
db.db.get("pad:" + padId + ":revs:" + curRevNum, function(err, rev) {
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => {
if (rev.meta) {
throw "The specified revision number could not be found.";
throw 'The specified revision number could not be found.';
}
var newRevNum = ++newPad.head;
var newRevId = "pad:" + newPad.id + ":revs:" + newRevNum;
const newRevNum = ++newPad.head;
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
db.db.set(newRevId, rev);
AuthorManager.addPad(rev.meta.author, newPad.id);
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
console.log("Created: Revision: pad:" + newPad.id + ":revs:" + newRevNum);
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
if (newRevNum == newRevHead) {
callback();
}
});
}
}, function(callback) {
},
function (callback) {
// Add saved revisions up to the new revision head
console.log(newPad.head);
var newSavedRevisions = [];
for(var i in oldPad.savedRevisions) {
savedRev = oldPad.savedRevisions[i]
const newSavedRevisions = [];
for (const i in oldPad.savedRevisions) {
savedRev = oldPad.savedRevisions[i];
if (savedRev.revNum <= newRevHead) {
newSavedRevisions.push(savedRev);
console.log("Added: Saved Revision: " + savedRev.revNum);
console.log(`Added: Saved Revision: ${savedRev.revNum}`);
}
}
newPad.savedRevisions = newSavedRevisions;
callback();
}, function(callback) {
},
function (callback) {
// Save the source pad
db.db.set("pad:"+newPadId, newPad, function(err) {
console.log("Created: Source Pad: pad:" + newPadId);
newPad.saveToDatabase();
callback();
db.db.set(`pad:${newPadId}`, newPad, (err) => {
console.log(`Created: Source Pad: pad:${newPadId}`);
newPad.saveToDatabase().then(() => callback(), callback);
});
}
], function (err) {
if(err) throw err;
else {
console.info("finished");
},
], (err) => {
if (err) { throw err; } else {
console.info('finished');
process.exit(0);
}
});

65
bin/release.js Normal file
View File

@ -0,0 +1,65 @@
'use strict';
const fs = require('fs');
const child_process = require('child_process');
const semver = require('../src/node_modules/semver');
/*
Usage
node bin/release.js patch
*/
const usage = 'node bin/release.js [patch/minor/major] -- example: "node bin/release.js patch"';
const release = process.argv[2];
if(!release) {
console.log(usage);
throw new Error('No release type included');
}
const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'});
let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'});
packageJson = JSON.parse(packageJson);
const currentVersion = packageJson.version;
const newVersion = semver.inc(currentVersion, release);
if(!newVersion) {
console.log(usage);
throw new Error('Unable to generate new version from input');
}
const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1;
if(!changelogIncludesVersion) {
throw new Error('No changelog record for ', newVersion, ' - please create changelog record');
}
console.log('Okay looks good, lets create the package.json and package-lock.json');
packageJson.version = newVersion;
fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2));
// run npm version `release` where release is patch, minor or major
child_process.execSync('npm install --package-lock-only', {cwd: `src/`});
// run npm install --package-lock-only <-- required???
child_process.execSync(`git checkout -b release/${newVersion}`);
child_process.execSync(`git add src/package.json`);
child_process.execSync(`git add src/package-lock.json`);
child_process.execSync(`git commit -m 'bump version'`);
child_process.execSync(`git push origin release/${newVersion}`);
child_process.execSync(`make docs`);
child_process.execSync(`git clone git@github.com:ether/ether.github.com.git`);
child_process.execSync(`cp -R out/doc/ ether.github.com/doc/${newVersion}`);
console.log('Once merged into master please run the following commands');
console.log(`git tag -a ${newVersion} && git push origin master`);
console.log(`cd ether.github.com && git add . && git commit -m ${newVersion} docs`);
console.log('Once the new docs are uploaded then modify the download link on etherpad.org and then pull master onto develop');
console.log('Finally go public with an announcement via our comms channels :)');

View File

@ -2,47 +2,47 @@
* 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) {
console.error("Use: node bin/repairPad.js $PADID");
console.error('Use: node bin/repairPad.js $PADID');
process.exit(1);
}
// get the padID
var padId = process.argv[2];
const padId = process.argv[2];
let npm = require("../src/node_modules/npm");
npm.load({}, async function(er) {
const npm = require('../src/node_modules/npm');
npm.load({}, async (er) => {
if (er) {
console.error("Could not load NPM: " + er)
console.error(`Could not load NPM: ${er}`);
process.exit(1);
}
try {
// intialize database
let settings = require('../src/node/utils/Settings');
let db = require('../src/node/db/DB');
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// get the pad
let padManager = require('../src/node/db/PadManager');
let pad = await padManager.getPad(padId);
const padManager = require('../src/node/db/PadManager');
const pad = await padManager.getPad(padId);
// accumulate the required keys
let neededDBValues = ["pad:" + padId];
const neededDBValues = [`pad:${padId}`];
// add all authors
neededDBValues.push(...pad.getAllAuthors().map(author => "globalAuthor:"));
neededDBValues.push(...pad.getAllAuthors().map((author) => 'globalAuthor:'));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push("pad:" + padId + ":revs:" + rev);
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push("pad:" + padId + ":chat:" + chat);
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
//
@ -55,21 +55,20 @@ npm.load({}, async function(er) {
//
// See gitlab issue #3545
//
console.info("aborting [gitlab #3545]");
console.info('aborting [gitlab #3545]');
process.exit(1);
// now fetch and reinsert every key
neededDBValues.forEach(function(key, value) {
console.log("Key: " + key+ ", value: " + value);
neededDBValues.forEach((key, value) => {
console.log(`Key: ${key}, value: ${value}`);
db.remove(key);
db.set(key, value);
});
console.info("finished");
console.info('finished');
process.exit(0);
} catch (er) {
if (er.name === "apierror") {
if (er.name === 'apierror') {
console.error(er);
} else {
console.trace(er);

View File

@ -1,13 +1,11 @@
#!/bin/sh
pecho() { printf %s\\n "$*"; }
log() { pecho "$@"; }
error() { log "ERROR: $@" >&2; }
fatal() { error "$@"; exit 1; }
# Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/..
# Source constants and usefull functions
. bin/functions.sh
ignoreRoot=0
for ARG in "$@"; do
if [ "$ARG" = "--root" ]; then
@ -34,4 +32,4 @@ bin/installDeps.sh "$@" || exit 1
log "Starting Etherpad..."
SCRIPTPATH=$(pwd -P)
exec node "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@"
exec node $(compute_node_args) "$SCRIPTPATH/node_modules/ep_etherpad-lite/node/server.js" "$@"

View File

@ -4,48 +4,114 @@ A hook function is registered with a hook via the plugin's `ep.json` file. See
the Plugins section for details. A hook may have many registered functions from
different plugins.
When a hook is invoked, its registered functions are called with three
arguments:
Some hooks call their registered functions one at a time until one of them
returns a value. Others always call all of their registered functions and
combine the results (if applicable).
1. hookName - The name of the hook being invoked.
2. context - An object with some relevant information about the context of the
## Registered hook functions
Note: The documentation in this section applies to every hook unless the
hook-specific documentation says otherwise.
### Arguments
Hook functions are called with three arguments:
1. `hookName` - The name of the hook being invoked.
2. `context` - An object with some relevant information about the context of the
call. See the hook-specific documentation for details.
3. callback - Function to call when done. This callback takes a single argument,
the meaning of which depends on the hook. See the "Return values" section for
general information that applies to most hooks. The value returned by this
callback must be returned by the hook function unless otherwise specified.
3. `cb` - For asynchronous operations this callback can be called to signal
completion and optionally provide a return value. The callback takes a single
argument, the meaning of which depends on the hook (see the "Return values"
section for general information that applies to most hooks). This callback
always returns `undefined`.
## Return values
### Expected behavior
Note: This section applies to every hook unless the hook-specific documentation
says otherwise.
The presence of a callback parameter suggests that every hook function can run
asynchronously. While that is the eventual goal, there are some legacy hooks
that expect their hook functions to provide a value synchronously. For such
hooks, the hook functions must do one of the following:
Hook functions return zero or more values to Etherpad by passing an array to the
provided callback. Hook functions typically provide a single value (array of
length one). If the function does not want to or need to provide a value, it may
pass an empty array or `undefined` (which is treated the same as an empty
array). Hook functions may also provide more than one value (array of length two
or more).
* Call the callback with a non-Promise value (`undefined` is acceptable) and
return `undefined`, in that order.
* Return a non-Promise value other than `undefined` (`null` is acceptable) and
never call the callback. Note that `async` functions *always* return a
Promise, so they must never be used for synchronous hooks.
* Only have two parameters (`hookName` and `context`) and return any non-Promise
value (`undefined` is acceptable).
Some hooks concatenate the arrays provided by its registered functions. For
example, if a hook's registered functions pass `[1, 2]`, `undefined`, `[3, 4]`,
`[]`, and `[5]` to the provided callback, then the hook's return value is `[1,
2, 3, 4, 5]`.
For hooks that permit asynchronous behavior, the hook functions must do one or
more of the following:
Other hooks only use the first non-empty array provided by a registered
function. In this case, each of the hook's registered functions is called one at
a time until one provides a non-empty array. The remaining functions are
skipped. If none of the functions provide a non-empty array, or there are no
registered functions, the hook's return value is `[]`.
* Return `undefined` and call the callback, in either order.
* Return something other than `undefined` (`null` is acceptable) and never call
the callback. Note that `async` functions *always* return a Promise, so they
must never call the callback.
* Only have two parameters (`hookName` and `context`).
Example:
Note that the acceptable behaviors for asynchronous hook functions is a superset
of the acceptable behaviors for synchronous hook functions.
```
exports.abstractHook = (hookName, context, callback) => {
if (notApplicableToThisPlugin(context)) {
return callback();
}
const value = doSomeProcessing(context);
return callback([value]);
WARNING: The number of parameters is determined by examining
[Function.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length),
which does not count [default
parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)
or ["rest"
parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters).
To avoid problems, do not use default or rest parameters when defining hook
functions.
### Return values
A hook function can provide a value to Etherpad in one of the following ways:
* Pass the desired value as the first argument to the callback.
* Return the desired value directly. The value must not be `undefined` unless
the hook function only has two parameters. (Hook functions with three
parameters that want to provide `undefined` should instead use the callback.)
* For hooks that permit asynchronous behavior, return a Promise that resolves to
the desired value.
* For hooks that permit asynchronous behavior, pass a Promise that resolves to
the desired value as the first argument to the callback.
Examples:
```javascript
exports.exampleOne = (hookName, context, callback) => {
return 'valueOne';
};
exports.exampleTwo = (hookName, context, callback) => {
callback('valueTwo');
return;
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleThree = (hookName, context, callback) => {
return new Promise('valueThree');
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleFour = (hookName, context, callback) => {
callback(new Promise('valueFour'));
return;
};
// ONLY FOR HOOKS THAT PERMIT ASYNCHRONOUS BEHAVIOR
exports.exampleFive = async (hookName, context) => {
// Note that this function is async, so it actually returns a Promise that
// is resolved to 'valueFive'.
return 'valueFive';
};
```
Etherpad collects the values provided by the hook functions into an array,
filters out all `undefined` values, then flattens the array one level.
Flattening one level makes it possible for a hook function to behave as if it
were multiple separate hook functions.
For example: Suppose a hook has eight registered functions that return the
following values: `1`, `[2]`, `['3a', '3b']` `[[4]]`, `undefined`,
`[undefined]`, `[]`, and `null`. The value returned to the caller of the hook is
`[1, 2, '3a', '3b', [4], undefined, null]`.

View File

@ -10,6 +10,28 @@ Things in context:
Use this hook to receive the global settings in your plugin.
## shutdown
Called from: src/node/server.js
Things in context: None
This hook runs before shutdown. Use it to stop timers, close sockets and files,
flush buffers, etc. The database is not available while this hook is running.
The shutdown function must not block for long because there is a short timeout
before the process is forcibly terminated.
The shutdown function must return a Promise, which must resolve to `undefined`.
Returning `callback(value)` will return a Promise that is resolved to `value`.
Example:
```
// using an async function
exports.shutdown = async (hookName, context) => {
await flushBuffers();
};
```
## pluginUninstall
Called from: src/static/js/pluginfw/installer.js
@ -54,6 +76,25 @@ Things in context:
This hook gets called after the application object has been created, but before it starts listening. This is similar to the expressConfigure hook, but it's not guaranteed that the application object will have all relevant configuration variables.
## expressCloseServer
Called from: src/node/hooks/express.js
Things in context: Nothing
This hook is called when the HTTP server is closing, which happens during
shutdown (see the shutdown hook) and when the server restarts (e.g., when a
plugin is installed via the `/admin/plugins` page). The HTTP server may or may
not already be closed when this hook executes.
Example:
```
exports.expressCloseServer = async () => {
await doSomeCleanup();
};
```
## eejsBlock_`<name>`
Called from: src/node/eejs/index.js
@ -96,7 +137,6 @@ Available blocks in `pad.html` are:
* `indexCustomStyles` - contains the `index.css` `<link>` tag, allows you to add your own or to customize the one provided by the active skin
* `indexWrapper` - contains the form for creating new pads
* `indexCustomScripts` - contains the `index.js` `<script>` tag, allows you to add your own or to customize the one provided by the active skin
* `indexCustomInlineScripts` - contains the inline `<script>` of home page, allows you to customize `go2Name()`, `go2Random()` or `randomPadName()` functions
## padInitToolbar
Called from: src/node/hooks/express/specialpages.js
@ -117,9 +157,8 @@ Called from: src/node/db/SecurityManager.js
Things in context:
1. padID - the pad the user wants to access
2. password - the password the user has given to access the pad
3. token - the token of the author
4. sessionCookie - the session the use has
2. token - the token of the author
3. sessionCookie - the session the use has
This hook gets called when the access to the concrete pad is being checked. Return `false` to deny access.
@ -149,6 +188,8 @@ Things in context:
1. pad - the pad instance
2. author - the id of the author who updated the pad
3. revs - the index of the new revision
4. changeset - the changeset of this revision (see [Changeset Library](#index_changeset_library))
This hook gets called when an existing pad was updated.
@ -190,6 +231,50 @@ Things in context:
I have no idea what this is useful for, someone else will have to add this description.
## preAuthorize
Called from: src/node/hooks/express/webaccess.js
Things in context:
1. req - the request object
2. res - the response object
3. next - bypass callback. If this is called instead of the normal callback then
all remaining access checks are skipped.
This hook is called for each HTTP request before any authentication checks are
performed. Example uses:
* Always grant access to static content.
* Process an OAuth callback.
* Drop requests from IP addresses that have failed N authentication checks
within the past X minutes.
A preAuthorize function is always called for each request unless a preAuthorize
function from another plugin (if any) has already explicitly granted or denied
the request.
You can pass the following values to the provided callback:
* `[]` defers the access decision to the normal authentication and authorization
checks (or to a preAuthorize function from another plugin, if one exists).
* `[true]` immediately grants access to the requested resource, unless the
request is for an `/admin` page in which case it is treated the same as `[]`.
(This prevents buggy plugins from accidentally granting admin access to the
general public.)
* `[false]` immediately denies the request. The preAuthnFailure hook will be
called to handle the failure.
Example:
```
exports.preAuthorize = (hookName, context, cb) => {
if (ipAddressIsFirewalled(context.req)) return cb([false]);
if (requestIsForStaticContent(context.req)) return cb([true]);
if (requestIsForOAuthCallback(context.req)) return cb([true]);
return cb([]);
};
```
## authorize
Called from: src/node/hooks/express/webaccess.js
@ -203,49 +288,33 @@ Things in context:
This hook is called to handle authorization. It is especially useful for
controlling access to specific paths.
A plugin's authorize function is typically called twice for each access: once
before authentication and again after. Specifically, it is called if all of the
following are true:
A plugin's authorize function is only called if all of the following are true:
* The request is not for static content or an API endpoint. (Requests for static
content and API endpoints are always authorized, even if unauthenticated.)
* Either authentication has not yet been performed (`context.req.session.user`
is undefined) or the user has successfully authenticated
(`context.req.session.user` is an object containing user-specific settings).
* If the user has successfully authenticated, the user is not an admin. (Admin
users are always authorized.)
* Either the request is for an `/admin` page or the `requireAuthentication`
setting is true.
* Either the request is for an `/admin` page, or the user has not yet
authenticated, or the user has authenticated and the `requireAuthorization`
setting is true.
* For pre-authentication invocations of a plugin's authorize function
(`context.req.session.user` is undefined), an authorize function from a
different plugin has not already caused the pre-authentication authorization
to pass or fail.
* For post-authentication invocations of a plugin's authorize function
(`context.req.session.user` is an object), an authorize function from a
different plugin has not already caused the post-authentication authorization
to pass or fail.
* The `requireAuthentication` and `requireAuthorization` settings are both true.
* The user has already successfully authenticated.
* The user is not an admin (admin users are always authorized).
* The path being accessed is not an `/admin` path (`/admin` paths can only be
accessed by admin users, and admin users are always authorized).
* An authorize function from a different plugin has not already caused
authorization to pass or fail.
For pre-authentication invocations of your authorize function, you can pass the
following values to the provided callback:
Note that the authorize hook cannot grant access to `/admin` pages. If admin
access is desired, the `is_admin` user setting must be set to true. This can be
set in the settings file or by the authenticate hook.
* `[true]` or `['create']` will immediately grant access without requiring the
user to authenticate.
* `[false]` will trigger authentication unless authentication is not required.
* `[]` or `undefined` will defer the decision to the next authorization plugin
(if any, otherwise it is the same as calling with `[false]`).
You can pass the following values to the provided callback:
**WARNING:** Your authorize function can be called for an `/admin` page even if
the user has not yet authenticated. It is your responsibility to fail or defer
authorization if you do not want to grant admin privileges to the general
public.
For post-authentication invocations of your authorize function, you can pass the
following values to the provided callback:
* `[true]` or `['create']` will grant access.
* `[true]` or `['create']` will grant access to modify or create the pad if the
request is for a pad, otherwise access is simply granted. Access to a pad will
be downgraded to modify-only if `settings.editOnly` is true or the user's
`canCreate` setting is set to `false`, and downgraded to read-only if the
user's `readOnly` setting is `true`.
* `['modify']` will grant access to modify but not create the pad if the request
is for a pad, otherwise access is simply granted. Access to a pad will be
downgraded to read-only if the user's `readOnly` setting is `true`.
* `['readOnly']` will grant read-only access.
* `[false]` will deny access.
* `[]` or `undefined` will defer the authorization decision to the next
authorization plugin (if any, otherwise deny).
@ -255,11 +324,6 @@ Example:
```
exports.authorize = (hookName, context, cb) => {
const user = context.req.session.user;
if (!user) {
// The user has not yet authenticated so defer the pre-authentication
// authorization decision to the next plugin.
return cb([]);
}
const path = context.req.path; // or context.resource
if (isExplicitlyProhibited(user, path)) return cb([false]);
if (isExplicitlyAllowed(user, path)) return cb([true]);
@ -282,7 +346,7 @@ Things in context:
This hook is called to handle authentication.
Plugins that supply an authenticate function should probably also supply an
authFailure function unless falling back to HTTP basic authentication is
authnFailure function unless falling back to HTTP basic authentication is
appropriate upon authentication failure.
This hook is only called if either the `requireAuthentication` setting is true
@ -333,10 +397,12 @@ Things in context:
2. res - the response object
3. next - ?
**DEPRECATED:** Use authnFailure or authzFailure instead.
This hook is called to handle an authentication or authorization failure.
Plugins that supply an authenticate function should probably also supply an
authFailure function unless falling back to HTTP basic authentication is
authnFailure function unless falling back to HTTP basic authentication is
appropriate upon authentication failure.
A plugin's authFailure function is only called if all of the following are true:
@ -344,11 +410,16 @@ A plugin's authFailure function is only called if all of the following are true:
* There was an authentication or authorization failure.
* The failure was not already handled by an authFailure function from another
plugin.
* For authentication failures: The failure was not already handled by the
authnFailure hook.
* For authorization failures: The failure was not already handled by the
authzFailure hook.
Calling the provided callback with `[true]` tells Etherpad that the failure was
handled and no further error handling is required. Calling the callback with
`[]` or `undefined` defers error handling to the next authFailure plugin (if
any, otherwise fall back to HTTP basic authentication).
any, otherwise fall back to HTTP basic authentication for an authentication
failure or a generic 403 page for an authorization failure).
Example:
@ -362,13 +433,107 @@ exports.authFailure = (hookName, context, cb) => {
};
```
## preAuthzFailure
Called from: src/node/hooks/express/webaccess.js
Things in context:
1. req - the request object
2. res - the response object
This hook is called to handle a pre-authentication authorization failure.
A plugin's preAuthzFailure function is only called if the pre-authentication
authorization failure was not already handled by a preAuthzFailure function from
another plugin.
Calling the provided callback with `[true]` tells Etherpad that the failure was
handled and no further error handling is required. Calling the callback with
`[]` or `undefined` defers error handling to a preAuthzFailure function from
another plugin (if any, otherwise fall back to a generic 403 error page).
Example:
```
exports.preAuthzFailure = (hookName, context, cb) => {
if (notApplicableToThisPlugin(context)) return cb([]);
context.res.status(403).send(renderFancy403Page(context.req));
return cb([true]);
};
```
## authnFailure
Called from: src/node/hooks/express/webaccess.js
Things in context:
1. req - the request object
2. res - the response object
This hook is called to handle an authentication failure.
Plugins that supply an authenticate function should probably also supply an
authnFailure function unless falling back to HTTP basic authentication is
appropriate upon authentication failure.
A plugin's authnFailure function is only called if the authentication failure
was not already handled by an authnFailure function from another plugin.
Calling the provided callback with `[true]` tells Etherpad that the failure was
handled and no further error handling is required. Calling the callback with
`[]` or `undefined` defers error handling to an authnFailure function from
another plugin (if any, otherwise fall back to the deprecated authFailure hook).
Example:
```
exports.authnFailure = (hookName, context, cb) => {
if (notApplicableToThisPlugin(context)) return cb([]);
context.res.redirect(makeLoginURL(context.req));
return cb([true]);
};
```
## authzFailure
Called from: src/node/hooks/express/webaccess.js
Things in context:
1. req - the request object
2. res - the response object
This hook is called to handle a post-authentication authorization failure.
A plugin's authzFailure function is only called if the authorization failure was
not already handled by an authzFailure function from another plugin.
Calling the provided callback with `[true]` tells Etherpad that the failure was
handled and no further error handling is required. Calling the callback with
`[]` or `undefined` defers error handling to an authzFailure function from
another plugin (if any, otherwise fall back to the deprecated authFailure hook).
Example:
```
exports.authzFailure = (hookName, context, cb) => {
if (notApplicableToThisPlugin(context)) return cb([]);
if (needsPremiumAccount(context.req) && !context.req.session.user.premium) {
context.res.status(200).send(makeUpgradeToPremiumAccountPage(context.req));
return cb([true]);
}
// Use the generic 403 forbidden response.
return cb([]);
};
```
## handleMessage
Called from: src/node/handler/PadMessageHandler.js
Things in context:
1. message - the message being handled
2. client - the socket.io Socket object
2. socket - the socket.io Socket object
3. client - **deprecated** synonym of socket
This hook allows plugins to drop or modify incoming socket.io messages from
clients, before Etherpad processes them.
@ -377,29 +542,26 @@ The handleMessage function must return a Promise. If the Promise resolves to
`null`, the message is dropped. Returning `callback(value)` will return a
Promise that is resolved to `value`.
**WARNING:** handleMessage is called for every message, even if the client is
not authorized to send the message. It is up to the plugin to check permissions.
Examples:
```
// Using an async function:
exports.handleMessage = async (hookName, {message, client}) => {
exports.handleMessage = async (hookName, {message, socket}) => {
if (message.type === 'USERINFO_UPDATE') {
// Force the display name to the name associated with the account.
const user = client.client.request.session.user || {};
const user = socket.client.request.session.user || {};
if (user.name) message.data.userInfo.name = user.name;
}
};
// Using a regular function:
exports.handleMessage = (hookName, {message, client}, callback) => {
exports.handleMessage = (hookName, {message, socket}, callback) => {
if (message.type === 'USERINFO_UPDATE') {
// Force the display name to the name associated with the account.
const user = client.client.request.session.user || {};
const user = socket.client.request.session.user || {};
if (user.name) message.data.userInfo.name = user.name;
}
return cb();
return callback();
};
```
@ -409,7 +571,8 @@ Called from: src/node/handler/PadMessageHandler.js
Things in context:
1. message - the message being handled
2. client - the socket.io Socket object
2. socket - the socket.io Socket object
3. client - **deprecated** synonym of socket
This hook allows plugins to grant temporary write access to a pad. It is called
for each incoming message from a client. If write access is granted, it applies
@ -422,22 +585,18 @@ The handleMessageSecurity function must return a Promise. If the Promise
resolves to `true`, write access is granted as described above. Returning
`callback(value)` will return a Promise that is resolved to `value`.
**WARNING:** handleMessageSecurity is called for every message, even if the
client is not authorized to send the message. It is up to the plugin to check
permissions.
Examples:
```
// Using an async function:
exports.handleMessageSecurity = async (hookName, {message, client}) => {
if (shouldGrantWriteAccess(message, client)) return true;
exports.handleMessageSecurity = async (hookName, {message, socket}) => {
if (shouldGrantWriteAccess(message, socket)) return true;
return;
};
// Using a regular function:
exports.handleMessageSecurity = (hookName, {message, client}, callback) => {
if (shouldGrantWriteAccess(message, client)) return callback(true);
exports.handleMessageSecurity = (hookName, {message, socket}, callback) => {
if (shouldGrantWriteAccess(message, socket)) return callback(true);
return callback();
};
```
@ -521,6 +680,24 @@ function _analyzeLine(alineAttrs, apool) {
}
```
## exportHTMLAdditionalContent
Called from: src/node/utils/ExportHtml.js
Things in context:
1. padId
This hook will allow a plug-in developer to include additional HTML content in
the body of the exported HTML.
Example:
```
exports.exportHTMLAdditionalContent = async (hookName, {padId}) => {
return 'I am groot in ' + padId;
};
```
## stylesForExport
Called from: src/node/utils/ExportHtml.js
@ -541,18 +718,21 @@ exports.stylesForExport = function(hook, padId, cb){
## aceAttribClasses
Called from: src/static/js/linestylefilter.js
Things in context:
1. Attributes - Object of Attributes
This hook is called when attributes are investigated on a line. It is useful if
you want to add another attribute type or property type to a pad.
This hook is called when attributes are investigated on a line. It is useful if you want to add another attribute type or property type to a pad.
An attributes object is passed to the aceAttribClasses hook functions instead of
the usual context object. A hook function can either modify this object directly
or provide an object whose properties will be assigned to the attributes object.
Example:
```
exports.aceAttribClasses = function(hook_name, attr, cb){
attr.sub = 'tag:sub';
cb(attr);
}
exports.aceAttribClasses = (hookName, attrs, cb) => {
return cb([{
sub: 'tag:sub',
}]);
};
```
## exportFileName
@ -608,6 +788,25 @@ exports.exportHtmlAdditionalTagsWithData = function(hook, pad, cb){
};
```
## exportEtherpadAdditionalContent
Called from src/node/utils/ExportEtherpad.js and
src/node/utils/ImportEtherpad.js
Things in context: Nothing
Useful for exporting and importing pad metadata that is stored in the database
but not in the pad's content or attributes. For example, in ep_comments_page the
comments are stored as `comments:padId:uniqueIdOfComment` so a complete export
of all pad data to an `.etherpad` file must include the `comments:padId:*`
records.
Example:
```
// Add support for exporting comments metadata
exports.exportEtherpadAdditionalContent = () => ['comments'];
```
## userLeave
Called from src/node/handler/PadMessageHandler.js

View File

@ -581,24 +581,6 @@ return true of false
* `{code: 0, message:"ok", data: {publicStatus: true}}`
* `{code: 1, message:"padID does not exist", data: null}`
#### setPassword(padID, password)
* API >= 1
returns ok or an error message
*Example returns:*
* `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}`
#### isPasswordProtected(padID)
* API >= 1
returns true or false
*Example returns:*
* `{code: 0, message:"ok", data: {passwordProtection: true}}`
* `{code: 1, message:"padID does not exist", data: null}`
#### listAuthorsOfPad(padID)
* API >= 1

View File

@ -1,14 +1,22 @@
# Plugin Framework
`require("ep_etherpad-lite/static/js/plugingfw/plugins")`
## plugins.update
`require("ep_etherpad-lite/static/js/plugingfw/plugins").update()` will use npm to list all installed modules and read their ep.json files, registering the contained hooks.
A hook registration is a pair of a hook name and a function reference (filename for require() plus function name)
`require("ep_etherpad-lite/static/js/plugingfw/plugins").update()` will use npm
to list all installed modules and read their ep.json files, registering the
contained hooks. A hook registration is a pair of a hook name and a function
reference (filename for require() plus function name)
## hooks.callAll
`require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value})` will call all hook functions registered for `hook_name` with `{argname:value}`.
`require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name",
{argname:value})` will call all hook functions registered for `hook_name` with
`{argname:value}`.
## hooks.aCallAll
?
## ...

View File

@ -171,7 +171,6 @@ For the editor container, you can also make it full width by adding `full-width-
| `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` |
| `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` |
| `EDIT_ONLY` | 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. | `false` |
| `SESSION_NO_PASSWORD` | If set to true, those users who have a valid session will automatically be granted access to password protected pads. | `false` |
| `MINIFY` | 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 | `true` |
| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) |
| `ABIWORD` | 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. | `null` |

View File

@ -1,28 +1,69 @@
# Plugins
Etherpad allows you to extend its functionality with plugins. A plugin registers hooks (functions) for certain events (thus certain features) in Etherpad-lite to execute its own functionality based on these events.
Publicly available plugins can be found in the npm registry (see <https://npmjs.org>). Etherpad-lite's naming convention for plugins is to prefix your plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install plugins from npm, using `npm install ep_flubberworm` in etherpad-lite's root directory.
Etherpad allows you to extend its functionality with plugins. A plugin registers
hooks (functions) for certain events (thus certain features) in Etherpad to
execute its own functionality based on these events.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will list all installed plugins and those available on npm. It even provides functionality to search through all available plugins.
Publicly available plugins can be found in the npm registry (see
<https://npmjs.org>). Etherpad's naming convention for plugins is to prefix your
plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install
plugins from npm, using `npm install ep_flubberworm` in Etherpad's root
directory.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will
list all installed plugins and those available on npm. It even provides
functionality to search through all available plugins.
## Folder structure
A basic plugin usually has the following folder structure:
Ideally a plugin has the following folder structure:
```
ep_<plugin>/
| static/
| templates/
| locales/
+ ep.json
+ package.json
├ .github/
│ └ workflows/
│ └ npmpublish.yml ◄─ GitHub workflow to auto-publish on push
├ static/
│ ├ css/ ◄─ static .css files
│ ├ images/ ◄─ static image files
│ ├ js/
│ │ └ index.js ◄─ static client-side code
│ └ tests/
│ ├ backend/
│ │ └ specs/ ◄─ backend (server) tests
│ └ frontend/
│ └ specs/ ◄─ frontend (client) tests
├ templates/ ◄─ EJS templates (.html, .js, .css, etc.)
├ locales/
│ ├ en.json ◄─ English (US) strings
│ └ qqq.json ◄─ optional hints for translators
├ .travis.yml ◄─ Travis CI config
├ LICENSE
├ README.md
├ ep.json ◄─ Etherpad plugin definition
├ index.js ◄─ server-side code
├ package.json
└ package-lock.json
```
If your plugin includes client-side hooks, put them in `static/js/`. If you're adding in CSS or image files, you should put those files in `static/css/ `and `static/image/`, respectively, and templates go into `templates/`. Translations go into `locales/`
A Standard directory structure like this makes it easier to navigate through your code. That said, do note, that this is not actually *required* to make your plugin run. If you want to make use of our i18n system, you need to put your translations into `locales/`, though, in order to have them integrated. (See "Localization" for more info on how to localize your plugin)
If your plugin includes client-side hooks, put them in `static/js/`. If you're
adding in CSS or image files, you should put those files in `static/css/ `and
`static/image/`, respectively, and templates go into `templates/`. Translations
go into `locales/`. Tests go in `static/tests/backend/specs/` and
`static/tests/frontend/specs/`.
A Standard directory structure like this makes it easier to navigate through
your code. That said, do note, that this is not actually *required* to make your
plugin run. If you want to make use of our i18n system, you need to put your
translations into `locales/`, though, in order to have them integrated. (See
"Localization" for more info on how to localize your plugin.)
## Plugin definition
Your plugin definition goes into `ep.json`. In this file you register your hooks, indicate the parts of your plugin and the order of execution. (A documentation of all available events to hook into can be found in chapter [hooks](#all_hooks).)
A hook registration is a pairs of a hook name and a function reference (filename to require() + exported function name)
Your plugin definition goes into `ep.json`. In this file you register your hook
functions, indicate the parts of your plugin and the order of execution. (A
documentation of all available events to hook into can be found in chapter
[hooks](#all_hooks).)
```json
{
@ -30,33 +71,78 @@ A hook registration is a pairs of a hook name and a function reference (filename
{
"name": "nameThisPartHoweverYouWant",
"hooks": {
"authenticate" : "ep_<plugin>/<file>:FUNCTIONNAME1",
"expressCreateServer": "ep_<plugin>/<file>:FUNCTIONNAME2"
"authenticate": "ep_<plugin>/<file>:functionName1",
"expressCreateServer": "ep_<plugin>/<file>:functionName2"
},
"client_hooks": {
"acePopulateDOMLine": "ep_plugin/<file>:FUNCTIONNAME3"
"acePopulateDOMLine": "ep_<plugin>/<file>:functionName3"
}
}
]
}
```
Etherpad-lite will expect the part of the hook definition before the colon to be a javascript file and will try to require it. The part after the colon is expected to be a valid function identifier of that module. So, you have to export your hooks, using [`module.exports`](https://nodejs.org/docs/latest/api/modules.html#modules_modules) and register it in `ep.json` as `ep_<plugin>/path/to/<file>:FUNCTIONNAME`.
You can omit the `FUNCTIONNAME` part, if the exported function has got the same name as the hook. So `"authorize" : "ep_flubberworm/foo"` will call the function `exports.authorize` in `ep_flubberworm/foo.js`
A hook function registration maps a hook name to a hook function specification.
The hook function specification looks like `ep_example/file.js:functionName`. It
consists of two parts separated by a colon: a module name to `require()` and the
name of a function exported by the named module. See
[`module.exports`](https://nodejs.org/docs/latest/api/modules.html#modules_module_exports)
for how to export a function.
For the module name you can omit the `.js` suffix, and if the file is `index.js`
you can use just the directory name. You can also omit the module name entirely,
in which case it defaults to the plugin name (e.g., `ep_example`).
You can also omit the function name. If you do, Etherpad will look for an
exported function whose name matches the name of the hook (e.g.,
`authenticate`).
If either the module name or the function name is omitted (or both), the colon
may also be omitted unless the provided module name contains a colon. (So if the
module name is `C:\foo.js` then the hook function specification with the
function name omitted would be `"C:\\foo.js:"`.)
Examples: Suppose the plugin name is `ep_example`. All of the following are
equivalent, and will cause the `authorize` hook to call the `exports.authorize`
function in `index.js` from the `ep_example` plugin:
* `"authorize": "ep_example/index.js:authorize"`
* `"authorize": "ep_example/index.js:"`
* `"authorize": "ep_example/index.js"`
* `"authorize": "ep_example/index:authorize"`
* `"authorize": "ep_example/index:"`
* `"authorize": "ep_example/index"`
* `"authorize": "ep_example:authorize"`
* `"authorize": "ep_example:"`
* `"authorize": "ep_example"`
* `"authorize": ":authorize"`
* `"authorize": ":"`
* `"authorize": ""`
### Client hooks and server hooks
There are server hooks, which will be executed on the server (e.g. `expressCreateServer`), and there are client hooks, which are executed on the client (e.g. `acePopulateDomLine`). Be sure to not make assumptions about the environment your code is running in, e.g. don't try to access `process`, if you know your code will be run on the client, and likewise, don't try to access `window` on the server...
There are server hooks, which will be executed on the server (e.g.
`expressCreateServer`), and there are client hooks, which are executed on the
client (e.g. `acePopulateDomLine`). Be sure to not make assumptions about the
environment your code is running in, e.g. don't try to access `process`, if you
know your code will be run on the client, and likewise, don't try to access
`window` on the server...
### Styling
When you install a client-side plugin (e.g. one that implements at least one client-side hook), the plugin name is added to the `class` attribute of the div `#editorcontainerbox` in the main window.
This gives you the opportunity of tuning the appearance of the main UI in your plugin.
When you install a client-side plugin (e.g. one that implements at least one
client-side hook), the plugin name is added to the `class` attribute of the div
`#editorcontainerbox` in the main window. This gives you the opportunity of
tuning the appearance of the main UI in your plugin.
For example, this is the markup with no plugins installed:
```html
<div id="editorcontainerbox" class="">
```
and this is the contents after installing `someplugin`:
```html
<div id="editorcontainerbox" class="ep_someplugin">
```
@ -64,7 +150,11 @@ and this is the contents after installing `someplugin`:
This feature was introduced in Etherpad **1.8**.
### Parts
As your plugins become more and more complex, you will find yourself in the need to manage dependencies between plugins. E.g. you want the hooks of a certain plugin to be executed before (or after) yours. You can also manage these dependencies in your plugin definition file `ep.json`:
As your plugins become more and more complex, you will find yourself in the need
to manage dependencies between plugins. E.g. you want the hooks of a certain
plugin to be executed before (or after) yours. You can also manage these
dependencies in your plugin definition file `ep.json`:
```json
{
@ -91,21 +181,41 @@ As your plugins become more and more complex, you will find yourself in the need
}
```
Usually a plugin will add only one functionality at a time, so it will probably only use one `part` definition to register its hooks. However, sometimes you have to put different (unrelated) functionalities into one plugin. For this you will want use parts, so other plugins can depend on them.
Usually a plugin will add only one functionality at a time, so it will probably
only use one `part` definition to register its hooks. However, sometimes you
have to put different (unrelated) functionalities into one plugin. For this you
will want use parts, so other plugins can depend on them.
#### pre/post
The `"pre"` and `"post"` definitions, affect the order in which parts of a plugin are executed. This ensures that plugins and their hooks are executed in the correct order.
`"pre"` lists parts that must be executed *before* the defining part. `"post"` lists parts that must be executed *after* the defining part.
The `"pre"` and `"post"` definitions, affect the order in which parts of a
plugin are executed. This ensures that plugins and their hooks are executed in
the correct order.
You can, on a basic level, think of this as double-ended dependency listing. If you have a dependency on another plugin, you can make sure it loads before yours by putting it in `"pre"`. If you are setting up things that might need to be used by a plugin later, you can ensure proper order by putting it in `"post"`.
`"pre"` lists parts that must be executed *before* the defining part. `"post"`
lists parts that must be executed *after* the defining part.
Note that it would be far more sane to use `"pre"` in almost any case, but if you want to change config variables for another plugin, or maybe modify its environment, `"post"` could definitely be useful.
You can, on a basic level, think of this as double-ended dependency listing. If
you have a dependency on another plugin, you can make sure it loads before yours
by putting it in `"pre"`. If you are setting up things that might need to be
used by a plugin later, you can ensure proper order by putting it in `"post"`.
Also, note that dependencies should *also* be listed in your package.json, so they can be `npm install`'d automagically when your plugin gets installed.
Note that it would be far more sane to use `"pre"` in almost any case, but if
you want to change config variables for another plugin, or maybe modify its
environment, `"post"` could definitely be useful.
Also, note that dependencies should *also* be listed in your package.json, so
they can be `npm install`'d automagically when your plugin gets installed.
## Package definition
Your plugin must also contain a [package definition file](https://docs.npmjs.com/files/package.json), called package.json, in the project root - this file contains various metadata relevant to your plugin, such as the name and version number, author, project hompage, contributors, a short description, etc. If you publish your plugin on npm, these metadata are used for package search etc., but it's necessary for Etherpad-lite plugins, even if you don't publish your plugin.
Your plugin must also contain a [package definition
file](https://docs.npmjs.com/files/package.json), called package.json, in the
project root - this file contains various metadata relevant to your plugin, such
as the name and version number, author, project hompage, contributors, a short
description, etc. If you publish your plugin on npm, these metadata are used for
package search etc., but it's necessary for Etherpad plugins, even if you don't
publish your plugin.
```json
{
@ -120,16 +230,18 @@ Your plugin must also contain a [package definition file](https://docs.npmjs.com
```
## Templates
If your plugin adds or modifies the front end HTML (e.g. adding buttons or changing their functions), you should put the necessary HTML code for such operations in `templates/`, in files of type ".ejs", since Etherpad uses EJS for HTML templating. See the following link for more information about EJS: <https://github.com/visionmedia/ejs>.
If your plugin adds or modifies the front end HTML (e.g. adding buttons or
changing their functions), you should put the necessary HTML code for such
operations in `templates/`, in files of type ".ejs", since Etherpad uses EJS for
HTML templating. See the following link for more information about EJS:
<https://github.com/visionmedia/ejs>.
## Writing and running front-end tests for your plugin
Etherpad allows you to easily create front-end tests for plugins.
1. Create a new folder
```
%your_plugin%/static/tests/frontend/specs
```
2. Put your spec file in here (Example spec files are visible in %etherpad_root_folder%/frontend/tests/specs)
3. Visit http://yourserver.com/frontend/tests your front-end tests will run.
1. Create a new folder: `%your_plugin%/static/tests/frontend/specs`
2. Put your spec file in there. (Example spec files are visible in
`%etherpad_root_folder%/frontend/tests/specs`.)
3. Visit http://yourserver.com/frontend/tests and your front-end tests will run.

1
node_modules/ep_etherpad-lite generated vendored Symbolic link
View File

@ -0,0 +1 @@
../src

10704
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

105
package.json Normal file
View File

@ -0,0 +1,105 @@
{
"dependencies": {
"ep_etherpad-lite": "file:src"
},
"devDependencies": {
"eslint": "^7.15.0",
"eslint-config-etherpad": "^1.0.20",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-arrow": "^1.2.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0"
},
"eslintConfig": {
"ignorePatterns": [
"/src/",
"/tests/frontend/lib/"
],
"overrides": [
{
"files": [
"**/.eslintrc.js"
],
"extends": "etherpad/node"
},
{
"files": [
"**/*"
],
"excludedFiles": [
"**/.eslintrc.js",
"tests/frontend/**/*"
],
"extends": "etherpad/node"
},
{
"files": [
"tests/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests",
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
},
{
"files": [
"tests/backend/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests/backend",
"overrides": [
{
"files": [
"tests/backend/**/*"
],
"excludedFiles": [
"tests/backend/specs/**/*"
],
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
}
]
},
{
"files": [
"tests/frontend/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests/frontend",
"overrides": [
{
"files": [
"tests/frontend/**/*"
],
"excludedFiles": [
"tests/frontend/specs/**/*"
],
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
}
]
}
],
"root": true
},
"scripts": {
"lint": "eslint ."
},
"engines": {
"node": ">=10.13.0"
}
}

View File

@ -260,12 +260,6 @@
*/
"editOnly": "${EDIT_ONLY:false}",
/*
* If set to true, those users who have a valid session will automatically be
* granted access to password protected pads.
*/
"sessionNoPassword": "${SESSION_NO_PASSWORD:false}",
/*
* If true, all css & js will be minified before sending to the client.
*
@ -336,6 +330,24 @@
*/
"trustProxy": "${TRUST_PROXY:false}",
/*
* Settings controlling the session cookie issued by Etherpad.
*/
"cookie": {
/*
* Value of the SameSite cookie property. "Lax" is recommended unless
* Etherpad will be embedded in an iframe from another site, in which case
* this must be set to "None". Note: "None" will not work (the browser will
* not send the cookie to Etherpad) unless https is used to access Etherpad
* (either directly or via a reverse proxy with "trustProxy" set to true).
*
* "Strict" is not recommended because it has few security benefits but
* significant usability drawbacks vs. "Lax". See
* https://stackoverflow.com/q/41841880 for discussion.
*/
"sameSite": "${COOKIE_SAME_SITE:Lax}"
},
/*
* Privacy: disable IP logging
*/
@ -391,10 +403,22 @@
},
/*
* Users for basic authentication.
* User accounts. These accounts are used by:
* - default HTTP basic authentication if no plugin handles authentication
* - some but not all authentication plugins
* - some but not all authorization plugins
*
* is_admin = true gives access to /admin.
* If you do not uncomment this, /admin will not be available!
* User properties:
* - password: The user's password. Some authentication plugins will ignore
* this.
* - is_admin: true gives access to /admin. Defaults to false. If you do not
* uncomment this, /admin will not be available!
* - readOnly: If true, this user will not be able to create new pads or
* modify existing pads. Defaults to false.
* - canCreate: If this is true and readOnly is false, this user can create
* new pads. Defaults to true.
*
* Authentication and authorization plugins may define additional properties.
*
* WARNING: passwords should not be stored in plaintext in this file.
* If you want to mitigate this, please install ep_hash_auth and

View File

@ -263,12 +263,6 @@
*/
"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.
*
@ -339,6 +333,24 @@
*/
"trustProxy": false,
/*
* Settings controlling the session cookie issued by Etherpad.
*/
"cookie": {
/*
* Value of the SameSite cookie property. "Lax" is recommended unless
* Etherpad will be embedded in an iframe from another site, in which case
* this must be set to "None". Note: "None" will not work (the browser will
* not send the cookie to Etherpad) unless https is used to access Etherpad
* (either directly or via a reverse proxy with "trustProxy" set to true).
*
* "Strict" is not recommended because it has few security benefits but
* significant usability drawbacks vs. "Lax". See
* https://stackoverflow.com/q/41841880 for discussion.
*/
"sameSite": "Lax"
},
/*
* Privacy: disable IP logging
*/
@ -394,10 +406,22 @@
},
/*
* Users for basic authentication.
* User accounts. These accounts are used by:
* - default HTTP basic authentication if no plugin handles authentication
* - some but not all authentication plugins
* - some but not all authorization plugins
*
* is_admin = true gives access to /admin.
* If you do not uncomment this, /admin will not be available!
* User properties:
* - password: The user's password. Some authentication plugins will ignore
* this.
* - is_admin: true gives access to /admin. Defaults to false. If you do not
* uncomment this, /admin will not be available!
* - readOnly: If true, this user will not be able to create new pads or
* modify existing pads. Defaults to false.
* - canCreate: If this is true and readOnly is false, this user can create
* new pads. Defaults to true.
*
* Authentication and authorization plugins may define additional properties.
*
* WARNING: passwords should not be stored in plaintext in this file.
* If you want to mitigate this, please install ep_hash_auth and
@ -467,19 +491,6 @@
*/
"importMaxFileSize": 52428800, // 50 * 1024 * 1024
/*
* From Etherpad 1.8.3 onwards import was restricted to authors who had
* content within the pad.
*
* This setting will override that restriction and allow any user to import
* without the requirement to add content to a pad.
*
* This setting is useful for when you use a plugin for authentication so you
* can already trust each user.
*/
"allowAnyoneToImport": false,
/*
* From Etherpad 1.9.0 onwards, when Etherpad is in production mode commits from individual users are rate limited
*

View File

@ -1,29 +1,123 @@
{
"parts": [
{ "name": "express", "hooks": {
"createServer": "ep_etherpad-lite/node/hooks/express:createServer",
"restartServer": "ep_etherpad-lite/node/hooks/express:restartServer"
} },
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } },
{ "name": "i18n", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/i18n:expressCreateServer" } },
{ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } },
{ "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } },
{ "name": "padreadonly", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly:expressCreateServer" } },
{ "name": "webaccess", "hooks": { "expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess:expressConfigure" } },
{ "name": "apicalls", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls:expressCreateServer" } },
{ "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } },
{ "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } },
{ "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } },
{ "name": "tests", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests:expressCreateServer" } },
{ "name": "admin", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin:expressCreateServer" } },
{ "name": "adminplugins", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" }
{
"name": "DB",
"hooks": {
"shutdown": "ep_etherpad-lite/node/db/DB"
}
},
{ "name": "adminsettings", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" }
{
"name": "Minify",
"hooks": {
"shutdown": "ep_etherpad-lite/node/utils/Minify"
}
},
{ "name": "openapi", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/openapi:expressCreateServer" } }
{
"name": "express",
"hooks": {
"createServer": "ep_etherpad-lite/node/hooks/express",
"restartServer": "ep_etherpad-lite/node/hooks/express",
"shutdown": "ep_etherpad-lite/node/hooks/express"
}
},
{
"name": "static",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/static"
}
},
{
"name": "stats",
"hooks": {
"shutdown": "ep_etherpad-lite/node/stats"
}
},
{
"name": "i18n",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/i18n"
}
},
{
"name": "specialpages",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages"
}
},
{
"name": "padurlsanitize",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
}
},
{
"name": "padreadonly",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly"
}
},
{
"name": "webaccess",
"hooks": {
"expressConfigure": "ep_etherpad-lite/node/hooks/express/webaccess"
}
},
{
"name": "apicalls",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls"
}
},
{
"name": "importexport",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport"
}
},
{
"name": "errorhandling",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
}
},
{
"name": "socketio",
"hooks": {
"expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio",
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio"
}
},
{
"name": "tests",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/tests"
}
},
{
"name": "admin",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin"
}
},
{
"name": "adminplugins",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins",
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
}
},
{
"name": "adminsettings",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings",
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
}
},
{
"name": "openapi",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/openapi"
}
}
]
}

View File

@ -65,9 +65,6 @@
"timeslider.month.december": "Desember",
"pad.userlist.entername": "Verskaf u naam",
"pad.userlist.unnamed": "sonder naam",
"pad.userlist.guest": "Gas",
"pad.userlist.deny": "Keur af",
"pad.userlist.approve": "Keur goed",
"pad.impexp.importbutton": "Voer nou in",
"pad.impexp.importing": "Besig met invoer...",
"pad.impexp.importfailed": "Invoer het gefaal"

View File

@ -14,6 +14,11 @@
"محمد أحمد عبد الفتاح"
]
},
"admin_plugins.description": "الوصف",
"admin_plugins.name": "الاسم",
"admin_plugins.version": "الإصدار",
"admin_plugins_info.version_number": "رقم الإصدار",
"admin_settings": "إعدادات",
"index.newPad": "باد جديد",
"index.createOpenPad": "أو صنع/فتح باد بوضع اسمه:",
"index.openPad": "افتح باد موجودة بالاسم:",
@ -38,9 +43,7 @@
"pad.colorpicker.cancel": "إلغاء",
"pad.loading": "جارٍ التحميل...",
"pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!",
"pad.passwordRequired": "تحتاج إلى كلمة سر للوصول إلى هذا الباد",
"pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد",
"pad.wrongPassword": "كانت كلمة السر خاطئة",
"pad.settings.padSettings": "إعدادات الباد",
"pad.settings.myView": "رؤيتي",
"pad.settings.stickychat": "الدردشة دائما على الشاشة",
@ -128,9 +131,6 @@
"pad.savedrevs.timeslider": "يمكنك عرض المراجعات المحفوظة بزيارة متصفح التاريخ",
"pad.userlist.entername": "أدخل اسمك",
"pad.userlist.unnamed": "غير مسمى",
"pad.userlist.guest": "ضيف",
"pad.userlist.deny": "رفض",
"pad.userlist.approve": "موافقة",
"pad.editbar.clearcolors": "مسح ألوان التأليف أو المستند بأكمله؟ هذا لا يمكن التراجع عنه",
"pad.impexp.importbutton": "الاستيراد الآن",
"pad.impexp.importing": "الاستيراد...",
@ -141,6 +141,5 @@
"pad.impexp.importfailed": "فشل الاستيراد",
"pad.impexp.copypaste": "الرجاء نسخ/لصق",
"pad.impexp.exportdisabled": "تصدير التنسيق {{type}} معطل. يرجى الاتصال بمسؤول النظام الخاص بك للحصول على التفاصيل.",
"pad.impexp.maxFileSize": "الملف كبير جدا. اتصل بإداري الموقع الخاص بك لزيادة حجم الملف المسموح به للاستيراد",
"pad.impexp.permission": "الاستيراد معطل لأنك لم تساهم مسبقا لهذه الباد. من فضلك ساهم على الأقل مرة واحدة قبل الاستيراد"
"pad.impexp.maxFileSize": "الملف كبير جدا. اتصل بإداري الموقع الخاص بك لزيادة حجم الملف المسموح به للاستيراد"
}

View File

@ -28,9 +28,7 @@
"pad.colorpicker.cancel": "Encaboxar",
"pad.loading": "Cargando...",
"pad.noCookie": "Nun pudo alcontrase la cookie. ¡Por favor, permite les cookies nel navegador! La sesión y preferencies nun se guarden ente visites. Esto pué debese a qu'Etherpad inclúyese nun iFrame en dalgunos restoladores. Asegúrate de qu'Etherpad tea nel mesmu subdominiu/dominiu que la iFrame padre",
"pad.passwordRequired": "Necesites una contraseña pa entrar a esti bloc",
"pad.permissionDenied": "Nun tienes permisu pa entrar a esti bloc",
"pad.wrongPassword": "La contraseña era incorreuta",
"pad.settings.padSettings": "Configuración del bloc",
"pad.settings.myView": "la mio vista",
"pad.settings.stickychat": "Alderique en pantalla siempres",
@ -118,9 +116,6 @@
"pad.savedrevs.timeslider": "Pues ver les revisiones guardaes visitando la llinia temporal",
"pad.userlist.entername": "Escribi'l to nome",
"pad.userlist.unnamed": "ensin nome",
"pad.userlist.guest": "Invitáu",
"pad.userlist.deny": "Refugar",
"pad.userlist.approve": "Aprobar",
"pad.editbar.clearcolors": "¿Llimpiar los colores d'autoría nel documentu ensembre? Esto nun pue desfacese",
"pad.impexp.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...",
@ -131,6 +126,5 @@
"pad.impexp.importfailed": "Falló la importación",
"pad.impexp.copypaste": "Por favor, copia y apega",
"pad.impexp.exportdisabled": "La esportación en formatu {{type}} ta desactivada. Por favor, comunica col alministrador del sistema pa más detalles.",
"pad.impexp.maxFileSize": "El ficheru ye demasiao grande. Comunícate col alministrador del sitiu p'aumentar el tamañu de ficheru permitíu na importación",
"pad.impexp.permission": "La importación ta desactivada porque nunca contribuisti nesti bloc. Contribuye polo menos una vez antes d'importar"
"pad.impexp.maxFileSize": "El ficheru ye demasiao grande. Comunícate col alministrador del sitiu p'aumentar el tamañu de ficheru permitíu na importación"
}

View File

@ -18,7 +18,6 @@
"pad.colorpicker.save": "सहेजा जाय",
"pad.colorpicker.cancel": "रद्द करा जाय",
"pad.loading": "लोड होत है...",
"pad.wrongPassword": "आप कय पासवर्ड गलत रहा",
"pad.settings.padSettings": "प्याड सेटिङ्ग",
"pad.settings.myView": "हमार दृष्य",
"pad.settings.colorcheck": "लेखकीय रङ्ग",
@ -58,9 +57,6 @@
"timeslider.month.december": "डिसेम्बर",
"timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: author, other: authors ]}",
"pad.userlist.unnamed": "बेनामी",
"pad.userlist.guest": "पहुना",
"pad.userlist.deny": "अस्वीकार",
"pad.userlist.approve": "स्वीकृत",
"pad.impexp.importing": "आयात होत है...",
"pad.impexp.importfailed": "आयात असफल रहा",
"pad.impexp.copypaste": "कृपया कपी पेस्ट कीन जाय"

View File

@ -5,6 +5,7 @@
"Archaeodontosaurus",
"Khan27",
"Mastizada",
"MuratTheTurkish",
"Mushviq Abdulla",
"Neriman2003",
"Vesely35",
@ -33,10 +34,8 @@
"pad.colorpicker.save": "Saxla",
"pad.colorpicker.cancel": "İmtina",
"pad.loading": "Yüklənir...",
"pad.noCookie": "Çərəz tapıla bilmədi. Lütfən səyyahınızda çərəzlərə icazə verinǃ",
"pad.passwordRequired": "Bu lövhəyə daxil olmaq üçün parol lazımdır",
"pad.noCookie": "Çərəz tapıla bilmədi. Lütfən səyyahınızda çərəzlərə icazə verinǃ! Səfəriniz və ayarlarınız ziyarətlər arasında qeyd olunmayacaq. Bunun səbəbi, Etherpad'ın bəzi brauzerlərdə iFrame-ə daxil edilməsidir. Zəhmət olmasa Etherpad'ın ana iFrame ilə eyni alt etki/domendə olduğundan əmin olun",
"pad.permissionDenied": "Bu lövhəyə daxil olmaq üçün icazəniz yoxdur",
"pad.wrongPassword": "Sizin parolunuz səhvdir",
"pad.settings.padSettings": "Lövhə nizamlamaları",
"pad.settings.myView": "Mənim Görüntüm",
"pad.settings.stickychat": "Söhbət həmişə ekranda",
@ -59,9 +58,9 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (açıq sənəd formatı)",
"pad.importExport.abiword.innerHTML": "Siz yalnız adi mətndən və ya HTML-dən idxal edə bilərsiniz. İdxalın daha mürəkkəb funksiyaları üçün, zəhmət olmasa, <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 quraşdırın</a>.",
"pad.importExport.abiword.innerHTML": "Siz yalnız adi mətndən və ya HTML-dən idxal edə bilərsiniz. İdxalın daha mürəkkəb funksiyaları üçün, zəhmət olmasa, <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 və ya LibreOffice quraşdırın</a>.",
"pad.modals.connected": "Bağlandı.",
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur..",
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur",
"pad.modals.forcereconnect": "Məcbur təkrarən bağlan",
"pad.modals.reconnecttimer": "Yenidən qoşulur",
"pad.modals.cancel": "Ləğv et",
@ -124,10 +123,7 @@
"pad.savedrevs.timeslider": "Siz görə bilərsiniz saxlanılan versiyası miqyasında vaxt",
"pad.userlist.entername": "Adınızı daxil edin",
"pad.userlist.unnamed": "adsız",
"pad.userlist.guest": "Qonaq",
"pad.userlist.deny": "İnkar etmək",
"pad.userlist.approve": "Təsdiqləmək",
"pad.editbar.clearcolors": "Bütün sənədlərdə müəllif rəngləri təmizlənsin?",
"pad.editbar.clearcolors": "Bütün sənədlərdə müəllif rəngləri təmizlənsin? Bu geri qaytarıla bilməz",
"pad.impexp.importbutton": "İndi idxal et",
"pad.impexp.importing": "İdxal...",
"pad.impexp.confirmimport": "Faylın idxalı lövhədəki cari mətni yeniləyəcək. Davam etmək istədiyinizə əminsinizmi?",

View File

@ -31,9 +31,7 @@
"pad.colorpicker.cancel": "وازگئچ",
"pad.loading": "یوکلنیر...",
"pad.noCookie": "کوکی تاپیلمادی. لوطفن براوزرینیزده کوکیلره ایجازه وئرین!",
"pad.passwordRequired": "بو نوت دفترچه سینه ال تاپماق اوچون بیر رمزه احتیاجینیز واردیر.",
"pad.permissionDenied": "بو نوت دفترچه سینه ال تاپماق اوچون ایجازه نیز یوخدور.",
"pad.wrongPassword": "سیزین رمزینیز دوز دئییل",
"pad.settings.padSettings": "یادداشت دفترچه سینین تنظیملر",
"pad.settings.myView": "منیم گورنتوم",
"pad.settings.stickychat": "نمایش صفحه سینده همیشه چت اولسون",
@ -107,9 +105,6 @@
"pad.savedrevs.marked": "بۇ نوسخه ایندی ذخیره اوْلونموش کیمی علامتلندی.",
"pad.userlist.entername": "آدینیزی یازین",
"pad.userlist.unnamed": "آدسیز",
"pad.userlist.guest": "قوْناق",
"pad.userlist.deny": "دانماق",
"pad.userlist.approve": "اوْنایلا",
"pad.editbar.clearcolors": "بوتون سندلرده یازار بوْیالاری سیلینسین می؟",
"pad.impexp.importbutton": "ایندی ایچری گتیر",
"pad.impexp.importing": "ایچری گتیریلیر...",

View File

@ -27,9 +27,7 @@
"pad.colorpicker.save": "زاپاس کورتین",
"pad.colorpicker.cancel": "کنسیل",
"pad.loading": "...بار بیت",
"pad.passwordRequired": "برای دسترسی به این دفترچه یادداشت نیاز به یک گذرواژه دارید",
"pad.permissionDenied": "شرمنده، شما را اجازت په دسترسی ای صفحه نیست.",
"pad.wrongPassword": "گذرواژه‌ی شما درست نیست",
"pad.settings.padSettings": "تنظیمات دفترچه یادداشت",
"pad.settings.myView": "منی سۏج",
"pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد",
@ -105,9 +103,6 @@
"pad.savedrevs.marked": "این بازنویسی هم اکنون به عنوان ذخیره شده علامت‌گذاری شد",
"pad.userlist.entername": "وتی یوزرنامء بلک ات",
"pad.userlist.unnamed": "بدون نام",
"pad.userlist.guest": "مهمان",
"pad.userlist.deny": "رد کردن",
"pad.userlist.approve": "تایید",
"pad.editbar.clearcolors": "رنگ نویسندگی از همه‌ی سند پاک شود؟",
"pad.impexp.importbutton": "هم اکنون درون‌ریزی کن",
"pad.impexp.importing": "در حال درون‌ریزی...",

View File

@ -31,9 +31,7 @@
"pad.colorpicker.cancel": "Скасаваць",
"pad.loading": "Загрузка...",
"pad.noCookie": "Кукі ня знойдзеныя. Калі ласка, дазвольце кукі ў вашым браўзэры! Паміж наведваньнямі вашая сэсія і налады ня будуць захаваныя. Гэта можа адбывацца таму, што ў некаторых броўзэрах Etherpad заключаны ўнутры iFrame. Праверце, калі ласка, што Etherpad знаходзіцца ў тым жа паддамэне/дамэне, што і бацькоўскі iFrame",
"pad.passwordRequired": "Для доступу да гэтага дакумэнта патрэбны пароль",
"pad.permissionDenied": "Вы ня маеце дазволу на доступ да гэтага дакумэнта",
"pad.wrongPassword": "Вы ўвялі няслушны пароль",
"pad.settings.padSettings": "Налады дакумэнта",
"pad.settings.myView": "Мой выгляд",
"pad.settings.stickychat": "Заўсёды паказваць чат",
@ -59,7 +57,7 @@
"pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта...",
"pad.modals.forcereconnect": "Прымусовае перападлучэньне",
"pad.modals.reconnecttimer": "Спрабуем перападключыцца праз",
"pad.modals.cancel": "Адмяніць",
"pad.modals.cancel": "Скасаваць",
"pad.modals.userdup": "Адкрыта ў іншым акне",
"pad.modals.userdup.explanation": "Падобна, дакумэнт адкрыты больш чым у адным акне браўзэра на гэтым кампутары.",
"pad.modals.userdup.advice": "Паўторна падключыць з выкарыстаньнем гэтага акна.",
@ -119,9 +117,6 @@
"pad.savedrevs.timeslider": "Вы можаце пабачыць захаваныя вэрсіі з дапамогай шкалы часу",
"pad.userlist.entername": "Увядзіце вашае імя",
"pad.userlist.unnamed": "безыменны",
"pad.userlist.guest": "Госьць",
"pad.userlist.deny": "Адхіліць",
"pad.userlist.approve": "Зацьвердзіць",
"pad.editbar.clearcolors": "Ачысьціць аўтарскія колеры ва ўсім дакумэнце? Гэта немагчыма будзе скасаваць",
"pad.impexp.importbutton": "Імпартаваць зараз",
"pad.impexp.importing": "Імпартаваньне…",
@ -132,6 +127,5 @@
"pad.impexp.importfailed": "Памылка імпарту",
"pad.impexp.copypaste": "Калі ласка, скапіюйце і ўстаўце",
"pad.impexp.exportdisabled": "Экспарт у фармаце {{type}} адключаны. Калі ласка, зьвярніцеся да вашага сыстэмнага адміністратара па падрабязнасьці.",
"pad.impexp.maxFileSize": "Файл завялікі. Зьвярніцеся да адміністратара сайту, каб павялічыць дазволены памер файлаў для імпарту",
"pad.impexp.permission": "Імпарт адключаны, бо вы ніколі не працавалі з гэтым нататнікам. Калі ласка, перад імпартам зрабеце хоць бы адзін унёсак"
"pad.impexp.maxFileSize": "Файл завялікі. Зьвярніцеся да адміністратара сайту, каб павялічыць дазволены памер файлаў для імпарту"
}

View File

@ -22,7 +22,6 @@
"pad.colorpicker.save": "Съхраняване",
"pad.colorpicker.cancel": "Отказ",
"pad.loading": "Зареждане...",
"pad.wrongPassword": "Неправилна парола",
"pad.settings.language": "Език:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
@ -67,6 +66,5 @@
"timeslider.month.october": "октомври",
"timeslider.month.november": "ноември",
"timeslider.month.december": "декември",
"pad.userlist.entername": "Въведете вашето име",
"pad.userlist.guest": "Гост"
"pad.userlist.entername": "Въведете вашето име"
}

View File

@ -24,7 +24,6 @@
"pad.colorpicker.save": "ذخیره",
"pad.colorpicker.cancel": "کنسیل",
"pad.loading": "لودینگ...",
"pad.wrongPassword": "شمی پاسورد جووان نه اینت",
"pad.settings.padSettings": "یاداشتئ دفترچه ئی تنظیمات",
"pad.settings.myView": "نئ دیست",
"pad.settings.stickychat": "هبر موچین وختا بی دیستئ تاکدیمئ سرا بیئت",
@ -63,9 +62,6 @@
"timeslider.unnamedauthors": "{{num}} بی نامین نویسوک",
"pad.userlist.entername": "وتئ ناما نیویشته بکنیت",
"pad.userlist.unnamed": "بی نام",
"pad.userlist.guest": "مهمان",
"pad.userlist.deny": "رد کورتین",
"pad.userlist.approve": "قبول کورتین",
"pad.impexp.importbutton": "انون بی تئ کن",
"pad.impexp.importing": "بی بی تئ کورتینی حالا...",
"pad.impexp.uploadFailed": "آپلود انجام نه بوت، پدا کوشش کن",

View File

@ -3,6 +3,7 @@
"authors": [
"Aftab1995",
"Aftabuzzaman",
"Al Riaz Uddin Ripon",
"Bellayet",
"Nasir8891",
"Sankarshan",
@ -10,6 +11,21 @@
"আফতাবুজ্জামান"
]
},
"admin_plugins.available_fetching": "আনা হচ্ছে...",
"admin_plugins.available_install.value": "ইন্সটল করুন",
"admin_plugins.description": "বিবরণ",
"admin_plugins.installed": "ইন্সটল হওয়া প্লাগিনসমূহ",
"admin_plugins.installed_fetching": "ইন্সটলকৃত প্লাগিন আনা হচ্ছে",
"admin_plugins.installed_uninstall.value": "আনইন্সটল করুন",
"admin_plugins.last-update": "সর্বশেষ হালনাগাদ",
"admin_plugins.name": "নাম",
"admin_plugins.version": "সংস্করণ",
"admin_plugins_info.version": "ইথারপ্যাড সংস্করণ",
"admin_plugins_info.version_latest": "সাম্প্রতিক উপলব্ধ সংস্করণ",
"admin_settings": "সেটিংসমূহ",
"admin_settings.current": "বর্তমান কনফিগারেশন",
"admin_settings.current_restart.value": "ইথারপ্যাড পুনরায় চালু করুন",
"admin_settings.current_save.value": "সেটিংসমূহ সংরক্ষণ করুন",
"index.newPad": "নতুন প্যাড",
"index.createOpenPad": "অথবা নাম লিখে প্যাড খুলুন/তৈরী করুন:",
"pad.toolbar.bold.title": "গাঢ় (Ctrl-B)",
@ -33,9 +49,7 @@
"pad.colorpicker.cancel": "বাতিল",
"pad.loading": "লোড হচ্ছে...",
"pad.noCookie": "কুকি পাওয়া যায়নি। দয়া করে আপনার ব্রাউজারে কুকি অনুমতি দিন!",
"pad.passwordRequired": "এই প্যাড-টি দেখার জন্য আপনাকে পাসওয়ার্ড ব্যবহার করতে হবে",
"pad.permissionDenied": "দুঃখিত, এ প্যাড-টি দেখার অধিকার আপনার নেই",
"pad.wrongPassword": "আপনার পাসওয়ার্ড সঠিক নয়",
"pad.settings.padSettings": "প্যাডের স্থাপন",
"pad.settings.myView": "আমার দৃশ্য",
"pad.settings.stickychat": "চ্যাট সক্রীনে প্রদর্শন করা হবে",
@ -100,9 +114,6 @@
"timeslider.unnamedauthors": "নামবিহীন {{num}} জন {[plural(num) one: লেখক, other: লেখক ]}",
"pad.userlist.entername": "আপনার নাম লিখুন",
"pad.userlist.unnamed": "কোন নাম নির্বাচন করা হয়নি",
"pad.userlist.guest": "অতিথি",
"pad.userlist.deny": "প্রত্যাখ্যান",
"pad.userlist.approve": "অনুমোদিত",
"pad.impexp.importbutton": "এখন আমদানি করুন",
"pad.impexp.importing": "আমদানি হচ্ছে...",
"pad.impexp.padHasData": "আমরা এই ফাইলটি আমদানি করতে সক্ষম হয়নি কারণ এই প্যাড ইতিমধ্যে পরিবর্তিত হয়েছে, দয়া করে একটি নতুন প্যাডে অামদানি করুন।",

View File

@ -30,9 +30,7 @@
"pad.colorpicker.cancel": "Nullañ",
"pad.loading": "O kargañ...",
"pad.noCookie": "N'eus ket gallet kavout an toupin. Aotreit an toupinoù en ho merdeer, mar plij !",
"pad.passwordRequired": "Ezhomm ho peus ur ger-tremen evit mont d'ar Pad-se",
"pad.permissionDenied": "\nN'oc'h ket aotreet da vont d'ar pad-mañ",
"pad.wrongPassword": "Fazius e oa ho ker-tremen",
"pad.settings.padSettings": "Arventennoù Pad",
"pad.settings.myView": "Ma diskwel",
"pad.settings.stickychat": "Diskwel ar flap bepred",
@ -118,9 +116,6 @@
"pad.savedrevs.timeslider": "Gallout a reot gwelet an adweladurioù enrollet en ur weladenniñ ar bignerez amzerel",
"pad.userlist.entername": "Ebarzhit hoc'h anv",
"pad.userlist.unnamed": "dizanv",
"pad.userlist.guest": "Den pedet",
"pad.userlist.deny": "Nac'h",
"pad.userlist.approve": "Aprouiñ",
"pad.editbar.clearcolors": "Diverkañ al livioù stag ouzh an aozerien en teul a-bezh ? Ne c'hallo ket bezañ disc'hraet",
"pad.impexp.importbutton": "Enporzhiañ bremañ",
"pad.impexp.importing": "Oc'h enporzhiañ...",
@ -131,6 +126,5 @@
"pad.impexp.importfailed": "C'hwitet eo an enporzhiadenn",
"pad.impexp.copypaste": "Eilit/pegit, mar plij",
"pad.impexp.exportdisabled": "Diweredekaet eo ezporzhiañ d'ar furmad {{type}}. Kit e darempred gant merour ar reizhiad evit gouzout hiroc'h.",
"pad.impexp.maxFileSize": "Re vras eo ar restr. Kit e daremrepd gant merour ho lec'hienn evit kreskiñ ment aoteet ar restroù evit enporzhiañ",
"pad.impexp.permission": "Diweredekaet eo an enporzhiañ peogwir n'hoc'h eus ket kemeret perzh gwech ebet er bloc'had-se. Kemerit perzh ur wech da nebeutañ a-raok enporzhiañ."
"pad.impexp.maxFileSize": "Re vras eo ar restr. Kit e daremrepd gant merour ho lec'hienn evit kreskiñ ment aoteet ar restroù evit enporzhiañ"
}

View File

@ -29,9 +29,7 @@
"pad.colorpicker.cancel": "Otkaži",
"pad.loading": "Učitavam...",
"pad.noCookie": "Kolačić nije pronađen. Molimo Vas dozvolite kolačiće u Vašem pregledniku!",
"pad.passwordRequired": "Treba Vam lozinka da bi ste pristupili ovom padu",
"pad.permissionDenied": "Nemate dopuštenje da pistupite ovom padu",
"pad.wrongPassword": "Vaša lozinka je pogrešna",
"pad.settings.padSettings": "Postavke stranice",
"pad.settings.myView": "Moj prikaz",
"pad.settings.stickychat": "Ćaskanje uvijek na ekranu",
@ -97,9 +95,6 @@
"pad.savedrevs.marked": "Ova revizija je sada označena kao sačuvana revizija",
"pad.userlist.entername": "Upišite svoje ime",
"pad.userlist.unnamed": "neimenovano",
"pad.userlist.guest": "Gost",
"pad.userlist.deny": "Odbij",
"pad.userlist.approve": "Odobri",
"pad.editbar.clearcolors": "Očisti autorske boje na čitavom dokumentu?",
"pad.impexp.importbutton": "Uvezi odmah",
"pad.impexp.importing": "Uvozim...",

View File

@ -13,19 +13,52 @@
"Toniher"
]
},
"admin.page-title": "Panell administratiu - Etherpad",
"admin_plugins": "Gestor de connectors",
"admin_plugins.available": "Connectors disponibles",
"admin_plugins.available_not-found": "No s'ha trobat cap complement.",
"admin_plugins.available_fetching": "Obtenint...",
"admin_plugins.available_install.value": "Instal·la",
"admin_plugins.available_search.placeholder": "Cerca connectors per instal·lar",
"admin_plugins.description": "Descripció",
"admin_plugins.installed": "Connectors instal·lats",
"admin_plugins.installed_fetching": "S'estan recuperant els connectors instal·lats...",
"admin_plugins.installed_nothing": "Encara no heu instal·lat cap connector.",
"admin_plugins.installed_uninstall.value": "Desinstal·la",
"admin_plugins.last-update": "Darrera actualització",
"admin_plugins.name": "Nom",
"admin_plugins.page-title": "Gestor de connectors - Etherpad",
"admin_plugins.version": "Versió",
"admin_plugins_info": "Informació de resolució de problemes",
"admin_plugins_info.hooks": "Actuadors instal·lats",
"admin_plugins_info.hooks_client": "Actuadors de la banda client",
"admin_plugins_info.hooks_server": "Actuadors del costat servidor",
"admin_plugins_info.parts": "Parts instal·lades",
"admin_plugins_info.plugins": "Connectors instal·lats",
"admin_plugins_info.page-title": "Informació del connector - Etherpad",
"admin_plugins_info.version": "Versió de l'Etherpad",
"admin_plugins_info.version_latest": "Última versió disponible",
"admin_plugins_info.version_number": "Número de versió",
"admin_settings": "Configuració",
"admin_settings.current": "Configuració actual",
"admin_settings.current_example-devel": "Plantilla d'exemple de configuració de desenvolupament",
"admin_settings.current_example-prod": "Exemple de model de paràmetres de producció",
"admin_settings.current_restart.value": "Reprèn Etherpad",
"admin_settings.current_save.value": "Desa la configuració",
"admin_settings.page-title": "Configuració - Etherpad",
"index.newPad": "Nou pad",
"index.createOpenPad": "o crea/obre un pad amb el nom:",
"index.openPad": "obrir un Pad existint amb el nom:",
"pad.toolbar.bold.title": "Negreta (Ctrl-B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
"pad.toolbar.underline.title": "Subratllat (Ctrl-U)",
"index.openPad": "obre un Pad existent amb el nom:",
"pad.toolbar.bold.title": "Negreta (Ctrl+B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl+I)",
"pad.toolbar.underline.title": "Subratllat (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Ratllat (Ctrl+5)",
"pad.toolbar.ol.title": "Llista ordenada (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Llista sense ordenar (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Sagnat (TAB)",
"pad.toolbar.unindent.title": "Sagnat invers (Majúsc+TAB)",
"pad.toolbar.undo.title": "Desfés (Ctrl-Z)",
"pad.toolbar.redo.title": "Refés (Ctrl-Y)",
"pad.toolbar.undo.title": "Desfés (Ctrl+Z)",
"pad.toolbar.redo.title": "Refés (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Neteja els colors d'autoria (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importa/exporta a partir de diferents formats de fitxer",
"pad.toolbar.timeslider.title": "Línia temporal",
@ -37,9 +70,7 @@
"pad.colorpicker.cancel": "Cancel·la",
"pad.loading": "S'està carregant...",
"pad.noCookie": "No s'ha trobat la galeta. Permeteu les galetes en el navegador!",
"pad.passwordRequired": "Us cal una contrasenya per a accedir a aquest pad",
"pad.permissionDenied": "No teniu permisos per a accedir a aquest pad",
"pad.wrongPassword": "La contrasenya és incorrecta",
"pad.settings.padSettings": "Paràmetres del pad",
"pad.settings.myView": "La meva vista",
"pad.settings.stickychat": "Xateja sempre a la pantalla",
@ -58,7 +89,7 @@
"pad.importExport.export": "Exporta el pad actual com a:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Text net",
"pad.importExport.exportplain": "Text sense format",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
@ -88,6 +119,8 @@
"pad.modals.deleted.explanation": "S'ha suprimit el pad.",
"pad.modals.rateLimited": "Tarifa limitada.",
"pad.modals.rateLimited.explanation": "Heu enviat massa missatges a aquest pad per això us han desconnectat.",
"pad.modals.rejected.explanation": "El servidor ha rebutjat un missatge enviat pel seu navegador.",
"pad.modals.rejected.cause": "Pot ser que el servidor s'hagi actualitzat mentre estàveu veient la plataforma, o potser hi ha un error a Etherpad. Intenta tornar a carregar la pàgina.",
"pad.modals.disconnected": "Heu estat desconnectat.",
"pad.modals.disconnected.explanation": "S'ha perdut la connexió amb el servidor",
"pad.modals.disconnected.cause": "El servidor sembla que no està disponible. Notifiqueu a l'administrador del servei si continua passant.",
@ -130,9 +163,6 @@
"pad.savedrevs.timeslider": "Les revisions que s'han desat les podeu veure amb la línia de temps",
"pad.userlist.entername": "Introduïu el vostre nom",
"pad.userlist.unnamed": "sense nom",
"pad.userlist.guest": "Convidat",
"pad.userlist.deny": "Refusa",
"pad.userlist.approve": "Aprova",
"pad.editbar.clearcolors": "Netejar els colors d'autor del document sencer?",
"pad.impexp.importbutton": "Importa ara",
"pad.impexp.importing": "Important...",
@ -143,6 +173,5 @@
"pad.impexp.importfailed": "Ha fallat la importació",
"pad.impexp.copypaste": "Si us plau, copieu i enganxeu",
"pad.impexp.exportdisabled": "Està inhabilitada l'exportació com a {{type}}. Contacteu amb el vostre administrador de sistemes per a més informació.",
"pad.impexp.maxFileSize": "Arxiu massa gran. Poseu-vos en contacte amb l'administrador del vostre lloc per augmentar la mida màxima dels fitxers importats",
"pad.impexp.permission": "La importació està desactivada perquè mai heu contribuït a aquest bloc. Si us plau, contribuïu almenys un cop abans d'importar"
"pad.impexp.maxFileSize": "Arxiu massa gran. Poseu-vos en contacte amb l'administrador del vostre lloc per augmentar la mida màxima dels fitxers importats"
}

View File

@ -36,9 +36,7 @@
"pad.colorpicker.cancel": "Zrušit",
"pad.loading": "Načítání...",
"pad.noCookie": "Nelze nalézt cookie. Povolte prosím cookie ve Vašem prohlížeči.",
"pad.passwordRequired": "Pro přístup k tomuto Padu je třeba znát heslo",
"pad.permissionDenied": "Nemáte oprávnění pro přístup k tomuto Padu",
"pad.wrongPassword": "Nesprávné heslo",
"pad.settings.padSettings": "Nastavení Padu",
"pad.settings.myView": "Vlastní pohled",
"pad.settings.stickychat": "Chat vždy na obrazovce",
@ -124,9 +122,6 @@
"pad.savedrevs.timeslider": "Návštěvou časové osy zobrazíte uložené revize",
"pad.userlist.entername": "Zadejte své jméno",
"pad.userlist.unnamed": "nejmenovaný",
"pad.userlist.guest": "Host",
"pad.userlist.deny": "Zakázat",
"pad.userlist.approve": "Povolit",
"pad.editbar.clearcolors": "Odstranit barvy autorů z celého dokumentu? Tuto změnu nelze vrátit!",
"pad.impexp.importbutton": "Importovat",
"pad.impexp.importing": "Importování…",

View File

@ -8,6 +8,10 @@
"Steenth"
]
},
"admin_plugins.available_fetching": "Henter...",
"admin_plugins.description": "Beskrivelse",
"admin_plugins.name": "Navn",
"admin_settings": "Indstillinger",
"index.newPad": "Ny Pad",
"index.createOpenPad": "eller opret/åbn en Pad med navnet:",
"pad.toolbar.bold.title": "Fed (Ctrl-B)",
@ -31,9 +35,7 @@
"pad.colorpicker.cancel": "Afbryd",
"pad.loading": "Indlæser ...",
"pad.noCookie": "Cookie kunne ikke findes. Tillad venligst cookier i din browser!",
"pad.passwordRequired": "Du skal bruge en adgangskode for at få adgang til denne pad",
"pad.permissionDenied": "Du har ikke tilladelse til at få adgang til denne pad.",
"pad.wrongPassword": "Din adgangskode er forkert",
"pad.settings.padSettings": "Pad indstillinger",
"pad.settings.myView": "Min visning",
"pad.settings.stickychat": "Chat altid på skærmen",
@ -119,9 +121,6 @@
"pad.savedrevs.timeslider": "Du kan se gemte revisioner ved at besøge tidslinjen",
"pad.userlist.entername": "Indtast dit navn",
"pad.userlist.unnamed": "ikke-navngivet",
"pad.userlist.guest": "Gæst",
"pad.userlist.deny": "Nægt",
"pad.userlist.approve": "Godkend",
"pad.editbar.clearcolors": "Fjern farver for ophavsmand i hele dokumentet? Dette kan ikke fortrydes",
"pad.impexp.importbutton": "Importer nu",
"pad.impexp.importing": "Importerer...",

View File

@ -3,6 +3,7 @@
"authors": [
"Bjarncraft",
"Dom",
"Killarnee",
"Metalhead64",
"Mklehr",
"Nipsky",
@ -13,6 +14,21 @@
"Wikinaut"
]
},
"admin_plugins": "Plugins verwalten",
"admin_plugins.available": "Verfügbare Plugins",
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
"admin_plugins.available_install.value": "Installieren",
"admin_plugins.description": "Beschreibung",
"admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.",
"admin_plugins.last-update": "Letze Aktualisierung",
"admin_plugins.name": "Name",
"admin_plugins.version": "Version",
"admin_plugins_info.hooks": "Installierte Hooks",
"admin_plugins_info.plugins": "Installierte Plugins",
"admin_plugins_info.version_number": "Versionsnummer",
"admin_settings": "Einstellungen",
"admin_settings.current": "Derzeitige Konfiguration",
"admin_settings.current_save.value": "Einstellungen speichern",
"index.newPad": "Neues Pad",
"index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:",
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
@ -37,9 +53,7 @@
"pad.colorpicker.cancel": "Abbrechen",
"pad.loading": "Lade …",
"pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.",
"pad.passwordRequired": "Du benötigst ein Kennwort, um auf dieses Pad zuzugreifen",
"pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen",
"pad.wrongPassword": "Dein eingegebenes Kennwort war falsch",
"pad.settings.padSettings": "Pad-Einstellungen",
"pad.settings.myView": "Eigene Ansicht",
"pad.settings.stickychat": "Unterhaltung immer anzeigen",
@ -51,7 +65,7 @@
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Sprache:",
"pad.settings.about": "Über",
"pad.settings.poweredBy": "Powered by $1",
"pad.settings.poweredBy": "Powered by",
"pad.importExport.import_export": "Import/Export",
"pad.importExport.import": "Textdatei oder Dokument hochladen",
"pad.importExport.importSuccessful": "Erfolgreich!",
@ -130,9 +144,6 @@
"pad.savedrevs.timeslider": "Du kannst gespeicherte Versionen durch den Aufruf des Bearbeitungsverlaufes ansehen.",
"pad.userlist.entername": "Dein Name?",
"pad.userlist.unnamed": "unbenannt",
"pad.userlist.guest": "Gast",
"pad.userlist.deny": "Verweigern",
"pad.userlist.approve": "Genehmigen",
"pad.editbar.clearcolors": "Autorenfarben im gesamten Dokument zurücksetzen? Dies kann nicht rückgängig gemacht werden",
"pad.impexp.importbutton": "Jetzt importieren",
"pad.impexp.importing": "Importiere …",
@ -143,6 +154,5 @@
"pad.impexp.importfailed": "Import fehlgeschlagen",
"pad.impexp.copypaste": "Bitte kopieren und einfügen",
"pad.impexp.exportdisabled": "Der Export im {{type}}-Format ist deaktiviert. Für Einzelheiten kontaktiere bitte deinen Systemadministrator.",
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen.",
"pad.impexp.permission": "Die Importfunktion ist deaktiviert, da du noch nichts zu diesem Pad beigetragen hast. Bitte trage etwas zu diesem Pad bei, bevor du importierst."
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen."
}

View File

@ -10,6 +10,39 @@
"Orbot707"
]
},
"admin.page-title": "Panoyê İdarekari - Etherpad",
"admin_plugins": "İdarekarê Dekerdeki",
"admin_plugins.available": "Mewcud Dekerdeki",
"admin_plugins.available_not-found": "Dekerdek nevineya",
"admin_plugins.available_fetching": "Aniyeno...",
"admin_plugins.available_install.value": "Bar ke",
"admin_plugins.available_search.placeholder": "Barbıyaye dekerdeka bıvinê",
"admin_plugins.description": ınasnayış",
"admin_plugins.installed": "Dekerdekê bariyayê",
"admin_plugins.installed_fetching": "Bariyayê dekerdeki gêriyenê...",
"admin_plugins.installed_nothing": "Heena şıma qet yew dekerdek bar nêkerdo",
"admin_plugins.installed_uninstall.value": "Wedarnê",
"admin_plugins.last-update": "Resnayışo Peyên",
"admin_plugins.name": "Name",
"admin_plugins.page-title": "İdarekarê dekerdeka - Etherpad",
"admin_plugins.version": "Versiyon",
"admin_plugins_info": "Melumatê xetay timari",
"admin_plugins_info.hooks": "Bariyaye qancey",
"admin_plugins_info.hooks_client": "Qancey kışta waşteri",
"admin_plugins_info.hooks_server": "Qancey kışta serveri",
"admin_plugins_info.parts": "Barbıyaye letey",
"admin_plugins_info.plugins": "Dekerdekê bariyayê",
"admin_plugins_info.page-title": "Melumatê dekerdeki - Etherpad",
"admin_plugins_info.version": "Versiyonê Etherpadi",
"admin_plugins_info.version_latest": "Mewcud versiyono peyên",
"admin_plugins_info.version_number": "Numrey versiyoni",
"admin_settings": "Eyari",
"admin_settings.current": "Konfigurasyono ravêrde",
"admin_settings.current_example-devel": "Şablonê emsalê ravêrberdışi eyari",
"admin_settings.current_example-prod": "Şablonê emsalê vıraştışê eyari",
"admin_settings.current_restart.value": "Etherpad'i reyna ake",
"admin_settings.current_save.value": "Eyaran qeyd ke",
"admin_settings.page-title": "Eyari - Etherpad",
"index.newPad": "Bloknoto newe",
"index.createOpenPad": "ya zi be nê nameyi ra yew bloknot vıraze/ake:",
"index.openPad": "yew Padê biyayeyi be nê nameyi ra ake:",
@ -34,9 +67,7 @@
"pad.colorpicker.cancel": "Bıtexelne",
"pad.loading": "Bar beno...",
"pad.noCookie": "Çerez nêvineya. Rovıter de çereza aktiv kerê.Ronıştısê u eyarê şıma mabênê ziyareti qeyd nêbenê.Çıkı, Etherpad tay rovıteran de tewrê yew iFrame belka biyo. Kerem ke Etherpad corên iFrame ya wa eyni bınca/ca de zey pê bo.",
"pad.passwordRequired": "Ena bloknot resayışi rê parola icab krna",
"pad.permissionDenied": "Ena bloknot resayışi rê icazeta şıma çıni ya",
"pad.wrongPassword": "Parola şıma ğeleta",
"pad.settings.padSettings": "Sazkerdışê Pedi",
"pad.settings.myView": "Asayışê mı",
"pad.settings.stickychat": "Ekran de tım mıhebet bıkerê",
@ -61,7 +92,7 @@
"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\">AbiWordi ya zi LibreOfficeyi bar kerên</a>.",
"pad.modals.connected": "Gıre diya.",
"pad.modals.reconnecting": "Bloknot da şıma rê fına irtibat kewê no",
"pad.modals.reconnecting": "Pada şıma rê fına irtibat kewê no",
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
"pad.modals.reconnecttimer": "Anciya gırê beno",
"pad.modals.cancel": "Bıtexelne",
@ -85,6 +116,8 @@
"pad.modals.deleted.explanation": "Ena ped wedariye",
"pad.modals.rateLimited": "Nısbeto kemeyeyın",
"pad.modals.rateLimited.explanation": "Na pad re ßıma vêşi mesac rışto, coki ra irtibat bıriyayo.",
"pad.modals.rejected.explanation": "Server, terefê browseri ra rışiyaye yew mesac red kerdo.",
"pad.modals.rejected.cause": ıma wexto ke ped weyniyayış de server belka biyo rocane ya ziEtherpad de yew xeta bena. Pela reyna bar kerê.",
"pad.modals.disconnected": "İrtibata şıma reyê",
"pad.modals.disconnected.explanation": "Rovıteri ya irtibata şıma reyyê",
"pad.modals.disconnected.cause": "Qay rovıtero nêkarên o. Ena xerpey deqam kena se idarekaranê sistemiya irtibat kewê",
@ -127,9 +160,6 @@
"pad.savedrevs.timeslider": "Xızberê zemani ziyer kerdış ra şıma şenê revizyonanê qeyd bıyayan bıvinê",
"pad.userlist.entername": "Namey xo cıkewe",
"pad.userlist.unnamed": "bêname",
"pad.userlist.guest": "Meyman",
"pad.userlist.deny": "Red ke",
"pad.userlist.approve": "Tesdiq ke",
"pad.editbar.clearcolors": "Wesiqa de renge nuştoğey bıesternê yê? No kar peyser nêgêrêno",
"pad.impexp.importbutton": "Nıka miyan ke",
"pad.impexp.importing": "Deyeno azere...",
@ -140,6 +170,5 @@
"pad.impexp.importfailed": "Zer kerdış mıwafaq nebı",
"pad.impexp.copypaste": "Reca keme kopya pronayış bıkeri",
"pad.impexp.exportdisabled": "Formatta {{type}} ya ateber kerdış dewra vıciya yo. Qandé teferruati idarekarana irtibat kewê",
"pad.impexp.maxFileSize": "Dosya zêde gırsa, azere kerdışi rê mısade deyaye ebatê dosyay zeydınayışi rê idarekarê siteya irtibat kewê",
"pad.impexp.permission": ıma ena ped rê qet iştirak nêkerdo coki ra azere kerdış dewre ra veto. Vêre azere kerdışi minimum yû iştirak bıkerê"
"pad.impexp.maxFileSize": "Dosya zêde gırsa, azere kerdışi rê mısade deyaye ebatê dosyay zeydınayışi rê idarekarê siteya irtibat kewê"
}

View File

@ -26,9 +26,7 @@
"pad.colorpicker.save": "Składowaś",
"pad.colorpicker.cancel": "Pśetergnuś",
"pad.loading": "Zacytujo se...",
"pad.passwordRequired": "Trjebaš gronidło, aby na toś ten zapisnik pśistup měł",
"pad.permissionDenied": "Njamaš pśistupne pšawo za toś ten zapisnik.",
"pad.wrongPassword": "Twójo gronidło jo wopaki było",
"pad.settings.padSettings": "Nastajenja zapisnika",
"pad.settings.myView": "Mój naglěd",
"pad.settings.stickychat": "Chat pśecej na wobrazowce pokazaś",
@ -104,9 +102,6 @@
"pad.savedrevs.marked": "Toś ta wersija jo se něnto ako składowana wersija markěrowała",
"pad.userlist.entername": "Zapódaj swójo mě",
"pad.userlist.unnamed": "bźez mjenja",
"pad.userlist.guest": "Gósć",
"pad.userlist.deny": "Wótpokazaś",
"pad.userlist.approve": "Pśizwóliś",
"pad.editbar.clearcolors": "Awtorowe barwy w cełem dokumenśe lašowaś?",
"pad.impexp.importbutton": "Něnto importěrowaś",
"pad.impexp.importing": "Importěrujo se...",

View File

@ -29,9 +29,7 @@
"pad.colorpicker.cancel": "खारेजी",
"pad.loading": "लोड हुन्नाछ़....",
"pad.noCookie": "कुकी पाउन नाइ सकियो। तमरा ब्राउजरमी कुकी राख्दाइ अनुमति दिय!",
"pad.passwordRequired": "यो प्याड खोल्लाकी पासवर्ड चाहिन्छ",
"pad.permissionDenied": "तमलाईँ यै प्याड खोल्लाकी अनुमति नाइथिन",
"pad.wrongPassword": "तमरो पासवर्ड गलत थ्यो",
"pad.settings.padSettings": "प्याड सेटिङ्गअन",
"pad.settings.myView": "मेरि हेराइ",
"pad.settings.stickychat": "जबलई पर्दामी कुरडी गद्य्या",
@ -114,9 +112,6 @@
"pad.savedrevs.timeslider": "समयस्लाइडर भेटिबर तम भँणार अरीयाऽ शंसोधनअनलाई हेरि सकन्छऽ",
"pad.userlist.entername": "तमरो नाउँ हाल",
"pad.userlist.unnamed": "बिननाउँइको",
"pad.userlist.guest": "पाउनो",
"pad.userlist.deny": "अस्वीकार",
"pad.userlist.approve": "अनुमोदन",
"pad.editbar.clearcolors": "सङताइ कागताजमी है लेखक रङ्ङअन साप अद्द्या?",
"pad.impexp.importbutton": "ऐलै आयात अरऽ",
"pad.impexp.importing": "आयात अद्दाछ़...",

View File

@ -33,9 +33,7 @@
"pad.colorpicker.cancel": "Άκυρο",
"pad.loading": "Φόρτωση...",
"pad.noCookie": "Το cookie δεν βρέθηκε. Παρακαλώ επιτρέψτε τα cookies στον περιηγητή σας! Η περίοδος σύνδεσης και οι ρυθμίσεις σας δεν θα αποθηκευτούν μεταξύ των επισκέψεων. Αυτό μπορεί να οφείλεται στο ότι το Etherpad περιλαμβάνεται σε ένα iFrame σε ορισμένα προγράμματα περιήγησης. Βεβαιωθείτε ότι το Etherpad βρίσκεται στον ίδιο υποτομέα/τομέα με το γονικό iFrame",
"pad.passwordRequired": "Χρειάζεστε συνθηματικό για πρόσβαση σε αυτό το pad",
"pad.permissionDenied": "Δεν έχετε δικαίωμα πρόσβασης σε αυτό το pad",
"pad.wrongPassword": "Το συνθηματικό σας ήταν λανθασμένο",
"pad.settings.padSettings": "Ρυθμίσεις Pad",
"pad.settings.myView": "Η προβολή μου",
"pad.settings.stickychat": "Να είναι πάντα ορατή η συνομιλία",
@ -123,9 +121,6 @@
"pad.savedrevs.timeslider": "Μπορείτε να δείτε αποθηκευμένες αναθεωρήσεις στο χρονοδιάγραμμα",
"pad.userlist.entername": "Εισάγετε το όνομά σας",
"pad.userlist.unnamed": "ανώνυμος",
"pad.userlist.guest": "Επισκέπτης",
"pad.userlist.deny": "Άρνηση",
"pad.userlist.approve": "Έγκριση",
"pad.editbar.clearcolors": "Να γίνει εκκαθάριση χρωμάτων σύνταξης σε ολόκληρο το έγγραφο; Αυτό δεν μπορεί να αναιρεθεί",
"pad.impexp.importbutton": "Εισαγωγή τώρα",
"pad.impexp.importing": "Εισάγεται...",
@ -136,6 +131,5 @@
"pad.impexp.importfailed": "Η εισαγωγή απέτυχε",
"pad.impexp.copypaste": "Παρακαλώ αντιγράψτε και επικολλήστε",
"pad.impexp.exportdisabled": "Η εξαγωγή σε μορφή {{type}} έχει απενεργοποιηθεί. Επικοινωνήστε με τον διαχειριστή του συστήματός σας για λεπτομέρειες.",
"pad.impexp.maxFileSize": "Πολύ μεγάλο αρχείο. Επικοινωνήστε με τον διαχειριστή για να αυξήσετε το επιτρεπόμενο μέγεθος αρχείου",
"pad.impexp.permission": "Η εισαγωγή είναι απενεργοποιημένη επειδή δεν συνεισφέρατε ποτέ σε αυτό το pad. Συνεισφέρετε τουλάχιστον μία φορά πριν από την εισαγωγή"
"pad.impexp.maxFileSize": "Πολύ μεγάλο αρχείο. Επικοινωνήστε με τον διαχειριστή για να αυξήσετε το επιτρεπόμενο μέγεθος αρχείου"
}

View File

@ -18,7 +18,7 @@
"pad.toolbar.ol.title": "Ordered list (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Unordered List (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Indent (Tab)",
"pad.toolbar.unindent.title": "Outdent (Shift+Tab)",
"pad.toolbar.unindent.title": "Outdent (Shift+TAB)",
"pad.toolbar.undo.title": "Undo (Ctrl+Z)",
"pad.toolbar.redo.title": "Redo (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Clear Authorship Colours (Ctrl+Shift+C)",
@ -32,9 +32,7 @@
"pad.colorpicker.cancel": "Cancel",
"pad.loading": "Loading...",
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
"pad.passwordRequired": "You need a password to access this pad",
"pad.permissionDenied": "You do not have permission to access this pad",
"pad.wrongPassword": "Your password was wrong",
"pad.settings.padSettings": "Pad Settings",
"pad.settings.myView": "My View",
"pad.settings.stickychat": "Chat always on screen",
@ -57,7 +55,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
"pad.modals.connected": "Connected.",
"pad.modals.reconnecting": "Reconnecting to your pad..",
"pad.modals.reconnecting": "Reconnecting to your pad",
"pad.modals.forcereconnect": "Force reconnect",
"pad.modals.reconnecttimer": "Trying to reconnect in",
"pad.modals.cancel": "Cancel",
@ -120,9 +118,6 @@
"pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
"pad.userlist.entername": "Enter your name",
"pad.userlist.unnamed": "unnamed",
"pad.userlist.guest": "Guest",
"pad.userlist.deny": "Deny",
"pad.userlist.approve": "Approve",
"pad.editbar.clearcolors": "Clear authorship colours on entire document? This cannot be undone",
"pad.impexp.importbutton": "Import Now",
"pad.impexp.importing": "Importing...",

View File

@ -1,4 +1,38 @@
{
"admin.page-title": "Admin Dashboard - Etherpad",
"admin_plugins": "Plugin manager",
"admin_plugins.available": "Available plugins",
"admin_plugins.available_not-found": "No plugins found.",
"admin_plugins.available_fetching": "Fetching…",
"admin_plugins.available_install.value": "Install",
"admin_plugins.available_search.placeholder": "Search for plugins to install",
"admin_plugins.description": "Description",
"admin_plugins.installed": "Installed plugins",
"admin_plugins.installed_fetching": "Fetching installed plugins…",
"admin_plugins.installed_nothing": "You haven't installed any plugins yet.",
"admin_plugins.installed_uninstall.value": "Uninstall",
"admin_plugins.last-update": "Last update",
"admin_plugins.name": "Name",
"admin_plugins.page-title": "Plugin manager - Etherpad",
"admin_plugins.version": "Version",
"admin_plugins_info": "Troubleshooting information",
"admin_plugins_info.hooks": "Installed hooks",
"admin_plugins_info.hooks_client": "Client-side hooks",
"admin_plugins_info.hooks_server": "Server-side hooks",
"admin_plugins_info.parts": "Installed parts",
"admin_plugins_info.plugins": "Installed plugins",
"admin_plugins_info.page-title": "Plugin information - Etherpad",
"admin_plugins_info.version": "Etherpad version",
"admin_plugins_info.version_latest": "Latest available version",
"admin_plugins_info.version_number": "Version number",
"admin_settings": "Settings",
"admin_settings.current": "Current configuration",
"admin_settings.current_example-devel": "Example development settings template",
"admin_settings.current_example-prod": "Example production settings template",
"admin_settings.current_restart.value": "Restart Etherpad",
"admin_settings.current_save.value": "Save Settings",
"admin_settings.page-title": "Settings - Etherpad",
"index.newPad": "New Pad",
"index.createOpenPad": "or create/open a Pad with the name:",
"index.openPad": "open an existing Pad with the name:",
@ -26,9 +60,7 @@
"pad.loading": "Loading...",
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
"pad.passwordRequired": "You need a password to access this pad",
"pad.permissionDenied": "You do not have permission to access this pad",
"pad.wrongPassword": "Your password was wrong",
"pad.settings.padSettings": "Pad Settings",
"pad.settings.myView": "My View",
@ -56,9 +88,9 @@
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
"pad.modals.connected": "Connected.",
"pad.modals.reconnecting": "Reconnecting to your pad..",
"pad.modals.reconnecting": "Reconnecting to your pad",
"pad.modals.forcereconnect": "Force reconnect",
"pad.modals.reconnecttimer": "Trying to reconnect in ",
"pad.modals.reconnecttimer": "Trying to reconnect in",
"pad.modals.cancel": "Cancel",
"pad.modals.userdup": "Opened in another window",
@ -90,6 +122,9 @@
"pad.modals.rateLimited": "Rate Limited.",
"pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.",
"pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.",
"pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.",
"pad.modals.disconnected": "You have been disconnected.",
"pad.modals.disconnected.explanation": "The connection to the server was lost",
"pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
@ -137,9 +172,6 @@
"pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
"pad.userlist.entername": "Enter your name",
"pad.userlist.unnamed": "unnamed",
"pad.userlist.guest": "Guest",
"pad.userlist.deny": "Deny",
"pad.userlist.approve": "Approve",
"pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
"pad.impexp.importbutton": "Import Now",
@ -151,6 +183,5 @@
"pad.impexp.importfailed": "Import failed",
"pad.impexp.copypaste": "Please copy paste",
"pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.",
"pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import",
"pad.impexp.permission": "Import is disabled because you never contributed to this pad. Please contribute at least once before importing"
"pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import"
}

View File

@ -31,9 +31,7 @@
"pad.colorpicker.cancel": "Nuligi",
"pad.loading": "Ŝargante...",
"pad.noCookie": "Kuketo ne estis trovigebla. Bonvolu permesi kuketojn en via retumilo!",
"pad.passwordRequired": "Vi bezonas pasvorton por aliri ĉi tiun tekston",
"pad.permissionDenied": "Vi ne havas permeson por aliri ĉi tiun tekston",
"pad.wrongPassword": "Via pasvorto estis malĝusta",
"pad.settings.padSettings": "Redaktilaj Agordoj",
"pad.settings.myView": "Mia vido",
"pad.settings.stickychat": "Babilejo ĉiam videbla",
@ -119,9 +117,6 @@
"pad.savedrevs.timeslider": "Vi povas rigardi konservitajn versiojn per la tempoŝovilo",
"pad.userlist.entername": "Entajpu vian nomon",
"pad.userlist.unnamed": "sennoma",
"pad.userlist.guest": "Gasto",
"pad.userlist.deny": "Malaprobi",
"pad.userlist.approve": "Aprobi",
"pad.editbar.clearcolors": "Forigi kolorojn de aŭtoreco en la tuta dokumento?",
"pad.impexp.importbutton": "Enporti Nun",
"pad.impexp.importing": "Enportante...",

View File

@ -18,6 +18,38 @@
"Xuacu"
]
},
"admin.page-title": "Panel administrativo. Etherpad",
"admin_plugins": "Gestor de complementos",
"admin_plugins.available": "Complementos disponibles",
"admin_plugins.available_not-found": "No se encontró ningún complemento.",
"admin_plugins.available_fetching": "Recuperando…",
"admin_plugins.available_install.value": "Instalar",
"admin_plugins.available_search.placeholder": "Buscar complementos para instalar",
"admin_plugins.description": "Descripción",
"admin_plugins.installed": "Complementos instalados",
"admin_plugins.installed_fetching": "Recuperando los complementos instalados…",
"admin_plugins.installed_nothing": "No se ha instalado ningún complemento aún.",
"admin_plugins.installed_uninstall.value": "Desinstalar",
"admin_plugins.last-update": "Última actualización",
"admin_plugins.name": "Nombre",
"admin_plugins.page-title": "Gestor de complementos. Etherpad",
"admin_plugins.version": "Versión",
"admin_plugins_info": "Información para solucionar problemas",
"admin_plugins_info.hooks": "Actuadores instalados",
"admin_plugins_info.hooks_client": "Actuadores del lado cliente",
"admin_plugins_info.hooks_server": "Actuadores del lado servidor",
"admin_plugins_info.parts": "Partes instaladas",
"admin_plugins_info.plugins": "Complementos instalados",
"admin_plugins_info.page-title": "Información del complemento. Etherpad",
"admin_plugins_info.version": "Versión de Etherpad",
"admin_plugins_info.version_latest": "Versión más reciente disponible",
"admin_plugins_info.version_number": "Número de versión",
"admin_settings": "Configuración",
"admin_settings.current": "Configuración actual",
"admin_settings.current_example-devel": "Plantilla de ejemplo de configuración de desarrollo",
"admin_settings.current_restart.value": "Reiniciar Etherpad",
"admin_settings.current_save.value": "Guardar configuración",
"admin_settings.page-title": "Configuración. Etherpad",
"index.newPad": "Nuevo pad",
"index.createOpenPad": "o crea/abre un pad con el nombre:",
"pad.toolbar.bold.title": "Negrita (Ctrl-B)",
@ -41,9 +73,7 @@
"pad.colorpicker.cancel": "Cancelar",
"pad.loading": "Cargando...",
"pad.noCookie": "No se pudo encontrar la «cookie». Permite la utilización de «cookies» en el navegador.",
"pad.passwordRequired": "Necesitas una contraseña para acceder a este pad",
"pad.permissionDenied": "No tienes permiso para acceder a este pad",
"pad.wrongPassword": "La contraseña era incorrecta",
"pad.settings.padSettings": "Configuración del pad",
"pad.settings.myView": "Preferencias personales",
"pad.settings.stickychat": "Chat siempre en pantalla",
@ -131,9 +161,6 @@
"pad.savedrevs.timeslider": "Puedes ver revisiones guardadas visitando la línea de tiempo",
"pad.userlist.entername": "Escribe tu nombre",
"pad.userlist.unnamed": "anónimo",
"pad.userlist.guest": "Invitado",
"pad.userlist.deny": "Denegar",
"pad.userlist.approve": "Aprobar",
"pad.editbar.clearcolors": "¿Desea borrar los colores de autoría en todo el documento? Esto no se puede deshacer",
"pad.impexp.importbutton": "Importar ahora",
"pad.impexp.importing": "Importando...",

View File

@ -27,9 +27,7 @@
"pad.colorpicker.save": "Salvesta",
"pad.colorpicker.cancel": "Loobu",
"pad.loading": "Laadimine...",
"pad.passwordRequired": "Sul peab olema parool selle klade rööptoimetamiseks",
"pad.permissionDenied": "Sul puuduvad ligipääsuõigused selle klade rööptoimetamiseks",
"pad.wrongPassword": "Vigane parool",
"pad.settings.padSettings": "Klade seadistused",
"pad.settings.myView": "Minu vaade",
"pad.settings.stickychat": "Näita vestlust alatiselt ekraanil",
@ -105,9 +103,6 @@
"pad.savedrevs.marked": "Versioon märgiti salvestatuna",
"pad.userlist.entername": "Sisesta oma nimi",
"pad.userlist.unnamed": "Nimetu",
"pad.userlist.guest": "Külaline",
"pad.userlist.deny": "Eira",
"pad.userlist.approve": "Nõustu",
"pad.editbar.clearcolors": "Kas soovid kustutada autorite värvid dokumendist?",
"pad.impexp.importbutton": "Impordi",
"pad.impexp.importing": "Importimine...",

View File

@ -3,106 +3,137 @@
"authors": [
"An13sa",
"HairyFotr",
"Izendegi",
"Mikel Ibaiba",
"Subi",
"Theklan",
"Xabier Armendaritz"
]
},
"admin.page-title": "Admin Aginte-panela - Etherpad",
"admin_plugins": "Plugin-en kudeaketa",
"admin_plugins.available": "Eskuragarri dauden plugin-ak",
"admin_plugins.available_not-found": "Ez da plugin-ik aurkitu",
"admin_plugins.available_fetching": "Eskuratzen...",
"admin_plugins.available_install.value": "Instalatu",
"admin_plugins.available_search.placeholder": "Bilatu instalatzeko plugin-ak",
"admin_plugins.description": "Deskribapena",
"admin_plugins.installed": "Instalatutako plugin-ak",
"admin_plugins.installed_fetching": "Instalatutako plugin-ak eskuratzen...",
"admin_plugins.installed_nothing": "Oraindik ez duzu inolako plugin-ik instalatu.",
"admin_plugins.installed_uninstall.value": "Desinstalatu",
"admin_plugins.last-update": "Azken eguneratzea",
"admin_plugins.name": "Izena",
"admin_plugins.page-title": "Plugin-en kudeaketa - Etherpad",
"admin_plugins.version": "Bertsioa",
"admin_plugins_info": "Arazoak konpontzeko informazioa",
"admin_plugins_info.plugins": "Instalatutako plugin-ak",
"admin_plugins_info.page-title": "Plugin-en informazioa - Etherpad",
"admin_plugins_info.version": "Etherpad bertsioa",
"admin_plugins_info.version_latest": "Eskuragarri dagoen bertsio berriena",
"admin_plugins_info.version_number": "Bertsio-zenbakia",
"admin_settings": "Ezarpenak",
"admin_settings.current": "Oraingo konfigurazioa",
"admin_settings.current_restart.value": "Berrabiarazi Etherpad",
"admin_settings.current_save.value": "Gorde Ezarpenak",
"admin_settings.page-title": "Ezarpenak - Etherpad",
"index.newPad": "Pad berria",
"index.createOpenPad": "edo sortu/ireki Pad bat honako izenarekin:",
"pad.toolbar.bold.title": "Lodia (Ctrl-B)",
"pad.toolbar.italic.title": "Etzana (Ctrl-I)",
"pad.toolbar.underline.title": "Azpimarratua (Ctrl-U)",
"pad.toolbar.strikethrough.title": "Ezabatua (Ctrl+5)",
"index.openPad": "ireki existitzen den eta hurrengo izena duen Pad-a:",
"pad.toolbar.bold.title": "Lodia (Ctrl+B)",
"pad.toolbar.italic.title": "Etzana (Ctrl+I)",
"pad.toolbar.underline.title": "Azpimarratua (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Marratua (Ctrl+5)",
"pad.toolbar.ol.title": "Zerrenda ordenatua (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Zerrenda ez-ordenatua (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Koska (TAB)",
"pad.toolbar.unindent.title": "Koska kendu (Shift+TAB)",
"pad.toolbar.undo.title": "Desegin (Ctrl-Z)",
"pad.toolbar.redo.title": "Berregin (Ctrl-Y)",
"pad.toolbar.undo.title": "Desegin (Ctrl+Z)",
"pad.toolbar.redo.title": "Berregin (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Ezabatu Egiletza Koloreak (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Inportatu/Esportatu fitxategi formatu ezberdinetara/ezberdinetatik",
"pad.toolbar.timeslider.title": "Denbora lerroa",
"pad.toolbar.timeslider.title": "Denbora-lerroa",
"pad.toolbar.savedRevision.title": "Gorde berrikuspena",
"pad.toolbar.settings.title": "Hobespenak",
"pad.toolbar.settings.title": "Ezarpenak",
"pad.toolbar.embed.title": "Partekatu eta Txertatu pad hau",
"pad.toolbar.showusers.title": "Erakutsi pad honetako erabiltzaileak",
"pad.colorpicker.save": "Gorde",
"pad.colorpicker.cancel": "Utzi",
"pad.loading": "Kargatzen...",
"pad.noCookie": "Cookiea ez da aurkitu. Mesedez, gaitu cookieak zure nabigatzailean!",
"pad.passwordRequired": "Pasahitza behar duzu pad honetara sartzeko",
"pad.permissionDenied": "Ez duzu bamienik pad honetara sartzeko",
"pad.wrongPassword": "Zure pasahitza oker zegoen",
"pad.settings.padSettings": "Pad hobespenak",
"pad.settings.myView": "Nire ikusmoldea",
"pad.noCookie": "Cookiea ez da aurkitu. Mesedez, gaitu cookieak zure nabigatzailean! Zure saioa eta ezarpenak ez dira bisiten artean gordeko. Nabigatzaile batzuetan baliteke hau Etherpad iFrame bitartez txertatuta egoteagatik izatea. Ziurtatu ezazu mesedez Etherpad iFrame-aren jatorrizko orriaren azpidomeinu/domeinu berean daudela.",
"pad.permissionDenied": "Ez duzu baimenik pad honetara sartzeko",
"pad.settings.padSettings": "Pad Ezarpenak",
"pad.settings.myView": "Nire Ikuspegia",
"pad.settings.stickychat": "Txata beti pantailan",
"pad.settings.chatandusers": "Erakutsi txata eta erabiltzaileak",
"pad.settings.colorcheck": "Egiletzaren koloreak",
"pad.settings.linenocheck": "Lerro zenbakiak",
"pad.settings.rtlcheck": "Edukia eskubitik ezkerrera irakurri?",
"pad.settings.fontType": "Tipografia:",
"pad.settings.rtlcheck": "Irakurri edukia eskuinetik ezkerrera?",
"pad.settings.fontType": "Letra-mota:",
"pad.settings.fontType.normal": "Arrunta",
"pad.settings.language": "Hizkuntza:",
"pad.settings.about": "Honi buruz",
"pad.settings.poweredBy": "Honek garatua:",
"pad.importExport.import_export": "Inportatu/Esportatu",
"pad.importExport.import": "Igo edozein testu fitxategi edo dokumentu",
"pad.importExport.importSuccessful": "Arrakastatsua!",
"pad.importExport.export": "Oraingo pad hau honela esportatu:",
"pad.importExport.import": "Igo edozein testu-fitxategi edo dokumentu",
"pad.importExport.importSuccessful": "Ondo!",
"pad.importExport.export": "Esportatu oraingo pad hau honela:",
"pad.importExport.exportetherpad": "Etherpad",
"pad.importExport.exporthtml": "HTML",
"pad.importExport.exportplain": "Testu laua",
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Testu laua edo HTML formatudun testuak bakarrik inporta ditzakezu. Aurreratuagoak diren inportazio aukerak izateko <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord instala ezazu</a>.",
"pad.importExport.abiword.innerHTML": "Testu laua edo HTML formatudun testuak bakarrik inporta ditzakezu. Aurreratuagoak diren inportazio aukerak izateko <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord edo LibreOffice instala ezazu</a>.",
"pad.modals.connected": "Konektatuta.",
"pad.modals.reconnecting": "Zure pad-era birkonektatu...",
"pad.modals.forcereconnect": "Berkonexioa fortzatu",
"pad.modals.reconnecting": "Zure pad-era birkonektatzen...",
"pad.modals.forcereconnect": "Behartu berkonexioa",
"pad.modals.reconnecttimer": "Berriz konektatzen saiatzen",
"pad.modals.cancel": "Deuseztatu",
"pad.modals.cancel": "Utzi",
"pad.modals.userdup": "Beste leiho batean ireki da",
"pad.modals.userdup.explanation": "Pad hau zure nabigatzailearen beste leiho batean irekita dagoela ematen du.",
"pad.modals.userdup.advice": "Berriro konektatu beste leiho hau erabiltzeko.",
"pad.modals.userdup.advice": "Berriro konektatu bestearen ordez leiho hau erabiltzeko.",
"pad.modals.unauth": "Baimenik gabe",
"pad.modals.unauth.explanation": "Orrialdea ikusten ari zinela zure baimenak aldatu dira. Saia zaitez berriro konektatzen.",
"pad.modals.looping.explanation": "Sinkronizazio zerbitzariarekin komunikazioa arazoak daude.",
"pad.modals.looping.cause": "Agian firewall edo proxy ez-bateragarri baten bidez konektatu zara.",
"pad.modals.initsocketfail": "Zerbitzarira ezin da iritsi.",
"pad.modals.initsocketfail.explanation": "Ezin izan da konektatu sinkronizazio zerbitzarira.",
"pad.modals.initsocketfail.cause": "Ziurrenik hau zure nabigatzailea edo internet konexioaren arazo bat dela eta izango da.",
"pad.modals.initsocketfail.cause": "Ziurrenik hau zure nabigatzailearen edo internet konexioaren arazo bat dela-eta izango da.",
"pad.modals.slowcommit.explanation": "Zerbitzariak ez du erantzuten.",
"pad.modals.slowcommit.cause": "Baliteke hau sarearen konexio arazoak direla eta izatea.",
"pad.modals.slowcommit.cause": "Baliteke hau sarearen konexio arazoak direla-eta izatea.",
"pad.modals.badChangeset.explanation": "Sinkronizazio zerbitzariak, zuk egindako aldaketa bat legez kanpokotzat jo du.",
"pad.modals.badChangeset.cause": "Honek zerbitzariaren konfigurazio okerra edo ustekabeko beste jokabidearen baten ondorio izan liteke. Jarri harremanetan zerbitzu-administratzailearekin, errore bat dela uste baduzu. Saiatu berriro konektatzen edizioarekin jarraitzeko.",
"pad.modals.badChangeset.cause": "Hau zerbitzariaren konfigurazio okerra edo ustekabeko beste jokabidearen baten ondorio izan liteke. Jarri harremanetan zerbitzu-administratzailearekin, errore bat dela uste baduzu. Saiatu berriro konektatzen edizioarekin jarraitzeko.",
"pad.modals.corruptPad.explanation": "Sartzen saiatzen ari zaren Pad-a hondatuta dago.",
"pad.modals.corruptPad.cause": "Baliteke zerbitzari okerreko konfigurazioa edo beste ustekabeko portaera batzuk izatea. Jarri harremanetan zerbitzu-administratzailearekin.",
"pad.modals.corruptPad.cause": "Baliteke zerbitzari okerreko konfigurazioagatik edo beste ustekabeko portaera batengatik izatea. Jarri harremanetan zerbitzu-administratzailearekin.",
"pad.modals.deleted": "Ezabatua.",
"pad.modals.deleted.explanation": "Pad hau ezabatu da.",
"pad.modals.rateLimited.explanation": "Pad honetara mezu gehiegi bidali dituzu eta ondorioz deskonektatu zaizu.",
"pad.modals.rejected.explanation": "Zerbitzariak zure nabigatzailetik bidali den mezu bat baztertu du.",
"pad.modals.disconnected": "Deskonektatua izan zara.",
"pad.modals.disconnected.explanation": "Zerbitzariaren konexioa galdu da",
"pad.modals.disconnected.cause": "Baliteke zerbitzaria eskuragarri ez egotea. Mesedez, jakinarazi zerbitzuko administrariari honek gertatzen jarraitzen badu.",
"pad.share": "Pad hau partekatu",
"pad.modals.disconnected.explanation": "Zerbitzariarekiko konexioa galdu da",
"pad.modals.disconnected.cause": "Baliteke zerbitzaria eskuragarri ez egotea. Mesedez, jakinarazi zerbitzuko administratzaileari honek gertatzen jarraitzen badu.",
"pad.share": "Partekatu pad hau",
"pad.share.readonly": "Irakurtzeko bakarrik",
"pad.share.link": "Lotura",
"pad.share.emebdcode": "URLa txertatu",
"pad.share.link": "Esteka",
"pad.share.emebdcode": "Txertatu URLa",
"pad.chat": "Txata",
"pad.chat.title": "Pad honetarako txata ireki.",
"pad.chat.loadmessages": "Mezu gehiago kargatu",
"pad.chat.stick.title": "Handitu",
"pad.chat.writeMessage.placeholder": "Zure mezua hemen idatzi",
"timeslider.pageTitle": "{{appTitle}} denbora lerroa",
"timeslider.toolbar.returnbutton": "Padera itzuli",
"pad.chat.title": "Ireki pad honentzako txata.",
"pad.chat.loadmessages": "Kargatu mezu gehiago",
"pad.chat.stick.title": "Itsatsi txata pantailan",
"pad.chat.writeMessage.placeholder": "Idatzi hemen zure mezua",
"timeslider.pageTitle": "{{appTitle}} Denbora-lerroa",
"timeslider.toolbar.returnbutton": "Itzuli pad-era",
"timeslider.toolbar.authors": "Egileak:",
"timeslider.toolbar.authorsList": "Egilerik gabe",
"timeslider.toolbar.exportlink.title": "Esportatu",
"timeslider.exportCurrent": "Gorde bertsio hau honela:",
"timeslider.version": "Bertsioa {{version}}",
"timeslider.saved": "{{year}}ko {{month}}ren {{day}}an gordeta",
"timeslider.playPause": "Berriro erreproduzitu / gelditu Pad edukiak",
"timeslider.backRevision": "Berrikusketa bat atzerago joan Pad honetan",
"timeslider.forwardRevision": "Berrikusketa bat aurrerago joan Pad honetan",
"timeslider.dateformat": "{{year}}-{{month}}-{{day}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.exportCurrent": "Esportatu bertsio hau honela:",
"timeslider.version": "{{version}} bertsioa",
"timeslider.saved": "{{year}}(e)ko {{month}}ren {{day}}(e)an gordeta",
"timeslider.playPause": "Erreproduzitu / Gelditu Pad-eko edukiak",
"timeslider.backRevision": "Joan berrikusketa bat atzerago Pad honetan",
"timeslider.forwardRevision": "Joan berrikusketa bat aurrerago Pad honetan",
"timeslider.dateformat": "{{year}}/{{month}}/{{day}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "Urtarrila",
"timeslider.month.february": "Otsaila",
"timeslider.month.march": "Martxoa",
@ -115,22 +146,20 @@
"timeslider.month.october": "Urria",
"timeslider.month.november": "Azaroa",
"timeslider.month.december": "Abendua",
"timeslider.unnamedauthors": "{{num}} izenik gabeko {[plural(num) one: egilea, other: egileak]}",
"pad.savedrevs.marked": "Berrikuspen hau markatua dago gordetako berrikuspen gisa",
"pad.savedrevs.timeslider": "Gordetako berrikusketak ikus ditzakezu denbora-graduatzailea bisitatuz",
"pad.userlist.entername": "Sartu zure erabiltzaile izena",
"timeslider.unnamedauthors": "izenik gabeko {{num}} {[plural(num) one: egile, other: egile]}",
"pad.savedrevs.marked": "Berrikuspen hau gordetako berrikuspen gisa markatua dago orain",
"pad.savedrevs.timeslider": "Gordetako berrikusketak denbora-lerroa bisitatuz ikus ditzakezu",
"pad.userlist.entername": "Sartu zure izena",
"pad.userlist.unnamed": "izenik gabe",
"pad.userlist.guest": "Gonbidatua",
"pad.userlist.deny": "Ukatu",
"pad.userlist.approve": "Onartu",
"pad.editbar.clearcolors": "Ezabatu egile koloreak dokumentu osoan?",
"pad.editbar.clearcolors": "Ezabatu egile koloreak dokumentu osoan? Honek ez du atzera bueltarik",
"pad.impexp.importbutton": "Inportatu orain",
"pad.impexp.importing": "Inportatzen...",
"pad.impexp.confirmimport": "Fitxategi bat inportatzen baduzu oraingo pad honen testua ezabatuko da. Ziur zaude jarraitu nahi duzula?",
"pad.impexp.convertFailed": "Ez gara gai fitxategi hau inportatzeko. Erabil ezazu, mesedez, beste dokumentu formatu bat edo kopiatu eta itsasi eskuz.",
"pad.impexp.padHasData": "Artxibo hau ezin izan dugu inportatu Pad hau aldaketak izan dituelako jada, Pad berria inportatu mesedez.",
"pad.impexp.uploadFailed": "Igotzean akatsa egon da, saia zaitez berriro",
"pad.impexp.importfailed": "Inportazioak akatsa egin du",
"pad.impexp.copypaste": "Mesedez kopiatu eta pegatu",
"pad.impexp.exportdisabled": "{{type}} formatuarekin esportatzea desgaituta dago. Kontakta ezazu administratzailea detaile gehiagorako."
"pad.impexp.convertFailed": "Ez gara fitxategi hau inportatzeko gai izan. Erabil ezazu, mesedez, beste dokumentu formatu bat edo eskuz kopiatu eta itsatsi ezazu.",
"pad.impexp.padHasData": "Artxibo hau ezin izan dugu inportatu Pad honek dagoeneko aldaketak izan dituelako, Pad berri batera inportatu mesedez.",
"pad.impexp.uploadFailed": "Igotzeak huts egin du, saia zaitez berriro",
"pad.impexp.importfailed": "Inportazioak huts egin du",
"pad.impexp.copypaste": "Mesedez kopiatu eta itsatsi ezazu",
"pad.impexp.exportdisabled": "{{type}} formatuarekin esportatzea desgaituta dago. Xehetasun gehiagorako zure sistemako administratzailearekin harremanetan jarri zaitez.",
"pad.impexp.maxFileSize": "Fitxategia handiegia da. Zure guneko administratzailearekin harremanetan jarri zaitez inportatu daitezkeen fitxategien gehienezko tamaina handitzeko"
}

View File

@ -34,9 +34,7 @@
"pad.colorpicker.cancel": "لغو",
"pad.loading": "در حال بارگذاری...",
"pad.noCookie": "کوکی یافت نشد. لطفاً اجازهٔ اجرای کوکی در مروگرتان را بدهید!",
"pad.passwordRequired": "برای دسترسی به این دفترچه یادداشت نیاز به یک گذرواژه دارید",
"pad.permissionDenied": "شما اجازه‌ی دسترسی به این دفترچه یادداشت را ندارید",
"pad.wrongPassword": "گذرواژه‌ی شما درست نیست",
"pad.settings.padSettings": "تنظیمات دفترچه یادداشت",
"pad.settings.myView": "نمای من",
"pad.settings.stickychat": "گفتگو همیشه روی صفحه نمایش باشد",
@ -122,9 +120,6 @@
"pad.savedrevs.timeslider": "شما می‌توانید نسخه‌های ذخیره شده را با دیدن نوار زمان ببنید",
"pad.userlist.entername": "نام خود را بنویسید",
"pad.userlist.unnamed": "بدون نام",
"pad.userlist.guest": "مهمان",
"pad.userlist.deny": "رد کردن",
"pad.userlist.approve": "پذیرفتن",
"pad.editbar.clearcolors": "رنگ نویسندگی از همه‌ی سند پاک شود؟",
"pad.impexp.importbutton": "هم اکنون درون‌ریزی کن",
"pad.impexp.importing": "در حال درون‌ریزی...",

View File

@ -5,6 +5,7 @@
"Espeox",
"Jl",
"Lliehu",
"Maantietäjä",
"Macofe",
"MrTapsa",
"Nedergard",
@ -17,6 +18,29 @@
"VezonThunder"
]
},
"admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia",
"admin_plugins.description": "Kuvaus",
"admin_plugins.installed": "Asennetut laajennukset",
"admin_plugins.installed_fetching": "Haetaan asennettuja laajennuksia.",
"admin_plugins.installed_nothing": "Et ole vielä asentanut laajennuksia.",
"admin_plugins.installed_uninstall.value": "Poista asennus",
"admin_plugins.last-update": "Viimeisin päivitys",
"admin_plugins.name": "Nimi",
"admin_plugins.page-title": "Laajennusten hallinta - Etherpad",
"admin_plugins.version": "Versio",
"admin_plugins_info": "Vianmääritystietoja",
"admin_plugins_info.hooks": "Asennetut koukut",
"admin_plugins_info.hooks_client": "Asiakaspuolen koukut.",
"admin_plugins_info.hooks_server": "Palvelinpuolen koukut.",
"admin_plugins_info.parts": "Asennetut osat",
"admin_plugins_info.plugins": "Asennetut laajennukset",
"admin_plugins_info.page-title": "Laajennustiedot - Etherpad",
"admin_plugins_info.version": "Etherpad-versio",
"admin_plugins_info.version_latest": "Viimeisin saatavilla oleva versio",
"admin_plugins_info.version_number": "Versionumero",
"admin_settings": "Asetukset",
"admin_settings.current": "Nykyinen kokoonpano",
"admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista",
"index.newPad": "Uusi muistio",
"index.createOpenPad": "tai luo tai avaa muistio nimellä:",
"pad.toolbar.bold.title": "Lihavointi (Ctrl-B)",
@ -40,9 +64,7 @@
"pad.colorpicker.cancel": "Peru",
"pad.loading": "Ladataan…",
"pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi!",
"pad.passwordRequired": "Tämä muistio on suojattu salasanalla.",
"pad.permissionDenied": "Käyttöoikeutesi eivät riitä tämän muistion käyttämiseen.",
"pad.wrongPassword": "Väärä salasana",
"pad.settings.padSettings": "Muistion asetukset",
"pad.settings.myView": "Oma näkymä",
"pad.settings.stickychat": "Keskustelu aina näkyvissä",
@ -130,9 +152,6 @@
"pad.savedrevs.timeslider": "Voit tarkastella tallennettuja versioita avaamalla aikajanan",
"pad.userlist.entername": "Kirjoita nimesi",
"pad.userlist.unnamed": "nimetön",
"pad.userlist.guest": "Vieras",
"pad.userlist.deny": "Estä",
"pad.userlist.approve": "Hyväksy",
"pad.editbar.clearcolors": "Poistetaanko asiakirjasta tekijävärit? Tätä ei voi perua",
"pad.impexp.importbutton": "Tuo nyt",
"pad.impexp.importing": "Tuodaan...",

View File

@ -20,9 +20,7 @@
"pad.colorpicker.save": "Goym",
"pad.colorpicker.cancel": "Ógilda",
"pad.loading": "Løðir...",
"pad.passwordRequired": "Tú hevur brúk fyri einum loyniorði fyri at fáa atgongd til henda paddin",
"pad.permissionDenied": "Tú hevur ikki loyvi til at fáa atgongd til henda paddin",
"pad.wrongPassword": "Títt loyniorð var skeivt",
"pad.settings.padSettings": "Pad innstillingar",
"pad.settings.myView": "Mín sýning",
"pad.settings.stickychat": "Kjatta altíð á skerminum",
@ -84,8 +82,5 @@
"timeslider.unnamedauthors": "{{num}} {[plural(num) one: ónevndur rithøvundur, other: ónevndir rithøvundar ]}",
"pad.savedrevs.marked": "Henda endurskoðanin er nú merkt sum ein goymd endurskoðan",
"pad.userlist.entername": "Skriva títt navn",
"pad.userlist.unnamed": "ikki-navngivið",
"pad.userlist.guest": "Gestur",
"pad.userlist.deny": "Nokta",
"pad.userlist.approve": "Góðkenn"
"pad.userlist.unnamed": "ikki-navngivið"
}

View File

@ -29,6 +29,39 @@
"Wladek92"
]
},
"admin.page-title": "Tableau de bord administrateur — Etherpad",
"admin_plugins": "Gestionnaire de compléments",
"admin_plugins.available": "Compléments disponibles",
"admin_plugins.available_not-found": "Aucun complément trouvé.",
"admin_plugins.available_fetching": "Récupération…",
"admin_plugins.available_install.value": "Installer",
"admin_plugins.available_search.placeholder": "Rechercher des compléments à installer",
"admin_plugins.description": "Description",
"admin_plugins.installed": "Compléments installés",
"admin_plugins.installed_fetching": "Récupération des compléments installés…",
"admin_plugins.installed_nothing": "Vous navez pas encore installé de complément.",
"admin_plugins.installed_uninstall.value": "Désinstaller",
"admin_plugins.last-update": "Dernière mise à jour",
"admin_plugins.name": "Nom",
"admin_plugins.page-title": "Gestionnaire de compléments — Etherpad",
"admin_plugins.version": "Version",
"admin_plugins_info": "Information de résolution de problème",
"admin_plugins_info.hooks": "Crochets installés",
"admin_plugins_info.hooks_client": "Crochets côté client",
"admin_plugins_info.hooks_server": "Crochets côté serveur",
"admin_plugins_info.parts": "Parties installées",
"admin_plugins_info.plugins": "Compléments installés",
"admin_plugins_info.page-title": "Information de complément — Etherpad",
"admin_plugins_info.version": "Version Etherpad",
"admin_plugins_info.version_latest": "Dernière version disponible",
"admin_plugins_info.version_number": "Numéro de version",
"admin_settings": "Paramètres",
"admin_settings.current": "Configuration actuelle",
"admin_settings.current_example-devel": "Exemple de modèle de paramètres de développement",
"admin_settings.current_example-prod": "Exemple de modèle de paramètres de production",
"admin_settings.current_restart.value": "Redémarrer Etherpad",
"admin_settings.current_save.value": "Enregistrer les paramètres",
"admin_settings.page-title": "Paramètres — Etherpad",
"index.newPad": "Nouveau bloc-notes",
"index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé:",
"index.openPad": "ouvrir un Pad existant avec le nom :",
@ -53,9 +86,7 @@
"pad.colorpicker.cancel": "Annuler",
"pad.loading": "Chargement...",
"pad.noCookie": "Un cookie na pas pu être trouvé. Veuillez autoriser les fichiers témoins (ou cookies) dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait quEtehrpad est inclus dans un iFrame dans certains navigateurs. Veuillez vous assurer que Etherpad est dans le même sous-domaine/domaine que son iFrame parent",
"pad.passwordRequired": "Vous avez besoin d'un mot de passe pour accéder à ce bloc-note",
"pad.permissionDenied": "Vous nêtes pas autorisé à accéder à ce bloc-notes",
"pad.wrongPassword": "Votre mot de passe est incorrect",
"pad.settings.padSettings": "Paramètres du bloc-notes",
"pad.settings.myView": "Ma vue",
"pad.settings.stickychat": "Toujours afficher le clavardage",
@ -104,6 +135,8 @@
"pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.",
"pad.modals.rateLimited": "Taux limité.",
"pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.",
"pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.",
"pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc, ou il y a peut-être un bogue dans Etherpad. Essayez de recharger la page.",
"pad.modals.disconnected": "Vous avez été déconnecté.",
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer ladministrateur du service.",
@ -146,9 +179,6 @@
"pad.savedrevs.timeslider": "Vous pouvez voir les révisions enregistrées en ouvrant lhistorique",
"pad.userlist.entername": "Entrez votre nom",
"pad.userlist.unnamed": "anonyme",
"pad.userlist.guest": "Invité",
"pad.userlist.deny": "Refuser",
"pad.userlist.approve": "Approuver",
"pad.editbar.clearcolors": "Effacer le surlignage par auteur dans tout le document? Cette action ne peut être annulée.",
"pad.impexp.importbutton": "Importer maintenant",
"pad.impexp.importing": "Import en cours...",
@ -159,6 +189,5 @@
"pad.impexp.importfailed": "Échec de limportation",
"pad.impexp.copypaste": "Veuillez copier-coller",
"pad.impexp.exportdisabled": "Lexportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails.",
"pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés",
"pad.impexp.permission": "Limportation est désactivée parce que vous navez jamais contribué à ce bloc. Veuillez contribuer au moins une fois avant dimporter"
"pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés"
}

View File

@ -28,9 +28,7 @@
"pad.colorpicker.cancel": "Cancelar",
"pad.loading": "Cargando...",
"pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!",
"pad.passwordRequired": "Cómpre un contrasinal para acceder a este documento",
"pad.permissionDenied": "Non ten permiso para acceder a este documento",
"pad.wrongPassword": "O contrasinal era incorrecto",
"pad.settings.padSettings": "Configuracións do documento",
"pad.settings.myView": "A miña vista",
"pad.settings.stickychat": "Chat sempre visible",
@ -114,9 +112,6 @@
"pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo",
"pad.userlist.entername": "Insira o seu nome",
"pad.userlist.unnamed": "anónimo",
"pad.userlist.guest": "Convidado",
"pad.userlist.deny": "Rexeitar",
"pad.userlist.approve": "Aprobar",
"pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?",
"pad.impexp.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...",

View File

@ -13,9 +13,7 @@
"pad.colorpicker.cancel": "રદ્દ કરો",
"pad.loading": "લાવે છે...",
"pad.noCookie": "કુકી મળી નહી. આપના બ્રાઉઝર સેટિંગમાં જઇ કુકી સક્રિય કરો!",
"pad.passwordRequired": "તમારે આ પેડના ઉપયોગ માટે ગુપ્તસંજ્ઞાની જરુર પડશે",
"pad.permissionDenied": "આ પેડના ઉપયોગની આપને પરવાનગી નથી",
"pad.wrongPassword": "આપની ગુપ્તસંજ્ઞા ખોટી છે",
"pad.settings.padSettings": "પેડ ગોઠવણીઓ",
"pad.settings.myView": "મારા મતે",
"pad.settings.fontType.normal": "સામાન્ય",

View File

@ -8,6 +8,39 @@
"תומר ט"
]
},
"admin.page-title": "לוח ניהול - Etherpad",
"admin_plugins": "מנהל תוספים",
"admin_plugins.available": "תוספים זמינים",
"admin_plugins.available_not-found": "לא נמצאו תוספים.",
"admin_plugins.available_fetching": "מתקבל…",
"admin_plugins.available_install.value": "התקנה",
"admin_plugins.available_search.placeholder": "חיפוש תוספים להתקנה",
"admin_plugins.description": "תיאור",
"admin_plugins.installed": "תוספים מותקנים",
"admin_plugins.installed_fetching": "התוספים המותקנים מתקבלים…",
"admin_plugins.installed_nothing": "לא התקנת תוספים עדיין.",
"admin_plugins.installed_uninstall.value": "הסרה",
"admin_plugins.last-update": "עדכון אחרון",
"admin_plugins.name": "שם",
"admin_plugins.page-title": "מנהל תוספים - Etherpad",
"admin_plugins.version": "גרסה",
"admin_plugins_info": "מידע לפתרון תקלות",
"admin_plugins_info.hooks": "התליות מותקנות",
"admin_plugins_info.hooks_client": "התליות מצד הלקוח",
"admin_plugins_info.hooks_server": "התליות מצד השרת",
"admin_plugins_info.parts": "חלקים מותקנים",
"admin_plugins_info.plugins": "תוספים מותקנים",
"admin_plugins_info.page-title": "פרטי תוסף - Etherpad",
"admin_plugins_info.version": "גרסת Etherpad",
"admin_plugins_info.version_latest": "הגרסה העדכנית ביותר הזמינה",
"admin_plugins_info.version_number": "מספר גרסה",
"admin_settings": "הגדרות",
"admin_settings.current": "הגדרות נוכחיות",
"admin_settings.current_example-devel": "תבנית הגדרות פיתוח לדוגמה",
"admin_settings.current_example-prod": "תבנית הגדרות פעילות מבצעית לדוגמה",
"admin_settings.current_restart.value": "הפעלת Etherpad מחדש",
"admin_settings.current_save.value": "שמירת הגדרות",
"admin_settings.page-title": "הגדרות - Etherpad",
"index.newPad": "פנקס חדש",
"index.createOpenPad": "ליצור או לפתוח פנקס בשם:",
"index.openPad": "פתיחת פנקס קיים עם השם:",
@ -18,7 +51,7 @@
"pad.toolbar.ol.title": "רשימה ממוספרת (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "רשימת תבליטים (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "הזחה (טאב)",
"pad.toolbar.unindent.title": "צמצום הזחה (שיפט–טאב)",
"pad.toolbar.unindent.title": "צמצום הזחה (Shift+TAB)",
"pad.toolbar.undo.title": "ביטול (Ctrl-Z)",
"pad.toolbar.redo.title": "ביצוע מחדש",
"pad.toolbar.clearAuthorship.title": "ניקוי צבעי כותבים (Ctrl-Shift-C)",
@ -31,10 +64,8 @@
"pad.colorpicker.save": "שמירה",
"pad.colorpicker.cancel": "ביטול",
"pad.loading": "טעינה...",
"pad.noCookie": "העוגייה לא נמצאה. נא לאפשר עוגיות בדפדפן שלך!",
"pad.passwordRequired": "דרושה ססמה כדי לגשת לפנקס הזה",
"pad.noCookie": "העוגייה לא נמצאה. נא לאפשר עוגיות בדפדפן שלך! ההפעלה וההגדרות שלך לא יישמרו בין ביקורים. זה יכול לקרות עם Etherpad נכלל בתוך חלונית פנימית (iframe) בחלק מהדפדפנים. נא לוודא ש־Etherpad הוא תחת אותו שם תחום/תת־שם תחום כמו החלונית הפנימית של ההורה",
"pad.permissionDenied": "אין לך הרשאה לגשת לפנקס הזה",
"pad.wrongPassword": "ססמתך הייתה שגויה",
"pad.settings.padSettings": "הגדרות פנקס",
"pad.settings.myView": "התצוגה שלי",
"pad.settings.stickychat": "השיחה תמיד על המסך",
@ -46,6 +77,7 @@
"pad.settings.fontType.normal": "רגיל",
"pad.settings.language": "שפה:",
"pad.settings.about": "על אודות",
"pad.settings.poweredBy": "מופעל על גבי",
"pad.importExport.import_export": "ייבוא/ייצוא",
"pad.importExport.import": "העלאת כל קובץ טקסט או מסמך",
"pad.importExport.importSuccessful": "זה עבד!",
@ -58,7 +90,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)",
"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 או LibreOffice</a>.",
"pad.modals.connected": "מחובר.",
"pad.modals.reconnecting": "מתבצע חיבור מחדש...",
"pad.modals.reconnecting": "מתבצע חיבור מחדש למחברת שלך…",
"pad.modals.forcereconnect": "לכפות חיבור מחדש",
"pad.modals.reconnecttimer": "מנסה להתחבר מחדש בעוד",
"pad.modals.cancel": "ביטול",
@ -80,6 +112,10 @@
"pad.modals.corruptPad.cause": "ייתכן שזה קרה בגלל הגדרות שרת שגויות או התנהגות בלתי־צפויה כלשהי. נא ליצור קשר עם המנהל של השירות אם נראה לך שזאת שגיאה.",
"pad.modals.deleted": "נמחק.",
"pad.modals.deleted.explanation": "הפנקס הזה הוסר.",
"pad.modals.rateLimited": "מוגבל קצב.",
"pad.modals.rateLimited.explanation": "שלחת יותר מדי הודעות לפנקס הזה ולכן הוא ניתק אותך.",
"pad.modals.rejected.explanation": "השרת דחה את ההודעה שנשלחה על ידי הדפדפן שלך.",
"pad.modals.rejected.cause": "יכול להיות שהשרת עודכן בזמן שצפית בפנקס או שיש תקלה ב־Etherpad. מומלץ לנסות לרענן את העמוד.",
"pad.modals.disconnected": "נותקת.",
"pad.modals.disconnected.explanation": "התקשורת לשרת אבדה",
"pad.modals.disconnected.cause": "ייתכן שהשרת אינו זמין. נא להודיע למנהל השירות אם זה ממשיך לקרות.",
@ -92,6 +128,7 @@
"pad.chat.loadmessages": "טעינת הודעות נוספות",
"pad.chat.stick.title": "הצמדת צ׳אט למסך",
"pad.chat.writeMessage.placeholder": "מקום לכתיבת ההודעה שלך",
"timeslider.followContents": "לעקוב אחר עדכוני תוכן פנקס",
"timeslider.pageTitle": "גולל זמן של {{appTitle}}",
"timeslider.toolbar.returnbutton": "חזרה אל הפנקס",
"timeslider.toolbar.authors": "כותבים:",
@ -121,19 +158,15 @@
"pad.savedrevs.timeslider": "אפשר להציג גרסאות שמורות באמצעות ביקור בגולל הזמן",
"pad.userlist.entername": "נא להזין את שמך",
"pad.userlist.unnamed": "ללא שם",
"pad.userlist.guest": "אורח",
"pad.userlist.deny": "לדחות",
"pad.userlist.approve": "לאשר",
"pad.editbar.clearcolors": "לנקות צבעים לסימון כותבים בכל המסמך? זו פעולה בלתי הפיכה",
"pad.impexp.importbutton": "לייבא כעת",
"pad.impexp.importing": "ייבוא...",
"pad.impexp.confirmimport": "ייבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. האם ברצונך להמשיך?",
"pad.impexp.importing": "מתבצע ייבוא…",
"pad.impexp.confirmimport": "ייבוא של קובץ יבטל את הטקסט הנוכחי בפנקס. להמשיך?",
"pad.impexp.convertFailed": "לא הצלחנו לייבא את הקובץ הזה. נא להשתמש בתסדיר מסמך שונה או להעתיק ולהדביק ידנית",
"pad.impexp.padHasData": "לא הצלחנו לייבא את הקובץ הזה, כי בפנקס הזה כבר יש שינויים. נא לייבא לפנקס חדש.",
"pad.impexp.uploadFailed": "ההעלאה נכשלה, נא לנסות שוב",
"pad.impexp.importfailed": "הייבוא נכשל",
"pad.impexp.copypaste": "נא להעתיק ולהדביק",
"pad.impexp.exportdisabled": "ייצוא בתסדיר {{type}} אינו פעיל. מנהל המערכת שלך יוכל לספר לך על זה עוד פרטים.",
"pad.impexp.maxFileSize": "הקובץ גדול מדי. נא ליצור קשר עם הנהלת האתר כדי להגדיל את הגודל המרבי שמותר לייבא.",
"pad.impexp.permission": "הייבוא מושבת כיוון שמעולם לא תרמת לפנקס הזה. נא לתרום לפחות פעם אחת בטרם ביצוע ניסיון ייבוא"
"pad.impexp.maxFileSize": "הקובץ גדול מדי. נא ליצור קשר עם הנהלת האתר כדי להגדיל את הגודל המרבי שמותר לייבא."
}

View File

@ -29,9 +29,7 @@
"pad.colorpicker.cancel": "Otkaži",
"pad.loading": "Učitavanje...",
"pad.noCookie": "Kolačić nije pronađen. Molimo Vas, omogućite kolačiće u Vašem pregledniku! Sesija i postavke neće biti sačuvane između Vaših posjećivanja. Razlog može biti uključenost Etherpada u iFrame u nekim preglednicima. Molimo Vas, osigurajte da je Etherpad na istoj poddomeni/domeni kao i ''roditeljski'' iFrame.",
"pad.passwordRequired": "Potrebna Vam je zaporka za pristup ovomu blokiću",
"pad.permissionDenied": "Nemate dopuštenje za pristup ovome blokiću",
"pad.wrongPassword": "Vaša zaporka nije valjana",
"pad.settings.padSettings": "Postavke blokića",
"pad.settings.myView": "Vaš prikaz",
"pad.settings.stickychat": "Stavi čavrljanje uvijek na ekranu",
@ -122,9 +120,6 @@
"pad.savedrevs.timeslider": "Možete vidjeti spremljene inačice rabeći vremensku lentu (timeslider)",
"pad.userlist.entername": "Unesite Vaše suradničko ime",
"pad.userlist.unnamed": "bez imena",
"pad.userlist.guest": "Gost",
"pad.userlist.deny": "Odbij",
"pad.userlist.approve": "Odobri",
"pad.editbar.clearcolors": "Ukloniti boje autorstva u cijelom blokiću? Radnju nije moguće poništiti jednom kad je izvršena.",
"pad.impexp.importbutton": "Uvezi odmah",
"pad.impexp.importing": "Uvoženje...",
@ -135,6 +130,5 @@
"pad.impexp.importfailed": "Uvoz nije uspio",
"pad.impexp.copypaste": "Molimo preslikajte/zalijepite",
"pad.impexp.exportdisabled": "Izvoz u formatu {{type}} nije omogućen. Molimo Vas, kontaktirajte Vašega administratora sustava za više pojedinosti.",
"pad.impexp.maxFileSize": "Datoteka je prevelika. Kontaktirajte administratora Vašega mrežnoga sjedišta kako biste zatražili povećanje dopuštene veličine datoteke za uvoz",
"pad.impexp.permission": "Uvoz je onemogućen jer niste doprinosili ovom blokiću Etherpada. Molimo Vas, doprinosite barem jednom prije radnje uvoza"
"pad.impexp.maxFileSize": "Datoteka je prevelika. Kontaktirajte administratora Vašega mrežnoga sjedišta kako biste zatražili povećanje dopuštene veličine datoteke za uvoz"
}

View File

@ -26,9 +26,7 @@
"pad.colorpicker.save": "Speichre",
"pad.colorpicker.cancel": "Abbreche",
"pad.loading": "Loode …",
"pad.passwordRequired": "Sie benötiche en Passwort, um uff das Pad zuzugreife",
"pad.permissionDenied": "Du host ken Berechtichung, um uff das Pad zuzugreif",
"pad.wrongPassword": "Dein Passwort woor falsch",
"pad.settings.padSettings": "Pad Einstellunge",
"pad.settings.myView": "Eichne Oonsicht",
"pad.settings.stickychat": "Chat immer oonzeiche",
@ -104,9 +102,6 @@
"pad.savedrevs.marked": "Die Version woard jetzt wie gespeicherte Version gekennzeichnet",
"pad.userlist.entername": "Tue en Noome ren gebe",
"pad.userlist.unnamed": "unbenannt",
"pad.userlist.guest": "Gast",
"pad.userlist.deny": "Verweihe (negiere)",
"pad.userlist.approve": "Genehmiche (approviere)",
"pad.editbar.clearcolors": "Autorefarbe im gesamte Dokument zurücksetze?",
"pad.impexp.importbutton": "Jetzt importiere",
"pad.impexp.importing": "Importiere …",

View File

@ -27,9 +27,7 @@
"pad.colorpicker.cancel": "Přetorhnyć",
"pad.loading": "Začituje so...",
"pad.noCookie": "Plack njeje so namakał. Prošu dopušćće placki w swojim wobhladowaku!",
"pad.passwordRequired": "Trjebaš hesło, zo by na tutón zapisnik přistup měł",
"pad.permissionDenied": "Nimaće prawo za přistup na tutón zapisnik.",
"pad.wrongPassword": "Twoje hesło bě wopak",
"pad.settings.padSettings": "Nastajenja zapisnika",
"pad.settings.myView": "Mój napohlad",
"pad.settings.stickychat": "Chat přeco na wobrazowce pokazać",
@ -111,9 +109,6 @@
"pad.savedrevs.timeslider": "Móžeš sej składowane wersije wobhladować, wopytujo historiju dokumenta.",
"pad.userlist.entername": "Zapodaj swoje mjeno",
"pad.userlist.unnamed": "bjez mjena",
"pad.userlist.guest": "Hósć",
"pad.userlist.deny": "Wotpokazać",
"pad.userlist.approve": "Schwalić",
"pad.editbar.clearcolors": "Awtorowe barby w cyłym dokumenće zhašeć?",
"pad.impexp.importbutton": "Nětko importować",
"pad.impexp.importing": "Importuje so...",

View File

@ -5,6 +5,7 @@
"Bencemac",
"Csega",
"Dj",
"Hanna Tardos",
"Misibacsi",
"Notramo",
"Ovari",
@ -12,6 +13,39 @@
"Tgr"
]
},
"admin.page-title": "Admin irányítópult - Etherpad",
"admin_plugins": "Bővítménykezelő",
"admin_plugins.available": "Elérhető bővítmények",
"admin_plugins.available_not-found": "Nem található bővítmény.",
"admin_plugins.available_fetching": "Lehívás...",
"admin_plugins.available_install.value": "Telepítés",
"admin_plugins.available_search.placeholder": "Telepíthető bővítmények keresése",
"admin_plugins.description": "Leírás",
"admin_plugins.installed": "Telepített bővítmények",
"admin_plugins.installed_fetching": "Telepített bővítmények lehívása...",
"admin_plugins.installed_nothing": "Még nem telepítettél bővítményeket.",
"admin_plugins.installed_uninstall.value": "Eltávolítás",
"admin_plugins.last-update": "Utolsó frissítés",
"admin_plugins.name": "Név",
"admin_plugins.page-title": "Bővítménykezelő - Etherpad",
"admin_plugins.version": "Verzió",
"admin_plugins_info": "Hibaelhárításra vonatkozó információ",
"admin_plugins_info.hooks": "Telepített hookok",
"admin_plugins_info.hooks_client": "Kliensoldali hookok",
"admin_plugins_info.hooks_server": "Szerveroldali hookok",
"admin_plugins_info.parts": "Telepített elemek",
"admin_plugins_info.plugins": "Telepített bővítmények",
"admin_plugins_info.page-title": "Információ bővítményről - Etherpad",
"admin_plugins_info.version": "Etherpad verzió",
"admin_plugins_info.version_latest": "Legfrissebb elérhető verzió",
"admin_plugins_info.version_number": "Verziószám",
"admin_settings": "Beállítások",
"admin_settings.current": "Jelenlegi beállítások",
"admin_settings.current_example-devel": "Fejlesztés beállítások sablon minta",
"admin_settings.current_example-prod": "Gyártás beállítások sablon minta",
"admin_settings.current_restart.value": "Etherpad újraindítása",
"admin_settings.current_save.value": "Beállítások mentése",
"admin_settings.page-title": "Beállítások - Etherpad",
"index.newPad": "Új jegyzetfüzet",
"index.createOpenPad": "vagy jegyzetfüzet létrehozása/megnyitása ezzel a névvel:",
"index.openPad": "nyisson meg egy meglévő jegyzetfüzetet névvel:",
@ -36,9 +70,7 @@
"pad.colorpicker.cancel": "Mégsem",
"pad.loading": "Betöltés…",
"pad.noCookie": "Nem található a süti. Engedélyezd a böngésződben a sütik használatát! A munkamenet és a beállítások nem kerülnek mentésre a látogatások között. Ennek oka lehet az, hogy az Etherpad egyes böngészőkben szerepel az iFrame-ben. Ellenőrizze, hogy az Etherpad ugyanabban az altartomány / tartományban van-e, mint a szülő iFrame",
"pad.passwordRequired": "Jelszóra van szükséged ezen jegyzetfüzet eléréséhez",
"pad.permissionDenied": "Nincs engedélyed ezen jegyzetfüzet eléréséhez",
"pad.wrongPassword": "A jelszó rossz volt",
"pad.settings.padSettings": "Jegyzetfüzet beállításai",
"pad.settings.myView": "Az én nézetem",
"pad.settings.stickychat": "Mindig mutasd a csevegés-dobozt",
@ -87,6 +119,8 @@
"pad.modals.deleted.explanation": "Ez a jegyzetfüzet el lett távolítva.",
"pad.modals.rateLimited": "Korlátozott.",
"pad.modals.rateLimited.explanation": "Túl sok üzenetet küldött erre a jegyzetfüzetre, így a kapcsolat bontva lett.",
"pad.modals.rejected.explanation": "A szerver elutasított egy üzenetet, amit a keresőd küldött.",
"pad.modals.rejected.cause": "Lehet, hogy a szerveren frissítés történt, miközben a padet nézted, vagy bugos az Etherpad. Próbáld meg frissíteni az oldalt.",
"pad.modals.disconnected": "Kapcsolat bontva.",
"pad.modals.disconnected.explanation": "A szerverrel való kapcsolat megszűnt",
"pad.modals.disconnected.cause": "Lehet, hogy a szerver nem elérhető. Kérlek, értesítsd a szolgáltatás adminisztrátorát, ha a probléma tartósan fennáll.",
@ -129,9 +163,6 @@
"pad.savedrevs.timeslider": "A mentett revíziókat az időcsúszkán tudod megnézni",
"pad.userlist.entername": "Add meg a nevedet",
"pad.userlist.unnamed": "névtelen",
"pad.userlist.guest": "Vendég",
"pad.userlist.deny": "Megtagad",
"pad.userlist.approve": "Jóváhagy",
"pad.editbar.clearcolors": "A szerzőséget jelző színeket törli a teljes dokumentumból? Ez nem vonható vissza.",
"pad.impexp.importbutton": "Importálás most",
"pad.impexp.importing": "Importálás…",
@ -142,6 +173,5 @@
"pad.impexp.importfailed": "Az importálás nem sikerült",
"pad.impexp.copypaste": "Kérjük másold be",
"pad.impexp.exportdisabled": "{{type}} formátumba az exportálás nem engedélyezett. Kérjük, a részletekért fordulj a rendszeradminisztrátorhoz.",
"pad.impexp.maxFileSize": "Túl nagy a fájl. Vegye fel a kapcsolatot a webhelygazdájával, hogy növelje az importálható fájl méretét",
"pad.impexp.permission": "Az importálás le van tiltva, mert soha nem járult hozzá ehhez a jegyzetfüzethez. Kérjük, járuljon hozzá legalább egyszer az importálás előtt"
"pad.impexp.maxFileSize": "Túl nagy a fájl. Vegye fel a kapcsolatot a webhelygazdájával, hogy növelje az importálható fájl méretét"
}

View File

@ -4,6 +4,12 @@
"Kareyac"
]
},
"admin_plugins.available_install.value": "Տեղադրել",
"admin_plugins.description": "Նկարագրություն",
"admin_plugins.version": "Տարբերակ",
"admin_settings": "Կարգավորումներ",
"index.newPad": "Ստեղծել",
"pad.toolbar.bold.title": "Թավատառ (Ctrl+B)",
"pad.toolbar.underline.title": "ընդգծելով (Ctrl-U)",
"pad.toolbar.undo.title": "Չեղարկել (Ctrl-Z)",
"pad.toolbar.redo.title": "Վերադարձնել (Ctrl-Y)",
@ -15,7 +21,6 @@
"pad.colorpicker.save": "Պահպանել",
"pad.colorpicker.cancel": "Չեղարկել",
"pad.loading": "Բեռնվում է…",
"pad.wrongPassword": "Սխալ գաղտնաբառ",
"pad.settings.myView": "Իմ տեսարան",
"pad.settings.rtlcheck": "Կարդալ բովանդակությունը աջից ձախ",
"pad.settings.fontType": "Տառատեսակի տեսակը",
@ -50,9 +55,6 @@
"timeslider.month.december": "Դեկտեմբեր",
"pad.userlist.entername": "Մուտքագրեք ձեր անունը",
"pad.userlist.unnamed": "անանուն",
"pad.userlist.guest": "Հյուր",
"pad.userlist.deny": "Մերժել",
"pad.userlist.approve": "Հաստատել",
"pad.impexp.importbutton": "Ներմուծել հիմա",
"pad.impexp.copypaste": "Խնդրում ենք պատճենել"
}

View File

@ -27,9 +27,7 @@
"pad.colorpicker.cancel": "Cancellar",
"pad.loading": "Cargamento…",
"pad.noCookie": "Le cookie non pote esser trovate. Per favor permitte le cookies in tu navigator!",
"pad.passwordRequired": "Un contrasigno es necessari pro acceder a iste pad",
"pad.permissionDenied": "Tu non ha le permission de acceder a iste pad",
"pad.wrongPassword": "Le contrasigno es incorrecte",
"pad.settings.padSettings": "Configuration del pad",
"pad.settings.myView": "Mi vista",
"pad.settings.stickychat": "Chat sempre visibile",
@ -113,9 +111,6 @@
"pad.savedrevs.timeslider": "Tu pote vider versiones salveguardate con le chronologia glissante.",
"pad.userlist.entername": "Entra tu nomine",
"pad.userlist.unnamed": "sin nomine",
"pad.userlist.guest": "Invitato",
"pad.userlist.deny": "Refusar",
"pad.userlist.approve": "Approbar",
"pad.editbar.clearcolors": "Rader le colores de autor in tote le documento? Isto non pote esser disfacite",
"pad.impexp.importbutton": "Importar ora",
"pad.impexp.importing": "Importation in curso…",

View File

@ -29,9 +29,7 @@
"pad.colorpicker.cancel": "Batalkan",
"pad.loading": "Memuat...",
"pad.noCookie": "Kuki tidak dapat ditemukan. Izinkan kuki di peramban Anda!",
"pad.passwordRequired": "Anda memerlukan kata sandi untuk mengakses pad ini",
"pad.permissionDenied": "Anda tidak memiliki izin untuk mengakses pad ini",
"pad.wrongPassword": "Kata sandi Anda salah",
"pad.settings.padSettings": "Pengaturan Pad",
"pad.settings.myView": "Tampilan Saya",
"pad.settings.stickychat": "Chat selalu di layar",
@ -116,9 +114,6 @@
"pad.savedrevs.timeslider": "Anda bisa melihat revisi yang tersimpan dengan mengunjungi timeslider",
"pad.userlist.entername": "Masukkan nama Anda",
"pad.userlist.unnamed": "tanpa nama",
"pad.userlist.guest": "Tamu",
"pad.userlist.deny": "Tolak",
"pad.userlist.approve": "Terima",
"pad.editbar.clearcolors": "Padamkan warna penulis pada seluruh dokumen?",
"pad.impexp.importbutton": "Impor Sekarang",
"pad.impexp.importing": "Mengimpor...",

View File

@ -28,9 +28,7 @@
"pad.colorpicker.cancel": "Hætta við",
"pad.loading": "Hleð inn...",
"pad.noCookie": "Smákaka fannst ekki. Þú verður að leyfa smákökur í vafranum þínum!",
"pad.passwordRequired": "Þú þarft að gefa upp lykilorð til að komast á þessa skrifblokk",
"pad.permissionDenied": "Þú hefur ekki réttindi til að nota þessa skrifblokk",
"pad.wrongPassword": "Lykilorðinu þínu var hafnað",
"pad.settings.padSettings": "Stillingar skrifblokkar",
"pad.settings.myView": "Mitt yfirlit",
"pad.settings.stickychat": "Spjall alltaf á skjánum",
@ -41,6 +39,8 @@
"pad.settings.fontType": "Leturgerð:",
"pad.settings.fontType.normal": "Venjulegt",
"pad.settings.language": "Tungumál:",
"pad.settings.about": "Um hugbúnaðinn",
"pad.settings.poweredBy": "Keyrt með",
"pad.importExport.import_export": "Flytja inn/út",
"pad.importExport.import": "Settu inn hverskyns texta eða skjal",
"pad.importExport.importSuccessful": "Heppnaðist!",
@ -51,7 +51,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">settu þá upp AbiWord forritið</a>.",
"pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">settu þá upp AbiWord forritið eða LibreOffice</a>.",
"pad.modals.connected": "Tengt.",
"pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni...",
"pad.modals.forcereconnect": "Þvinga endurtengingu",
@ -116,10 +116,7 @@
"pad.savedrevs.timeslider": "Þú getur skoðað vistaðar útgáfur með því að fara á tímalínuna",
"pad.userlist.entername": "Settu inn nafnið þitt",
"pad.userlist.unnamed": "ónefnt",
"pad.userlist.guest": "Gestur",
"pad.userlist.deny": "Hafna",
"pad.userlist.approve": "Samþykkja",
"pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu?",
"pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu? Þetta er ekki hægt að afturkalla",
"pad.impexp.importbutton": "Flytja inn núna",
"pad.impexp.importing": "Flyt inn...",
"pad.impexp.confirmimport": "Innflutningur á skrá mun skrifa yfir þann texta sem er á skrifblokkinni núna. \nErtu viss um að þú viljir halda áfram?",

View File

@ -34,9 +34,7 @@
"pad.colorpicker.cancel": "Annulla",
"pad.loading": "Caricamento in corso…",
"pad.noCookie": "Il cookie non è stato trovato. Consenti i cookie nel tuo browser! La sessione e le impostazioni non verranno salvate tra le diverse visite. Ciò può essere dovuto al fatto che Etherpad è stato incluso in un iFrame in alcuni browser. Assicurati che Etherpad si trovi sullo stesso sottodominio/dominio dell'iFrame principale",
"pad.passwordRequired": "Per accedere a questo Pad è necessaria una password",
"pad.permissionDenied": "Non si dispone dei permessi necessari per accedere a questo Pad",
"pad.wrongPassword": "La password è sbagliata",
"pad.settings.padSettings": "Impostazioni del Pad",
"pad.settings.myView": "Mia visualizzazione",
"pad.settings.stickychat": "Chat sempre sullo schermo",
@ -61,7 +59,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "È possibile importare solo i formati di testo semplice o HTML. Per metodi più avanzati di importazione <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installare AbiWord o LibreOffice</a>.",
"pad.modals.connected": "Connesso.",
"pad.modals.reconnecting": "Riconnessione al pad in corso...",
"pad.modals.reconnecting": "Riconnessione al pad in corso",
"pad.modals.forcereconnect": "Forza la riconnessione",
"pad.modals.reconnecttimer": "Tentativo di riconnessione",
"pad.modals.cancel": "Annulla",
@ -124,9 +122,6 @@
"pad.savedrevs.timeslider": "Puoi vedere le versioni salvate visitando la cronologia",
"pad.userlist.entername": "Inserisci il tuo nome",
"pad.userlist.unnamed": "senza nome",
"pad.userlist.guest": "Ospite",
"pad.userlist.deny": "Nega",
"pad.userlist.approve": "Approva",
"pad.editbar.clearcolors": "Eliminare i colori degli autori sull'intero documento? Questa azione non può essere annullata",
"pad.impexp.importbutton": "Importa ora",
"pad.impexp.importing": "Importazione in corso...",
@ -137,6 +132,5 @@
"pad.impexp.importfailed": "Importazione fallita",
"pad.impexp.copypaste": "Si prega di copiare e incollare",
"pad.impexp.exportdisabled": "L'esportazione come {{type}} è disabilitata. Contattare l'amministratore per i dettagli.",
"pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione",
"pad.impexp.permission": "L'importazione è disabilitata perché non hai mai contribuito a questo pad. Per favore, contribuisci almeno una volta, prima di fare importazioni"
"pad.impexp.maxFileSize": "File troppo grande. Contatta l'amministratore del sito per incrementare la dimensione consentita per l'importazione"
}

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