reset
13
CHANGELOG.md
|
@ -1,13 +0,0 @@
|
||||||
# v1.1
|
|
||||||
* Introduced Plugin framework
|
|
||||||
* Many bugfixes
|
|
||||||
* Faster page loading
|
|
||||||
* Various UI polishes
|
|
||||||
* Saved Revisions
|
|
||||||
* Read only Real time view
|
|
||||||
* More API functionality
|
|
||||||
|
|
||||||
# v 1.0.1
|
|
||||||
|
|
||||||
* Updated MySQL driver, this fixes some problems with mysql
|
|
||||||
* Fixed export,import and timeslider link when embed parameters are used
|
|
202
LICENSE
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2012 THE ETHERPAD FOUNDATION
|
|
||||||
|
|
||||||
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.
|
|
130
README.md
|
@ -1,130 +0,0 @@
|
||||||
# Our goal is to make collaborative editing the standard on the web
|
|
||||||
|
|
||||||
# About
|
|
||||||
Etherpad lite is a really-real time collaborative editor spawned from the Hell fire of Etherpad.
|
|
||||||
We're reusing the well tested Etherpad easysync library to make it really realtime. Etherpad Lite
|
|
||||||
is based on node.js ergo is much lighter and more stable than the original Etherpad. Our hope
|
|
||||||
is that this will encourage more users to use and install a realtime collaborative editor. A smaller, manageable and well
|
|
||||||
documented codebase makes it easier for developers to improve the code and contribute towards the project.
|
|
||||||
|
|
||||||
Etherpad Lite is optimized to be easy embeddable. It provides a [HTTP API](https://github.com/Pita/etherpad-lite/wiki/HTTP-API)
|
|
||||||
that allows your web application to manage pads, users and groups.
|
|
||||||
There are several clients in for this API:
|
|
||||||
|
|
||||||
* [PHP](https://github.com/TomNomNom/etherpad-lite-client), thx to [TomNomNom](https://github.com/TomNomNom)
|
|
||||||
* [.Net](https://github.com/ja-jo/EtherpadLiteDotNet), thx to [ja-jo](https://github.com/ja-jo)
|
|
||||||
* [Node.js](https://github.com/tomassedovic/etherpad-lite-client-js), thx to [tomassedovic](https://github.com/tomassedovic)
|
|
||||||
* [Ruby](https://github.com/jhollinger/ruby-etherpad-lite), thx to [jhollinger](https://github.com/jhollinger)
|
|
||||||
* [Python](https://github.com/devjones/PyEtherpadLite), thx to [devjones](https://github.com/devjones)
|
|
||||||
|
|
||||||
There is also a [jQuery plugin](https://github.com/johnyma22/etherpad-lite-jquery-plugin) that helps you to embed Pads into your website
|
|
||||||
|
|
||||||
**Online demo**<br>
|
|
||||||
Visit <http://beta.etherpad.org> to test it live
|
|
||||||
|
|
||||||
Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
|
|
||||||
|
|
||||||
# Etherpad vs Etherpad Lite
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td> </td><td><b>Etherpad</b></td><td><b>Etherpad Lite</b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="right">Size of the folder (without git history)</td><td>30 MB</td><td>1.5 MB</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="right">Languages used server side</td><td>Javascript (Rhino), Java, Scala</td><td>Javascript (node.js)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="right">Lines of server side Javascript code</td><td>~101k</td><td>~9k</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="right">RAM Usage immediately after start</td><td>257 MB (grows to ~1GB)</td><td>16 MB (grows to ~30MB)</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
|
|
||||||
1. Download <http://etherpad.org/etherpad-lite-win.zip>
|
|
||||||
2. Extract the file
|
|
||||||
3. Open the extracted folder and double click `start.bat`
|
|
||||||
4. Open your web browser and browse to <http://localhost:9001>. You like it? Look at the 'Next Steps' section below
|
|
||||||
|
|
||||||
## Linux
|
|
||||||
|
|
||||||
**As root:**
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc. <br><i>For Debian/Ubuntu</i> <code>apt-get install gzip git-core curl python libssl-dev pkg-config build-essential</code><br>
|
|
||||||
<i>For Fedora/CentOS</i> <code>yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"</code>
|
|
||||||
</li><br>
|
|
||||||
<li>Install node.js
|
|
||||||
<ol type="a">
|
|
||||||
<li>Download the latest <b>0.6.x</b> node.js release from <a href="http://nodejs.org/#download">http://nodejs.org/#download</a></li>
|
|
||||||
<li>Extract it with <code>tar xf node-v0.6*</code></li>
|
|
||||||
<li>Move into the node folder <code>cd node-v0.6*</code> and build node with <code>./configure && make && make install</code></li>
|
|
||||||
</ol>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
**As any user (we recommend creating a separate user called etherpad-lite):**
|
|
||||||
|
|
||||||
<ol start="3">
|
|
||||||
<li>Move to a folder where you want to install Etherpad Lite. Clone the git repository <code>git clone 'git://github.com/Pita/etherpad-lite.git'</code><br></li>
|
|
||||||
<li>Change into the directory containing the Etherpad Lite source code clone with <code>cd etherpad-lite</code><br></li>
|
|
||||||
<li>Start it with <code>bin/run.sh</code><br> </li>
|
|
||||||
<li>Open your web browser and visit <a href="http://localhost:9001">http://localhost:9001</a>. You like it? Look at the 'Next Steps' section below</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
You can modify the settings in the file `settings.json`
|
|
||||||
|
|
||||||
If you have multiple settings files, you may pass one to `bin/run.sh` using the `-s|--settings` option. This allows you to run multiple Etherpad Lite instances from the same installation.
|
|
||||||
|
|
||||||
You should use a dedicated database such as "mysql" if you are planning on using etherpad-lite in a production environment, the "dirty" database driver is only for testing and/or development purposes.
|
|
||||||
|
|
||||||
You can update to the latest version with `git pull origin`. The next start with bin/run.sh will update the dependencies
|
|
||||||
|
|
||||||
Look at this wiki pages:
|
|
||||||
|
|
||||||
* [How to deploy Etherpad Lite as a service](https://github.com/Pita/etherpad-lite/wiki/How-to-deploy-Etherpad-Lite-as-a-service)
|
|
||||||
* [How to put Etherpad Lite behind a reverse Proxy](https://github.com/Pita/etherpad-lite/wiki/How-to-put-Etherpad-Lite-behind-a-reverse-Proxy)
|
|
||||||
* [How to customize your Etherpad Lite installation](https://github.com/Pita/etherpad-lite/wiki/How-to-customize-your-Etherpad-Lite-installation)
|
|
||||||
* [How to use Etherpad-Lite with jQuery](https://github.com/Pita/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-jQuery)
|
|
||||||
* [How to use Etherpad Lite with MySQL](https://github.com/Pita/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-MySQL)
|
|
||||||
* [Sites that run Etherpad Lite](https://github.com/Pita/etherpad-lite/wiki/Sites-that-run-Etherpad-Lite)
|
|
||||||
* [How to migrate the database from Etherpad to Etherpad Lite](https://github.com/Pita/etherpad-lite/wiki/How-to-migrate-the-database-from-Etherpad-to-Etherpad-Lite)
|
|
||||||
|
|
||||||
You can find more information in the [wiki](https://github.com/Pita/etherpad-lite/wiki). Feel free to improve these wiki pages
|
|
||||||
|
|
||||||
# Develop
|
|
||||||
If you're new to git and github, start by watching [this video](http://youtu.be/67-Q26YH97E) then read this [git guide](http://learn.github.com/p/intro.html).
|
|
||||||
|
|
||||||
If you're new to node.js, start with this video <http://youtu.be/jo_B4LTHi3I>.
|
|
||||||
|
|
||||||
You can debug with `bin/debugRun.sh`
|
|
||||||
|
|
||||||
If you want to find out how Etherpads Easysync works (the library that makes it really realtime), start with this [PDF](https://github.com/Pita/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).
|
|
||||||
|
|
||||||
You know all this and just want to know how you can help? Look at the [TODO list](https://github.com/Pita/etherpad-lite/wiki/TODO).
|
|
||||||
You can join the [mailinglist](http://groups.google.com/group/etherpad-lite-dev) or go to the freenode irc channel [#etherpad-lite-dev](http://webchat.freenode.net?channels=#etherpad-lite-dev)
|
|
||||||
|
|
||||||
You also help the project, if you only host a Etherpad Lite instance and share your experience with us.
|
|
||||||
|
|
||||||
Please consider using [jshint](http://www.jshint.com/about/) if you plan to
|
|
||||||
contribute to Etherpad Lite.
|
|
||||||
|
|
||||||
# Modules created for this project
|
|
||||||
|
|
||||||
* [ueberDB](https://github.com/Pita/ueberDB) "transforms every database into a object key value store" - manages all database access
|
|
||||||
* [channels](https://github.com/Pita/channels) "Event channels in node.js" - ensures that ueberDB operations are atomic and in series for each key
|
|
||||||
* [async-stacktrace](https://github.com/Pita/async-stacktrace) "Improves node.js stacktraces and makes it easier to handle errors"
|
|
||||||
|
|
||||||
# Donations
|
|
||||||
* [Etherpad Foundation Flattr] (http://flattr.com/thing/71378/Etherpad-Foundation)
|
|
||||||
* [Paypal] (http://etherpad.org) <-- Click the donate button
|
|
||||||
|
|
||||||
# License
|
|
||||||
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
|
|
|
@ -1,16 +0,0 @@
|
||||||
So, a plugin is an npm package whose name starts with ep_ and that contains a file ep.json
|
|
||||||
require("ep_etherpad-lite/static/js/plugingfw/plugins").update() will use npm to list all installed modules and read their ep.json files. These will contain registrations for hooks which are loaded
|
|
||||||
A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name)
|
|
||||||
require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value}) will call all hook functions registered for hook_name
|
|
||||||
That is the basis.
|
|
||||||
Ok, so that was a slight simplification: inside ep.json, hook registrations are grouped into groups called "parts". Parts from all plugins are ordered using a topological sort according to "pre" and "post" pointers to other plugins/parts (just like dependencies, but non-installed plugins are silently ignored).
|
|
||||||
This ordering is honored when you do callAll(hook_name) - hook functions for that hook_name are called in that order
|
|
||||||
Ordering between plugins is undefined, only parts are ordered.
|
|
||||||
|
|
||||||
A plugin usually has one part, but it van have multiple.
|
|
||||||
This is so that it can insert some hook registration before that of another plugin, and another one after.
|
|
||||||
This is important for e.g. registering URL-handlers for the express webserver, if you have some very generic and some very specific url-regexps
|
|
||||||
So, that's basically it... apart from client-side hooks
|
|
||||||
which works the same way, but uses a separate member of the part (part.client_hooks vs part.hooks), and where the hook function must obviously reside in a file require():able from the client...
|
|
||||||
One thing more: The main etherpad tree is actually a plugin itself, called ep_etherpad-lite, and it has it's own ep.json...
|
|
||||||
was that clear?
|
|
|
@ -1,7 +0,0 @@
|
||||||
.git*
|
|
||||||
docs/
|
|
||||||
examples/
|
|
||||||
support/
|
|
||||||
test/
|
|
||||||
testing.js
|
|
||||||
.DS_Store
|
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"parts": [
|
|
||||||
{
|
|
||||||
"name": "somepart",
|
|
||||||
"pre": [],
|
|
||||||
"post": ["ep_onemoreplugin/partone"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "partlast",
|
|
||||||
"pre": ["ep_fintest/otherpart"],
|
|
||||||
"post": [],
|
|
||||||
"hooks": {
|
|
||||||
"somehookname": "ep_fintest/partlast:somehook"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "partfirst",
|
|
||||||
"pre": [],
|
|
||||||
"post": ["ep_onemoreplugin/somepart"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "otherpart",
|
|
||||||
"pre": ["ep_fintest/somepart", "ep_otherplugin/main"],
|
|
||||||
"post": [],
|
|
||||||
"hooks": {
|
|
||||||
"somehookname": "ep_fintest/otherpart:somehook",
|
|
||||||
"morehook": "ep_fintest/otherpart:morehook",
|
|
||||||
"expressCreateServer": "ep_fintest/otherpart:expressServer",
|
|
||||||
"eejsBlock_editbarMenuLeft": "ep_fintest/otherpart:eejsBlock_editbarMenuLeft"
|
|
||||||
},
|
|
||||||
"client_hooks": {
|
|
||||||
"somehookname": "ep_fintest/static/js/test:bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
test = require("ep_fintest/static/js/test.js");
|
|
||||||
console.log("FOOO:", test.foo);
|
|
||||||
|
|
||||||
exports.somehook = function (hook_name, args, cb) {
|
|
||||||
return cb(["otherpart:somehook was here"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.morehook = function (hook_name, args, cb) {
|
|
||||||
return cb(["otherpart:morehook was here"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.expressServer = function (hook_name, args, cb) {
|
|
||||||
args.app.get('/otherpart', function(req, res) {
|
|
||||||
res.send("<em>Abra cadabra</em>");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) {
|
|
||||||
args.content = args.content + '\
|
|
||||||
<li id="testButton" onClick="window.pad&&pad.editbarClick(\'clearauthorship\');return false;">\
|
|
||||||
<a class="buttonicon buttonicon-test" title="Test test test"></a>\
|
|
||||||
</li>\
|
|
||||||
';
|
|
||||||
return cb();
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "ep_fintest",
|
|
||||||
"description": "A test plugin",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"author": "RedHog (Egil Moeller) <egil.moller@freecode.no>",
|
|
||||||
"contributors": [],
|
|
||||||
"dependencies": {},
|
|
||||||
"engines": { "node": ">= 0.4.1 < 0.7.0" }
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
exports.somehook = function (hook_name, args, cb) {
|
|
||||||
return cb(["partlast:somehook was here"]);
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
exports.foo = 42;
|
|
||||||
|
|
||||||
exports.bar = function (hook_name, args, cb) {
|
|
||||||
return cb(["FOOOO"]);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
<em>Test bla bla</em>
|
|
|
@ -1,63 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
NODE_VERSION="0.6.5"
|
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
|
||||||
cd `dirname $0`
|
|
||||||
|
|
||||||
#Was this script started in the bin folder? if yes move out
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Is wget installed?
|
|
||||||
hash wget > /dev/null 2>&1 || {
|
|
||||||
echo "Please install wget" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Is zip installed?
|
|
||||||
hash zip > /dev/null 2>&1 || {
|
|
||||||
echo "Please install zip" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Is zip installed?
|
|
||||||
hash unzip > /dev/null 2>&1 || {
|
|
||||||
echo "Please install unzip" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
START_FOLDER=$(pwd);
|
|
||||||
|
|
||||||
echo "create a clean environment in /tmp/etherpad-lite-win..."
|
|
||||||
rm -rf /tmp/etherpad-lite-win
|
|
||||||
cp -ar . /tmp/etherpad-lite-win
|
|
||||||
cd /tmp/etherpad-lite-win
|
|
||||||
rm -rf node_modules
|
|
||||||
rm -f etherpad-lite-win.zip
|
|
||||||
|
|
||||||
echo "do a normal unix install first..."
|
|
||||||
bin/installDeps.sh || exit 1
|
|
||||||
|
|
||||||
echo "copy the windows settings template..."
|
|
||||||
cp settings.json.template_windows settings.json
|
|
||||||
|
|
||||||
echo "resolve symbolic links..."
|
|
||||||
cp -rL node_modules node_modules_resolved
|
|
||||||
rm -rf node_modules
|
|
||||||
mv node_modules_resolved node_modules
|
|
||||||
|
|
||||||
echo "download windows node..."
|
|
||||||
cd bin
|
|
||||||
wget "http://nodejs.org/dist/v$NODE_VERSION/node.exe" -O node.exe
|
|
||||||
|
|
||||||
echo "create the zip..."
|
|
||||||
cd /tmp
|
|
||||||
zip -9 -r etherpad-lite-win.zip etherpad-lite-win
|
|
||||||
mv etherpad-lite-win.zip $START_FOLDER
|
|
||||||
|
|
||||||
echo "clean up..."
|
|
||||||
rm -rf /tmp/etherpad-lite-win
|
|
||||||
|
|
||||||
echo "Finished. You can find the zip in the Etherpad Lite root folder, it's called etherpad-lite-win.zip"
|
|
134
bin/checkPad.js
|
@ -1,134 +0,0 @@
|
||||||
/*
|
|
||||||
This is a debug tool. It checks all revisions for data corruption
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(process.argv.length != 3)
|
|
||||||
{
|
|
||||||
console.error("Use: node bin/checkPad.js $PADID");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
//get the padID
|
|
||||||
var padId = process.argv[2];
|
|
||||||
|
|
||||||
//initalize the database
|
|
||||||
var log4js = require("../src/node_modules/log4js");
|
|
||||||
log4js.setGlobalLogLevel("INFO");
|
|
||||||
var async = require("../src/node_modules/async");
|
|
||||||
var db = require('../src/node/db/DB');
|
|
||||||
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var padManager;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//intallize the database
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
db.init(callback);
|
|
||||||
},
|
|
||||||
//get the pad
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager = require('../src/node/db/PadManager');
|
|
||||||
|
|
||||||
padManager.doesPadExists(padId, function(err, exists)
|
|
||||||
{
|
|
||||||
if(!exists)
|
|
||||||
{
|
|
||||||
console.error("Pad does not exist");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
pad = _pad;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
//create an array with key kevisions
|
|
||||||
//key revisions always save the full pad atext
|
|
||||||
var head = pad.getHeadRevisionNumber();
|
|
||||||
var keyRevisions = [];
|
|
||||||
for(var i=0;i<head;i+=100)
|
|
||||||
{
|
|
||||||
keyRevisions.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//run trough all key revisions
|
|
||||||
async.forEachSeries(keyRevisions, function(keyRev, callback)
|
|
||||||
{
|
|
||||||
//create an array of revisions we need till the next keyRevision or the End
|
|
||||||
var revisionsNeeded = [];
|
|
||||||
for(var i=keyRev;i<=keyRev+100 && i<=head; i++)
|
|
||||||
{
|
|
||||||
revisionsNeeded.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//this array will hold all revision changesets
|
|
||||||
var revisions = [];
|
|
||||||
|
|
||||||
//run trough all needed revisions and get them from the database
|
|
||||||
async.forEach(revisionsNeeded, function(revNum, callback)
|
|
||||||
{
|
|
||||||
db.db.get("pad:"+padId+":revs:" + revNum, function(err, revision)
|
|
||||||
{
|
|
||||||
revisions[revNum] = revision;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}, function(err)
|
|
||||||
{
|
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if the pad has a pool
|
|
||||||
if(pad.pool === undefined )
|
|
||||||
{
|
|
||||||
console.error("Attribute pool is missing");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if there is a atext in the keyRevisions
|
|
||||||
if(revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined)
|
|
||||||
{
|
|
||||||
console.error("No atext in key revision " + keyRev);
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var apool = pad.pool;
|
|
||||||
var atext = revisions[keyRev].meta.atext;
|
|
||||||
|
|
||||||
for(var i=keyRev+1;i<=keyRev+100 && i<=head; i++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//console.log("check revision " + i);
|
|
||||||
var cs = revisions[i].changeset;
|
|
||||||
atext = Changeset.applyToAText(cs, atext, apool);
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
console.error("Bad changeset at revision " + i + " - " + e.message);
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], function (err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("finished");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
454
bin/convert.js
|
@ -1,454 +0,0 @@
|
||||||
|
|
||||||
var startTime = new Date().getTime();
|
|
||||||
var fs = require("fs");
|
|
||||||
var ueberDB = require("ueberDB");
|
|
||||||
var mysql = require("mysql");
|
|
||||||
var async = require("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");
|
|
||||||
|
|
||||||
var settingsFile = process.argv[2];
|
|
||||||
var sqlOutputFile = process.argv[3];
|
|
||||||
|
|
||||||
//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("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";
|
|
||||||
fs.writeSync(sqlOutput, sql);
|
|
||||||
log("done");
|
|
||||||
|
|
||||||
//set setings for ep db
|
|
||||||
var etherpadDB= new mysql.Client();
|
|
||||||
etherpadDB.host = settings.etherpadDB.host;
|
|
||||||
etherpadDB.port = settings.etherpadDB.port;
|
|
||||||
etherpadDB.database = settings.etherpadDB.database;
|
|
||||||
etherpadDB.user = settings.etherpadDB.user;
|
|
||||||
etherpadDB.password = settings.etherpadDB.password;
|
|
||||||
|
|
||||||
//get the timestamp once
|
|
||||||
var timestamp = new Date().getTime();
|
|
||||||
|
|
||||||
var padIDs;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//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)
|
|
||||||
{
|
|
||||||
padIDs = _padIDs;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
log("done");
|
|
||||||
|
|
||||||
//create a queue with a concurrency 100
|
|
||||||
var queue = async.queue(function (padId, callback)
|
|
||||||
{
|
|
||||||
convertPad(padId, function(err)
|
|
||||||
{
|
|
||||||
incrementPadStats();
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
//set the step callback as the queue callback
|
|
||||||
queue.drain = callback;
|
|
||||||
|
|
||||||
//add the padids to the worker queue
|
|
||||||
for(var i=0,length=padIDs.length;i<length;i++)
|
|
||||||
{
|
|
||||||
queue.push(padIDs[i].ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
|
|
||||||
//write the groups
|
|
||||||
var sql = "";
|
|
||||||
for(var proID in proID2groupID)
|
|
||||||
{
|
|
||||||
var groupID = proID2groupID[proID];
|
|
||||||
var subdomain = proID2subdomain[proID];
|
|
||||||
|
|
||||||
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("group:" + groupID) + ", " + etherpadDB.escape(JSON.stringify(groups[groupID]))+ ");\n";
|
|
||||||
sql+="REPLACE INTO store VALUES (" + etherpadDB.escape("mapper2group:subdomain:" + subdomain) + ", " + etherpadDB.escape(groupID)+ ");\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
//close transaction
|
|
||||||
sql+="COMMIT;";
|
|
||||||
|
|
||||||
//end the sql file
|
|
||||||
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
|
|
||||||
fs.closeSync(sqlOutput);
|
|
||||||
|
|
||||||
log("finished.");
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
function log(str)
|
|
||||||
{
|
|
||||||
console.log((new Date().getTime() - startTime)/1000 + "\t" + str);
|
|
||||||
}
|
|
||||||
|
|
||||||
var padsDone = 0;
|
|
||||||
|
|
||||||
function incrementPadStats()
|
|
||||||
{
|
|
||||||
padsDone++;
|
|
||||||
|
|
||||||
if(padsDone%100 == 0)
|
|
||||||
{
|
|
||||||
var averageTime = Math.round(padsDone/((new Date().getTime() - startTime)/1000));
|
|
||||||
log(padsDone + "/" + padIDs.length + "\t" + averageTime + " pad/s")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var proID2groupID = {};
|
|
||||||
var proID2subdomain = {};
|
|
||||||
var groups = {};
|
|
||||||
|
|
||||||
function convertPad(padId, callback)
|
|
||||||
{
|
|
||||||
var changesets = [];
|
|
||||||
var changesetsMeta = [];
|
|
||||||
var chatMessages = [];
|
|
||||||
var authors = [];
|
|
||||||
var apool;
|
|
||||||
var subdomain;
|
|
||||||
var padmeta;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//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=?)";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [padId], function(err, results)
|
|
||||||
{
|
|
||||||
if(!err)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//parse the pages
|
|
||||||
for(var i=0,length=results.length;i<length;i++)
|
|
||||||
{
|
|
||||||
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
|
|
||||||
}
|
|
||||||
}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=?)";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [padId], function(err, results)
|
|
||||||
{
|
|
||||||
if(!err)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//parse the pages
|
|
||||||
for(var i=0,length=results.length;i<length;i++)
|
|
||||||
{
|
|
||||||
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false);
|
|
||||||
}
|
|
||||||
}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=?)";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [padId], function(err, results)
|
|
||||||
{
|
|
||||||
if(!err)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//parse the pages
|
|
||||||
for(var i=0,length=results.length;i<length;i++)
|
|
||||||
{
|
|
||||||
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
|
|
||||||
}
|
|
||||||
}catch(e) {err = e}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the attribute pool of this pad
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var 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}
|
|
||||||
}
|
|
||||||
|
|
||||||
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=?)";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [padId], function(err, results)
|
|
||||||
{
|
|
||||||
if(!err)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//parse the pages
|
|
||||||
for(var i=0, length=results.length;i<length;i++)
|
|
||||||
{
|
|
||||||
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
|
|
||||||
}
|
|
||||||
}catch(e) {err = e}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the pad information
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var sql = "SELECT JSON FROM `PAD_META` WHERE ID=?";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [padId], function(err, results)
|
|
||||||
{
|
|
||||||
if(!err)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
padmeta = JSON.parse(results[0].JSON).x;
|
|
||||||
}catch(e) {err = e}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//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];
|
|
||||||
|
|
||||||
var sql = "SELECT subDomain FROM pro_domains WHERE ID = ?";
|
|
||||||
|
|
||||||
etherpadDB.query(sql, [proID], function(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 = {};
|
|
||||||
|
|
||||||
//this is a pro pad, let's convert it to a group pad
|
|
||||||
if(padId.indexOf("$") != -1)
|
|
||||||
{
|
|
||||||
var padIdParts = padId.split("$");
|
|
||||||
var proID = padIdParts[0];
|
|
||||||
var padName = padIdParts[1];
|
|
||||||
|
|
||||||
var groupID
|
|
||||||
|
|
||||||
//this proID is not converted so far, do it
|
|
||||||
if(proID2groupID[proID] == null)
|
|
||||||
{
|
|
||||||
groupID = "g." + randomString(16);
|
|
||||||
|
|
||||||
//create the mappers for this new group
|
|
||||||
proID2groupID[proID] = groupID;
|
|
||||||
proID2subdomain[proID] = subdomain;
|
|
||||||
groups[groupID] = {pads: {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
//use the generated groupID;
|
|
||||||
groupID = proID2groupID[proID];
|
|
||||||
|
|
||||||
//rename the pad
|
|
||||||
padId = groupID + "$" + padName;
|
|
||||||
|
|
||||||
//set the value for this pad in the group
|
|
||||||
groups[groupID].pads[padId] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newAuthorIDs = {};
|
|
||||||
var oldName2newName = {};
|
|
||||||
|
|
||||||
//replace the authors with generated authors
|
|
||||||
// we need to do that cause etherpad saves pad local authors, etherpad lite uses them global
|
|
||||||
for(var i in apool.numToAttrib)
|
|
||||||
{
|
|
||||||
var key = apool.numToAttrib[i][0];
|
|
||||||
var value = apool.numToAttrib[i][1];
|
|
||||||
|
|
||||||
//skip non authors and anonymous authors
|
|
||||||
if(key != "author" || value == "")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
//generate new author values
|
|
||||||
var authorID = "a." + randomString(16);
|
|
||||||
var authorColorID = authors[i].colorId || Math.floor(Math.random()*32);
|
|
||||||
var authorName = authors[i].name || null;
|
|
||||||
|
|
||||||
//overwrite the authorID of the attribute pool
|
|
||||||
apool.numToAttrib[i][1] = authorID;
|
|
||||||
|
|
||||||
//write the author to the database
|
|
||||||
values["globalAuthor:" + authorID] = {"colorId" : authorColorID, "name": authorName, "timestamp": timestamp};
|
|
||||||
|
|
||||||
//save in mappers
|
|
||||||
newAuthorIDs[i] = authorID;
|
|
||||||
oldName2newName[value] = authorID;
|
|
||||||
}
|
|
||||||
|
|
||||||
//save all revisions
|
|
||||||
for(var i=0;i<changesets.length;i++)
|
|
||||||
{
|
|
||||||
values["pad:" + padId + ":revs:" + i] = {changeset: changesets[i],
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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)
|
|
||||||
{
|
|
||||||
curRev++;
|
|
||||||
var 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 skiped");
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeSync(sqlOutput, sql, undefined, "utf-8");
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This parses a Page like Etherpad uses them in the databases
|
|
||||||
* The offsets descripes 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(",");
|
|
||||||
|
|
||||||
for(var i=0;i<lengths.length;i++)
|
|
||||||
{
|
|
||||||
var unitLength = lengths[i];
|
|
||||||
|
|
||||||
//skip empty units
|
|
||||||
if(unitLength == "")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
//parse the number
|
|
||||||
unitLength = Number(unitLength);
|
|
||||||
|
|
||||||
//cut the unit out of data
|
|
||||||
var unit = data.substr(start, unitLength);
|
|
||||||
|
|
||||||
//put it into the array
|
|
||||||
array[pageStart + i] = json ? JSON.parse(unit) : unit;
|
|
||||||
|
|
||||||
//update start
|
|
||||||
start+=unitLength;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"etherpadDB":
|
|
||||||
{
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 3306,
|
|
||||||
"database": "etherpad",
|
|
||||||
"user": "etherpaduser",
|
|
||||||
"password": "yourpassword"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
|
||||||
cd `dirname $0`
|
|
||||||
|
|
||||||
#Was this script started in the bin folder? if yes move out
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#prepare the enviroment
|
|
||||||
bin/installDeps.sh || exit 1
|
|
||||||
|
|
||||||
hash node-inspector > /dev/null 2>&1 || {
|
|
||||||
echo "You need to install node-inspector to run the tests!" >&2
|
|
||||||
echo "You can install it with npm" >&2
|
|
||||||
echo "Run: npm install -g node-inspector" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
node-inspector &
|
|
||||||
|
|
||||||
echo "If you are new to node-inspector, take a look at this video: http://youtu.be/AOnK3NVnxL8"
|
|
||||||
|
|
||||||
node --debug node_modules/ep_etherpad-lite/node/server.js $*
|
|
||||||
|
|
||||||
#kill node-inspector before ending
|
|
||||||
kill $!
|
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
This is a debug tool. It helps to extract all datas of a pad and move it from an productive enviroment and to a develop enviroment to reproduce bugs there. It outputs a dirtydb file
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(process.argv.length != 3)
|
|
||||||
{
|
|
||||||
console.error("Use: node extractPadData.js $PADID");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
//get the padID
|
|
||||||
var padId = process.argv[2];
|
|
||||||
|
|
||||||
//initalize the database
|
|
||||||
var log4js = require("log4js");
|
|
||||||
log4js.setGlobalLogLevel("INFO");
|
|
||||||
var async = require("async");
|
|
||||||
var db = require('../node/db/DB');
|
|
||||||
var dirty = require("dirty")(padId + ".db");
|
|
||||||
var padManager;
|
|
||||||
var pad;
|
|
||||||
var neededDBValues = ["pad:"+padId];
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//intallize the database
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
db.init(callback);
|
|
||||||
},
|
|
||||||
//get the pad
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager = require('../node/db/PadManager');
|
|
||||||
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
pad = _pad;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
//add all authors
|
|
||||||
var authors = pad.getAllAuthors();
|
|
||||||
for(var i=0;i<authors.length;i++)
|
|
||||||
{
|
|
||||||
neededDBValues.push("globalAuthor:" + authors[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//add all revisions
|
|
||||||
var revHead = pad.head;
|
|
||||||
for(var i=0;i<=revHead;i++)
|
|
||||||
{
|
|
||||||
neededDBValues.push("pad:"+padId+":revs:" + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all chat values
|
|
||||||
var chatHead = pad.chatHead;
|
|
||||||
for(var i=0;i<=chatHead;i++)
|
|
||||||
{
|
|
||||||
neededDBValues.push("pad:"+padId+":chat:" + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//get and set all values
|
|
||||||
async.forEach(neededDBValues, function(dbkey, callback)
|
|
||||||
{
|
|
||||||
db.db.db.wrappedDB.get(dbkey, function(err, dbvalue)
|
|
||||||
{
|
|
||||||
if(err) { callback(err); return}
|
|
||||||
dbvalue=JSON.parse(dbvalue);
|
|
||||||
|
|
||||||
dirty.set(dbkey, dbvalue, callback);
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], function (err)
|
|
||||||
{
|
|
||||||
if(err) throw err;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("finished");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//get the pad object
|
|
||||||
//get all revisions of this pad
|
|
||||||
//get all authors related to this pad
|
|
||||||
//get the readonly link releated to this pad
|
|
||||||
//get the chat entrys releated to this pad
|
|
|
@ -1,101 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
|
||||||
cd `dirname $0`
|
|
||||||
|
|
||||||
#Was this script started in the bin folder? if yes move out
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Is wget installed?
|
|
||||||
hash curl > /dev/null 2>&1 || {
|
|
||||||
echo "Please install curl" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Is node installed?
|
|
||||||
hash node > /dev/null 2>&1 || {
|
|
||||||
echo "Please install node.js ( http://nodejs.org )" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Is npm installed?
|
|
||||||
hash npm > /dev/null 2>&1 || {
|
|
||||||
echo "Please install npm ( http://npmjs.org )" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#check npm version
|
|
||||||
NPM_VERSION=$(npm --version)
|
|
||||||
if [ ! $(echo $NPM_VERSION | cut -d "." -f 1) = "1" ]; then
|
|
||||||
echo "You're running a wrong version of npm, you're using $NPM_VERSION, we need 1.x" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#check node version
|
|
||||||
NODE_VERSION=$(node --version)
|
|
||||||
if [ ! $(echo $NODE_VERSION | cut -d "." -f 1-2) = "v0.6" ]; then
|
|
||||||
echo "You're running a wrong version of node, you're using $NODE_VERSION, we need v0.6.x" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Get the name of the settings file
|
|
||||||
settings="settings.json"
|
|
||||||
a='';
|
|
||||||
for arg in $*; do
|
|
||||||
if [ "$a" = "--settings" ] || [ "$a" = "-s" ]; then settings=$arg; fi
|
|
||||||
a=$arg
|
|
||||||
done
|
|
||||||
|
|
||||||
#Does a $settings exist? if no copy the template
|
|
||||||
if [ ! -f $settings ]; then
|
|
||||||
echo "Copy the settings template to $settings..."
|
|
||||||
cp -v settings.json.template $settings || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Ensure that all dependencies are up to date..."
|
|
||||||
(
|
|
||||||
mkdir -p node_modules
|
|
||||||
cd node_modules
|
|
||||||
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
|
|
||||||
cd ep_etherpad-lite
|
|
||||||
npm install
|
|
||||||
) || {
|
|
||||||
rm -rf node_modules
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Ensure jQuery is downloaded and up to date..."
|
|
||||||
DOWNLOAD_JQUERY="true"
|
|
||||||
NEEDED_VERSION="1.7.1"
|
|
||||||
if [ -f "src/static/js/jquery.js" ]; then
|
|
||||||
VERSION=$(cat src/static/js/jquery.js | head -n 3 | grep -o "v[0-9]\.[0-9]\(\.[0-9]\)\?");
|
|
||||||
|
|
||||||
if [ ${VERSION#v} = $NEEDED_VERSION ]; then
|
|
||||||
DOWNLOAD_JQUERY="false"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $DOWNLOAD_JQUERY = "true" ]; then
|
|
||||||
curl -lo src/static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Remove all minified data to force node creating it new
|
|
||||||
echo "Clear minfified cache..."
|
|
||||||
rm -f var/minified*
|
|
||||||
|
|
||||||
echo "ensure custom css/js files are created..."
|
|
||||||
|
|
||||||
for f in "index" "pad" "timeslider"
|
|
||||||
do
|
|
||||||
if [ ! -f "src/static/custom/$f.js" ]; then
|
|
||||||
cp -v "src/static/custom/js.template" "src/static/custom/$f.js" || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "src/static/custom/$f.css" ]; then
|
|
||||||
cp -v "src/static/custom/css.template" "src/static/custom/$f.css" || exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,9 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
JSHINT=./node_modules/jshint/bin/hint
|
|
||||||
|
|
||||||
$JSHINT ./node/
|
|
|
@ -1,75 +0,0 @@
|
||||||
This load tester is extremely useful for testing how many dormant clients can connect to etherpad lite.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
Emulate characters being typed into a pad
|
|
||||||
|
|
||||||
HOW TO USE (from @mjd75) proper formatting at: https://github.com/Pita/etherpad-lite/issues/360
|
|
||||||
|
|
||||||
Server 1:
|
|
||||||
Installed Node.js (etc), EtherPad Lite and MySQL
|
|
||||||
|
|
||||||
Server 2:
|
|
||||||
Installed Xvfb and PhantomJS
|
|
||||||
|
|
||||||
I installed Xvfb following (roughly) this guide: http://blog.martin-lyness.com/archives/installing-xvfb-on-ubuntu-9-10-karmic-koala
|
|
||||||
|
|
||||||
#sudo apt-get install xvfb
|
|
||||||
#sudo apt-get install xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic
|
|
||||||
|
|
||||||
Launched two instances of Xvfb directly from the terminal:
|
|
||||||
|
|
||||||
#Xvfb :0 -ac
|
|
||||||
#Xvfb :1 -ac
|
|
||||||
|
|
||||||
I installed PhantomJS following this guide: http://code.google.com/p/phantomjs/wiki/Installation
|
|
||||||
|
|
||||||
#sudo add-apt-repository ppa:jerome-etienne/neoip
|
|
||||||
#sudo apt-get update
|
|
||||||
#sudo apt-get install phantomjs
|
|
||||||
|
|
||||||
I created a small JavaScript file for PhatomJS to use to control the browser instances:
|
|
||||||
|
|
||||||
### BEGIN JAVASCRIPT ###
|
|
||||||
|
|
||||||
var page = new WebPage(),
|
|
||||||
t, address;
|
|
||||||
|
|
||||||
if (phantom.args.length === 0) {
|
|
||||||
console.log('Usage: loader.js <some URL>');
|
|
||||||
phantom.exit();
|
|
||||||
} else {
|
|
||||||
t = Date.now();
|
|
||||||
address = phantom.args[0];
|
|
||||||
|
|
||||||
var page = new WebPage();
|
|
||||||
page.onResourceRequested = function (request) {
|
|
||||||
console.log('Request ' + JSON.stringify(request, undefined, 4));
|
|
||||||
};
|
|
||||||
page.onResourceReceived = function (response) {
|
|
||||||
console.log('Receive ' + JSON.stringify(response, undefined, 4));
|
|
||||||
};
|
|
||||||
page.open(address);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
### END JAVASCRIPT ###
|
|
||||||
|
|
||||||
And finally a launcher script that uses screen to run 400 instances of PhantomJS with the above script:
|
|
||||||
|
|
||||||
### BEGIN SHELL SCRIPT ###
|
|
||||||
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# connect 200 instances to display :0
|
|
||||||
for i in {1..200}
|
|
||||||
do
|
|
||||||
DISPLAY=:0 screen -d -m phantomjs loader.js http://ec2-50-17-168-xx.compute-1.amazonaws.com:9001/p/pad2 && sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# connect 200 instances to display :1
|
|
||||||
for i in {1..200}
|
|
||||||
do
|
|
||||||
DISPLAY=:1 screen -d -m phantomjs loader.js http://ec2-50-17-168-xx.compute-1.amazonaws.com:9001/p/pad2 && sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
### END SHELL SCRIPT ###
|
|
|
@ -1,16 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# connect 500 instances to display :0
|
|
||||||
for i in {1..500}
|
|
||||||
do
|
|
||||||
echo $i
|
|
||||||
echo "Displaying Some shit"
|
|
||||||
DISPLAY=:0 screen -d -m /home/phantomjs/bin/phantomjs loader.js http://10.0.0.55:9001/p/pad2 && sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
# connect 500 instances to display :1
|
|
||||||
for i in {1..500}
|
|
||||||
do
|
|
||||||
echo $i
|
|
||||||
DISPLAY=:1 screen -d -m /home/phantomjs/bin/phantomjs loader.js http://10.0.0.55:9001/p/pad2 && sleep 2
|
|
||||||
done
|
|
|
@ -1,20 +0,0 @@
|
||||||
var page = new WebPage(),
|
|
||||||
t, address;
|
|
||||||
|
|
||||||
if (phantom.args.length === 0) {
|
|
||||||
console.log('Usage: loader.js <some URL>');
|
|
||||||
phantom.exit();
|
|
||||||
} else {
|
|
||||||
t = Date.now();
|
|
||||||
address = phantom.args[0];
|
|
||||||
|
|
||||||
var page = new WebPage();
|
|
||||||
page.onResourceRequested = function (request) {
|
|
||||||
console.log('Request ' + JSON.stringify(request, undefined, 4));
|
|
||||||
};
|
|
||||||
page.onResourceReceived = function (response) {
|
|
||||||
console.log('Receive ' + JSON.stringify(response, undefined, 4));
|
|
||||||
};
|
|
||||||
page.open(address);
|
|
||||||
|
|
||||||
}
|
|
28
bin/run.sh
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
|
||||||
cd `dirname $0`
|
|
||||||
|
|
||||||
#Was this script started in the bin folder? if yes move out
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Stop the script if its started as root
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
echo "You shouldn't start Etherpad-Lite as root!"
|
|
||||||
echo "Please type 'Etherpad Lite rocks my socks' if you still want to start it as root"
|
|
||||||
read rocks
|
|
||||||
if [ ! $rocks = "Etherpad Lite rocks my socks" ]
|
|
||||||
then
|
|
||||||
echo "Your input was incorrect"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#prepare the enviroment
|
|
||||||
bin/installDeps.sh $* || exit 1
|
|
||||||
|
|
||||||
#Move to the node folder and start
|
|
||||||
echo "start..."
|
|
||||||
node node_modules/ep_etherpad-lite/node/server.js $*
|
|
|
@ -1,66 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#This script ensures that ep-lite is automatically restarting after an error happens
|
|
||||||
|
|
||||||
#Handling Errors
|
|
||||||
# 0 silent
|
|
||||||
# 1 email
|
|
||||||
ERROR_HANDLING=0
|
|
||||||
# Your email address which should recieve the error messages
|
|
||||||
EMAIL_ADDRESS="no-reply@example.com"
|
|
||||||
# Sets the minimun amount of time betweens the sending of error emails.
|
|
||||||
# This ensures you not get spamed while a endless reboot loop
|
|
||||||
# It's the time in seconds
|
|
||||||
TIME_BETWEEN_EMAILS=600 # 10 minutes
|
|
||||||
|
|
||||||
# DON'T EDIT AFTER THIS LINE
|
|
||||||
|
|
||||||
LAST_EMAIL_SEND=0
|
|
||||||
|
|
||||||
#Move to the folder where ep-lite is installed
|
|
||||||
cd `dirname $0`
|
|
||||||
|
|
||||||
#Was this script started in the bin folder? if yes move out
|
|
||||||
if [ -d "../bin" ]; then
|
|
||||||
cd "../"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#check if a logfile parameter is set
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Set a logfile as the first parameter"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while [ 1 ]
|
|
||||||
do
|
|
||||||
#try to touch the file if it doesn't exist
|
|
||||||
if [ ! -f $1 ]; then
|
|
||||||
touch $1 || ( echo "Logfile '$1' is not writeable" && exit 1 )
|
|
||||||
fi
|
|
||||||
|
|
||||||
#check if the file is writeable
|
|
||||||
if [ ! -w $1 ]; then
|
|
||||||
echo "Logfile '$1' is not writeable"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#start the application
|
|
||||||
bin/run.sh >>$1 2>>$1
|
|
||||||
|
|
||||||
#Send email
|
|
||||||
if [ $ERROR_HANDLING = 1 ]; then
|
|
||||||
TIME_NOW=$(date +%s)
|
|
||||||
TIME_SINCE_LAST_SEND=$(($TIME_NOW - $LAST_EMAIL_SEND))
|
|
||||||
|
|
||||||
if [ $TIME_SINCE_LAST_SEND -gt $TIME_BETWEEN_EMAILS ]; then
|
|
||||||
printf "Server was restared at: $(date)\nThe last 50 lines of the log before the error happens:\n $(tail -n 50 $1)" | mail -s "Pad Server was restarted" $EMAIL_ADDRESS
|
|
||||||
|
|
||||||
LAST_EMAIL_SEND=$TIME_NOW
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "RESTART!" >>$1
|
|
||||||
|
|
||||||
#Sleep 10 seconds before restart
|
|
||||||
sleep 10
|
|
||||||
done
|
|
|
@ -1,62 +0,0 @@
|
||||||
# Database structure
|
|
||||||
|
|
||||||
### pad:$PADID
|
|
||||||
Saves all informations about pads
|
|
||||||
|
|
||||||
* **atext** - the latest attributed text
|
|
||||||
* **pool** - the attribute pool
|
|
||||||
* **head** - the number of the latest revision
|
|
||||||
* **chatHead** - the number of the latest chat entry
|
|
||||||
* **public** - flag that disables security for this pad
|
|
||||||
* **passwordHash** - string that contains a bcrypt hashed password for this pad
|
|
||||||
|
|
||||||
### pad:$PADID:revs:$REVNUM
|
|
||||||
Saves a revision $REVNUM of pad $PADID
|
|
||||||
|
|
||||||
* **meta**
|
|
||||||
* **author** - the autorID of this revision
|
|
||||||
* **timestamp** - the timestamp of when this revision was created
|
|
||||||
* **changeset** - the changeset of this revision
|
|
||||||
|
|
||||||
### pad:$PADID:chat:$CHATNUM
|
|
||||||
Saves a chatentry with num $CHATNUM of pad $PADID
|
|
||||||
|
|
||||||
* **text** - the text of this chat entry
|
|
||||||
* **userId** - the autorID of this chat entry
|
|
||||||
* **time** - the timestamp of this chat entry
|
|
||||||
|
|
||||||
### pad2readonly:$PADID
|
|
||||||
Translates a padID to a readonlyID
|
|
||||||
### readonly2pad:$READONLYID
|
|
||||||
Translates a readonlyID to a padID
|
|
||||||
### token2author:$TOKENID
|
|
||||||
Translates a token to an authorID
|
|
||||||
### globalAuthor:$AUTHORID
|
|
||||||
Information about an author
|
|
||||||
|
|
||||||
* **name** - the name of this author as shown in the pad
|
|
||||||
* **colorID** - the colorID of this author as shown in the pad
|
|
||||||
|
|
||||||
### mapper2group:$MAPPER
|
|
||||||
Maps an external application identifier to an internal group
|
|
||||||
### mapper2author:$MAPPER
|
|
||||||
Maps an external application identifier to an internal author
|
|
||||||
### group:$GROUPID
|
|
||||||
a group of pads
|
|
||||||
|
|
||||||
* **pads** - object with pad names in it, values are 1
|
|
||||||
### session:$SESSIONID
|
|
||||||
a session between an author and a group
|
|
||||||
|
|
||||||
* **groupID** - the groupID the session belongs too
|
|
||||||
* **authorID** - the authorID the session belongs too
|
|
||||||
* **validUntil** - the timestamp until this session is valid
|
|
||||||
|
|
||||||
### author2sessions:$AUTHORID
|
|
||||||
saves the sessions of an author
|
|
||||||
|
|
||||||
* **sessionsIDs** - object with sessionIDs in it, values are 1
|
|
||||||
|
|
||||||
### group2sessions:$GROUPID
|
|
||||||
|
|
||||||
* **sessionsIDs** - object with sessionIDs in it, values are 1
|
|
|
@ -1,2 +0,0 @@
|
||||||
# About this folder
|
|
||||||
We put all documentations we found about the old Etherpad together in this folder. Most of this is still valid for Etherpad Lite
|
|
|
@ -1,372 +0,0 @@
|
||||||
\documentclass{article}
|
|
||||||
\usepackage{hyperref}
|
|
||||||
|
|
||||||
\begin{document}
|
|
||||||
|
|
||||||
\title{Etherpad and EasySync Technical Manual}
|
|
||||||
\author{AppJet, Inc., with modifications by the Etherpad Foundation}
|
|
||||||
\date{\today}
|
|
||||||
|
|
||||||
\maketitle
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
||||||
\tableofcontents
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
||||||
|
|
||||||
\section{Documents}
|
|
||||||
\begin{itemize}
|
|
||||||
\item A document is a list of characters, or a string.
|
|
||||||
\item A document can also be represented as a list of \emph{changesets}.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\section{Changesets}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item A changeset represents a change to a document.
|
|
||||||
\item A changeset can be applied to a document to produce a new document.
|
|
||||||
\item When a document is represented as a list of changesets, it is assumed that the first changeset applies to the empty document, [].
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
|
|
||||||
\section{Changeset representation} \label{representation}
|
|
||||||
|
|
||||||
$$(\ell \rightarrow \ell')[c_1,c_2,c_3,...]$$
|
|
||||||
|
|
||||||
where
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item[] $\ell$ is the length of the document before the change,
|
|
||||||
\item[] $\ell'$ is the length of the document after the change,
|
|
||||||
\item[] $[c_1,c_2,c_3,...]$ is an array of $\ell'$ characters that described the document after the change.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
Note that $\forall c_i : 0 \leq i \leq \ell'$ is either an integer or a character.
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item Integers represent retained characters in the original document.
|
|
||||||
\item Characters represent insertions.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\section{Constraints on Changesets}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item Changesets are canonical and therefor comparable. When represented in computer memory, we always use the same representation for the same changeset. If the memory representation of two changesets differ, they must be different changesets.
|
|
||||||
\item Changesets are compact. Thus, if there are two ways to represent a changeset in computer memory, then we always use the representation that takes up the fewest bytes.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
Later we will discuss optimizations to changeset
|
|
||||||
representation (using ``strips'' and other such
|
|
||||||
techniques). The two constraints must apply to any
|
|
||||||
representation of changesets.
|
|
||||||
|
|
||||||
\section{Notation}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item We use the algebraic multiplication notation to represent changeset application.
|
|
||||||
\item While changesets are defined as operations on documents, documents themselves are represented as a list of changesets, initially applying to the empty document.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\paragraph{Example}
|
|
||||||
$A=(0\rightarrow 5)[``hello"]$
|
|
||||||
$B=(5\rightarrow 11)[0-4, ``\ world"]$
|
|
||||||
|
|
||||||
We can write the document ``hello world'' as $A\cdot B$ or
|
|
||||||
just $AB$. Note that the ``initial document'' can be made
|
|
||||||
into the changeset $(0\rightarrow
|
|
||||||
N)[``<\mathit{the\ document\ text}>"]$.
|
|
||||||
|
|
||||||
When $A$ and $B$ are changesets, we can also refer to $(AB)$ as ``the composition'' of $A$ and $B$. Changesets are closed under composition.
|
|
||||||
|
|
||||||
\section{Composition of Changesets}
|
|
||||||
|
|
||||||
For any two changesets $A$, $B$ such that
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item[] $A=(n_1\rightarrow n_2)[\cdots]$
|
|
||||||
\item[] $A=(n_2\rightarrow n_3)[\cdots]$
|
|
||||||
\end{itemize}
|
|
||||||
it is clear that there is a third changeset $C=(n_1\rightarrow n_3)[\cdots]$ such that applying $C$ to a document $X$ yeilds the same resulting document as does applying $A$ and then $B$. In this case, we write $AB=C$.
|
|
||||||
|
|
||||||
Given the representation from Section \ref{representation}, it is straightforward to compute the composition of two changesets.
|
|
||||||
|
|
||||||
\section{Changeset Merging}
|
|
||||||
|
|
||||||
Now we come to realtime document editing. Suppose two different users make two different changes to the same document at the same time. It is impossible to compose these changes. For example, if we have the document $X$ of length $n$, we may have $A=(n\rightarrow n_a)[\ldots n_a \mathrm{characters}]$, $B=(n\rightarrow n_b)[\ldots n_b \mathrm{characters}]$ where $n\neq n_a\neq n_b$.
|
|
||||||
|
|
||||||
It is impossible to compute $(XA)B$ because $B$ can only be applied to a document of length $n$, and $(XA)$ has length $n_a$. Similarly, $A$ cannot be appliet to $(XB)$ because $(XB)$ has length $n_b$.
|
|
||||||
|
|
||||||
This is where \emph{merging} comes in. Merging takes two changesets that apply to the same initial document (and that cannot be composed), and computes a single new changeset that presevers the intent of both changes. The merge of $A$ and $B$ is written as $m(A,B)$. For the Etherpad system to work, we require that $m(A,B)=m(B,A)$.
|
|
||||||
|
|
||||||
Aside from what we have said so far about merging, there aremany different implementations that will lead to a workable system. We have created one implementation for text that has the following constraints.
|
|
||||||
|
|
||||||
\section{Follows} \label{follows}
|
|
||||||
|
|
||||||
When users $A$ and $B$ have the same document $X$ on their screen, and they proceed to make respective changesets $A$ and $B$, it is no use to compute $m(A,B)$, because $m(A,B)$ applies to document $X$, but the users are already looking at document $XA$ and $XB$. What we really want is to compute $B'$ and $A'$ such that
|
|
||||||
$$XAB' = XBA' = Xm(A,B)$$
|
|
||||||
|
|
||||||
``Following'' computes these $B'$ and $A'$ changesets. The definition of the ``follow'' function $f$ is such that $Af(A,B)=Bf(B,A)=m(A,B)=m(B,A)$. When we computer $f(A,B)$.
|
|
||||||
\begin{itemize}
|
|
||||||
\item Insertions in $A$ become retained characters in $f(A,B)$
|
|
||||||
\item Insertions in $B$ become insertions in $f(A,B)$
|
|
||||||
\item Retain whatever characters are retained in \emph{both} $A$ and $B$
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\paragraph{Example}
|
|
||||||
|
|
||||||
Suppose we have the initial document $X=(0\rightarrow 8)[``\mathit{baseball}"]$ and user $A$ changes it to ``basil'' with changeset $A$, and user $B$ changes it to ``below'' with changeset $B$.
|
|
||||||
|
|
||||||
We have
|
|
||||||
$X=(0\rightarrow 8)[``\mathit{baseball}"]$ \\
|
|
||||||
$A=(8\rightarrow 5)[0-1, ``\mathit{si}", 7]$ \\
|
|
||||||
$B=(8\rightarrow 5)[0, ``\mathit{e}", 6, ``\mathit{ow}"]$ \\
|
|
||||||
|
|
||||||
First we compute the merge $m(A,B)=m(B,A)$ according to the constraints
|
|
||||||
|
|
||||||
$$m(A,B)=(8\rightarrow 6)[0, "e", "si", "ow"] = (8\rightarrow 6)[0, ``\mathit{esiow}"]$$
|
|
||||||
|
|
||||||
Then we need to compute the follows $B'=f(A,B)$ and $A'=f(B,A)$.
|
|
||||||
|
|
||||||
$$B'=f(A,B)=(5\rightarrow 6)[0,``\mathit{e}",2,3,``\mathit{ow}"]$$
|
|
||||||
|
|
||||||
Note that the numbers $0$, $2$, and $3$ are indices into $A=(8\rightarrow 5)[0,1,``\mathit{si}",7]$
|
|
||||||
|
|
||||||
\begin{tabular}{ccccc}
|
|
||||||
0 & 1 & 2 & 3 & 4 \\
|
|
||||||
0 & 1 & s & i & 7
|
|
||||||
\end{tabular}
|
|
||||||
|
|
||||||
$A'=f(B,A)=(5\rightarrow 6)[0,1,"si",3,4]$
|
|
||||||
|
|
||||||
We can now double check that $AB'=BA'=m(A,B)=(8\rightarrow 6)[0,``\mathit{esiow}"]$.
|
|
||||||
|
|
||||||
Now that we have made the mathematical meaning of the
|
|
||||||
preceding pages complete, we can build a client/server
|
|
||||||
system to support realtime editing by multiple users.
|
|
||||||
|
|
||||||
\section{System Overview}
|
|
||||||
|
|
||||||
There is a server that holds the current state of a
|
|
||||||
document. Clients (users) can connect to the server from
|
|
||||||
their web browsers. The clients and server maintain state
|
|
||||||
and can send messages to one another in real-time, but
|
|
||||||
because we are in a web browser scenario, clients cannot
|
|
||||||
send each other messages directly, and must go through the
|
|
||||||
server always. (This may distinguish from prior art?)
|
|
||||||
|
|
||||||
The other critical design feature of the system is that
|
|
||||||
\emph{A client must always be able to edit their local
|
|
||||||
copy of the document, so the user is never blocked from
|
|
||||||
typing because of waiting to to send or receive data.}
|
|
||||||
|
|
||||||
\section{Client State}
|
|
||||||
|
|
||||||
At any moment in time, a client maintains its state in the
|
|
||||||
form of 3 changesets. The client document looks like
|
|
||||||
$A\cdot X \cdot Y$, where
|
|
||||||
|
|
||||||
$A$ is the latest server version, the composition of all
|
|
||||||
changesets committed to the server, from this client or
|
|
||||||
from others, that the server has informed this client
|
|
||||||
about. Initially $A=(0\rightarrow N)[<\mathit{initial\ document\ text}>]$.
|
|
||||||
|
|
||||||
$X$ is the composition of all changesets this client has
|
|
||||||
submitted to the server but has not heard back about yet.
|
|
||||||
Initially $X=(N\rightarrow N)[0,1,2,\ldots, N-1]$, in
|
|
||||||
other words, the identity, henceforth denoted $I_N$.
|
|
||||||
|
|
||||||
$Y$ is the composition of all changesets this client has
|
|
||||||
made but has not yet submitted to the server yet.
|
|
||||||
Initially $Y=(N\rightarrow N)[0,1,2,\ldots, N-1]$.
|
|
||||||
|
|
||||||
\section{Client Operations}
|
|
||||||
|
|
||||||
A client can do 5 things.
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Incorporate new typing into local state
|
|
||||||
\item Submit a changeset to the server
|
|
||||||
\item Hear back acknowledgement of a submitted changeset
|
|
||||||
\item Hear from the server about other clients' changesets
|
|
||||||
\item Connect to the server and request the initial document
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
As these 5 events happen, the client updates its
|
|
||||||
representation $A\cdot X \cdot Y$ according to the
|
|
||||||
relations that follow. Changes ``move left'' as time goes
|
|
||||||
by: into $Y$ when the user types, into $X$ when change
|
|
||||||
sets are submitted to the server, and into $A$ when the
|
|
||||||
server acknowledges changesets.
|
|
||||||
|
|
||||||
\subsection{New local typing}
|
|
||||||
|
|
||||||
When a user makes an edit $E$ to the document, the client
|
|
||||||
computes the composition $(Y\cdot E)$ and updates its local
|
|
||||||
state, i.e. $Y \leftarrow Y\cdot E$. I.e., if $Y$ is the
|
|
||||||
variable holding local unsubmitted changes, it will be
|
|
||||||
assigned the new value $(Y\cdot E)$.
|
|
||||||
|
|
||||||
\subsection{Submitting changesets to server}
|
|
||||||
|
|
||||||
When a client submit its local changes to the server, it
|
|
||||||
transmits a copy of $Y$ and then assigns $Y$ to $X$, and
|
|
||||||
assigns the identity to $Y$. I.e.,
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Send $Y$ to server,
|
|
||||||
\item $X \leftarrow Y$
|
|
||||||
\item $Y \leftarrow I_N$
|
|
||||||
(the identity).
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
This happens every 500ms as long as it receives an
|
|
||||||
acknowledgement. Must receive ACK before submitting
|
|
||||||
again. Note that $X$ is always equal to the identity
|
|
||||||
before the second step occurs, so no information is lost.
|
|
||||||
|
|
||||||
\subsection{Hear ACK from server}
|
|
||||||
|
|
||||||
When the client hears ACK from server,
|
|
||||||
|
|
||||||
$A \leftarrow A\cdot X$ \\
|
|
||||||
$X \leftarrow I_N$
|
|
||||||
|
|
||||||
\subsection{Hear about another client's changeset}
|
|
||||||
|
|
||||||
When a client hears about another client's changeset $B$,
|
|
||||||
it computes a new $A$, $X$, and $Y$, which we will call
|
|
||||||
$A'$, $X'$, and $Y'$ respectively. It also computes a
|
|
||||||
changeset $D$ which is applied to the current text view on
|
|
||||||
the client, $V$. Because $AXY$ must always equal the
|
|
||||||
current view, $AXY=V$ before the client hears about $B$,
|
|
||||||
and $A'X'Y'=VD$ after the computation is performed.
|
|
||||||
|
|
||||||
The steps are:
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Compute $A' = AB$
|
|
||||||
\item Compute $X' = f(B,X)$
|
|
||||||
\item Compute $Y' = f(f(X,B), Y)$
|
|
||||||
\item Compute $D=f(Y,f(X,B))$
|
|
||||||
\item Assign $A \leftarrow A'$, $X \leftarrow X'$, $Y \leftarrow Y'$.
|
|
||||||
\item Apply $D$ to the current view of the document
|
|
||||||
displayed on the user's screen.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
In steps 2,3, and 4, $f$ is the follow operation described
|
|
||||||
in Section \ref{follows}.
|
|
||||||
|
|
||||||
\paragraph{Proof that $\mathbf{AXY=V \Rightarrow A'X'Y'=VD}$.}
|
|
||||||
Substituting $A'X'Y'=(AB)(f(B,X))(f(f(X,B),Y))$, we
|
|
||||||
recall that merges are commutative. So for any two
|
|
||||||
changesets $P$ and $Q$,
|
|
||||||
$$m(P,Q)=m(Q,P)=Qf(Q,P)=Pf(P,Q)$$
|
|
||||||
|
|
||||||
Applying this to the relation above, we see
|
|
||||||
\begin{eqnarray*}
|
|
||||||
A'X'Y'&=& AB f(B,X) f(f(X,B),Y) \\
|
|
||||||
&=&AX f(X,B) f(f(X,B),Y) \\
|
|
||||||
&=&A X Y f(Y, f(X,B)) \\
|
|
||||||
&=&A X Y D \\
|
|
||||||
&=&V D
|
|
||||||
\end{eqnarray*}
|
|
||||||
As claimed.
|
|
||||||
|
|
||||||
\subsection{Connect to server}
|
|
||||||
|
|
||||||
When a client connects to the server for the first time,
|
|
||||||
it first generates a random unique ID and sends this to
|
|
||||||
the server. The client remembers this ID and sends it
|
|
||||||
with each changeset to the server.
|
|
||||||
|
|
||||||
The client receives the latest version of the document
|
|
||||||
from the server, called HEADTEXT. The client then sets
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item[] $A \leftarrow \mathrm{HEADTEXT}$
|
|
||||||
\item[] $X \leftarrow I_N$
|
|
||||||
\item[] $Y \leftarrow I_N$
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
And finally, the client displays HEADTEXT on the screen.
|
|
||||||
|
|
||||||
\section{Server Overview}
|
|
||||||
|
|
||||||
Like the client(s), the server has state and performs
|
|
||||||
operations. Operations are only performed in response to
|
|
||||||
messages from clients.
|
|
||||||
|
|
||||||
\section{Server State}
|
|
||||||
|
|
||||||
The server maintains a document as an ordered list of
|
|
||||||
\emph{revision records}. A revision record is a data
|
|
||||||
structure that contains a changeset and authorship
|
|
||||||
information.
|
|
||||||
|
|
||||||
\begin{verbatim}
|
|
||||||
RevisionRecord = {
|
|
||||||
ChangeSet,
|
|
||||||
Source (unique ID),
|
|
||||||
Revision Number (consecutive order, starting at 0)
|
|
||||||
}
|
|
||||||
\end{verbatim}
|
|
||||||
|
|
||||||
For efficiency, the server may also store a variable
|
|
||||||
called HEADTEXT, which is the composition of all
|
|
||||||
changesets in the list of revision records. This is an
|
|
||||||
optimization, because clearly this can be computed from
|
|
||||||
the set of revision records.
|
|
||||||
|
|
||||||
\section{Server Operations Overview}
|
|
||||||
|
|
||||||
The server does two things in addition to maintaining
|
|
||||||
state representing the set of connected clients and
|
|
||||||
remembering what revision number each client is up to date
|
|
||||||
with:
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Respond to a client's connection requesting the initial document.
|
|
||||||
\item Respond to a client's submission of a new changeset.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
\subsection{Respond to client connect}
|
|
||||||
When a server recieves a connection request from a client,
|
|
||||||
it receives the client's unique ID and stores that in the
|
|
||||||
server's set of connected clients. It then sends the
|
|
||||||
client the contents of HEADTEXT, and the corresponding
|
|
||||||
revision number. Finally the server notes that this
|
|
||||||
client is up to date with that revision number.
|
|
||||||
|
|
||||||
\subsection{Respond to client changeset}
|
|
||||||
|
|
||||||
When the server receives information from a client about
|
|
||||||
the client's changeset $C$, it does five things:
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Notes that this change applies to revision number
|
|
||||||
$r_c$ (the client's latest revision).
|
|
||||||
\item Creates a new changeset $C'$ that is relative to the
|
|
||||||
server's most recent revision number, which we call
|
|
||||||
$r_H$ ($H$ for HEAD). $C'$ can be computed using
|
|
||||||
follows (Section \ref{follows}). Remember that the server has a series of
|
|
||||||
changesets,
|
|
||||||
$$S_0\rightarrow S_1\rightarrow \ldots S_{r_c}\rightarrow S_{r_c+1} \rightarrow \ldots \rightarrow S_{r_H} $$
|
|
||||||
$C$ is relative to $S_{r_c}$, but we need to compute $C'$ relative to $S_{r_H}$.
|
|
||||||
We can compute a new $C$ relative to $S_{r_c+1}$ by computing $f(S_{r_c+1},C)$. Similarly we can repeat for
|
|
||||||
$S_{r_c+2}$ and so forth until we have $C'$ represented relative to $S_{r_H}$.
|
|
||||||
\item Send $C'$ to all other clients
|
|
||||||
\item Send ACK back to original client
|
|
||||||
\item Add $C'$ to the server's list of revision records by creating a new revision record out of this and the client's ID.
|
|
||||||
|
|
||||||
\appendix
|
|
||||||
|
|
||||||
\section*{Additional topics}
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Optimizations (strips, more caching, etc.)
|
|
||||||
\item Pseudocode for composition, merge, and follow
|
|
||||||
\item How authorship information is used to color-code the document based on who typed what
|
|
||||||
\item How persistent connections are maintained between client and server
|
|
||||||
\end{enumerate}
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
|
|
||||||
\end{document}
|
|
|
@ -1,200 +0,0 @@
|
||||||
\documentclass[12pt]{article}
|
|
||||||
|
|
||||||
\usepackage[T1]{fontenc}
|
|
||||||
\usepackage[USenglish]{babel}
|
|
||||||
|
|
||||||
|
|
||||||
\begin{document}
|
|
||||||
|
|
||||||
\title{Easysync Protocol}
|
|
||||||
\author{AppJet, Inc., with modifications by the Etherpad Foundation}
|
|
||||||
\date{\today}
|
|
||||||
|
|
||||||
\maketitle
|
|
||||||
|
|
||||||
\section{Attributes}
|
|
||||||
|
|
||||||
An ``attribute'' is a (key,value) pair such as
|
|
||||||
\verb|(author,abc123)| or \verb|(bold,true)|.
|
|
||||||
Sometimes an attribute is treated as an instruction to add
|
|
||||||
that attribute, in which case an empty value means to
|
|
||||||
remove it. So \verb|(bold,)| removes the ``bold''
|
|
||||||
attribute. Attributes are interned and given numeric IDs,
|
|
||||||
so the number ``\verb|6|'' could represent
|
|
||||||
``\verb|(bold,true)|'', for example. This mapping is
|
|
||||||
stored in an attribute pool which may be shared by
|
|
||||||
multiple changesets.
|
|
||||||
|
|
||||||
Entries in the pool must be unique, so that attributes can
|
|
||||||
be compared by their IDs. Attribute names cannot contain
|
|
||||||
commas.
|
|
||||||
|
|
||||||
A changeset looks something like the following:
|
|
||||||
|
|
||||||
\begin{verbatim}
|
|
||||||
Z:5g>1|5=2p=v*4*5+1$x
|
|
||||||
\end{verbatim}
|
|
||||||
|
|
||||||
With the corresponding pool containing these entries (among others):
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item[] \verb|4| $\rightarrow$ \verb|(author,1059348573)|
|
|
||||||
\item[] \verb|5| $\rightarrow$ \verb|(bold,true)|
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
This changeset, together with the attribute pool,
|
|
||||||
represents inserting a bold letter ``x'' into the middle
|
|
||||||
of a line.
|
|
||||||
|
|
||||||
The string consists of:
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item a letter \verb|Z| (the ``magic character'' and
|
|
||||||
format version identifier)
|
|
||||||
\item a series punctuation marks (operation codes or
|
|
||||||
``opcodes'' for short), together with alphanumerics
|
|
||||||
(numeric values in base 36).
|
|
||||||
\item a dollar sign (\verb|$|)
|
|
||||||
\item a string of characters used for insertion operations
|
|
||||||
(the ``char bank'')
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
In the example above, if we separate out the operations
|
|
||||||
and convert the numbers to base 10, then we get:
|
|
||||||
\begin{verbatim}
|
|
||||||
Z :196 >1 |5=97 =31 *4 *5 +1 $x
|
|
||||||
\end{verbatim}
|
|
||||||
Here are descriptions of the operations, where capital
|
|
||||||
letters are variables:
|
|
||||||
|
|
||||||
\begin{description}
|
|
||||||
\item{{\bf :N}} \quad \\
|
|
||||||
Source text has length $N$ (must be first op)
|
|
||||||
\item{{\bf >N}} \quad \\
|
|
||||||
Final text is $N$ (positive) characters longer than source
|
|
||||||
text (must be second op)
|
|
||||||
\item{{\bf <N }} \quad \\
|
|
||||||
Final text is $N$ (positive) characters shorter than
|
|
||||||
source text (must be second op)
|
|
||||||
\item{{\bf >0 }} \quad \\
|
|
||||||
Final text is same length as source text
|
|
||||||
\item{{\bf +N }} \quad \\
|
|
||||||
Insert $N$ characters from the bank, none of them newlines
|
|
||||||
\item{{\bf -N}} \quad \\
|
|
||||||
Skip over (delete) $N$ characters from the source text,
|
|
||||||
none of them newlines
|
|
||||||
\item{{\bf =N}} \quad \\
|
|
||||||
Keep $N$ characters from the source text, none of them newlines
|
|
||||||
\item{{\bf |L+N}} \quad \\
|
|
||||||
Insert $N$ characters from the source text, containing $L$
|
|
||||||
newlines. The last character inserted MUST be a newline,
|
|
||||||
but not the (new) document's final newline.
|
|
||||||
\item{{\bf |L-N}} \quad \\
|
|
||||||
Delete $N$ characters from the source text, containing $L$
|
|
||||||
newlines. The last character inserted MUST be a newline,
|
|
||||||
but not the (old) document's final newline.
|
|
||||||
\item{{\bf |L=N}} \quad \\
|
|
||||||
Keep $N$ characters from the source text, containing L
|
|
||||||
newlines. The last character kept MUST be a newline, and
|
|
||||||
the final newline of the document is allowed.
|
|
||||||
\item{{\bf *I}} \quad \\
|
|
||||||
Apply attribute $I$ from the pool to the following
|
|
||||||
\verb|+|, \verb|=|, \verb_|+_, or \verb_|=_ command. In
|
|
||||||
other words, any number of \verb|*| ops can come before a
|
|
||||||
\verb_+_, \verb_=_, or \verb_|_ but not between a \verb_|_
|
|
||||||
and the corresponding \verb_+_ or \verb_=_. If \verb_+_,
|
|
||||||
text is inserted having this attribute. If \verb_=_, text
|
|
||||||
is kept but with the attribute applied as an attribute
|
|
||||||
addition or removal. Consecutive attributes must be sorted
|
|
||||||
lexically by (key,value) with key and value taken as
|
|
||||||
strings. It's illegal to have duplicate keys for
|
|
||||||
(key,value) pairs that apply to the same text. It's
|
|
||||||
illegal to have an empty value for a key in the case of an
|
|
||||||
insertion (\verb_+_), the pair should just be omitted.
|
|
||||||
\end{description}
|
|
||||||
|
|
||||||
Characters from the source text that aren't accounted for
|
|
||||||
are assumed to be kept with the same attributes.
|
|
||||||
|
|
||||||
\paragraph{Additional Constraints}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item Consecutive \verb_+_, \verb_-_, and \verb_=_ ops of
|
|
||||||
the same type that could be combined are not allowed.
|
|
||||||
Whether combination is possible depends on the
|
|
||||||
attributes of the ops and whether each is multiline or
|
|
||||||
not. For example, two multiline deletions can never be
|
|
||||||
consecutive, nor can any insertion come after a
|
|
||||||
non-multiline insertion with the same attributes.
|
|
||||||
\item ``No-op'' ops are not allowed, such as deleting 0
|
|
||||||
characters. However, attribute applications that don't
|
|
||||||
have any effect are allowed.
|
|
||||||
\item Characters at the end of the source text cannot be
|
|
||||||
explicitly kept with no changes; if the change doesn't
|
|
||||||
affect the last $N$ characters, those ``keep'' ops must
|
|
||||||
be left off.
|
|
||||||
\item In any consecutive sequence of insertions (\verb_+_)
|
|
||||||
and deletions (\verb_-_) with no keeps (\verb_=_), the
|
|
||||||
deletions must come before the insertions.
|
|
||||||
\item The document text before and after will always end
|
|
||||||
with a newline. This policy avoids a lot of
|
|
||||||
special-casing of the end of the document. If a final
|
|
||||||
newline is always added when importing text and removed
|
|
||||||
when exporting text, then the changeset representation
|
|
||||||
can be used to process text files that may or may not
|
|
||||||
have a final newline.
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\paragraph{Attribution string}
|
|
||||||
|
|
||||||
An \emph{attribution string} is a series of inserts with
|
|
||||||
no deletions or keeps. For example, ``\verb_*3+8|1+5_''
|
|
||||||
describes the attributes of a string of length 13, where
|
|
||||||
the first 8 chars have attribute 3 and the next 5 chars
|
|
||||||
have no attributes, with the last of these 5 chars being a
|
|
||||||
newline. Constraints apply similar to those affecting
|
|
||||||
changesets, but the restriction about the final newline of
|
|
||||||
the new document being added doesn't apply.
|
|
||||||
|
|
||||||
Attributes in an attribution string cannot be empty, like
|
|
||||||
``\verb|(bold,)|'', they should instead be absent.
|
|
||||||
|
|
||||||
|
|
||||||
\section{Further Considerations}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item composing changesets/attributions with different
|
|
||||||
pools.
|
|
||||||
\item generalizing ``applyToAttribution'' to make
|
|
||||||
``mutateAttributionLines'' and ``compose''
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\section{Using Unicode?}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item no unicode (for efficient escaping, sightliness)
|
|
||||||
\item efficient operations for ACE and collab (attributed text, etc.)
|
|
||||||
\item good for time-slider
|
|
||||||
\item good for API
|
|
||||||
\item line-ending aware
|
|
||||||
X more coherent (deleting or styling text merging with insertion)
|
|
||||||
\item server-side syntax highlighting?
|
|
||||||
\item unify author map with attribute pool
|
|
||||||
\item unify attributed text with changeset rep
|
|
||||||
\item not: reversible
|
|
||||||
\item force final newline of document to be preserved
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
\paragraph{Unicode bad!}
|
|
||||||
|
|
||||||
\begin{itemize}
|
|
||||||
\item ugly (hard to read)
|
|
||||||
\item more complex to parse
|
|
||||||
\item harder to store and transmit correctly
|
|
||||||
\item doesn't save all that much space anyway
|
|
||||||
\item blows up in size when string-escaped
|
|
||||||
\item embarrassing for API
|
|
||||||
\end{itemize}
|
|
||||||
|
|
||||||
|
|
||||||
\end{document}
|
|
|
@ -1,133 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
Copied from the old Etherpad. Found in /infrastructure/ace/
|
|
||||||
|
|
||||||
Goals:
|
|
||||||
|
|
||||||
- no unicode (for efficient escaping, sightliness)
|
|
||||||
- efficient operations for ACE and collab (attributed text, etc.)
|
|
||||||
- good for time-slider
|
|
||||||
- good for API
|
|
||||||
- line-ending aware
|
|
||||||
X more coherent (deleting or styling text merging with insertion)
|
|
||||||
- server-side syntax highlighting?
|
|
||||||
- unify author map with attribute pool
|
|
||||||
- unify attributed text with changeset rep
|
|
||||||
- not: reversible
|
|
||||||
- force final newline of document to be preserved
|
|
||||||
|
|
||||||
- Unicode bad:
|
|
||||||
- ugly (hard to read)
|
|
||||||
- more complex to parse
|
|
||||||
- harder to store and transmit correctly
|
|
||||||
- doesn't save all that much space anyway
|
|
||||||
- blows up in size when string-escaped
|
|
||||||
- embarrassing for API
|
|
||||||
|
|
||||||
|
|
||||||
# Attributes:
|
|
||||||
|
|
||||||
An "attribute" is a (key,value) pair such as (author,abc123456) or
|
|
||||||
(bold,true). Sometimes an attribute is treated as an instruction to
|
|
||||||
add that attribute, in which case an empty value means to remove it.
|
|
||||||
So (bold,) removes the "bold" attribute. Attributes are interned and
|
|
||||||
given numeric IDs, so the number "6" could represent "(bold,true)",
|
|
||||||
for example. This mapping is stored in an attribute "pool" which may
|
|
||||||
be shared by multiple changesets.
|
|
||||||
|
|
||||||
Entries in the pool must be unique, so that attributes can be compared
|
|
||||||
by their IDs. Attribute names cannot contain commas.
|
|
||||||
|
|
||||||
A changeset looks something like the following:
|
|
||||||
|
|
||||||
Z:5g>1|5=2p=v*4*5+1$x
|
|
||||||
|
|
||||||
With the corresponding pool containing these entries:
|
|
||||||
|
|
||||||
...
|
|
||||||
4 -> (author,1059348573)
|
|
||||||
5 -> (bold,true)
|
|
||||||
...
|
|
||||||
|
|
||||||
This changeset, together with the pool, represents inserting
|
|
||||||
a bold letter "x" into the middle of a line. The string consists of:
|
|
||||||
|
|
||||||
- a letter Z (the "magic character" and format version identifier)
|
|
||||||
- a series of opcodes (punctuation) and numeric values in base 36 (the
|
|
||||||
alphanumerics)
|
|
||||||
- a dollar sign ($)
|
|
||||||
- a string of characters used by insertion operations (the "char bank")
|
|
||||||
|
|
||||||
If we separate out the operations and convert the numbers to base 10, we get:
|
|
||||||
|
|
||||||
Z :196 >1 |5=97 =31 *4 *5 +1 $"x"
|
|
||||||
|
|
||||||
Here are descriptions of the operations, where capital letters are variables:
|
|
||||||
|
|
||||||
":N" : Source text has length N (must be first op)
|
|
||||||
">N" : Final text is N (positive) characters longer than source text (must be second op)
|
|
||||||
"<N" : Final text is N (positive) characters shorter than source text (must be second op)
|
|
||||||
">0" : Final text is same length as source text
|
|
||||||
"+N" : Insert N characters from the bank, none of them newlines
|
|
||||||
"-N" : Skip over (delete) N characters from the source text, none of them newlines
|
|
||||||
"=N" : Keep N characters from the source text, none of them newlines
|
|
||||||
"|L+N" : Insert N characters from the source text, containing L newlines. The last
|
|
||||||
character inserted MUST be a newline, but not the (new) document's final newline.
|
|
||||||
"|L-N" : Delete N characters from the source text, containing L newlines. The last
|
|
||||||
character inserted MUST be a newline, but not the (old) document's final newline.
|
|
||||||
"|L=N" : Keep N characters from the source text, containing L newlines. The last character
|
|
||||||
kept MUST be a newline, and the final newline of the document is allowed.
|
|
||||||
"*I" : Apply attribute I from the pool to the following +, =, |+, or |= command.
|
|
||||||
In other words, any number of * ops can come before a +, =, or | but not
|
|
||||||
between a | and the corresponding + or =.
|
|
||||||
If +, text is inserted having this attribute. If =, text is kept but with
|
|
||||||
the attribute applied as an attribute addition or removal.
|
|
||||||
Consecutive attributes must be sorted lexically by (key,value) with key
|
|
||||||
and value taken as strings. It's illegal to have duplicate keys
|
|
||||||
for (key,value) pairs that apply to the same text. It's illegal to
|
|
||||||
have an empty value for a key in the case of an insertion (+), the
|
|
||||||
pair should just be omitted.
|
|
||||||
|
|
||||||
Characters from the source text that aren't accounted for are assumed to be kept
|
|
||||||
with the same attributes.
|
|
||||||
|
|
||||||
Additional Constraints:
|
|
||||||
|
|
||||||
- Consecutive +, -, and = ops of the same type that could be combined are not allowed.
|
|
||||||
Whether combination is possible depends on the attributes of the ops and whether
|
|
||||||
each is multiline or not. For example, two multiline deletions can never be
|
|
||||||
consecutive, nor can any insertion come after a non-multiline insertion with the
|
|
||||||
same attributes.
|
|
||||||
- "No-op" ops are not allowed, such as deleting 0 characters. However, attribute
|
|
||||||
applications that don't have any effect are allowed.
|
|
||||||
- Characters at the end of the source text cannot be explicitly kept with no changes;
|
|
||||||
if the change doesn't affect the last N characters, those "keep" ops must be left off.
|
|
||||||
- In any consecutive sequence of insertions (+) and deletions (-) with no keeps (=),
|
|
||||||
the deletions must come before the insertions.
|
|
||||||
- The document text before and after will always end with a newline. This policy avoids
|
|
||||||
a lot of special-casing of the end of the document. If a final newline is
|
|
||||||
always added when importing text and removed when exporting text, then the
|
|
||||||
changeset representation can be used to process text files that may or may not
|
|
||||||
have a final newline.
|
|
||||||
|
|
||||||
Attribution string:
|
|
||||||
|
|
||||||
An "attribution string" is a series of inserts with no deletions or keeps.
|
|
||||||
For example, "*3+8|1+5" describes the attributes of a string of length 13,
|
|
||||||
where the first 8 chars have attribute 3 and the next 5 chars have no
|
|
||||||
attributes, with the last of these 5 chars being a newline. Constraints
|
|
||||||
apply similar to those affecting changesets, but the restriction about
|
|
||||||
the final newline of the new document being added doesn't apply.
|
|
||||||
|
|
||||||
Attributes in an attribution string cannot be empty, like "(bold,)", they should
|
|
||||||
instead be absent.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-------
|
|
||||||
Considerations:
|
|
||||||
|
|
||||||
- composing changesets/attributions with different pools
|
|
||||||
- generalizing "applyToAttribution" to make "mutateAttributionLines" and "compose"
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
This file must be valid JSON. But comments are allowed
|
|
||||||
|
|
||||||
Please edit settings.json, not settings.json.template
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
//Ip and port which etherpad should bind at
|
|
||||||
"ip": "0.0.0.0",
|
|
||||||
"port" : 9001,
|
|
||||||
|
|
||||||
//The Type of the database. You can choose between dirty, sqlite and mysql
|
|
||||||
//You should use mysql or sqlite for anything else than testing or development
|
|
||||||
"dbType" : "dirty",
|
|
||||||
//the database specific settings
|
|
||||||
"dbSettings" : {
|
|
||||||
"filename" : "var/dirty.db"
|
|
||||||
},
|
|
||||||
|
|
||||||
/* An Example of MySQL Configuration
|
|
||||||
"dbType" : "mysql",
|
|
||||||
"dbSettings" : {
|
|
||||||
"user" : "root",
|
|
||||||
"host" : "localhost",
|
|
||||||
"password": "",
|
|
||||||
"database": "store"
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
//the default text of a pad
|
|
||||||
"defaultPadText" : "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n",
|
|
||||||
|
|
||||||
/* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
|
|
||||||
"requireSession" : false,
|
|
||||||
|
|
||||||
/* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
|
|
||||||
"editOnly" : false,
|
|
||||||
|
|
||||||
/* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
|
|
||||||
but makes it impossible to debug the javascript/css */
|
|
||||||
"minify" : true,
|
|
||||||
|
|
||||||
/* How long may clients use served javascript code (in seconds)? Without versioning this
|
|
||||||
may cause problems during deployment. Set to 0 to disable caching */
|
|
||||||
"maxAge" : 21600, // 60 * 60 * 6 = 6 hours
|
|
||||||
|
|
||||||
/* This is the path to the Abiword executable. Setting it to null, disables abiword.
|
|
||||||
Abiword is needed to enable the import/export of pads*/
|
|
||||||
"abiword" : null,
|
|
||||||
|
|
||||||
/* This setting is used if you require authentication of all users.
|
|
||||||
Note: /admin always requires authentication. */
|
|
||||||
"requireAuthentication": false,
|
|
||||||
|
|
||||||
/* Require authorization by a module, or a user with is_admin set, see below. */
|
|
||||||
"requireAuthorization": false,
|
|
||||||
|
|
||||||
/* Users for basic authentication. is_admin = true gives access to /admin.
|
|
||||||
If you do not uncomment this, /admin will not be available! */
|
|
||||||
/*
|
|
||||||
"users": {
|
|
||||||
"admin": {
|
|
||||||
"password": "changeme1",
|
|
||||||
"is_admin": true
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"password": "changeme1",
|
|
||||||
"is_admin": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
|
|
||||||
"loglevel": "INFO"
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
This file must be valid JSON. But comments are allowed
|
|
||||||
|
|
||||||
Please edit settings.json, not settings.json.template
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
//Ip and port which etherpad should bind at
|
|
||||||
"ip": "0.0.0.0",
|
|
||||||
"port" : 9001,
|
|
||||||
|
|
||||||
//The Type of the database. You can choose between sqlite and mysql
|
|
||||||
"dbType" : "dirty",
|
|
||||||
//the database specific settings
|
|
||||||
"dbSettings" : {
|
|
||||||
"filename" : "var/dirty.db"
|
|
||||||
},
|
|
||||||
|
|
||||||
/* An Example of MySQL Configuration
|
|
||||||
"dbType" : "mysql",
|
|
||||||
"dbSettings" : {
|
|
||||||
"user" : "root",
|
|
||||||
"host" : "localhost",
|
|
||||||
"password": "",
|
|
||||||
"database": "store"
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
//the default text of a pad
|
|
||||||
"defaultPadText" : "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n",
|
|
||||||
|
|
||||||
/* Users must have a session to access pads. This effectively allows only group pads to be accessed. */
|
|
||||||
"requireSession" : false,
|
|
||||||
|
|
||||||
/* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */
|
|
||||||
"editOnly" : false,
|
|
||||||
|
|
||||||
/* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly,
|
|
||||||
but makes it impossible to debug the javascript/css */
|
|
||||||
"minify" : false,
|
|
||||||
|
|
||||||
/* This is the path to the Abiword executable. Setting it to null, disables abiword.
|
|
||||||
Abiword is needed to enable the import/export of pads*/
|
|
||||||
"abiword" : null,
|
|
||||||
|
|
||||||
/* cache 6 hours = 1000*60*60*6 */
|
|
||||||
"maxAge": 21600000
|
|
||||||
|
|
||||||
}
|
|
16
src/ep.json
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"parts": [
|
|
||||||
{ "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static: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": "adminplugins", "hooks": {
|
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer",
|
|
||||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
# About the folder structure
|
|
||||||
|
|
||||||
* **db** - all modules that are accesing the data structure and are communicating directly to the database
|
|
||||||
* **handler** - all modules that responds directly to requests/messages of the browser
|
|
||||||
* **utils** - helper modules
|
|
||||||
|
|
||||||
# Module name conventions
|
|
||||||
|
|
||||||
Module file names start with a capital letter and uses camelCase
|
|
||||||
|
|
||||||
# Where does it start?
|
|
||||||
|
|
||||||
server.js is started directly
|
|
|
@ -1,520 +0,0 @@
|
||||||
/**
|
|
||||||
* This module provides all API functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
|
||||||
var padManager = require("./PadManager");
|
|
||||||
var padMessageHandler = require("../handler/PadMessageHandler");
|
|
||||||
var readOnlyManager = require("./ReadOnlyManager");
|
|
||||||
var groupManager = require("./GroupManager");
|
|
||||||
var authorManager = require("./AuthorManager");
|
|
||||||
var sessionManager = require("./SessionManager");
|
|
||||||
var async = require("async");
|
|
||||||
var exportHtml = require("../utils/ExportHtml");
|
|
||||||
var importHtml = require("../utils/ImportHtml");
|
|
||||||
var cleanText = require("./Pad").cleanText;
|
|
||||||
|
|
||||||
/**********************/
|
|
||||||
/**GROUP FUNCTIONS*****/
|
|
||||||
/**********************/
|
|
||||||
|
|
||||||
exports.createGroup = groupManager.createGroup;
|
|
||||||
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
|
|
||||||
exports.deleteGroup = groupManager.deleteGroup;
|
|
||||||
exports.listPads = groupManager.listPads;
|
|
||||||
exports.createGroupPad = groupManager.createGroupPad;
|
|
||||||
|
|
||||||
/**********************/
|
|
||||||
/**AUTHOR FUNCTIONS****/
|
|
||||||
/**********************/
|
|
||||||
|
|
||||||
exports.createAuthor = authorManager.createAuthor;
|
|
||||||
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
|
|
||||||
|
|
||||||
/**********************/
|
|
||||||
/**SESSION FUNCTIONS***/
|
|
||||||
/**********************/
|
|
||||||
|
|
||||||
exports.createSession = sessionManager.createSession;
|
|
||||||
exports.deleteSession = sessionManager.deleteSession;
|
|
||||||
exports.getSessionInfo = sessionManager.getSessionInfo;
|
|
||||||
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
|
|
||||||
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
|
|
||||||
|
|
||||||
/************************/
|
|
||||||
/**PAD CONTENT FUNCTIONS*/
|
|
||||||
/************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
getText(padID, [rev]) returns the text of a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: {text:"Welcome Text"}}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.getText = function(padID, rev, callback)
|
|
||||||
{
|
|
||||||
//check if rev is set
|
|
||||||
if(typeof rev == "function")
|
|
||||||
{
|
|
||||||
callback = rev;
|
|
||||||
rev = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if rev is a number
|
|
||||||
if(rev !== undefined && typeof rev != "number")
|
|
||||||
{
|
|
||||||
//try to parse the number
|
|
||||||
if(!isNaN(parseInt(rev)))
|
|
||||||
{
|
|
||||||
rev = parseInt(rev);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(new customError("rev is not a number", "apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//ensure this is not a negativ number
|
|
||||||
if(rev !== undefined && rev < 0)
|
|
||||||
{
|
|
||||||
callback(new customError("rev is a negativ number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//ensure this is not a float value
|
|
||||||
if(rev !== undefined && !is_int(rev))
|
|
||||||
{
|
|
||||||
callback(new customError("rev is a float value","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//the client asked for a special revision
|
|
||||||
if(rev !== undefined)
|
|
||||||
{
|
|
||||||
//check if this is a valid revision
|
|
||||||
if(rev > pad.getHeadRevisionNumber())
|
|
||||||
{
|
|
||||||
callback(new customError("rev is higher than the head revision of the pad","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the text of this revision
|
|
||||||
pad.getInternalRevisionAText(rev, function(err, atext)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
data = {text: atext.text};
|
|
||||||
|
|
||||||
callback(null, data);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
//the client wants the latest text, lets return it to him
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null, {"text": pad.text()});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
setText(padID, text) sets the text of a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
{code: 1, message:"text too long", data: null}
|
|
||||||
*/
|
|
||||||
exports.setText = function(padID, text, callback)
|
|
||||||
{
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//set the text
|
|
||||||
pad.setText(text);
|
|
||||||
|
|
||||||
//update the clients on the pad
|
|
||||||
padMessageHandler.updatePadClients(pad, callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
getHTML(padID, [rev]) returns the html of a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: {text:"Welcome <strong>Text</strong>"}}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.getHTML = function(padID, rev, callback)
|
|
||||||
{
|
|
||||||
if(typeof rev == "function")
|
|
||||||
{
|
|
||||||
callback = rev;
|
|
||||||
rev = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rev !== undefined && typeof rev != "number")
|
|
||||||
{
|
|
||||||
if (!isNaN(parseInt(rev)))
|
|
||||||
{
|
|
||||||
rev = parseInt(rev);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(new customError("rev is not a number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(rev !== undefined && rev < 0)
|
|
||||||
{
|
|
||||||
callback(new customError("rev is a negative number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(rev !== undefined && !is_int(rev))
|
|
||||||
{
|
|
||||||
callback(new customError("rev is a float value","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//the client asked for a special revision
|
|
||||||
if(rev !== undefined)
|
|
||||||
{
|
|
||||||
//check if this is a valid revision
|
|
||||||
if(rev > pad.getHeadRevisionNumber())
|
|
||||||
{
|
|
||||||
callback(new customError("rev is higher than the head revision of the pad","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the html of this revision
|
|
||||||
exportHtml.getPadHTML(pad, rev, function(err, html)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
data = {html: html};
|
|
||||||
callback(null, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//the client wants the latest text, lets return it to him
|
|
||||||
else
|
|
||||||
{
|
|
||||||
exportHtml.getPadHTML(pad, undefined, function (err, html)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
data = {html: html};
|
|
||||||
|
|
||||||
callback(null, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setHTML = function(padID, html, callback)
|
|
||||||
{
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
// add a new changeset with the new html to the pad
|
|
||||||
importHtml.setPadHTML(pad, cleanText(html));
|
|
||||||
|
|
||||||
//update the clients on the pad
|
|
||||||
padMessageHandler.updatePadClients(pad, callback);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*****************/
|
|
||||||
/**PAD FUNCTIONS */
|
|
||||||
/*****************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
getRevisionsCount(padID) returns the number of revisions of this pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: {revisions: 56}}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.getRevisionsCount = function(padID, callback)
|
|
||||||
{
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
callback(null, {revisions: pad.getHeadRevisionNumber()});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
createPad(padName [, text]) creates a new pad in this group
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"pad does already exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.createPad = function(padID, text, callback)
|
|
||||||
{
|
|
||||||
//ensure there is no $ in the padID
|
|
||||||
if(padID && padID.indexOf("$") != -1)
|
|
||||||
{
|
|
||||||
callback(new customError("createPad can't create group pads","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//create pad
|
|
||||||
getPadSafe(padID, false, text, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
deletePad(padID) deletes a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.deletePad = function(padID, callback)
|
|
||||||
{
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
pad.remove(callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
getReadOnlyLink(padID) returns the read only link of a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.getReadOnlyID = function(padID, callback)
|
|
||||||
{
|
|
||||||
//we don't need the pad object, but this function does all the security stuff for us
|
|
||||||
getPadSafe(padID, true, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//get the readonlyId
|
|
||||||
readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, {readOnlyID: readOnlyId});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.setPublicStatus = function(padID, publicStatus, callback)
|
|
||||||
{
|
|
||||||
//ensure this is a group pad
|
|
||||||
if(padID && padID.indexOf("$") == -1)
|
|
||||||
{
|
|
||||||
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//convert string to boolean
|
|
||||||
if(typeof publicStatus == "string")
|
|
||||||
publicStatus = publicStatus == "true" ? true : false;
|
|
||||||
|
|
||||||
//set the password
|
|
||||||
pad.setPublicStatus(publicStatus);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
getPublicStatus(padID) return true of false
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: {publicStatus: true}}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.getPublicStatus = function(padID, callback)
|
|
||||||
{
|
|
||||||
//ensure this is a group pad
|
|
||||||
if(padID && padID.indexOf("$") == -1)
|
|
||||||
{
|
|
||||||
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
callback(null, {publicStatus: pad.getPublicStatus()});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
setPassword(padID, password) returns ok or a error message
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: null}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.setPassword = function(padID, password, callback)
|
|
||||||
{
|
|
||||||
//ensure this is a group pad
|
|
||||||
if(padID && padID.indexOf("$") == -1)
|
|
||||||
{
|
|
||||||
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//set the password
|
|
||||||
pad.setPassword(password == "" ? null : password);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
isPasswordProtected(padID) returns true or false
|
|
||||||
|
|
||||||
Example returns:
|
|
||||||
|
|
||||||
{code: 0, message:"ok", data: {passwordProtection: true}}
|
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
|
||||||
*/
|
|
||||||
exports.isPasswordProtected = function(padID, callback)
|
|
||||||
{
|
|
||||||
//ensure this is a group pad
|
|
||||||
if(padID && padID.indexOf("$") == -1)
|
|
||||||
{
|
|
||||||
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pad
|
|
||||||
getPadSafe(padID, true, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
callback(null, {isPasswordProtected: pad.isPasswordProtected()});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/******************************/
|
|
||||||
/** INTERNAL HELPER FUNCTIONS */
|
|
||||||
/******************************/
|
|
||||||
|
|
||||||
//checks if a number is an int
|
|
||||||
function is_int(value)
|
|
||||||
{
|
|
||||||
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
//gets a pad safe
|
|
||||||
function getPadSafe(padID, shouldExist, text, callback)
|
|
||||||
{
|
|
||||||
if(typeof text == "function")
|
|
||||||
{
|
|
||||||
callback = text;
|
|
||||||
text = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if padID is a string
|
|
||||||
if(typeof padID != "string")
|
|
||||||
{
|
|
||||||
callback(new customError("padID is not a string","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if the padID maches the requirements
|
|
||||||
if(!padManager.isValidPadId(padID))
|
|
||||||
{
|
|
||||||
callback(new customError("padID did not match requirements","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if the pad exists
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//does not exist, but should
|
|
||||||
if(exists == false && shouldExist == true)
|
|
||||||
{
|
|
||||||
callback(new customError("padID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//does exists, but shouldn't
|
|
||||||
else if(exists == true && shouldExist == false)
|
|
||||||
{
|
|
||||||
callback(new customError("padID does already exist","apierror"));
|
|
||||||
}
|
|
||||||
//pad exists, let's get it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
padManager.getPad(padID, text, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,181 +0,0 @@
|
||||||
/**
|
|
||||||
* The AuthorManager controlls all information about the Pad authors
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the author exists
|
|
||||||
*/
|
|
||||||
exports.doesAuthorExists = function (authorID, callback)
|
|
||||||
{
|
|
||||||
//check if the database entry of this author exists
|
|
||||||
db.get("globalAuthor:" + authorID, function (err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, author != null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the AuthorID for a token.
|
|
||||||
* @param {String} token The token
|
|
||||||
* @param {Function} callback callback (err, author)
|
|
||||||
*/
|
|
||||||
exports.getAuthor4Token = function (token, callback)
|
|
||||||
{
|
|
||||||
mapAuthorWithDBKey("token2author", token, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
//return only the sub value authorID
|
|
||||||
callback(null, author ? author.authorID : author);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the AuthorID for a mapper.
|
|
||||||
* @param {String} token The mapper
|
|
||||||
* @param {Function} callback callback (err, author)
|
|
||||||
*/
|
|
||||||
exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback)
|
|
||||||
{
|
|
||||||
mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//set the name of this author
|
|
||||||
if(name)
|
|
||||||
exports.setAuthorName(author.authorID, name);
|
|
||||||
|
|
||||||
//return the authorID
|
|
||||||
callback(null, author);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
|
||||||
* so far this is token2author and mapper2author
|
|
||||||
* @param {String} mapperkey The database key name for this mapper
|
|
||||||
* @param {String} mapper The mapper
|
|
||||||
* @param {Function} callback callback (err, author)
|
|
||||||
*/
|
|
||||||
function mapAuthorWithDBKey (mapperkey, mapper, callback)
|
|
||||||
{
|
|
||||||
//try to map to an author
|
|
||||||
db.get(mapperkey + ":" + mapper, function (err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//there is no author with this mapper, so create one
|
|
||||||
if(author == null)
|
|
||||||
{
|
|
||||||
exports.createAuthor(null, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//create the token2author relation
|
|
||||||
db.set(mapperkey + ":" + mapper, author.authorID);
|
|
||||||
|
|
||||||
//return the author
|
|
||||||
callback(null, author);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//there is a author with this mapper
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//update the timestamp of this author
|
|
||||||
db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime());
|
|
||||||
|
|
||||||
//return the author
|
|
||||||
callback(null, {authorID: author});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal function that creates the database entry for an author
|
|
||||||
* @param {String} name The name of the author
|
|
||||||
*/
|
|
||||||
exports.createAuthor = function(name, callback)
|
|
||||||
{
|
|
||||||
//create the new author name
|
|
||||||
var author = "a." + randomString(16);
|
|
||||||
|
|
||||||
//create the globalAuthors db entry
|
|
||||||
var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()};
|
|
||||||
|
|
||||||
//set the global author db entry
|
|
||||||
db.set("globalAuthor:" + author, authorObj);
|
|
||||||
|
|
||||||
callback(null, {authorID: author});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Author Obj of the author
|
|
||||||
* @param {String} author The id of the author
|
|
||||||
* @param {Function} callback callback(err, authorObj)
|
|
||||||
*/
|
|
||||||
exports.getAuthor = function (author, callback)
|
|
||||||
{
|
|
||||||
db.get("globalAuthor:" + author, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the color Id of the author
|
|
||||||
* @param {String} author The id of the author
|
|
||||||
* @param {Function} callback callback(err, colorId)
|
|
||||||
*/
|
|
||||||
exports.getAuthorColorId = function (author, callback)
|
|
||||||
{
|
|
||||||
db.getSub("globalAuthor:" + author, ["colorId"], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the color Id of the author
|
|
||||||
* @param {String} author The id of the author
|
|
||||||
* @param {Function} callback (optional)
|
|
||||||
*/
|
|
||||||
exports.setAuthorColorId = function (author, colorId, callback)
|
|
||||||
{
|
|
||||||
db.setSub("globalAuthor:" + author, ["colorId"], colorId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the author
|
|
||||||
* @param {String} author The id of the author
|
|
||||||
* @param {Function} callback callback(err, name)
|
|
||||||
*/
|
|
||||||
exports.getAuthorName = function (author, callback)
|
|
||||||
{
|
|
||||||
db.getSub("globalAuthor:" + author, ["name"], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the name of the author
|
|
||||||
* @param {String} author The id of the author
|
|
||||||
* @param {Function} callback (optional)
|
|
||||||
*/
|
|
||||||
exports.setAuthorName = function (author, name, callback)
|
|
||||||
{
|
|
||||||
db.setSub("globalAuthor:" + author, ["name"], name, callback);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/**
|
|
||||||
* The DB Module provides a database initalized with the settings
|
|
||||||
* provided by the settings module
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ueberDB = require("ueberDB");
|
|
||||||
var settings = require("../utils/Settings");
|
|
||||||
var log4js = require('log4js');
|
|
||||||
|
|
||||||
//set database settings
|
|
||||||
var db = new ueberDB.database(settings.dbType, settings.dbSettings, null, log4js.getLogger("ueberDB"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The UeberDB Object that provides the database functions
|
|
||||||
*/
|
|
||||||
exports.db = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initalizes the database with the settings provided by the settings module
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
exports.init = function(callback)
|
|
||||||
{
|
|
||||||
//initalize the database async
|
|
||||||
db.init(function(err)
|
|
||||||
{
|
|
||||||
//there was an error while initializing the database, output it and stop
|
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
console.error("ERROR: Problem while initalizing the database");
|
|
||||||
console.error(err.stack ? err.stack : err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
//everything ok
|
|
||||||
else
|
|
||||||
{
|
|
||||||
exports.db = db;
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,263 +0,0 @@
|
||||||
/**
|
|
||||||
* The Group Manager provides functions to manage groups in the database
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var padManager = require("./PadManager");
|
|
||||||
var sessionManager = require("./SessionManager");
|
|
||||||
|
|
||||||
exports.deleteGroup = function(groupID, callback)
|
|
||||||
{
|
|
||||||
var group;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//ensure group exists
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
//try to get the group entry
|
|
||||||
db.get("group:" + groupID, function (err, _group)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(_group == null)
|
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//group exists, everything is fine
|
|
||||||
else
|
|
||||||
{
|
|
||||||
group = _group;
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//iterate trough all pads of this groups and delete them
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//collect all padIDs in an array, that allows us to use async.forEach
|
|
||||||
var padIDs = [];
|
|
||||||
for(var i in group.pads)
|
|
||||||
{
|
|
||||||
padIDs.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop trough all pads and delete them
|
|
||||||
async.forEach(padIDs, function(padID, callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padID, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
pad.remove(callback);
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
},
|
|
||||||
//iterate trough group2sessions and delete all sessions
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//try to get the group entry
|
|
||||||
db.get("group2sessions:" + groupID, function (err, group2sessions)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//skip if there is no group2sessions entry
|
|
||||||
if(group2sessions == null) {callback(); return}
|
|
||||||
|
|
||||||
//collect all sessions in an array, that allows us to use async.forEach
|
|
||||||
var sessions = [];
|
|
||||||
for(var i in group2sessions.sessionsIDs)
|
|
||||||
{
|
|
||||||
sessions.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop trough all sessions and delete them
|
|
||||||
async.forEach(sessions, function(session, callback)
|
|
||||||
{
|
|
||||||
sessionManager.deleteSession(session, callback);
|
|
||||||
}, callback);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//remove group and group2sessions entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.remove("group2sessions:" + groupID);
|
|
||||||
db.remove("group:" + groupID);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.doesGroupExist = function(groupID, callback)
|
|
||||||
{
|
|
||||||
//try to get the group entry
|
|
||||||
db.get("group:" + groupID, function (err, group)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, group != null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createGroup = function(callback)
|
|
||||||
{
|
|
||||||
//search for non existing groupID
|
|
||||||
var groupID = "g." + randomString(16);
|
|
||||||
|
|
||||||
//create the group
|
|
||||||
db.set("group:" + groupID, {pads: {}});
|
|
||||||
callback(null, {groupID: groupID});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createGroupIfNotExistsFor = function(groupMapper, callback)
|
|
||||||
{
|
|
||||||
//ensure mapper is optional
|
|
||||||
if(typeof groupMapper != "string")
|
|
||||||
{
|
|
||||||
callback(new customError("groupMapper is no string","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//try to get a group for this mapper
|
|
||||||
db.get("mapper2group:"+groupMapper, function(err, groupID)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//there is no group for this mapper, let's create a group
|
|
||||||
if(groupID == null)
|
|
||||||
{
|
|
||||||
exports.createGroup(function(err, responseObj)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//create the mapper entry for this group
|
|
||||||
db.set("mapper2group:"+groupMapper, responseObj.groupID);
|
|
||||||
|
|
||||||
callback(null, responseObj);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//there is a group for this mapper, let's return it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, {groupID: groupID});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createGroupPad = function(groupID, padName, text, callback)
|
|
||||||
{
|
|
||||||
//create the padID
|
|
||||||
var padID = groupID + "$" + padName;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//ensure group exists
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
exports.doesGroupExist(groupID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//group exists, everything is fine
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//ensure pad does not exists
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//pad exists already
|
|
||||||
if(exists == true)
|
|
||||||
{
|
|
||||||
callback(new customError("padName does already exist","apierror"));
|
|
||||||
}
|
|
||||||
//pad does not exist, everything is fine
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//create the pad
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padID, text, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//create an entry in the group for this pad
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
db.setSub("group:" + groupID, ["pads", padID], 1);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, {padID: padID});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.listPads = function(groupID, callback)
|
|
||||||
{
|
|
||||||
exports.doesGroupExist(groupID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//group exists, let's get the pads
|
|
||||||
else
|
|
||||||
{
|
|
||||||
db.getSub("group:" + groupID, ["pads"], function(err, result)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
var pads = [];
|
|
||||||
for ( var padId in result ) {
|
|
||||||
pads.push(padId);
|
|
||||||
}
|
|
||||||
callback(null, {padIDs: pads});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,517 +0,0 @@
|
||||||
/**
|
|
||||||
* The pad object, defined with joose
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var settings = require('../utils/Settings');
|
|
||||||
var authorManager = require("./AuthorManager");
|
|
||||||
var padManager = require("./PadManager");
|
|
||||||
var padMessageHandler = require("../handler/PadMessageHandler");
|
|
||||||
var readOnlyManager = require("./ReadOnlyManager");
|
|
||||||
var crypto = require("crypto");
|
|
||||||
var randomString = require("../utils/randomstring");
|
|
||||||
|
|
||||||
//serialization/deserialization attributes
|
|
||||||
var attributeBlackList = ["id"];
|
|
||||||
var jsonableList = ["pool"];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
|
|
||||||
* @param txt
|
|
||||||
*/
|
|
||||||
exports.cleanText = function (txt) {
|
|
||||||
return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var Pad = function Pad(id) {
|
|
||||||
|
|
||||||
this.atext = Changeset.makeAText("\n");
|
|
||||||
this.pool = new AttributePool();
|
|
||||||
this.head = -1;
|
|
||||||
this.chatHead = -1;
|
|
||||||
this.publicStatus = false;
|
|
||||||
this.passwordHash = null;
|
|
||||||
this.id = id;
|
|
||||||
this.savedRevisions = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Pad = Pad;
|
|
||||||
|
|
||||||
Pad.prototype.apool = function apool() {
|
|
||||||
return this.pool;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() {
|
|
||||||
return this.head;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getPublicStatus = function getPublicStatus() {
|
|
||||||
return this.publicStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
|
|
||||||
if(!author)
|
|
||||||
author = '';
|
|
||||||
|
|
||||||
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
|
||||||
Changeset.copyAText(newAText, this.atext);
|
|
||||||
|
|
||||||
var newRev = ++this.head;
|
|
||||||
|
|
||||||
var newRevData = {};
|
|
||||||
newRevData.changeset = aChangeset;
|
|
||||||
newRevData.meta = {};
|
|
||||||
newRevData.meta.author = author;
|
|
||||||
newRevData.meta.timestamp = new Date().getTime();
|
|
||||||
|
|
||||||
//ex. getNumForAuthor
|
|
||||||
if(author != '')
|
|
||||||
this.pool.putAttrib(['author', author || '']);
|
|
||||||
|
|
||||||
if(newRev % 100 == 0)
|
|
||||||
{
|
|
||||||
newRevData.meta.atext = this.atext;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
|
|
||||||
this.saveToDatabase();
|
|
||||||
};
|
|
||||||
|
|
||||||
//save all attributes to the database
|
|
||||||
Pad.prototype.saveToDatabase = function saveToDatabase(){
|
|
||||||
var dbObject = {};
|
|
||||||
|
|
||||||
for(var attr in this){
|
|
||||||
if(typeof this[attr] === "function") continue;
|
|
||||||
if(attributeBlackList.indexOf(attr) !== -1) continue;
|
|
||||||
|
|
||||||
dbObject[attr] = this[attr];
|
|
||||||
|
|
||||||
if(jsonableList.indexOf(attr) !== -1){
|
|
||||||
dbObject[attr] = dbObject[attr].toJsonable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.set("pad:"+this.id, dbObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
|
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) {
|
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) {
|
|
||||||
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getAllAuthors = function getAllAuthors() {
|
|
||||||
var authors = [];
|
|
||||||
|
|
||||||
for(key in this.pool.numToAttrib)
|
|
||||||
{
|
|
||||||
if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
|
|
||||||
{
|
|
||||||
authors.push(this.pool.numToAttrib[key][1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return authors;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
var keyRev = this.getKeyRevisionNumber(targetRev);
|
|
||||||
var atext;
|
|
||||||
var changesets = [];
|
|
||||||
|
|
||||||
//find out which changesets are needed
|
|
||||||
var neededChangesets = [];
|
|
||||||
var curRev = keyRev;
|
|
||||||
while (curRev < targetRev)
|
|
||||||
{
|
|
||||||
curRev++;
|
|
||||||
neededChangesets.push(curRev);
|
|
||||||
}
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get all needed data out of the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
//get the atext of the key revision
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
atext = Changeset.cloneAText(_atext);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get all needed changesets
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
async.forEach(neededChangesets, function(item, callback)
|
|
||||||
{
|
|
||||||
_this.getRevisionChangeset(item, function(err, changeset)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
changesets[item] = changeset;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//apply all changesets to the key changeset
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var apool = _this.apool();
|
|
||||||
var curRev = keyRev;
|
|
||||||
|
|
||||||
while (curRev < targetRev)
|
|
||||||
{
|
|
||||||
curRev++;
|
|
||||||
var cs = changesets[curRev];
|
|
||||||
atext = Changeset.applyToAText(cs, atext, apool);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, atext);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) {
|
|
||||||
return Math.floor(revNum / 100) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.text = function text() {
|
|
||||||
return this.atext.text;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.setText = function setText(newText) {
|
|
||||||
//clean the new text
|
|
||||||
newText = exports.cleanText(newText);
|
|
||||||
|
|
||||||
var oldText = this.text();
|
|
||||||
|
|
||||||
//create the changeset
|
|
||||||
var changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
|
|
||||||
|
|
||||||
//append the changeset
|
|
||||||
this.appendRevision(changeset);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
|
|
||||||
this.chatHead++;
|
|
||||||
//save the chat entry in the database
|
|
||||||
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
|
|
||||||
this.saveToDatabase();
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
|
|
||||||
var _this = this;
|
|
||||||
var entry;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the chat entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
entry = _entry;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//add the authorName
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//this chat message doesn't exist, return null
|
|
||||||
if(entry == null)
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the authorName
|
|
||||||
authorManager.getAuthorName(entry.userId, function(err, authorName)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
entry.userName = authorName;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, entry);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getLastChatMessages = function getLastChatMessages(count, callback) {
|
|
||||||
//return an empty array if there are no chat messages
|
|
||||||
if(this.chatHead == -1)
|
|
||||||
{
|
|
||||||
callback(null, []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
//works only if we decrement the amount, for some reason
|
|
||||||
count--;
|
|
||||||
|
|
||||||
//set the startpoint
|
|
||||||
var start = this.chatHead-count;
|
|
||||||
if(start < 0)
|
|
||||||
start = 0;
|
|
||||||
|
|
||||||
//set the endpoint
|
|
||||||
var end = this.chatHead;
|
|
||||||
|
|
||||||
//collect the numbers of chat entries and in which order we need them
|
|
||||||
var neededEntries = [];
|
|
||||||
var order = 0;
|
|
||||||
for(var i=start;i<=end; i++)
|
|
||||||
{
|
|
||||||
neededEntries.push({entryNum:i, order: order});
|
|
||||||
order++;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all entries out of the database
|
|
||||||
var entries = [];
|
|
||||||
async.forEach(neededEntries, function(entryObject, callback)
|
|
||||||
{
|
|
||||||
_this.getChatMessage(entryObject.entryNum, function(err, entry)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
entries[entryObject.order] = entry;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//sort out broken chat entries
|
|
||||||
//it looks like in happend in the past that the chat head was
|
|
||||||
//incremented, but the chat message wasn't added
|
|
||||||
var cleanedEntries = [];
|
|
||||||
for(var i=0;i<entries.length;i++)
|
|
||||||
{
|
|
||||||
if(entries[i]!=null)
|
|
||||||
cleanedEntries.push(entries[i]);
|
|
||||||
else
|
|
||||||
console.warn("WARNING: Found broken chat entry in pad " + _this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, cleanedEntries);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.init = function init(text, callback) {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
//replace text with default text if text isn't set
|
|
||||||
if(text == null)
|
|
||||||
{
|
|
||||||
text = settings.defaultPadText;
|
|
||||||
}
|
|
||||||
|
|
||||||
//try to load the pad
|
|
||||||
db.get("pad:"+this.id, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//if this pad exists, load it
|
|
||||||
if(value != null)
|
|
||||||
{
|
|
||||||
//copy all attr. To a transfrom via fromJsonable if necassary
|
|
||||||
for(var attr in value){
|
|
||||||
if(jsonableList.indexOf(attr) !== -1){
|
|
||||||
_this[attr] = _this[attr].fromJsonable(value[attr]);
|
|
||||||
} else {
|
|
||||||
_this[attr] = value[attr];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//this pad doesn't exist, so create it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
|
|
||||||
|
|
||||||
_this.appendRevision(firstChangeset, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.remove = function remove(callback) {
|
|
||||||
var padID = this.id;
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
//kick everyone from this pad
|
|
||||||
padMessageHandler.kickSessionsFromPad(padID);
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//delete all relations
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
//is it a group pad? -> delete the entry of this pad in the group
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//is it a group pad?
|
|
||||||
if(padID.indexOf("$")!=-1)
|
|
||||||
{
|
|
||||||
var groupID = padID.substring(0,padID.indexOf("$"));
|
|
||||||
|
|
||||||
db.get("group:" + groupID, function (err, group)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//remove the pad entry
|
|
||||||
delete group.pads[padID];
|
|
||||||
|
|
||||||
//set the new value
|
|
||||||
db.set("group:" + groupID, group);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//its no group pad, nothing to do here
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
//remove the readonly entries
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
readOnlyManager.getReadOnlyId(padID, function(err, readonlyID)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
db.remove("pad2readonly:" + padID);
|
|
||||||
db.remove("readonly2pad:" + readonlyID);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//delete all chat messages
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var chatHead = _this.chatHead;
|
|
||||||
|
|
||||||
for(var i=0;i<=chatHead;i++)
|
|
||||||
{
|
|
||||||
db.remove("pad:"+padID+":chat:"+i);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
//delete all revisions
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var revHead = _this.head;
|
|
||||||
|
|
||||||
for(var i=0;i<=revHead;i++)
|
|
||||||
{
|
|
||||||
db.remove("pad:"+padID+":revs:"+i);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//delete the pad entry and delete pad from padManager
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.remove("pad:"+padID);
|
|
||||||
padManager.unloadPad(padID);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
//set in db
|
|
||||||
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
|
|
||||||
this.publicStatus = publicStatus;
|
|
||||||
this.saveToDatabase();
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.setPassword = function setPassword(password) {
|
|
||||||
this.passwordHash = password == null ? null : hash(password, generateSalt());
|
|
||||||
this.saveToDatabase();
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
|
|
||||||
return compare(this.passwordHash, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.isPasswordProtected = function isPasswordProtected() {
|
|
||||||
return this.passwordHash != null;
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
|
|
||||||
//if this revision is already saved, return silently
|
|
||||||
for(var i in this.savedRevisions){
|
|
||||||
if(this.savedRevisions.revNum === revNum){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//build the saved revision object
|
|
||||||
var savedRevision = {};
|
|
||||||
savedRevision.revNum = revNum;
|
|
||||||
savedRevision.savedById = savedById;
|
|
||||||
savedRevision.label = label || "Revision " + revNum;
|
|
||||||
savedRevision.timestamp = new Date().getTime();
|
|
||||||
savedRevision.id = randomString(10);
|
|
||||||
|
|
||||||
//save this new saved revision
|
|
||||||
this.savedRevisions.push(savedRevision);
|
|
||||||
this.saveToDatabase();
|
|
||||||
};
|
|
||||||
|
|
||||||
Pad.prototype.getSavedRevisions = function getSavedRevisions() {
|
|
||||||
return this.savedRevisions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Crypto helper methods */
|
|
||||||
|
|
||||||
function hash(password, salt)
|
|
||||||
{
|
|
||||||
var shasum = crypto.createHash('sha512');
|
|
||||||
shasum.update(password + salt);
|
|
||||||
return shasum.digest("hex") + "$" + salt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSalt()
|
|
||||||
{
|
|
||||||
return randomString(86);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(hashStr, password)
|
|
||||||
{
|
|
||||||
return hash(password, hashStr.split("$")[1]) === hashStr;
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
/**
|
|
||||||
* The Pad Manager is a Factory for pad Objects
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
|
||||||
var Pad = require("../db/Pad").Pad;
|
|
||||||
var db = require("./DB").db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Object containing all known Pads. Provides "get" and "set" functions,
|
|
||||||
* which should be used instead of indexing with brackets. These prepend a
|
|
||||||
* colon to the key, to avoid conflicting with built-in Object methods or with
|
|
||||||
* these functions themselves.
|
|
||||||
*
|
|
||||||
* If this is needed in other places, it would be wise to make this a prototype
|
|
||||||
* that's defined somewhere more sensible.
|
|
||||||
*/
|
|
||||||
var globalPads = {
|
|
||||||
get: function (name) { return this[':'+name]; },
|
|
||||||
set: function (name, value) { this[':'+name] = value; },
|
|
||||||
remove: function (name) { delete this[':'+name]; }
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of padId transformations. These represent changes in pad name policy over
|
|
||||||
* time, and allow us to "play back" these changes so legacy padIds can be found.
|
|
||||||
*/
|
|
||||||
var padIdTransforms = [
|
|
||||||
[/\s+/g, '_'],
|
|
||||||
[/:+/g, '_']
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Pad Object with the callback
|
|
||||||
* @param id A String with the id of the pad
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
exports.getPad = function(id, text, callback)
|
|
||||||
{
|
|
||||||
//check if this is a valid padId
|
|
||||||
if(!exports.isValidPadId(id))
|
|
||||||
{
|
|
||||||
callback(new customError(id + " is not a valid padId","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//make text an optional parameter
|
|
||||||
if(typeof text == "function")
|
|
||||||
{
|
|
||||||
callback = text;
|
|
||||||
text = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if this is a valid text
|
|
||||||
if(text != null)
|
|
||||||
{
|
|
||||||
//check if text is a string
|
|
||||||
if(typeof text != "string")
|
|
||||||
{
|
|
||||||
callback(new customError("text is not a string","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if text is less than 100k chars
|
|
||||||
if(text.length > 100000)
|
|
||||||
{
|
|
||||||
callback(new customError("text must be less than 100k chars","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pad = globalPads.get(id);
|
|
||||||
|
|
||||||
//return pad if its already loaded
|
|
||||||
if(pad != null)
|
|
||||||
{
|
|
||||||
callback(null, pad);
|
|
||||||
}
|
|
||||||
//try to load pad
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pad = new Pad(id);
|
|
||||||
|
|
||||||
//initalize the pad
|
|
||||||
pad.init(text, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
globalPads.set(id, pad);
|
|
||||||
callback(null, pad);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks if a pad exists
|
|
||||||
exports.doesPadExists = function(padId, callback)
|
|
||||||
{
|
|
||||||
db.get("pad:"+padId, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
if(value != null && value.atext){
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns a sanitized padId, respecting legacy pad id formats
|
|
||||||
exports.sanitizePadId = function(padId, callback) {
|
|
||||||
var transform_index = arguments[2] || 0;
|
|
||||||
//we're out of possible transformations, so just return it
|
|
||||||
if(transform_index >= padIdTransforms.length)
|
|
||||||
{
|
|
||||||
callback(padId);
|
|
||||||
}
|
|
||||||
//check if padId exists
|
|
||||||
else
|
|
||||||
{
|
|
||||||
exports.doesPadExists(padId, function(junk, exists)
|
|
||||||
{
|
|
||||||
if(exists)
|
|
||||||
{
|
|
||||||
callback(padId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//get the next transformation *that's different*
|
|
||||||
var transformedPadId = padId;
|
|
||||||
while(transformedPadId == padId && transform_index < padIdTransforms.length)
|
|
||||||
{
|
|
||||||
transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]);
|
|
||||||
transform_index += 1;
|
|
||||||
}
|
|
||||||
//check the next transform
|
|
||||||
exports.sanitizePadId(transformedPadId, callback, transform_index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.isValidPadId = function(padId)
|
|
||||||
{
|
|
||||||
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
|
||||||
}
|
|
||||||
|
|
||||||
//removes a pad from the array
|
|
||||||
exports.unloadPad = function(padId)
|
|
||||||
{
|
|
||||||
if(globalPads.get(padId))
|
|
||||||
globalPads.remove(padId);
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/**
|
|
||||||
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a read only id for a pad
|
|
||||||
* @param {String} padId the id of the pad
|
|
||||||
*/
|
|
||||||
exports.getReadOnlyId = function (padId, callback)
|
|
||||||
{
|
|
||||||
var readOnlyId;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
//check if there is a pad2readonly entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.get("pad2readonly:" + padId, callback);
|
|
||||||
},
|
|
||||||
function(dbReadOnlyId, callback)
|
|
||||||
{
|
|
||||||
//there is no readOnly Entry in the database, let's create one
|
|
||||||
if(dbReadOnlyId == null)
|
|
||||||
{
|
|
||||||
readOnlyId = "r." + randomString(16);
|
|
||||||
|
|
||||||
db.set("pad2readonly:" + padId, readOnlyId);
|
|
||||||
db.set("readonly2pad:" + readOnlyId, padId);
|
|
||||||
}
|
|
||||||
//there is a readOnly Entry in the database, let's take this one
|
|
||||||
else
|
|
||||||
{
|
|
||||||
readOnlyId = dbReadOnlyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
//return the results
|
|
||||||
callback(null, readOnlyId);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a the padId for a read only id
|
|
||||||
* @param {String} readOnlyId read only id
|
|
||||||
*/
|
|
||||||
exports.getPadId = function(readOnlyId, callback)
|
|
||||||
{
|
|
||||||
db.get("readonly2pad:" + readOnlyId, callback);
|
|
||||||
}
|
|
|
@ -1,280 +0,0 @@
|
||||||
/**
|
|
||||||
* Controls the security of pad access
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var authorManager = require("./AuthorManager");
|
|
||||||
var padManager = require("./PadManager");
|
|
||||||
var sessionManager = require("./SessionManager");
|
|
||||||
var settings = require("../utils/Settings")
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function controlls the access to a pad, it checks if the user can access a pad.
|
|
||||||
* @param padID the pad the user wants to access
|
|
||||||
* @param sesssionID the session the user has (set via api)
|
|
||||||
* @param token the token of the author (randomly generated at client side, used for public pads)
|
|
||||||
* @param password the password the user has given to access this pad, can be null
|
|
||||||
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
|
|
||||||
*/
|
|
||||||
exports.checkAccess = function (padID, sessionID, token, password, callback)
|
|
||||||
{
|
|
||||||
var statusObject;
|
|
||||||
|
|
||||||
// a valid session is required (api-only mode)
|
|
||||||
if(settings.requireSession)
|
|
||||||
{
|
|
||||||
// no sessionID, access is denied
|
|
||||||
if(!sessionID)
|
|
||||||
{
|
|
||||||
callback(null, {accessStatus: "deny"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// a session is not required, so we'll check if it's a public pad
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// it's not a group pad, means we can grant access
|
|
||||||
if(padID.indexOf("$") == -1)
|
|
||||||
{
|
|
||||||
//get author for this token
|
|
||||||
authorManager.getAuthor4Token(token, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
// assume user has access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: author};
|
|
||||||
// user can't create pads
|
|
||||||
if(settings.editOnly)
|
|
||||||
{
|
|
||||||
// check if pad exists
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
// pad doesn't exist - user can't have access
|
|
||||||
if(!exists) statusObject.accessStatus = "deny";
|
|
||||||
// grant or deny access, with author of token
|
|
||||||
callback(null, statusObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// user may create new pads - no need to check anything
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// grant access, with author of token
|
|
||||||
callback(null, statusObject);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//don't continue
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupID = padID.split("$")[0];
|
|
||||||
var padExists = false;
|
|
||||||
var validSession = false;
|
|
||||||
var sessionAuthor;
|
|
||||||
var tokenAuthor;
|
|
||||||
var isPublic;
|
|
||||||
var isPasswordProtected;
|
|
||||||
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get basic informations from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
//does pad exists
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.doesPadExists(padID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
padExists = exists;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get informations about this session
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
|
|
||||||
{
|
|
||||||
//skip session validation if the session doesn't exists
|
|
||||||
if(err && err.message == "sessionID does not exist")
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
var now = Math.floor(new Date().getTime()/1000);
|
|
||||||
|
|
||||||
//is it for this group? and is validUntil still ok? --> validSession
|
|
||||||
if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now)
|
|
||||||
{
|
|
||||||
validSession = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionAuthor = sessionInfo.authorID;
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get author for token
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get author for this token
|
|
||||||
authorManager.getAuthor4Token(token, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
tokenAuthor = author;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//get more informations of this pad, if avaiable
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//skip this if the pad doesn't exists
|
|
||||||
if(padExists == false)
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
padManager.getPad(padID, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//is it a public pad?
|
|
||||||
isPublic = pad.getPublicStatus();
|
|
||||||
|
|
||||||
//is it password protected?
|
|
||||||
isPasswordProtected = pad.isPasswordProtected();
|
|
||||||
|
|
||||||
//is password correct?
|
|
||||||
if(isPasswordProtected && password && pad.isCorrectPassword(password))
|
|
||||||
{
|
|
||||||
passwordStatus = "correct";
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//- a valid session for this group is avaible AND pad exists
|
|
||||||
if(validSession && padExists)
|
|
||||||
{
|
|
||||||
//- the pad is not password protected
|
|
||||||
if(!isPasswordProtected)
|
|
||||||
{
|
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
|
||||||
}
|
|
||||||
//- the pad is password protected and password is correct
|
|
||||||
else if(isPasswordProtected && passwordStatus == "correct")
|
|
||||||
{
|
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
|
||||||
}
|
|
||||||
//- the pad is password protected but wrong password given
|
|
||||||
else if(isPasswordProtected && passwordStatus == "wrong")
|
|
||||||
{
|
|
||||||
//--> deny access, ask for new password and tell them that the password is wrong
|
|
||||||
statusObject = {accessStatus: "wrongPassword"};
|
|
||||||
}
|
|
||||||
//- the pad is password protected but no password given
|
|
||||||
else if(isPasswordProtected && passwordStatus == "notGiven")
|
|
||||||
{
|
|
||||||
//--> ask for password
|
|
||||||
statusObject = {accessStatus: "needPassword"};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Error("Ops, something wrong happend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//- a valid session for this group avaible but pad doesn't exists
|
|
||||||
else if(validSession && !padExists)
|
|
||||||
{
|
|
||||||
//--> grant access
|
|
||||||
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
|
|
||||||
//--> deny access if user isn't allowed to create the pad
|
|
||||||
if(settings.editOnly) statusObject.accessStatus = "deny";
|
|
||||||
}
|
|
||||||
// there is no valid session avaiable AND pad exists
|
|
||||||
else if(!validSession && padExists)
|
|
||||||
{
|
|
||||||
//-- its public and not password protected
|
|
||||||
if(isPublic && !isPasswordProtected)
|
|
||||||
{
|
|
||||||
//--> grant access, with author of token
|
|
||||||
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
|
|
||||||
}
|
|
||||||
//- its public and password protected and password is correct
|
|
||||||
else if(isPublic && isPasswordProtected && passwordStatus == "correct")
|
|
||||||
{
|
|
||||||
//--> grant access, with author of token
|
|
||||||
statusObject = {accessStatus: "grant", authorID: tokenAuthor};
|
|
||||||
}
|
|
||||||
//- its public and the pad is password protected but wrong password given
|
|
||||||
else if(isPublic && isPasswordProtected && passwordStatus == "wrong")
|
|
||||||
{
|
|
||||||
//--> deny access, ask for new password and tell them that the password is wrong
|
|
||||||
statusObject = {accessStatus: "wrongPassword"};
|
|
||||||
}
|
|
||||||
//- its public and the pad is password protected but no password given
|
|
||||||
else if(isPublic && isPasswordProtected && passwordStatus == "notGiven")
|
|
||||||
{
|
|
||||||
//--> ask for password
|
|
||||||
statusObject = {accessStatus: "needPassword"};
|
|
||||||
}
|
|
||||||
//- its not public
|
|
||||||
else if(!isPublic)
|
|
||||||
{
|
|
||||||
//--> deny access
|
|
||||||
statusObject = {accessStatus: "deny"};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Error("Ops, something wrong happend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// there is no valid session avaiable AND pad doesn't exists
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//--> deny access
|
|
||||||
statusObject = {accessStatus: "deny"};
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, statusObject);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,367 +0,0 @@
|
||||||
/**
|
|
||||||
* The Session Manager provides functions to manage session in the database
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var customError = require("../utils/customError");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
var db = require("./DB").db;
|
|
||||||
var async = require("async");
|
|
||||||
var groupMangager = require("./GroupManager");
|
|
||||||
var authorMangager = require("./AuthorManager");
|
|
||||||
|
|
||||||
exports.doesSessionExist = function(sessionID, callback)
|
|
||||||
{
|
|
||||||
//check if the database entry of this session exists
|
|
||||||
db.get("session:" + sessionID, function (err, session)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, session != null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new session between an author and a group
|
|
||||||
*/
|
|
||||||
exports.createSession = function(groupID, authorID, validUntil, callback)
|
|
||||||
{
|
|
||||||
var sessionID;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//check if group exists
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
groupMangager.doesGroupExist(groupID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//everything is fine, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//check if author exists
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorMangager.doesAuthorExists(authorID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//author does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("authorID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//everything is fine, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//check validUntil and create the session db entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//check if rev is a number
|
|
||||||
if(typeof validUntil != "number")
|
|
||||||
{
|
|
||||||
//try to parse the number
|
|
||||||
if(!isNaN(parseInt(validUntil)))
|
|
||||||
{
|
|
||||||
validUntil = parseInt(validUntil);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(new customError("validUntil is not a number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//ensure this is not a negativ number
|
|
||||||
if(validUntil < 0)
|
|
||||||
{
|
|
||||||
callback(new customError("validUntil is a negativ number","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//ensure this is not a float value
|
|
||||||
if(!is_int(validUntil))
|
|
||||||
{
|
|
||||||
callback(new customError("validUntil is a float value","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if validUntil is in the future
|
|
||||||
if(Math.floor(new Date().getTime()/1000) > validUntil)
|
|
||||||
{
|
|
||||||
callback(new customError("validUntil is in the past","apierror"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//generate sessionID
|
|
||||||
sessionID = "s." + randomString(16);
|
|
||||||
|
|
||||||
//set the session into the database
|
|
||||||
db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil});
|
|
||||||
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
//set the group2sessions entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get the entry
|
|
||||||
db.get("group2sessions:" + groupID, function(err, group2sessions)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//the entry doesn't exist so far, let's create it
|
|
||||||
if(group2sessions == null)
|
|
||||||
{
|
|
||||||
group2sessions = {sessionIDs : {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
//add the entry for this session
|
|
||||||
group2sessions.sessionIDs[sessionID] = 1;
|
|
||||||
|
|
||||||
//save the new element back
|
|
||||||
db.set("group2sessions:" + groupID, group2sessions);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//set the author2sessions entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get the entry
|
|
||||||
db.get("author2sessions:" + authorID, function(err, author2sessions)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//the entry doesn't exist so far, let's create it
|
|
||||||
if(author2sessions == null)
|
|
||||||
{
|
|
||||||
author2sessions = {sessionIDs : {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
//add the entry for this session
|
|
||||||
author2sessions.sessionIDs[sessionID] = 1;
|
|
||||||
|
|
||||||
//save the new element back
|
|
||||||
db.set("author2sessions:" + authorID, author2sessions);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//return error and sessionID
|
|
||||||
callback(null, {sessionID: sessionID});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getSessionInfo = function(sessionID, callback)
|
|
||||||
{
|
|
||||||
//check if the database entry of this session exists
|
|
||||||
db.get("session:" + sessionID, function (err, session)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//session does not exists
|
|
||||||
if(session == null)
|
|
||||||
{
|
|
||||||
callback(new customError("sessionID does not exist","apierror"))
|
|
||||||
}
|
|
||||||
//everything is fine, return the sessioninfos
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null, session);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a session
|
|
||||||
*/
|
|
||||||
exports.deleteSession = function(sessionID, callback)
|
|
||||||
{
|
|
||||||
var authorID, groupID;
|
|
||||||
var group2sessions, author2sessions;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get the session entry
|
|
||||||
db.get("session:" + sessionID, function (err, session)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//session does not exists
|
|
||||||
if(session == null)
|
|
||||||
{
|
|
||||||
callback(new customError("sessionID does not exist","apierror"))
|
|
||||||
}
|
|
||||||
//everything is fine, return the sessioninfos
|
|
||||||
else
|
|
||||||
{
|
|
||||||
authorID = session.authorID;
|
|
||||||
groupID = session.groupID;
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the group2sessions entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.get("group2sessions:" + groupID, function (err, _group2sessions)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
group2sessions = _group2sessions;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the author2sessions entry
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
db.get("author2sessions:" + authorID, function (err, _author2sessions)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
author2sessions = _author2sessions;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//remove the values from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//remove the session
|
|
||||||
db.remove("session:" + sessionID);
|
|
||||||
|
|
||||||
//remove session from group2sessions
|
|
||||||
delete group2sessions.sessionIDs[sessionID];
|
|
||||||
db.set("group2sessions:" + groupID, group2sessions);
|
|
||||||
|
|
||||||
//remove session from author2sessions
|
|
||||||
delete author2sessions.sessionIDs[sessionID];
|
|
||||||
db.set("author2sessions:" + authorID, author2sessions);
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.listSessionsOfGroup = function(groupID, callback)
|
|
||||||
{
|
|
||||||
groupMangager.doesGroupExist(groupID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("groupID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//everything is fine, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
listSessionsWithDBKey("group2sessions:" + groupID, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.listSessionsOfAuthor = function(authorID, callback)
|
|
||||||
{
|
|
||||||
authorMangager.doesAuthorExists(authorID, function(err, exists)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//group does not exist
|
|
||||||
if(exists == false)
|
|
||||||
{
|
|
||||||
callback(new customError("authorID does not exist","apierror"));
|
|
||||||
}
|
|
||||||
//everything is fine, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
listSessionsWithDBKey("author2sessions:" + authorID, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
|
||||||
function listSessionsWithDBKey (dbkey, callback)
|
|
||||||
{
|
|
||||||
var sessions;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get the group2sessions entry
|
|
||||||
db.get(dbkey, function(err, sessionObject)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
sessions = sessionObject ? sessionObject.sessionIDs : null;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//collect all sessionIDs in an arrary
|
|
||||||
var sessionIDs = [];
|
|
||||||
for (var i in sessions)
|
|
||||||
{
|
|
||||||
sessionIDs.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
//foreach trough the sessions and get the sessioninfos
|
|
||||||
async.forEach(sessionIDs, function(sessionID, callback)
|
|
||||||
{
|
|
||||||
exports.getSessionInfo(sessionID, function(err, sessionInfo)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
sessions[sessionID] = sessionInfo;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, sessions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks if a number is an int
|
|
||||||
function is_int(value)
|
|
||||||
{
|
|
||||||
return (parseFloat(value) == parseInt(value)) && !isNaN(value)
|
|
||||||
}
|
|
|
@ -1,949 +0,0 @@
|
||||||
/**
|
|
||||||
* I found this tests in the old Etherpad and used it to test if the Changeset library can be run on node.js.
|
|
||||||
* It has no use for ep-lite, but I thought I keep it cause it may help someone to understand the Changeset library
|
|
||||||
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2_tests.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
|
||||||
|
|
||||||
function random() {
|
|
||||||
this.nextInt = function (maxValue) {
|
|
||||||
return Math.floor(Math.random() * maxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.nextDouble = function (maxValue) {
|
|
||||||
return Math.random();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runTests() {
|
|
||||||
|
|
||||||
function print(str) {
|
|
||||||
console.log(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function assert(code, optMsg) {
|
|
||||||
if (!eval(code)) throw new Error("FALSE: " + (optMsg || code));
|
|
||||||
}
|
|
||||||
|
|
||||||
function literal(v) {
|
|
||||||
if ((typeof v) == "string") {
|
|
||||||
return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"';
|
|
||||||
} else
|
|
||||||
return JSON.stringify(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertEqualArrays(a, b) {
|
|
||||||
assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertEqualStrings(a, b) {
|
|
||||||
assert(literal(a) + " == " + literal(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
function throughIterator(opsStr) {
|
|
||||||
var iter = Changeset.opIterator(opsStr);
|
|
||||||
var assem = Changeset.opAssembler();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
assem.append(iter.next());
|
|
||||||
}
|
|
||||||
return assem.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function throughSmartAssembler(opsStr) {
|
|
||||||
var iter = Changeset.opIterator(opsStr);
|
|
||||||
var assem = Changeset.smartOpAssembler();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
assem.append(iter.next());
|
|
||||||
}
|
|
||||||
assem.endDocument();
|
|
||||||
return assem.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
print("> throughIterator");
|
|
||||||
var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
|
|
||||||
assert("throughIterator(" + literal(x) + ") == " + literal(x));
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
print("> throughSmartAssembler");
|
|
||||||
var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
|
|
||||||
assert("throughSmartAssembler(" + literal(x) + ") == " + literal(x));
|
|
||||||
})();
|
|
||||||
|
|
||||||
function applyMutations(mu, arrayOfArrays) {
|
|
||||||
arrayOfArrays.forEach(function (a) {
|
|
||||||
var result = mu[a[0]].apply(mu, a.slice(1));
|
|
||||||
if (a[0] == 'remove' && a[3]) {
|
|
||||||
assertEqualStrings(a[3], result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mutationsToChangeset(oldLen, arrayOfArrays) {
|
|
||||||
var assem = Changeset.smartOpAssembler();
|
|
||||||
var op = Changeset.newOp();
|
|
||||||
var bank = Changeset.stringAssembler();
|
|
||||||
var oldPos = 0;
|
|
||||||
var newLen = 0;
|
|
||||||
arrayOfArrays.forEach(function (a) {
|
|
||||||
if (a[0] == 'skip') {
|
|
||||||
op.opcode = '=';
|
|
||||||
op.chars = a[1];
|
|
||||||
op.lines = (a[2] || 0);
|
|
||||||
assem.append(op);
|
|
||||||
oldPos += op.chars;
|
|
||||||
newLen += op.chars;
|
|
||||||
} else if (a[0] == 'remove') {
|
|
||||||
op.opcode = '-';
|
|
||||||
op.chars = a[1];
|
|
||||||
op.lines = (a[2] || 0);
|
|
||||||
assem.append(op);
|
|
||||||
oldPos += op.chars;
|
|
||||||
} else if (a[0] == 'insert') {
|
|
||||||
op.opcode = '+';
|
|
||||||
bank.append(a[1]);
|
|
||||||
op.chars = a[1].length;
|
|
||||||
op.lines = (a[2] || 0);
|
|
||||||
assem.append(op);
|
|
||||||
newLen += op.chars;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
newLen += oldLen - oldPos;
|
|
||||||
assem.endDocument();
|
|
||||||
return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
function runMutationTest(testId, origLines, muts, correct) {
|
|
||||||
print("> runMutationTest#" + testId);
|
|
||||||
var lines = origLines.slice();
|
|
||||||
var mu = Changeset.textLinesMutator(lines);
|
|
||||||
applyMutations(mu, muts);
|
|
||||||
mu.close();
|
|
||||||
assertEqualArrays(correct, lines);
|
|
||||||
|
|
||||||
var inText = origLines.join('');
|
|
||||||
var cs = mutationsToChangeset(inText.length, muts);
|
|
||||||
lines = origLines.slice();
|
|
||||||
Changeset.mutateTextLines(cs, lines);
|
|
||||||
assertEqualArrays(correct, lines);
|
|
||||||
|
|
||||||
var correctText = correct.join('');
|
|
||||||
//print(literal(cs));
|
|
||||||
var outText = Changeset.applyToText(cs, inText);
|
|
||||||
assertEqualStrings(correctText, outText);
|
|
||||||
}
|
|
||||||
|
|
||||||
runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
|
|
||||||
['remove', 1, 0, "a"],
|
|
||||||
['insert', "tu"],
|
|
||||||
['remove', 1, 0, "p"],
|
|
||||||
['skip', 4, 1],
|
|
||||||
['skip', 7, 1],
|
|
||||||
['insert', "cream\npie\n", 2],
|
|
||||||
['skip', 2],
|
|
||||||
['insert', "bot"],
|
|
||||||
['insert', "\n", 1],
|
|
||||||
['insert', "bu"],
|
|
||||||
['skip', 3],
|
|
||||||
['remove', 3, 1, "ge\n"],
|
|
||||||
['remove', 6, 0, "duffle"]
|
|
||||||
], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
|
|
||||||
|
|
||||||
runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
|
|
||||||
['remove', 1, 0, "a"],
|
|
||||||
['remove', 1, 0, "p"],
|
|
||||||
['insert', "tu"],
|
|
||||||
['skip', 11, 2],
|
|
||||||
['insert', "cream\npie\n", 2],
|
|
||||||
['skip', 2],
|
|
||||||
['insert', "bot"],
|
|
||||||
['insert', "\n", 1],
|
|
||||||
['insert', "bu"],
|
|
||||||
['skip', 3],
|
|
||||||
['remove', 3, 1, "ge\n"],
|
|
||||||
['remove', 6, 0, "duffle"]
|
|
||||||
], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
|
|
||||||
|
|
||||||
runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
|
|
||||||
['remove', 6, 1, "apple\n"],
|
|
||||||
['skip', 15, 2],
|
|
||||||
['skip', 6],
|
|
||||||
['remove', 1, 1, "\n"],
|
|
||||||
['remove', 8, 0, "eggplant"],
|
|
||||||
['skip', 1, 1]
|
|
||||||
], ["banana\n", "cabbage\n", "duffle\n"]);
|
|
||||||
|
|
||||||
runMutationTest(4, ["15\n"], [
|
|
||||||
['skip', 1],
|
|
||||||
['insert', "\n2\n3\n4\n", 4],
|
|
||||||
['skip', 2, 1]
|
|
||||||
], ["1\n", "2\n", "3\n", "4\n", "5\n"]);
|
|
||||||
|
|
||||||
runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [
|
|
||||||
['skip', 1],
|
|
||||||
['remove', 7, 4, "\n2\n3\n4\n"],
|
|
||||||
['skip', 2, 1]
|
|
||||||
], ["15\n"]);
|
|
||||||
|
|
||||||
runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [
|
|
||||||
['insert', "0"],
|
|
||||||
['skip', 4, 1],
|
|
||||||
['skip', 4, 1],
|
|
||||||
['remove', 8, 2, "def\nghi\n"],
|
|
||||||
['skip', 4, 1]
|
|
||||||
], ["0123\n", "abc\n", "xyz\n"]);
|
|
||||||
|
|
||||||
runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
|
|
||||||
['remove', 6, 1, "apple\n"],
|
|
||||||
['skip', 15, 2, true],
|
|
||||||
['skip', 6, 0, true],
|
|
||||||
['remove', 1, 1, "\n"],
|
|
||||||
['remove', 8, 0, "eggplant"],
|
|
||||||
['skip', 1, 1, true]
|
|
||||||
], ["banana\n", "cabbage\n", "duffle\n"]);
|
|
||||||
|
|
||||||
function poolOrArray(attribs) {
|
|
||||||
if (attribs.getAttrib) {
|
|
||||||
return attribs; // it's already an attrib pool
|
|
||||||
} else {
|
|
||||||
// assume it's an array of attrib strings to be split and added
|
|
||||||
var p = new AttributePool();
|
|
||||||
attribs.forEach(function (kv) {
|
|
||||||
p.putAttrib(kv.split(','));
|
|
||||||
});
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) {
|
|
||||||
print("> applyToAttribution#" + testId);
|
|
||||||
var p = poolOrArray(attribs);
|
|
||||||
var result = Changeset.applyToAttribution(
|
|
||||||
Changeset.checkRep(cs), inAttr, p);
|
|
||||||
assertEqualStrings(outCorrect, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
|
|
||||||
runApplyToAttributionTest(1, ['bold,', 'bold,true'], "Z:7>3-1*0=1*1=1=3+4$abcd", "+1*1+1|1+5", "+1*1+1|1+8");
|
|
||||||
|
|
||||||
// turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
|
|
||||||
runApplyToAttributionTest(2, ['bold,', 'bold,true'], "Z:g<4*1|1=6*1=5-4$", "|2+g", "*1|1+6*1+5|1+1");
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
print("> mutatorHasMore");
|
|
||||||
var lines = ["1\n", "2\n", "3\n", "4\n"];
|
|
||||||
var mu;
|
|
||||||
|
|
||||||
mu = Changeset.textLinesMutator(lines);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.skip(8, 4);
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
mu.close();
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
|
|
||||||
// still 1,2,3,4
|
|
||||||
mu = Changeset.textLinesMutator(lines);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.remove(2, 1);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.skip(2, 1);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.skip(2, 1);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.skip(2, 1);
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
mu.insert("5\n", 1);
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
mu.close();
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
|
|
||||||
// 2,3,4,5 now
|
|
||||||
mu = Changeset.textLinesMutator(lines);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.remove(6, 3);
|
|
||||||
assert(mu.hasMore() + ' == true');
|
|
||||||
mu.remove(2, 1);
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
mu.insert("hello\n", 1);
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
mu.close();
|
|
||||||
assert(mu.hasMore() + ' == false');
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) {
|
|
||||||
print("> runMutateAttributionTest#" + testId);
|
|
||||||
var p = poolOrArray(attribs);
|
|
||||||
var alines2 = Array.prototype.slice.call(alines);
|
|
||||||
var result = Changeset.mutateAttributionLines(
|
|
||||||
Changeset.checkRep(cs), alines2, p);
|
|
||||||
assertEqualArrays(outCorrect, alines2);
|
|
||||||
|
|
||||||
print("> runMutateAttributionTest#" + testId + ".applyToAttribution");
|
|
||||||
|
|
||||||
function removeQuestionMarks(a) {
|
|
||||||
return a.replace(/\?/g, '');
|
|
||||||
}
|
|
||||||
var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
|
|
||||||
var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
|
|
||||||
var mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
|
|
||||||
assertEqualStrings(correctMerged, mergedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
|
|
||||||
runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], ["|1+4", "+1*0+1|1+2", "|1+4"]);
|
|
||||||
|
|
||||||
// make a document bold
|
|
||||||
runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], ["*0|1+4", "*0|1+4", "*0|1+4"]);
|
|
||||||
|
|
||||||
// clear bold on document
|
|
||||||
runMutateAttributionTest(3, ["bold,", "bold,true"], "Z:c>0*0|3=c$", ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], ["|1+4", "|1+4", "|1+4"]);
|
|
||||||
|
|
||||||
// add a character on line 3 of a document with 5 blank lines, and make sure
|
|
||||||
// the optimization that skips purely-kept lines is working; if any attribution string
|
|
||||||
// with a '?' is parsed it will cause an error.
|
|
||||||
runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
|
|
||||||
|
|
||||||
var testPoolWithChars = (function () {
|
|
||||||
var p = new AttributePool();
|
|
||||||
p.putAttrib(['char', 'newline']);
|
|
||||||
for (var i = 1; i < 36; i++) {
|
|
||||||
p.putAttrib(['char', Changeset.numToString(i)]);
|
|
||||||
}
|
|
||||||
p.putAttrib(['char', '']);
|
|
||||||
return p;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// based on runMutationTest#1
|
|
||||||
runMutateAttributionTest(5, testPoolWithChars, "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$" + "tucream\npie\nbot\nbu", ["*a+1*p+2*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", "*d+1*u+1*f+2*l+1*e+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], ["*t+1*u+1*p+1*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "|1+6", "|1+4", "*c+1*a+1*b+1*o+1*t+1*0|1+1", "*b+1*u+1*b+2*a+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]);
|
|
||||||
|
|
||||||
// based on runMutationTest#3
|
|
||||||
runMutateAttributionTest(6, testPoolWithChars, "Z:11<f|1-6|2=f=6|1-1-8$", ["*a|1+6", "*b|1+7", "*c|1+8", "*d|1+7", "*e|1+9"], ["*b|1+7", "*c|1+8", "*d+6*e|1+1"]);
|
|
||||||
|
|
||||||
// based on runMutationTest#4
|
|
||||||
runMutateAttributionTest(7, testPoolWithChars, "Z:3>7=1|4+7$\n2\n3\n4\n", ["*1+1*5|1+2"], ["*1+1|1+1", "|1+2", "|1+2", "|1+2", "*5|1+2"]);
|
|
||||||
|
|
||||||
// based on runMutationTest#5
|
|
||||||
runMutateAttributionTest(8, testPoolWithChars, "Z:a<7=1|4-7$", ["*1|1+2", "*2|1+2", "*3|1+2", "*4|1+2", "*5|1+2"], ["*1+1*5|1+2"]);
|
|
||||||
|
|
||||||
// based on runMutationTest#6
|
|
||||||
runMutateAttributionTest(9, testPoolWithChars, "Z:k<7*0+1*10|2=8|2-8$0", ["*1+1*2+1*3+1|1+1", "*a+1*b+1*c+1|1+1", "*d+1*e+1*f+1|1+1", "*g+1*h+1*i+1|1+1", "?*x+1*y+1*z+1|1+1"], ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]);
|
|
||||||
|
|
||||||
runMutateAttributionTest(10, testPoolWithChars, "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", ["|1+3", "|1+3"], ["|1+5", "+2*0+1|1+2"]);
|
|
||||||
|
|
||||||
|
|
||||||
runMutateAttributionTest(11, testPoolWithChars, "Z:s>1|1=4=6|1+1$\n", ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]);
|
|
||||||
|
|
||||||
function randomInlineString(len, rand) {
|
|
||||||
var assem = Changeset.stringAssembler();
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
assem.append(String.fromCharCode(rand.nextInt(26) + 97));
|
|
||||||
}
|
|
||||||
return assem.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomMultiline(approxMaxLines, approxMaxCols, rand) {
|
|
||||||
var numParts = rand.nextInt(approxMaxLines * 2) + 1;
|
|
||||||
var txt = Changeset.stringAssembler();
|
|
||||||
txt.append(rand.nextInt(2) ? '\n' : '');
|
|
||||||
for (var i = 0; i < numParts; i++) {
|
|
||||||
if ((i % 2) == 0) {
|
|
||||||
if (rand.nextInt(10)) {
|
|
||||||
txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
|
|
||||||
} else {
|
|
||||||
txt.append('\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txt.append('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return txt.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomStringOperation(numCharsLeft, rand) {
|
|
||||||
var result;
|
|
||||||
switch (rand.nextInt(9)) {
|
|
||||||
case 0:
|
|
||||||
{
|
|
||||||
// insert char
|
|
||||||
result = {
|
|
||||||
insert: randomInlineString(1, rand)
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
{
|
|
||||||
// delete char
|
|
||||||
result = {
|
|
||||||
remove: 1
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
{
|
|
||||||
// skip char
|
|
||||||
result = {
|
|
||||||
skip: 1
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
{
|
|
||||||
// insert small
|
|
||||||
result = {
|
|
||||||
insert: randomInlineString(rand.nextInt(4) + 1, rand)
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
{
|
|
||||||
// delete small
|
|
||||||
result = {
|
|
||||||
remove: rand.nextInt(4) + 1
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 5:
|
|
||||||
{
|
|
||||||
// skip small
|
|
||||||
result = {
|
|
||||||
skip: rand.nextInt(4) + 1
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 6:
|
|
||||||
{
|
|
||||||
// insert multiline;
|
|
||||||
result = {
|
|
||||||
insert: randomMultiline(5, 20, rand)
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 7:
|
|
||||||
{
|
|
||||||
// delete multiline
|
|
||||||
result = {
|
|
||||||
remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 8:
|
|
||||||
{
|
|
||||||
// skip multiline
|
|
||||||
result = {
|
|
||||||
skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 9:
|
|
||||||
{
|
|
||||||
// delete to end
|
|
||||||
result = {
|
|
||||||
remove: numCharsLeft
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 10:
|
|
||||||
{
|
|
||||||
// skip to end
|
|
||||||
result = {
|
|
||||||
skip: numCharsLeft
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var maxOrig = numCharsLeft - 1;
|
|
||||||
if ('remove' in result) {
|
|
||||||
result.remove = Math.min(result.remove, maxOrig);
|
|
||||||
} else if ('skip' in result) {
|
|
||||||
result.skip = Math.min(result.skip, maxOrig);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomTwoPropAttribs(opcode, rand) {
|
|
||||||
// assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
|
|
||||||
if (opcode == '-' || rand.nextInt(3)) {
|
|
||||||
return '';
|
|
||||||
} else if (rand.nextInt(3)) {
|
|
||||||
if (opcode == '+' || rand.nextInt(2)) {
|
|
||||||
return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1);
|
|
||||||
} else {
|
|
||||||
return '*' + Changeset.numToString(rand.nextInt(2) * 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (opcode == '+' || rand.nextInt(4) == 0) {
|
|
||||||
return '*1*3';
|
|
||||||
} else {
|
|
||||||
return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomTestChangeset(origText, rand, withAttribs) {
|
|
||||||
var charBank = Changeset.stringAssembler();
|
|
||||||
var textLeft = origText; // always keep final newline
|
|
||||||
var outTextAssem = Changeset.stringAssembler();
|
|
||||||
var opAssem = Changeset.smartOpAssembler();
|
|
||||||
var oldLen = origText.length;
|
|
||||||
|
|
||||||
var nextOp = Changeset.newOp();
|
|
||||||
|
|
||||||
function appendMultilineOp(opcode, txt) {
|
|
||||||
nextOp.opcode = opcode;
|
|
||||||
if (withAttribs) {
|
|
||||||
nextOp.attribs = randomTwoPropAttribs(opcode, rand);
|
|
||||||
}
|
|
||||||
txt.replace(/\n|[^\n]+/g, function (t) {
|
|
||||||
if (t == '\n') {
|
|
||||||
nextOp.chars = 1;
|
|
||||||
nextOp.lines = 1;
|
|
||||||
opAssem.append(nextOp);
|
|
||||||
} else {
|
|
||||||
nextOp.chars = t.length;
|
|
||||||
nextOp.lines = 0;
|
|
||||||
opAssem.append(nextOp);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function doOp() {
|
|
||||||
var o = randomStringOperation(textLeft.length, rand);
|
|
||||||
if (o.insert) {
|
|
||||||
var txt = o.insert;
|
|
||||||
charBank.append(txt);
|
|
||||||
outTextAssem.append(txt);
|
|
||||||
appendMultilineOp('+', txt);
|
|
||||||
} else if (o.skip) {
|
|
||||||
var txt = textLeft.substring(0, o.skip);
|
|
||||||
textLeft = textLeft.substring(o.skip);
|
|
||||||
outTextAssem.append(txt);
|
|
||||||
appendMultilineOp('=', txt);
|
|
||||||
} else if (o.remove) {
|
|
||||||
var txt = textLeft.substring(0, o.remove);
|
|
||||||
textLeft = textLeft.substring(o.remove);
|
|
||||||
appendMultilineOp('-', txt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (textLeft.length > 1) doOp();
|
|
||||||
for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
|
|
||||||
var outText = outTextAssem.toString() + '\n';
|
|
||||||
opAssem.endDocument();
|
|
||||||
var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
|
|
||||||
Changeset.checkRep(cs);
|
|
||||||
return [cs, outText];
|
|
||||||
}
|
|
||||||
|
|
||||||
function testCompose(randomSeed) {
|
|
||||||
var rand = new random();
|
|
||||||
print("> testCompose#" + randomSeed);
|
|
||||||
|
|
||||||
var p = new AttributePool();
|
|
||||||
|
|
||||||
var startText = randomMultiline(10, 20, rand) + '\n';
|
|
||||||
|
|
||||||
var x1 = randomTestChangeset(startText, rand);
|
|
||||||
var change1 = x1[0];
|
|
||||||
var text1 = x1[1];
|
|
||||||
|
|
||||||
var x2 = randomTestChangeset(text1, rand);
|
|
||||||
var change2 = x2[0];
|
|
||||||
var text2 = x2[1];
|
|
||||||
|
|
||||||
var x3 = randomTestChangeset(text2, rand);
|
|
||||||
var change3 = x3[0];
|
|
||||||
var text3 = x3[1];
|
|
||||||
|
|
||||||
//print(literal(Changeset.toBaseTen(startText)));
|
|
||||||
//print(literal(Changeset.toBaseTen(change1)));
|
|
||||||
//print(literal(Changeset.toBaseTen(change2)));
|
|
||||||
var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p));
|
|
||||||
var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p));
|
|
||||||
var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p));
|
|
||||||
var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p));
|
|
||||||
assertEqualStrings(change123, change123a);
|
|
||||||
|
|
||||||
assertEqualStrings(text2, Changeset.applyToText(change12, startText));
|
|
||||||
assertEqualStrings(text3, Changeset.applyToText(change23, text1));
|
|
||||||
assertEqualStrings(text3, Changeset.applyToText(change123, startText));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < 30; i++) testCompose(i);
|
|
||||||
|
|
||||||
(function simpleComposeAttributesTest() {
|
|
||||||
print("> simpleComposeAttributesTest");
|
|
||||||
var p = new AttributePool();
|
|
||||||
p.putAttrib(['bold', '']);
|
|
||||||
p.putAttrib(['bold', 'true']);
|
|
||||||
var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
|
|
||||||
var cs2 = Changeset.checkRep("Z:3>0*0|1=3$");
|
|
||||||
var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p));
|
|
||||||
assertEqualStrings("Z:2>1+1*0|1=2$x", cs12);
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function followAttributesTest() {
|
|
||||||
var p = new AttributePool();
|
|
||||||
p.putAttrib(['x', '']);
|
|
||||||
p.putAttrib(['x', 'abc']);
|
|
||||||
p.putAttrib(['x', 'def']);
|
|
||||||
p.putAttrib(['y', '']);
|
|
||||||
p.putAttrib(['y', 'abc']);
|
|
||||||
p.putAttrib(['y', 'def']);
|
|
||||||
|
|
||||||
function testFollow(a, b, afb, bfa, merge) {
|
|
||||||
assertEqualStrings(afb, Changeset.followAttributes(a, b, p));
|
|
||||||
assertEqualStrings(bfa, Changeset.followAttributes(b, a, p));
|
|
||||||
assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
|
|
||||||
assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
|
|
||||||
}
|
|
||||||
|
|
||||||
testFollow('', '', '', '', '');
|
|
||||||
testFollow('*0', '', '', '*0', '*0');
|
|
||||||
testFollow('*0', '*0', '', '', '*0');
|
|
||||||
testFollow('*0', '*1', '', '*0', '*0');
|
|
||||||
testFollow('*1', '*2', '', '*1', '*1');
|
|
||||||
testFollow('*0*1', '', '', '*0*1', '*0*1');
|
|
||||||
testFollow('*0*4', '*2*3', '*3', '*0', '*0*3');
|
|
||||||
testFollow('*0*4', '*2', '', '*0*4', '*0*4');
|
|
||||||
})();
|
|
||||||
|
|
||||||
function testFollow(randomSeed) {
|
|
||||||
var rand = new random();
|
|
||||||
print("> testFollow#" + randomSeed);
|
|
||||||
|
|
||||||
var p = new AttributePool();
|
|
||||||
|
|
||||||
var startText = randomMultiline(10, 20, rand) + '\n';
|
|
||||||
|
|
||||||
var cs1 = randomTestChangeset(startText, rand)[0];
|
|
||||||
var cs2 = randomTestChangeset(startText, rand)[0];
|
|
||||||
|
|
||||||
var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p));
|
|
||||||
var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p));
|
|
||||||
|
|
||||||
var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb));
|
|
||||||
var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
|
|
||||||
|
|
||||||
assertEqualStrings(merge1, merge2);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < 30; i++) testFollow(i);
|
|
||||||
|
|
||||||
function testSplitJoinAttributionLines(randomSeed) {
|
|
||||||
var rand = new random();
|
|
||||||
print("> testSplitJoinAttributionLines#" + randomSeed);
|
|
||||||
|
|
||||||
var doc = randomMultiline(10, 20, rand) + '\n';
|
|
||||||
|
|
||||||
function stringToOps(str) {
|
|
||||||
var assem = Changeset.mergingOpAssembler();
|
|
||||||
var o = Changeset.newOp('+');
|
|
||||||
o.chars = 1;
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
var c = str.charAt(i);
|
|
||||||
o.lines = (c == '\n' ? 1 : 0);
|
|
||||||
o.attribs = (c == 'a' || c == 'b' ? '*' + c : '');
|
|
||||||
assem.append(o);
|
|
||||||
}
|
|
||||||
return assem.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
var theJoined = stringToOps(doc);
|
|
||||||
var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
|
|
||||||
|
|
||||||
assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
|
|
||||||
assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
|
|
||||||
|
|
||||||
(function testMoveOpsToNewPool() {
|
|
||||||
print("> testMoveOpsToNewPool");
|
|
||||||
|
|
||||||
var pool1 = new AttributePool();
|
|
||||||
var pool2 = new AttributePool();
|
|
||||||
|
|
||||||
pool1.putAttrib(['baz', 'qux']);
|
|
||||||
pool1.putAttrib(['foo', 'bar']);
|
|
||||||
|
|
||||||
pool2.putAttrib(['foo', 'bar']);
|
|
||||||
|
|
||||||
assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab');
|
|
||||||
assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
(function testMakeSplice() {
|
|
||||||
print("> testMakeSplice");
|
|
||||||
|
|
||||||
var t = "a\nb\nc\n";
|
|
||||||
var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t);
|
|
||||||
assertEqualStrings("a\nb\ncdef\n", t2);
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function testToSplices() {
|
|
||||||
print("> testToSplices");
|
|
||||||
|
|
||||||
var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk');
|
|
||||||
var correctSplices = [
|
|
||||||
[5, 8, "123456789"],
|
|
||||||
[9, 17, "abcdefghijk"]
|
|
||||||
];
|
|
||||||
assertEqualArrays(correctSplices, Changeset.toSplices(cs));
|
|
||||||
})();
|
|
||||||
|
|
||||||
function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) {
|
|
||||||
print("> testCharacterRangeFollow#" + testId);
|
|
||||||
|
|
||||||
var cs = Changeset.checkRep(cs);
|
|
||||||
assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]);
|
|
||||||
testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]);
|
|
||||||
testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0, 3], false, [3, 3]);
|
|
||||||
testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0, 3], true, [0, 0]);
|
|
||||||
testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]);
|
|
||||||
testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]);
|
|
||||||
testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]);
|
|
||||||
testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]);
|
|
||||||
testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]);
|
|
||||||
testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]);
|
|
||||||
testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]);
|
|
||||||
|
|
||||||
(function testOpAttributeValue() {
|
|
||||||
print("> testOpAttributeValue");
|
|
||||||
|
|
||||||
var p = new AttributePool();
|
|
||||||
p.putAttrib(['name', 'david']);
|
|
||||||
p.putAttrib(['color', 'green']);
|
|
||||||
|
|
||||||
assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
|
|
||||||
assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
|
|
||||||
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
|
|
||||||
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p));
|
|
||||||
assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p));
|
|
||||||
assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p));
|
|
||||||
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p));
|
|
||||||
assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p));
|
|
||||||
})();
|
|
||||||
|
|
||||||
function testAppendATextToAssembler(testId, atext, correctOps) {
|
|
||||||
print("> testAppendATextToAssembler#" + testId);
|
|
||||||
|
|
||||||
var assem = Changeset.smartOpAssembler();
|
|
||||||
Changeset.appendATextToAssembler(atext, assem);
|
|
||||||
assertEqualStrings(correctOps, assem.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
testAppendATextToAssembler(1, {
|
|
||||||
text: "\n",
|
|
||||||
attribs: "|1+1"
|
|
||||||
}, "");
|
|
||||||
testAppendATextToAssembler(2, {
|
|
||||||
text: "\n\n",
|
|
||||||
attribs: "|2+2"
|
|
||||||
}, "|1+1");
|
|
||||||
testAppendATextToAssembler(3, {
|
|
||||||
text: "\n\n",
|
|
||||||
attribs: "*x|2+2"
|
|
||||||
}, "*x|1+1");
|
|
||||||
testAppendATextToAssembler(4, {
|
|
||||||
text: "\n\n",
|
|
||||||
attribs: "*x|1+1|1+1"
|
|
||||||
}, "*x|1+1");
|
|
||||||
testAppendATextToAssembler(5, {
|
|
||||||
text: "foo\n",
|
|
||||||
attribs: "|1+4"
|
|
||||||
}, "+3");
|
|
||||||
testAppendATextToAssembler(6, {
|
|
||||||
text: "\nfoo\n",
|
|
||||||
attribs: "|2+5"
|
|
||||||
}, "|1+1+3");
|
|
||||||
testAppendATextToAssembler(7, {
|
|
||||||
text: "\nfoo\n",
|
|
||||||
attribs: "*x|2+5"
|
|
||||||
}, "*x|1+1*x+3");
|
|
||||||
testAppendATextToAssembler(8, {
|
|
||||||
text: "\n\n\nfoo\n",
|
|
||||||
attribs: "|2+2*x|2+5"
|
|
||||||
}, "|2+2*x|1+1*x+3");
|
|
||||||
|
|
||||||
function testMakeAttribsString(testId, pool, opcode, attribs, correctString) {
|
|
||||||
print("> testMakeAttribsString#" + testId);
|
|
||||||
|
|
||||||
var p = poolOrArray(pool);
|
|
||||||
var str = Changeset.makeAttribsString(opcode, attribs, p);
|
|
||||||
assertEqualStrings(correctString, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
testMakeAttribsString(1, ['bold,'], '+', [
|
|
||||||
['bold', '']
|
|
||||||
], '');
|
|
||||||
testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [
|
|
||||||
['bold', '']
|
|
||||||
], '*1');
|
|
||||||
testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [
|
|
||||||
['abc', 'def'],
|
|
||||||
['bold', 'true']
|
|
||||||
], '*0*1');
|
|
||||||
testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [
|
|
||||||
['bold', 'true'],
|
|
||||||
['abc', 'def']
|
|
||||||
], '*0*1');
|
|
||||||
|
|
||||||
function testSubattribution(testId, astr, start, end, correctOutput) {
|
|
||||||
print("> testSubattribution#" + testId);
|
|
||||||
|
|
||||||
var str = Changeset.subattribution(astr, start, end);
|
|
||||||
assertEqualStrings(correctOutput, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
testSubattribution(1, "+1", 0, 0, "");
|
|
||||||
testSubattribution(2, "+1", 0, 1, "+1");
|
|
||||||
testSubattribution(3, "+1", 0, undefined, "+1");
|
|
||||||
testSubattribution(4, "|1+1", 0, 0, "");
|
|
||||||
testSubattribution(5, "|1+1", 0, 1, "|1+1");
|
|
||||||
testSubattribution(6, "|1+1", 0, undefined, "|1+1");
|
|
||||||
testSubattribution(7, "*0+1", 0, 0, "");
|
|
||||||
testSubattribution(8, "*0+1", 0, 1, "*0+1");
|
|
||||||
testSubattribution(9, "*0+1", 0, undefined, "*0+1");
|
|
||||||
testSubattribution(10, "*0|1+1", 0, 0, "");
|
|
||||||
testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1");
|
|
||||||
testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1");
|
|
||||||
testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1");
|
|
||||||
testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2");
|
|
||||||
testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1");
|
|
||||||
testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1");
|
|
||||||
testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2");
|
|
||||||
testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3");
|
|
||||||
testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3");
|
|
||||||
testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3");
|
|
||||||
testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3");
|
|
||||||
testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3");
|
|
||||||
testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3");
|
|
||||||
testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2");
|
|
||||||
testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1");
|
|
||||||
testSubattribution(26, "*0+2+1*1+3", 6, undefined, "");
|
|
||||||
testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1");
|
|
||||||
testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2");
|
|
||||||
testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1");
|
|
||||||
testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1");
|
|
||||||
testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2");
|
|
||||||
testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3");
|
|
||||||
testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3");
|
|
||||||
testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3");
|
|
||||||
testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3");
|
|
||||||
testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3");
|
|
||||||
testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3");
|
|
||||||
testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2");
|
|
||||||
testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1");
|
|
||||||
testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2");
|
|
||||||
testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3");
|
|
||||||
testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3");
|
|
||||||
|
|
||||||
function testFilterAttribNumbers(testId, cs, filter, correctOutput) {
|
|
||||||
print("> testFilterAttribNumbers#" + testId);
|
|
||||||
|
|
||||||
var str = Changeset.filterAttribNumbers(cs, filter);
|
|
||||||
assertEqualStrings(correctOutput, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
|
|
||||||
return (n % 2) == 0;
|
|
||||||
}, "*0+1+2+3+4*2+5*0*2*c+6");
|
|
||||||
testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
|
|
||||||
return (n % 2) == 1;
|
|
||||||
}, "*1+1+2+3*1+4+5*1*b+6");
|
|
||||||
|
|
||||||
function testInverse(testId, cs, lines, alines, pool, correctOutput) {
|
|
||||||
print("> testInverse#" + testId);
|
|
||||||
|
|
||||||
pool = poolOrArray(pool);
|
|
||||||
var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
|
|
||||||
assertEqualStrings(correctOutput, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--"
|
|
||||||
testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,', 'bold,true'], "Z:9>0=2*0=1=2*1=2$");
|
|
||||||
|
|
||||||
function testMutateTextLines(testId, cs, lines, correctLines) {
|
|
||||||
print("> testMutateTextLines#" + testId);
|
|
||||||
|
|
||||||
var a = lines.slice();
|
|
||||||
Changeset.mutateTextLines(cs, a);
|
|
||||||
assertEqualArrays(correctLines, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]);
|
|
||||||
testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]);
|
|
||||||
|
|
||||||
function testInverseRandom(randomSeed) {
|
|
||||||
var rand = new random();
|
|
||||||
print("> testInverseRandom#" + randomSeed);
|
|
||||||
|
|
||||||
var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']);
|
|
||||||
|
|
||||||
var startText = randomMultiline(10, 20, rand) + '\n';
|
|
||||||
var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText);
|
|
||||||
var lines = startText.slice(0, -1).split('\n').map(function (s) {
|
|
||||||
return s + '\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
var stylifier = randomTestChangeset(startText, rand, true)[0];
|
|
||||||
|
|
||||||
//print(alines.join('\n'));
|
|
||||||
Changeset.mutateAttributionLines(stylifier, alines, p);
|
|
||||||
//print(stylifier);
|
|
||||||
//print(alines.join('\n'));
|
|
||||||
Changeset.mutateTextLines(stylifier, lines);
|
|
||||||
|
|
||||||
var changeset = randomTestChangeset(lines.join(''), rand, true)[0];
|
|
||||||
var inverseChangeset = Changeset.inverse(changeset, lines, alines, p);
|
|
||||||
|
|
||||||
var origLines = lines.slice();
|
|
||||||
var origALines = alines.slice();
|
|
||||||
|
|
||||||
Changeset.mutateTextLines(changeset, lines);
|
|
||||||
Changeset.mutateAttributionLines(changeset, alines, p);
|
|
||||||
//print(origALines.join('\n'));
|
|
||||||
//print(changeset);
|
|
||||||
//print(inverseChangeset);
|
|
||||||
//print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n'));
|
|
||||||
//print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n'));
|
|
||||||
//print(alines.join('\n'));
|
|
||||||
Changeset.mutateTextLines(inverseChangeset, lines);
|
|
||||||
Changeset.mutateAttributionLines(inverseChangeset, alines, p);
|
|
||||||
//print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
|
|
||||||
assertEqualArrays(origLines, lines);
|
|
||||||
assertEqualArrays(origALines, alines);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < 30; i++) testInverseRandom(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
runTests();
|
|
|
@ -1,9 +0,0 @@
|
||||||
a
|
|
||||||
<% e.begin_block("bar"); %>
|
|
||||||
A
|
|
||||||
<% e.begin_block("foo"); %>
|
|
||||||
XX
|
|
||||||
<% e.end_block(); %>
|
|
||||||
B
|
|
||||||
<% e.end_block(); %>
|
|
||||||
b
|
|
|
@ -1,7 +0,0 @@
|
||||||
<% e.inherit("./bar.ejs"); %>
|
|
||||||
|
|
||||||
<% e.begin_define_block("foo"); %>
|
|
||||||
YY
|
|
||||||
<% e.super(); %>
|
|
||||||
ZZ
|
|
||||||
<% e.end_define_block(); %>
|
|
|
@ -1,131 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Basic usage:
|
|
||||||
*
|
|
||||||
* require("./index").require("./examples/foo.ejs")
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ejs = require("ejs");
|
|
||||||
var fs = require("fs");
|
|
||||||
var path = require("path");
|
|
||||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
|
|
||||||
var resolve = require("resolve");
|
|
||||||
|
|
||||||
exports.info = {
|
|
||||||
buf_stack: [],
|
|
||||||
block_stack: [],
|
|
||||||
blocks: {},
|
|
||||||
file_stack: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
exports._init = function (b, recursive) {
|
|
||||||
exports.info.buf_stack.push(exports.info.buf);
|
|
||||||
exports.info.buf = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports._exit = function (b, recursive) {
|
|
||||||
exports.info.file_stack[exports.info.file_stack.length-1].inherit.forEach(function (item) {
|
|
||||||
exports._require(item.name, item.args);
|
|
||||||
});
|
|
||||||
exports.info.buf = exports.info.buf_stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.begin_capture = function() {
|
|
||||||
exports.info.buf_stack.push(exports.info.buf.concat());
|
|
||||||
exports.info.buf.splice(0, exports.info.buf.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.end_capture = function () {
|
|
||||||
var res = exports.info.buf.join("");
|
|
||||||
exports.info.buf.splice.apply(
|
|
||||||
exports.info.buf,
|
|
||||||
[0, exports.info.buf.length].concat(exports.info.buf_stack.pop()));
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.begin_define_block = function (name) {
|
|
||||||
if (typeof exports.info.blocks[name] == "undefined")
|
|
||||||
exports.info.blocks[name] = {};
|
|
||||||
exports.info.block_stack.push(name);
|
|
||||||
exports.begin_capture();
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.super = function () {
|
|
||||||
exports.info.buf.push('<!eejs!super!>');
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.end_define_block = function () {
|
|
||||||
content = exports.end_capture();
|
|
||||||
var name = exports.info.block_stack.pop();
|
|
||||||
if (typeof exports.info.blocks[name].content == "undefined")
|
|
||||||
exports.info.blocks[name].content = content;
|
|
||||||
else if (typeof exports.info.blocks[name].content.indexOf('<!eejs!super!>'))
|
|
||||||
exports.info.blocks[name].content = exports.info.blocks[name].content.replace('<!eejs!super!>', content);
|
|
||||||
|
|
||||||
return exports.info.blocks[name].content;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.end_block = function () {
|
|
||||||
var name = exports.info.block_stack[exports.info.block_stack.length-1];
|
|
||||||
var args = {content: exports.end_define_block()};
|
|
||||||
hooks.callAll("eejsBlock_" + name, args);
|
|
||||||
exports.info.buf.push(args.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.begin_block = exports.begin_define_block;
|
|
||||||
|
|
||||||
exports.inherit = function (name, args) {
|
|
||||||
exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.require = function (name, args, mod) {
|
|
||||||
if (args == undefined) args = {};
|
|
||||||
|
|
||||||
var basedir = __dirname;
|
|
||||||
var paths = [];
|
|
||||||
|
|
||||||
if (exports.info.file_stack.length) {
|
|
||||||
basedir = path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path);
|
|
||||||
}
|
|
||||||
if (mod) {
|
|
||||||
basedir = path.dirname(mod.filename);
|
|
||||||
paths = mod.paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ejspath = resolve.sync(
|
|
||||||
name,
|
|
||||||
{
|
|
||||||
paths : paths,
|
|
||||||
basedir : basedir,
|
|
||||||
extensions : [ '.html', '.ejs' ],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
args.e = exports;
|
|
||||||
args.require = require;
|
|
||||||
var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>';
|
|
||||||
|
|
||||||
exports.info.file_stack.push({path: ejspath, inherit: []});
|
|
||||||
var res = ejs.render(template, args);
|
|
||||||
exports.info.file_stack.pop();
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports._require = function (name, args) {
|
|
||||||
exports.info.buf.push(exports.require(name, args));
|
|
||||||
}
|
|
|
@ -1,161 +0,0 @@
|
||||||
/**
|
|
||||||
* The API Handler handles all API http requests
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var fs = require("fs");
|
|
||||||
var api = require("../db/API");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
|
|
||||||
//ensure we have an apikey
|
|
||||||
var apikey = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
apikey = fs.readFileSync("./APIKEY.txt","utf8");
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
apikey = randomString(32);
|
|
||||||
fs.writeFileSync("./APIKEY.txt",apikey,"utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
//a list of all functions
|
|
||||||
var functions = {
|
|
||||||
"createGroup" : [],
|
|
||||||
"createGroupIfNotExistsFor" : ["groupMapper"],
|
|
||||||
"deleteGroup" : ["groupID"],
|
|
||||||
"listPads" : ["groupID"],
|
|
||||||
"createPad" : ["padID", "text"],
|
|
||||||
"createGroupPad" : ["groupID", "padName", "text"],
|
|
||||||
"createAuthor" : ["name"],
|
|
||||||
"createAuthorIfNotExistsFor": ["authorMapper" , "name"],
|
|
||||||
"createSession" : ["groupID", "authorID", "validUntil"],
|
|
||||||
"deleteSession" : ["sessionID"],
|
|
||||||
"getSessionInfo" : ["sessionID"],
|
|
||||||
"listSessionsOfGroup" : ["groupID"],
|
|
||||||
"listSessionsOfAuthor" : ["authorID"],
|
|
||||||
"getText" : ["padID", "rev"],
|
|
||||||
"setText" : ["padID", "text"],
|
|
||||||
"getHTML" : ["padID", "rev"],
|
|
||||||
"setHTML" : ["padID", "html"],
|
|
||||||
"getRevisionsCount" : ["padID"],
|
|
||||||
"deletePad" : ["padID"],
|
|
||||||
"getReadOnlyID" : ["padID"],
|
|
||||||
"setPublicStatus" : ["padID", "publicStatus"],
|
|
||||||
"getPublicStatus" : ["padID"],
|
|
||||||
"setPassword" : ["padID", "password"],
|
|
||||||
"isPasswordProtected" : ["padID"]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a HTTP API call
|
|
||||||
* @param functionName the name of the called function
|
|
||||||
* @param fields the params of the called function
|
|
||||||
* @req express request object
|
|
||||||
* @res express response object
|
|
||||||
*/
|
|
||||||
exports.handle = function(functionName, fields, req, res)
|
|
||||||
{
|
|
||||||
//check the api key!
|
|
||||||
if(fields["apikey"] != apikey.trim())
|
|
||||||
{
|
|
||||||
res.send({code: 4, message: "no or wrong API Key", data: null});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if this is a valid function name
|
|
||||||
var isKnownFunctionname = false;
|
|
||||||
for(var knownFunctionname in functions)
|
|
||||||
{
|
|
||||||
if(knownFunctionname == functionName)
|
|
||||||
{
|
|
||||||
isKnownFunctionname = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//say goodbye if this is a unkown function
|
|
||||||
if(!isKnownFunctionname)
|
|
||||||
{
|
|
||||||
res.send({code: 3, message: "no such function", data: null});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//sanitize any pad id's before continuing
|
|
||||||
if(fields["padID"])
|
|
||||||
{
|
|
||||||
padManager.sanitizePadId(fields["padID"], function(padId)
|
|
||||||
{
|
|
||||||
fields["padID"] = padId;
|
|
||||||
callAPI(functionName, fields, req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if(fields["padName"])
|
|
||||||
{
|
|
||||||
padManager.sanitizePadId(fields["padName"], function(padId)
|
|
||||||
{
|
|
||||||
fields["padName"] = padId;
|
|
||||||
callAPI(functionName, fields, req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callAPI(functionName, fields, req, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//calls the api function
|
|
||||||
function callAPI(functionName, fields, req, res)
|
|
||||||
{
|
|
||||||
//put the function parameters in an array
|
|
||||||
var functionParams = [];
|
|
||||||
for(var i=0;i<functions[functionName].length;i++)
|
|
||||||
{
|
|
||||||
functionParams.push(fields[functions[functionName][i]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//add a callback function to handle the response
|
|
||||||
functionParams.push(function(err, data)
|
|
||||||
{
|
|
||||||
// no error happend, everything is fine
|
|
||||||
if(err == null)
|
|
||||||
{
|
|
||||||
if(!data)
|
|
||||||
data = null;
|
|
||||||
|
|
||||||
res.send({code: 0, message: "ok", data: data});
|
|
||||||
}
|
|
||||||
// parameters were wrong and the api stopped execution, pass the error
|
|
||||||
else if(err.name == "apierror")
|
|
||||||
{
|
|
||||||
res.send({code: 1, message: err.message, data: null});
|
|
||||||
}
|
|
||||||
//an unkown error happend
|
|
||||||
else
|
|
||||||
{
|
|
||||||
res.send({code: 2, message: "internal error", data: null});
|
|
||||||
ERR(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//call the api function
|
|
||||||
api[functionName](functionParams[0],functionParams[1],functionParams[2],functionParams[3],functionParams[4]);
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
/**
|
|
||||||
* Handles the export requests
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var exporthtml = require("../utils/ExportHtml");
|
|
||||||
var exportdokuwiki = require("../utils/ExportDokuWiki");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var async = require("async");
|
|
||||||
var fs = require("fs");
|
|
||||||
var settings = require('../utils/Settings');
|
|
||||||
var os = require('os');
|
|
||||||
|
|
||||||
//load abiword only if its enabled
|
|
||||||
if(settings.abiword != null)
|
|
||||||
var abiword = require("../utils/Abiword");
|
|
||||||
|
|
||||||
var tempDirectory = "/tmp";
|
|
||||||
|
|
||||||
//tempDirectory changes if the operating system is windows
|
|
||||||
if(os.type().indexOf("Windows") > -1)
|
|
||||||
{
|
|
||||||
tempDirectory = process.env.TEMP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* do a requested export
|
|
||||||
*/
|
|
||||||
exports.doExport = function(req, res, padId, type)
|
|
||||||
{
|
|
||||||
//tell the browser that this is a downloadable file
|
|
||||||
res.attachment(padId + "." + type);
|
|
||||||
|
|
||||||
//if this is a plain text export, we can do this directly
|
|
||||||
if(type == "txt")
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, pad)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
if(req.params.rev){
|
|
||||||
pad.getInternalRevisionAText(req.params.rev, function(junk, text)
|
|
||||||
{
|
|
||||||
res.send(text.text ? text.text : null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
res.send(pad.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if(type == 'dokuwiki')
|
|
||||||
{
|
|
||||||
var randNum;
|
|
||||||
var srcFile, destFile;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//render the dokuwiki document
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki)
|
|
||||||
{
|
|
||||||
res.send(dokuwiki);
|
|
||||||
callback("stop");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(err && err != "stop") throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var html;
|
|
||||||
var randNum;
|
|
||||||
var srcFile, destFile;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//render the html document
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
exporthtml.getPadHTMLDocument(padId, req.params.rev, false, function(err, _html)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
html = _html;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//decide what to do with the html export
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//if this is a html export, we can send this from here directly
|
|
||||||
if(type == "html")
|
|
||||||
{
|
|
||||||
res.send(html);
|
|
||||||
callback("stop");
|
|
||||||
}
|
|
||||||
else //write the html export to a file
|
|
||||||
{
|
|
||||||
randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
|
||||||
srcFile = tempDirectory + "/eplite_export_" + randNum + ".html";
|
|
||||||
fs.writeFile(srcFile, html, callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
//send the convert job to abiword
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//ensure html can be collected by the garbage collector
|
|
||||||
html = null;
|
|
||||||
|
|
||||||
destFile = tempDirectory + "/eplite_export_" + randNum + "." + type;
|
|
||||||
abiword.convertFile(srcFile, destFile, type, callback);
|
|
||||||
},
|
|
||||||
//send the file
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
res.sendfile(destFile, null, callback);
|
|
||||||
},
|
|
||||||
//clean up temporary files
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
fs.unlink(srcFile, callback);
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//100ms delay to accomidate for slow windows fs
|
|
||||||
if(os.type().indexOf("Windows") > -1)
|
|
||||||
{
|
|
||||||
setTimeout(function()
|
|
||||||
{
|
|
||||||
fs.unlink(destFile, callback);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fs.unlink(destFile, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(err && err != "stop") ERR(err);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,201 +0,0 @@
|
||||||
/**
|
|
||||||
* Handles the import requests
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var padMessageHandler = require("./PadMessageHandler");
|
|
||||||
var async = require("async");
|
|
||||||
var fs = require("fs");
|
|
||||||
var settings = require('../utils/Settings');
|
|
||||||
var formidable = require('formidable');
|
|
||||||
var os = require("os");
|
|
||||||
|
|
||||||
//load abiword only if its enabled
|
|
||||||
if(settings.abiword != null)
|
|
||||||
var abiword = require("../utils/Abiword");
|
|
||||||
|
|
||||||
var tempDirectory = "/tmp/";
|
|
||||||
|
|
||||||
//tempDirectory changes if the operating system is windows
|
|
||||||
if(os.type().indexOf("Windows") > -1)
|
|
||||||
{
|
|
||||||
tempDirectory = process.env.TEMP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* do a requested import
|
|
||||||
*/
|
|
||||||
exports.doImport = function(req, res, padId)
|
|
||||||
{
|
|
||||||
//pipe to a file
|
|
||||||
//convert file to text via abiword
|
|
||||||
//set text in the pad
|
|
||||||
|
|
||||||
var srcFile, destFile;
|
|
||||||
var pad;
|
|
||||||
var text;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//save the uploaded file to /tmp
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var form = new formidable.IncomingForm();
|
|
||||||
form.keepExtensions = true;
|
|
||||||
form.uploadDir = tempDirectory;
|
|
||||||
|
|
||||||
form.parse(req, function(err, fields, files)
|
|
||||||
{
|
|
||||||
//the upload failed, stop at this point
|
|
||||||
if(err || files.file === undefined)
|
|
||||||
{
|
|
||||||
console.warn("Uploading Error: " + err.stack);
|
|
||||||
callback("uploadFailed");
|
|
||||||
}
|
|
||||||
//everything ok, continue
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//save the path of the uploaded file
|
|
||||||
srcFile = files.file.path;
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//ensure this is a file ending we know, else we change the file ending to .txt
|
|
||||||
//this allows us to accept source code files like .c or .java
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var fileEnding = (srcFile.split(".")[1] || "").toLowerCase();
|
|
||||||
var knownFileEndings = ["txt", "doc", "docx", "pdf", "odt", "html", "htm"];
|
|
||||||
|
|
||||||
//find out if this is a known file ending
|
|
||||||
var fileEndingKnown = false;
|
|
||||||
for(var i in knownFileEndings)
|
|
||||||
{
|
|
||||||
if(fileEnding == knownFileEndings[i])
|
|
||||||
{
|
|
||||||
fileEndingKnown = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//if the file ending is known, continue as normal
|
|
||||||
if(fileEndingKnown)
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
//we need to rename this file with a .txt ending
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var oldSrcFile = srcFile;
|
|
||||||
srcFile = srcFile.split(".")[0] + ".txt";
|
|
||||||
|
|
||||||
fs.rename(oldSrcFile, srcFile, callback);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//convert file to text
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
|
|
||||||
destFile = tempDirectory + "eplite_import_" + randNum + ".txt";
|
|
||||||
abiword.convertFile(srcFile, destFile, "txt", function(err){
|
|
||||||
//catch convert errors
|
|
||||||
if(err){
|
|
||||||
console.warn("Converting Error:", err);
|
|
||||||
return callback("convertFailed");
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//get the pad object
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//read the text
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
fs.readFile(destFile, "utf8", function(err, _text)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
text = _text;
|
|
||||||
|
|
||||||
//node on windows has a delay on releasing of the file lock.
|
|
||||||
//We add a 100ms delay to work around this
|
|
||||||
if(os.type().indexOf("Windows") > -1)
|
|
||||||
{
|
|
||||||
setTimeout(function()
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//change text of the pad and broadcast the changeset
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
pad.setText(text);
|
|
||||||
padMessageHandler.updatePadClients(pad, callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
//clean up temporary files
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
fs.unlink(srcFile, callback);
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
fs.unlink(destFile, callback);
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
var status = "ok";
|
|
||||||
|
|
||||||
//check for known errors and replace the status
|
|
||||||
if(err == "uploadFailed" || err == "convertFailed")
|
|
||||||
{
|
|
||||||
status = err;
|
|
||||||
err = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
//close the connection
|
|
||||||
res.send("<script type='text/javascript' src='/static/js/jquery.js'></script><script> if ( (!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf(\"1.8.\") == 0)) ){document.domain = document.domain;}var impexp = window.top.require('/pad_impexp').padimpexp.handleFrameCall('" + status + "');</script>", 200);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,957 +0,0 @@
|
||||||
/**
|
|
||||||
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var async = require("async");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
|
||||||
var AttributeManager = require("ep_etherpad-lite/static/js/AttributeManager");
|
|
||||||
var authorManager = require("../db/AuthorManager");
|
|
||||||
var readOnlyManager = require("../db/ReadOnlyManager");
|
|
||||||
var settings = require('../utils/Settings');
|
|
||||||
var securityManager = require("../db/SecurityManager");
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js");
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var messageLogger = log4js.getLogger("message");
|
|
||||||
var _ = require('underscore');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A associative array that translates a session to a pad
|
|
||||||
*/
|
|
||||||
var session2pad = {};
|
|
||||||
/**
|
|
||||||
* A associative array that saves which sessions belong to a pad
|
|
||||||
*/
|
|
||||||
var pad2sessions = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A associative array that saves some general informations about a session
|
|
||||||
* key = sessionId
|
|
||||||
* values = author, rev
|
|
||||||
* rev = That last revision that was send to this client
|
|
||||||
* author = the author name of this session
|
|
||||||
*/
|
|
||||||
var sessioninfos = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the Socket class we need to send and recieve data from the client
|
|
||||||
*/
|
|
||||||
var socketio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This Method is called by server.js to tell the message handler on which socket it should send
|
|
||||||
* @param socket_io The Socket
|
|
||||||
*/
|
|
||||||
exports.setSocketIO = function(socket_io)
|
|
||||||
{
|
|
||||||
socketio=socket_io;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the connection of a new user
|
|
||||||
* @param client the new client
|
|
||||||
*/
|
|
||||||
exports.handleConnect = function(client)
|
|
||||||
{
|
|
||||||
//Initalize session2pad and sessioninfos for this new session
|
|
||||||
session2pad[client.id]=null;
|
|
||||||
sessioninfos[client.id]={};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kicks all sessions from a pad
|
|
||||||
* @param client the new client
|
|
||||||
*/
|
|
||||||
exports.kickSessionsFromPad = function(padID)
|
|
||||||
{
|
|
||||||
//skip if there is nobody on this pad
|
|
||||||
if(!pad2sessions[padID])
|
|
||||||
return;
|
|
||||||
|
|
||||||
//disconnect everyone from this pad
|
|
||||||
for(var i in pad2sessions[padID])
|
|
||||||
{
|
|
||||||
socketio.sockets.sockets[pad2sessions[padID][i]].json.send({disconnect:"deleted"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the disconnection of a user
|
|
||||||
* @param client the client that leaves
|
|
||||||
*/
|
|
||||||
exports.handleDisconnect = function(client)
|
|
||||||
{
|
|
||||||
//save the padname of this session
|
|
||||||
var sessionPad=session2pad[client.id];
|
|
||||||
|
|
||||||
//if this connection was already etablished with a handshake, send a disconnect message to the others
|
|
||||||
if(sessioninfos[client.id] && sessioninfos[client.id].author)
|
|
||||||
{
|
|
||||||
var author = sessioninfos[client.id].author;
|
|
||||||
|
|
||||||
//get the author color out of the db
|
|
||||||
authorManager.getAuthorColorId(author, function(err, color)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
//prepare the notification for the other users on the pad, that this user left
|
|
||||||
var messageToTheOtherUsers = {
|
|
||||||
"type": "COLLABROOM",
|
|
||||||
"data": {
|
|
||||||
type: "USER_LEAVE",
|
|
||||||
userInfo: {
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"colorId": color,
|
|
||||||
"userAgent": "Anonymous",
|
|
||||||
"userId": author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Go trough all user that are still on the pad, and send them the USER_LEAVE message
|
|
||||||
for(i in pad2sessions[sessionPad])
|
|
||||||
{
|
|
||||||
var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]];
|
|
||||||
if(socket !== undefined){
|
|
||||||
socket.json.send(messageToTheOtherUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//Go trough all sessions of this pad, search and destroy the entry of this client
|
|
||||||
for(i in pad2sessions[sessionPad])
|
|
||||||
{
|
|
||||||
if(pad2sessions[sessionPad][i] == client.id)
|
|
||||||
{
|
|
||||||
pad2sessions[sessionPad].splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delete the session2pad and sessioninfos entrys of this session
|
|
||||||
delete session2pad[client.id];
|
|
||||||
delete sessioninfos[client.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a message from a user
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
exports.handleMessage = function(client, message)
|
|
||||||
{
|
|
||||||
if(message == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Message is null!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!message.type)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Message has no type attribute!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check what type of message we get and delegate to the other methodes
|
|
||||||
if(message.type == "CLIENT_READY")
|
|
||||||
{
|
|
||||||
handleClientReady(client, message);
|
|
||||||
}
|
|
||||||
else if(message.type == "COLLABROOM" && typeof message.data == 'object'){
|
|
||||||
if (message.data.type == "USER_CHANGES")
|
|
||||||
{
|
|
||||||
handleUserChanges(client, message);
|
|
||||||
}
|
|
||||||
else if (message.data.type == "USERINFO_UPDATE")
|
|
||||||
{
|
|
||||||
handleUserInfoUpdate(client, message);
|
|
||||||
}
|
|
||||||
else if(message.data.type == "CHAT_MESSAGE")
|
|
||||||
{
|
|
||||||
handleChatMessage(client, message);
|
|
||||||
}
|
|
||||||
else if(message.data.type == "CLIENT_MESSAGE" &&
|
|
||||||
typeof message.data.payload == 'object' &&
|
|
||||||
message.data.payload.type == "suggestUserName")
|
|
||||||
{
|
|
||||||
handleSuggestUserName(client, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//if the message type is unknown, throw an exception
|
|
||||||
else
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, unknown Message Type " + message.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a save revision message
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleSaveRevisionMessage(client, message){
|
|
||||||
var padId = session2pad[client.id];
|
|
||||||
var userId = sessioninfos[client.id].author;
|
|
||||||
|
|
||||||
padManager.getPad(padId, function(err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err)) return;
|
|
||||||
|
|
||||||
pad.addSavedRevision(pad.head, userId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a Chat Message
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleChatMessage(client, message)
|
|
||||||
{
|
|
||||||
var time = new Date().getTime();
|
|
||||||
var userId = sessioninfos[client.id].author;
|
|
||||||
var text = message.data.text;
|
|
||||||
var padId = session2pad[client.id];
|
|
||||||
|
|
||||||
var pad;
|
|
||||||
var userName;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthorName(userId, function(err, _userName)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
userName = _userName;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//save the chat message and broadcast it
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//save the chat message
|
|
||||||
pad.appendChatMessage(text, userId, time);
|
|
||||||
|
|
||||||
var msg = {
|
|
||||||
type: "COLLABROOM",
|
|
||||||
data: {
|
|
||||||
type: "CHAT_MESSAGE",
|
|
||||||
userId: userId,
|
|
||||||
userName: userName,
|
|
||||||
time: time,
|
|
||||||
text: text
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//broadcast the chat message to everyone on the pad
|
|
||||||
for(var i in pad2sessions[padId])
|
|
||||||
{
|
|
||||||
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a handleSuggestUserName, that means a user have suggest a userName for a other user
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleSuggestUserName(client, message)
|
|
||||||
{
|
|
||||||
//check if all ok
|
|
||||||
if(message.data.payload.newName == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, suggestUserName Message has no newName!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.payload.unnamedId == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var padId = session2pad[client.id];
|
|
||||||
|
|
||||||
//search the author and send him this message
|
|
||||||
for(var i in pad2sessions[padId])
|
|
||||||
{
|
|
||||||
if(sessioninfos[pad2sessions[padId][i]].author == message.data.payload.unnamedId)
|
|
||||||
{
|
|
||||||
socketio.sockets.sockets[pad2sessions[padId][i]].send(message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleUserInfoUpdate(client, message)
|
|
||||||
{
|
|
||||||
//check if all ok
|
|
||||||
if(message.data.userInfo.colorId == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Find out the author name of this session
|
|
||||||
var author = sessioninfos[client.id].author;
|
|
||||||
|
|
||||||
//Tell the authorManager about the new attributes
|
|
||||||
authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
|
|
||||||
authorManager.setAuthorName(author, message.data.userInfo.name);
|
|
||||||
|
|
||||||
var padId = session2pad[client.id];
|
|
||||||
|
|
||||||
//set a null name, when there is no name set. cause the client wants it null
|
|
||||||
if(message.data.userInfo.name == null)
|
|
||||||
{
|
|
||||||
message.data.userInfo.name = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//The Client don't know about a USERINFO_UPDATE, it can handle only new user_newinfo, so change the message type
|
|
||||||
message.data.type = "USER_NEWINFO";
|
|
||||||
|
|
||||||
//Send the other clients on the pad the update message
|
|
||||||
for(var i in pad2sessions[padId])
|
|
||||||
{
|
|
||||||
if(pad2sessions[padId][i] != client.id)
|
|
||||||
{
|
|
||||||
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
|
|
||||||
* This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
|
|
||||||
* Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleUserChanges(client, message)
|
|
||||||
{
|
|
||||||
//check if all ok
|
|
||||||
if(message.data.baseRev == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.apool == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.changeset == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all Vars we need
|
|
||||||
var baseRev = message.data.baseRev;
|
|
||||||
var wireApool = (new AttributePool()).fromJsonable(message.data.apool);
|
|
||||||
var changeset = message.data.changeset;
|
|
||||||
|
|
||||||
var r, apool, pad;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(session2pad[client.id], function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//create the changeset
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//ex. _checkChangesetAndPool
|
|
||||||
|
|
||||||
//Copied from Etherpad, don't know what it does exactly
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//this looks like a changeset check, it throws errors sometimes
|
|
||||||
Changeset.checkRep(changeset);
|
|
||||||
|
|
||||||
Changeset.eachAttribNumber(changeset, function(n) {
|
|
||||||
if (! wireApool.getAttrib(n)) {
|
|
||||||
throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//there is an error in this changeset, so just refuse it
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep");
|
|
||||||
client.json.send({disconnect:"badChangeset"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//ex. adoptChangesetAttribs
|
|
||||||
|
|
||||||
//Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
|
||||||
changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
|
|
||||||
|
|
||||||
//ex. applyUserChanges
|
|
||||||
apool = pad.pool;
|
|
||||||
r = baseRev;
|
|
||||||
|
|
||||||
//https://github.com/caolan/async#whilst
|
|
||||||
async.whilst(
|
|
||||||
function() { return r < pad.getHeadRevisionNumber(); },
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
r++;
|
|
||||||
|
|
||||||
pad.getRevisionChangeset(r, function(err, c)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
changeset = Changeset.follow(c, changeset, false, apool);
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//use the callback of the series function
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
},
|
|
||||||
//do correction changesets, and send it to all users
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
var prevText = pad.text();
|
|
||||||
|
|
||||||
if (Changeset.oldLen(changeset) != prevText.length)
|
|
||||||
{
|
|
||||||
console.warn("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length);
|
|
||||||
client.json.send({disconnect:"badChangeset"});
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var thisAuthor = sessioninfos[client.id].author;
|
|
||||||
|
|
||||||
pad.appendRevision(changeset, thisAuthor);
|
|
||||||
|
|
||||||
var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
|
|
||||||
if (correctionChangeset) {
|
|
||||||
pad.appendRevision(correctionChangeset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
|
|
||||||
var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n");
|
|
||||||
pad.appendRevision(nlChangeset);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updatePadClients(pad, callback);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updatePadClients = function(pad, callback)
|
|
||||||
{
|
|
||||||
//skip this step if noone is on this pad
|
|
||||||
if(!pad2sessions[pad.id])
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//go trough all sessions on this pad
|
|
||||||
async.forEach(pad2sessions[pad.id], function(session, callback)
|
|
||||||
{
|
|
||||||
var lastRev = sessioninfos[session].rev;
|
|
||||||
|
|
||||||
//https://github.com/caolan/async#whilst
|
|
||||||
//send them all new changesets
|
|
||||||
async.whilst(
|
|
||||||
function (){ return lastRev < pad.getHeadRevisionNumber()},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var author, revChangeset;
|
|
||||||
|
|
||||||
var r = ++lastRev;
|
|
||||||
|
|
||||||
async.parallel([
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
pad.getRevisionAuthor(r, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
author = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
pad.getRevisionChangeset(r, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
revChangeset = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
// next if session has not been deleted
|
|
||||||
if(sessioninfos[session] == null)
|
|
||||||
{
|
|
||||||
callback(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(author == sessioninfos[session].author)
|
|
||||||
{
|
|
||||||
socketio.sockets.sockets[session].json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var forWire = Changeset.prepareForWire(revChangeset, pad.pool);
|
|
||||||
var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r,
|
|
||||||
changeset: forWire.translated,
|
|
||||||
apool: forWire.pool,
|
|
||||||
author: author}};
|
|
||||||
|
|
||||||
socketio.sockets.sockets[session].json.send(wireMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
|
|
||||||
if(sessioninfos[session] != null)
|
|
||||||
{
|
|
||||||
sessioninfos[session].rev = pad.getHeadRevisionNumber();
|
|
||||||
}
|
|
||||||
},callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copied from the Etherpad Source Code. Don't know what this methode does excatly...
|
|
||||||
*/
|
|
||||||
function _correctMarkersInPad(atext, apool) {
|
|
||||||
var text = atext.text;
|
|
||||||
|
|
||||||
// collect char positions of line markers (e.g. bullets) in new atext
|
|
||||||
// that aren't at the start of a line
|
|
||||||
var badMarkers = [];
|
|
||||||
var iter = Changeset.opIterator(atext.attribs);
|
|
||||||
var offset = 0;
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
var op = iter.next();
|
|
||||||
|
|
||||||
var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){
|
|
||||||
return Changeset.opAttributeValue(op, attribute, apool);
|
|
||||||
}) !== undefined;
|
|
||||||
|
|
||||||
if (hasMarker) {
|
|
||||||
for(var i=0;i<op.chars;i++) {
|
|
||||||
if (offset > 0 && text.charAt(offset-1) != '\n') {
|
|
||||||
badMarkers.push(offset);
|
|
||||||
}
|
|
||||||
offset++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
offset += op.chars;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (badMarkers.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create changeset that removes these bad markers
|
|
||||||
offset = 0;
|
|
||||||
var builder = Changeset.builder(text.length);
|
|
||||||
badMarkers.forEach(function(pos) {
|
|
||||||
builder.keepText(text.substring(offset, pos));
|
|
||||||
builder.remove(1);
|
|
||||||
offset = pos+1;
|
|
||||||
});
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
|
|
||||||
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
function handleClientReady(client, message)
|
|
||||||
{
|
|
||||||
//check if all ok
|
|
||||||
if(!message.token)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, CLIENT_READY Message has no token!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!message.padId)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, CLIENT_READY Message has no padId!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(!message.protocolVersion)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, CLIENT_READY Message has no protocolVersion!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.protocolVersion != 2)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, CLIENT_READY Message has a unknown protocolVersion '" + message.protocolVersion + "'!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var author;
|
|
||||||
var authorName;
|
|
||||||
var authorColorId;
|
|
||||||
var pad;
|
|
||||||
var historicalAuthorData = {};
|
|
||||||
var readOnlyId;
|
|
||||||
var chatMessages;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//check permissions
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//access was granted
|
|
||||||
if(statusObject.accessStatus == "grant")
|
|
||||||
{
|
|
||||||
author = statusObject.authorID;
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
//no access, send the client a message that tell him why
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.json.send({accessStatus: statusObject.accessStatus})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get all authordata of this new user
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
//get colorId
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthorColorId(author, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
authorColorId = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get author name
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthorName(author, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
authorName = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(message.padId, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
readOnlyManager.getReadOnlyId(message.padId, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
readOnlyId = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//these db requests all need the pad object
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var authors = pad.getAllAuthors();
|
|
||||||
|
|
||||||
async.parallel([
|
|
||||||
//get all author data out of the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.forEach(authors, function(authorId, callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthor(authorId, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
delete author.timestamp;
|
|
||||||
historicalAuthorData[authorId] = author;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
},
|
|
||||||
//get the latest chat messages
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
pad.getLastChatMessages(100, function(err, _chatMessages)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
chatMessages = _chatMessages;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//Check if this author is already on the pad, if yes, kick the other sessions!
|
|
||||||
if(pad2sessions[message.padId])
|
|
||||||
{
|
|
||||||
for(var i in pad2sessions[message.padId])
|
|
||||||
{
|
|
||||||
if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author)
|
|
||||||
{
|
|
||||||
var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]];
|
|
||||||
if(socket) socket.json.send({disconnect:"userdup"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Save in session2pad that this session belonges to this pad
|
|
||||||
var sessionId=String(client.id);
|
|
||||||
session2pad[sessionId] = message.padId;
|
|
||||||
|
|
||||||
//check if there is already a pad2sessions entry, if not, create one
|
|
||||||
if(!pad2sessions[message.padId])
|
|
||||||
{
|
|
||||||
pad2sessions[message.padId] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
//Saves in pad2sessions that this session belongs to this pad
|
|
||||||
pad2sessions[message.padId].push(sessionId);
|
|
||||||
|
|
||||||
//prepare all values for the wire
|
|
||||||
var atext = Changeset.cloneAText(pad.atext);
|
|
||||||
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
|
||||||
var apool = attribsForWire.pool.toJsonable();
|
|
||||||
atext.attribs = attribsForWire.translated;
|
|
||||||
|
|
||||||
var clientVars = {
|
|
||||||
"accountPrivs": {
|
|
||||||
"maxRevisions": 100
|
|
||||||
},
|
|
||||||
"initialRevisionList": [],
|
|
||||||
"initialOptions": {
|
|
||||||
"guestPolicy": "deny"
|
|
||||||
},
|
|
||||||
"collab_client_vars": {
|
|
||||||
"initialAttributedText": atext,
|
|
||||||
"clientIp": "127.0.0.1",
|
|
||||||
//"clientAgent": "Anonymous Agent",
|
|
||||||
"padId": message.padId,
|
|
||||||
"historicalAuthorData": historicalAuthorData,
|
|
||||||
"apool": apool,
|
|
||||||
"rev": pad.getHeadRevisionNumber(),
|
|
||||||
"globalPadId": message.padId
|
|
||||||
},
|
|
||||||
"colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"],
|
|
||||||
"clientIp": "127.0.0.1",
|
|
||||||
"userIsGuest": true,
|
|
||||||
"userColor": authorColorId,
|
|
||||||
"padId": message.padId,
|
|
||||||
"initialTitle": "Pad: " + message.padId,
|
|
||||||
"opts": {},
|
|
||||||
"chatHistory": chatMessages,
|
|
||||||
"numConnectedUsers": pad2sessions[message.padId].length,
|
|
||||||
"isProPad": false,
|
|
||||||
"readOnlyId": readOnlyId,
|
|
||||||
"serverTimestamp": new Date().getTime(),
|
|
||||||
"globalPadId": message.padId,
|
|
||||||
"userId": author,
|
|
||||||
"cookiePrefsToSet": {
|
|
||||||
"fullWidth": false,
|
|
||||||
"hideSidebar": false
|
|
||||||
},
|
|
||||||
"abiwordAvailable": settings.abiwordAvailable(),
|
|
||||||
"plugins": {
|
|
||||||
"plugins": plugins.plugins,
|
|
||||||
"parts": plugins.parts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add a username to the clientVars if one avaiable
|
|
||||||
if(authorName != null)
|
|
||||||
{
|
|
||||||
clientVars.userName = authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sessioninfos[client.id] !== undefined)
|
|
||||||
{
|
|
||||||
//This is a reconnect, so we don't have to send the client the ClientVars again
|
|
||||||
if(message.reconnect == true)
|
|
||||||
{
|
|
||||||
//Save the revision in sessioninfos, we take the revision from the info the client send to us
|
|
||||||
sessioninfos[client.id].rev = message.client_rev;
|
|
||||||
}
|
|
||||||
//This is a normal first connect
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//Send the clientVars to the Client
|
|
||||||
client.json.send(clientVars);
|
|
||||||
//Save the revision in sessioninfos
|
|
||||||
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Save the revision and the author id in sessioninfos
|
|
||||||
sessioninfos[client.id].author = author;
|
|
||||||
}
|
|
||||||
|
|
||||||
//prepare the notification for the other users on the pad, that this user joined
|
|
||||||
var messageToTheOtherUsers = {
|
|
||||||
"type": "COLLABROOM",
|
|
||||||
"data": {
|
|
||||||
type: "USER_NEWINFO",
|
|
||||||
userInfo: {
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"colorId": authorColorId,
|
|
||||||
"userAgent": "Anonymous",
|
|
||||||
"userId": author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Add the authorname of this new User, if avaiable
|
|
||||||
if(authorName != null)
|
|
||||||
{
|
|
||||||
messageToTheOtherUsers.data.userInfo.name = authorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Run trough all sessions of this pad
|
|
||||||
async.forEach(pad2sessions[message.padId], function(sessionID, callback)
|
|
||||||
{
|
|
||||||
var author, socket, sessionAuthorName, sessionAuthorColorId;
|
|
||||||
|
|
||||||
//Since sessioninfos might change while being enumerated, check if the
|
|
||||||
//sessionID is still assigned to a valid session
|
|
||||||
if(sessioninfos[sessionID] !== undefined &&
|
|
||||||
socketio.sockets.sockets[sessionID] !== undefined){
|
|
||||||
author = sessioninfos[sessionID].author;
|
|
||||||
socket = socketio.sockets.sockets[sessionID];
|
|
||||||
}else {
|
|
||||||
// If the sessionID is not valid, callback();
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
async.series([
|
|
||||||
//get the authorname & colorId
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
async.parallel([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthorColorId(author, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
sessionAuthorColorId = value;
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthorName(author, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
sessionAuthorName = value;
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
],callback);
|
|
||||||
},
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
//Jump over, if this session is the connection session
|
|
||||||
if(sessionID != client.id)
|
|
||||||
{
|
|
||||||
//Send this Session the Notification about the new user
|
|
||||||
socket.json.send(messageToTheOtherUsers);
|
|
||||||
|
|
||||||
//Send the new User a Notification about this other user
|
|
||||||
var messageToNotifyTheClientAboutTheOthers = {
|
|
||||||
"type": "COLLABROOM",
|
|
||||||
"data": {
|
|
||||||
type: "USER_NEWINFO",
|
|
||||||
userInfo: {
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"colorId": sessionAuthorColorId,
|
|
||||||
"name": sessionAuthorName,
|
|
||||||
"userAgent": "Anonymous",
|
|
||||||
"userId": author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.json.send(messageToNotifyTheClientAboutTheOthers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
],function(err)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
/**
|
|
||||||
* This is the Socket.IO Router. It routes the Messages between the
|
|
||||||
* components of the Server. The components are at the moment: pad and timeslider
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var messageLogger = log4js.getLogger("message");
|
|
||||||
var securityManager = require("../db/SecurityManager");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves all components
|
|
||||||
* key is the component name
|
|
||||||
* value is the component module
|
|
||||||
*/
|
|
||||||
var components = {};
|
|
||||||
|
|
||||||
var socket;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* adds a component
|
|
||||||
*/
|
|
||||||
exports.addComponent = function(moduleName, module)
|
|
||||||
{
|
|
||||||
//save the component
|
|
||||||
components[moduleName] = module;
|
|
||||||
|
|
||||||
//give the module the socket
|
|
||||||
module.setSocketIO(socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sets the socket.io and adds event functions for routing
|
|
||||||
*/
|
|
||||||
exports.setSocketIO = function(_socket)
|
|
||||||
{
|
|
||||||
//save this socket internaly
|
|
||||||
socket = _socket;
|
|
||||||
|
|
||||||
socket.sockets.on('connection', function(client)
|
|
||||||
{
|
|
||||||
var clientAuthorized = false;
|
|
||||||
|
|
||||||
//wrap the original send function to log the messages
|
|
||||||
client._send = client.send;
|
|
||||||
client.send = function(message)
|
|
||||||
{
|
|
||||||
messageLogger.info("to " + client.id + ": " + stringifyWithoutPassword(message));
|
|
||||||
client._send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
//tell all components about this connect
|
|
||||||
for(var i in components)
|
|
||||||
{
|
|
||||||
components[i].handleConnect(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
//try to handle the message of this client
|
|
||||||
function handleMessage(message)
|
|
||||||
{
|
|
||||||
if(message.component && components[message.component])
|
|
||||||
{
|
|
||||||
//check if component is registered in the components array
|
|
||||||
if(components[message.component])
|
|
||||||
{
|
|
||||||
messageLogger.info("from " + client.id + ": " + stringifyWithoutPassword(message));
|
|
||||||
components[message.component].handleMessage(client, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on('message', function(message)
|
|
||||||
{
|
|
||||||
if(message.protocolVersion && message.protocolVersion != 2)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//client is authorized, everything ok
|
|
||||||
if(clientAuthorized)
|
|
||||||
{
|
|
||||||
handleMessage(message);
|
|
||||||
}
|
|
||||||
//try to authorize the client
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//this message has everything to try an authorization
|
|
||||||
if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined)
|
|
||||||
{
|
|
||||||
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
//access was granted, mark the client as authorized and handle the message
|
|
||||||
if(statusObject.accessStatus == "grant")
|
|
||||||
{
|
|
||||||
clientAuthorized = true;
|
|
||||||
handleMessage(message);
|
|
||||||
}
|
|
||||||
//no access, send the client a message that tell him why
|
|
||||||
else
|
|
||||||
{
|
|
||||||
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
|
|
||||||
client.json.send({accessStatus: statusObject.accessStatus});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//drop message
|
|
||||||
else
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('disconnect', function()
|
|
||||||
{
|
|
||||||
//tell all components about this disconnect
|
|
||||||
for(var i in components)
|
|
||||||
{
|
|
||||||
components[i].handleDisconnect(client);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns a stringified representation of a message, removes the password
|
|
||||||
//this ensures there are no passwords in the log
|
|
||||||
function stringifyWithoutPassword(message)
|
|
||||||
{
|
|
||||||
var newMessage = {};
|
|
||||||
|
|
||||||
for(var i in message)
|
|
||||||
{
|
|
||||||
if(i == "password" && message[i] != null)
|
|
||||||
newMessage["password"] = "xxx";
|
|
||||||
else
|
|
||||||
newMessage[i]=message[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(newMessage);
|
|
||||||
}
|
|
|
@ -1,534 +0,0 @@
|
||||||
/**
|
|
||||||
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var async = require("async");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
|
|
||||||
var settings = require('../utils/Settings');
|
|
||||||
var authorManager = require("../db/AuthorManager");
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var messageLogger = log4js.getLogger("message");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the Socket class we need to send and recieve data from the client
|
|
||||||
*/
|
|
||||||
var socketio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This Method is called by server.js to tell the message handler on which socket it should send
|
|
||||||
* @param socket_io The Socket
|
|
||||||
*/
|
|
||||||
exports.setSocketIO = function(socket_io)
|
|
||||||
{
|
|
||||||
socketio=socket_io;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the connection of a new user
|
|
||||||
* @param client the new client
|
|
||||||
*/
|
|
||||||
exports.handleConnect = function(client)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the disconnection of a user
|
|
||||||
* @param client the client that leaves
|
|
||||||
*/
|
|
||||||
exports.handleDisconnect = function(client)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a message from a user
|
|
||||||
* @param client the client that send this message
|
|
||||||
* @param message the message from the client
|
|
||||||
*/
|
|
||||||
exports.handleMessage = function(client, message)
|
|
||||||
{
|
|
||||||
//Check what type of message we get and delegate to the other methodes
|
|
||||||
if(message.type == "CLIENT_READY")
|
|
||||||
{
|
|
||||||
handleClientReady(client, message);
|
|
||||||
}
|
|
||||||
else if(message.type == "CHANGESET_REQ")
|
|
||||||
{
|
|
||||||
handleChangesetRequest(client, message);
|
|
||||||
}
|
|
||||||
//if the message type is unkown, throw an exception
|
|
||||||
else
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, unknown Message Type: '" + message.type + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClientReady(client, message)
|
|
||||||
{
|
|
||||||
if(message.padId == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no padId!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//send the timeslider client the clientVars, with this values its able to start
|
|
||||||
createTimesliderClientVars (message.padId, function(err, clientVars)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
client.json.send({type: "CLIENT_VARS", data: clientVars});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a request for a rough changeset, the timeslider client needs it
|
|
||||||
*/
|
|
||||||
function handleChangesetRequest(client, message)
|
|
||||||
{
|
|
||||||
//check if all ok
|
|
||||||
if(message.data == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no data!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.padId == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no padId!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.granularity == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no granularity!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.start == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no start!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(message.data.requestID == null)
|
|
||||||
{
|
|
||||||
messageLogger.warn("Dropped message, changeset request has no requestID!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var granularity = message.data.granularity;
|
|
||||||
var start = message.data.start;
|
|
||||||
var end = start + (100 * granularity);
|
|
||||||
var padId = message.padId;
|
|
||||||
|
|
||||||
//build the requested rough changesets and send them back
|
|
||||||
getChangesetInfo(padId, start, end, granularity, function(err, changesetInfo)
|
|
||||||
{
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
var data = changesetInfo;
|
|
||||||
data.requestID = message.data.requestID;
|
|
||||||
|
|
||||||
client.json.send({type: "CHANGESET_REQ", data: data});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTimesliderClientVars (padId, callback)
|
|
||||||
{
|
|
||||||
var clientVars = {
|
|
||||||
viewId: padId,
|
|
||||||
colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"],
|
|
||||||
savedRevisions: [],
|
|
||||||
padIdForUrl: padId,
|
|
||||||
fullWidth: false,
|
|
||||||
disableRightBar: false,
|
|
||||||
initialChangesets: [],
|
|
||||||
abiwordAvailable: settings.abiwordAvailable(),
|
|
||||||
hooks: [],
|
|
||||||
initialStyledContents: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
var pad;
|
|
||||||
var initialChangesets = [];
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get all saved revisions and add them
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
clientVars.savedRevisions = pad.getSavedRevisions();
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
//get all authors and add them to
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var historicalAuthorData = {};
|
|
||||||
//get all authors out of the attribut pool
|
|
||||||
var authors = pad.getAllAuthors();
|
|
||||||
|
|
||||||
//get all author data out of the database
|
|
||||||
async.forEach(authors, function(authorId, callback)
|
|
||||||
{
|
|
||||||
authorManager.getAuthor(authorId, function(err, author)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
historicalAuthorData[authorId] = author;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
//add historicalAuthorData to the clientVars and continue
|
|
||||||
clientVars.historicalAuthorData = historicalAuthorData;
|
|
||||||
clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the timestamp of the last revision
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
clientVars.currentTime = date;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get the head revision Number
|
|
||||||
var lastRev = pad.getHeadRevisionNumber();
|
|
||||||
|
|
||||||
//add the revNum to the client Vars
|
|
||||||
clientVars.revNum = lastRev;
|
|
||||||
clientVars.totalRevs = lastRev;
|
|
||||||
|
|
||||||
var atext = Changeset.cloneAText(pad.atext);
|
|
||||||
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
|
||||||
var apool = attribsForWire.pool.toJsonable();
|
|
||||||
atext.attribs = attribsForWire.translated;
|
|
||||||
|
|
||||||
clientVars.initialStyledContents.apool = apool;
|
|
||||||
clientVars.initialStyledContents.atext = atext;
|
|
||||||
|
|
||||||
var granularities = [100, 10, 1];
|
|
||||||
|
|
||||||
//get the latest rough changesets
|
|
||||||
async.forEach(granularities, function(granularity, callback)
|
|
||||||
{
|
|
||||||
var topGranularity = granularity*10;
|
|
||||||
|
|
||||||
getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity,
|
|
||||||
Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity,
|
|
||||||
function(err, changeset)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
clientVars.initialChangesets.push(changeset);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, clientVars);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to rebuild the getChangestInfo function of the original Etherpad
|
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
|
|
||||||
*/
|
|
||||||
function getChangesetInfo(padId, startNum, endNum, granularity, callback)
|
|
||||||
{
|
|
||||||
var forwardsChangesets = [];
|
|
||||||
var backwardsChangesets = [];
|
|
||||||
var timeDeltas = [];
|
|
||||||
var apool = new AttributePool();
|
|
||||||
var pad;
|
|
||||||
var composedChangesets = {};
|
|
||||||
var revisionDate = [];
|
|
||||||
var lines;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//calculate the last full endnum
|
|
||||||
var lastRev = pad.getHeadRevisionNumber();
|
|
||||||
if (endNum > lastRev+1) {
|
|
||||||
endNum = lastRev+1;
|
|
||||||
}
|
|
||||||
endNum = Math.floor(endNum / granularity)*granularity;
|
|
||||||
|
|
||||||
var compositesChangesetNeeded = [];
|
|
||||||
var revTimesNeeded = [];
|
|
||||||
|
|
||||||
//figure out which composite Changeset and revTimes we need, to load them in bulk
|
|
||||||
var compositeStart = startNum;
|
|
||||||
while (compositeStart < endNum)
|
|
||||||
{
|
|
||||||
var compositeEnd = compositeStart + granularity;
|
|
||||||
|
|
||||||
//add the composite Changeset we needed
|
|
||||||
compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd});
|
|
||||||
|
|
||||||
//add the t1 time we need
|
|
||||||
revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1);
|
|
||||||
//add the t2 time we need
|
|
||||||
revTimesNeeded.push(compositeEnd - 1);
|
|
||||||
|
|
||||||
compositeStart += granularity;
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all needed db values parallel
|
|
||||||
async.parallel([
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get all needed composite Changesets
|
|
||||||
async.forEach(compositesChangesetNeeded, function(item, callback)
|
|
||||||
{
|
|
||||||
composePadChangesets(padId, item.start, item.end, function(err, changeset)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
composedChangesets[item.start + "/" + item.end] = changeset;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//get all needed revision Dates
|
|
||||||
async.forEach(revTimesNeeded, function(revNum, callback)
|
|
||||||
{
|
|
||||||
pad.getRevisionDate(revNum, function(err, revDate)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
revisionDate[revNum] = Math.floor(revDate/1000);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
},
|
|
||||||
//get the lines
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
getPadLines(padId, startNum-1, function(err, _lines)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
lines = _lines;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
},
|
|
||||||
//doesn't know what happens here excatly :/
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var compositeStart = startNum;
|
|
||||||
|
|
||||||
while (compositeStart < endNum)
|
|
||||||
{
|
|
||||||
if (compositeStart + granularity > endNum)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var compositeEnd = compositeStart + granularity;
|
|
||||||
|
|
||||||
var forwards = composedChangesets[compositeStart + "/" + compositeEnd];
|
|
||||||
var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
|
|
||||||
|
|
||||||
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
|
|
||||||
Changeset.mutateTextLines(forwards, lines.textlines);
|
|
||||||
|
|
||||||
var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
|
|
||||||
var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
|
|
||||||
|
|
||||||
var t1, t2;
|
|
||||||
if (compositeStart == 0)
|
|
||||||
{
|
|
||||||
t1 = revisionDate[0];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
t1 = revisionDate[compositeStart - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
t2 = revisionDate[compositeEnd - 1];
|
|
||||||
|
|
||||||
timeDeltas.push(t2 - t1);
|
|
||||||
forwardsChangesets.push(forwards2);
|
|
||||||
backwardsChangesets.push(backwards2);
|
|
||||||
|
|
||||||
compositeStart += granularity;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
callback(null, {forwardsChangesets: forwardsChangesets,
|
|
||||||
backwardsChangesets: backwardsChangesets,
|
|
||||||
apool: apool.toJsonable(),
|
|
||||||
actualEndNum: endNum,
|
|
||||||
timeDeltas: timeDeltas,
|
|
||||||
start: startNum,
|
|
||||||
granularity: granularity });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to rebuild the getPadLines function of the original Etherpad
|
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
|
|
||||||
*/
|
|
||||||
function getPadLines(padId, revNum, callback)
|
|
||||||
{
|
|
||||||
var atext;
|
|
||||||
var result = {};
|
|
||||||
var pad;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//get the atext
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
if(revNum >= 0)
|
|
||||||
{
|
|
||||||
pad.getInternalRevisionAText(revNum, function(err, _atext)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
atext = _atext;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
atext = Changeset.makeAText("\n");
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
result.textlines = Changeset.splitTextLines(atext.text);
|
|
||||||
result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to rebuild the composePadChangeset function of the original Etherpad
|
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
|
|
||||||
*/
|
|
||||||
function composePadChangesets(padId, startNum, endNum, callback)
|
|
||||||
{
|
|
||||||
var pad;
|
|
||||||
var changesets = [];
|
|
||||||
var changeset;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//get the pad from the database
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function(err, _pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
pad = _pad;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//fetch all changesets we need
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
var changesetsNeeded=[];
|
|
||||||
|
|
||||||
//create a array for all changesets, we will
|
|
||||||
//replace the values with the changeset later
|
|
||||||
for(var r=startNum;r<endNum;r++)
|
|
||||||
{
|
|
||||||
changesetsNeeded.push(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
//get all changesets
|
|
||||||
async.forEach(changesetsNeeded, function(revNum,callback)
|
|
||||||
{
|
|
||||||
pad.getRevisionChangeset(revNum, function(err, value)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
changesets[revNum] = value;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},callback);
|
|
||||||
},
|
|
||||||
//compose Changesets
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
changeset = changesets[startNum];
|
|
||||||
var pool = pad.apool();
|
|
||||||
|
|
||||||
for(var r=startNum+1;r<endNum;r++)
|
|
||||||
{
|
|
||||||
var cs = changesets[r];
|
|
||||||
changeset = Changeset.compose(changeset, cs, pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
//return err and changeset
|
|
||||||
function(err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, changeset);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var eejs = require('ep_etherpad-lite/node/eejs');
|
|
||||||
var installer = require('ep_etherpad-lite/static/js/pluginfw/installer');
|
|
||||||
var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
args.app.get('/admin/plugins', function(req, res) {
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
|
||||||
var render_args = {
|
|
||||||
plugins: plugins.plugins,
|
|
||||||
search_results: {},
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
res.send(eejs.require(
|
|
||||||
"ep_etherpad-lite/templates/admin/plugins.html",
|
|
||||||
render_args), {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.socketio = function (hook_name, args, cb) {
|
|
||||||
var io = args.io.of("/pluginfw/installer");
|
|
||||||
io.on('connection', function (socket) {
|
|
||||||
if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return;
|
|
||||||
|
|
||||||
socket.on("load", function (query) {
|
|
||||||
socket.emit("installed-results", {results: plugins.plugins});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("search", function (query) {
|
|
||||||
socket.emit("progress", {progress:0, message:'Fetching results...'});
|
|
||||||
installer.search(query, true, function (progress) {
|
|
||||||
if (progress.results)
|
|
||||||
socket.emit("search-result", progress);
|
|
||||||
socket.emit("progress", progress);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("install", function (plugin_name) {
|
|
||||||
socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."});
|
|
||||||
installer.install(plugin_name, function (progress) {
|
|
||||||
socket.emit("progress", progress);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("uninstall", function (plugin_name) {
|
|
||||||
socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."});
|
|
||||||
installer.uninstall(plugin_name, function (progress) {
|
|
||||||
socket.emit("progress", progress);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
var log4js = require('log4js');
|
|
||||||
var apiLogger = log4js.getLogger("API");
|
|
||||||
var formidable = require('formidable');
|
|
||||||
var apiHandler = require('../../handler/APIHandler');
|
|
||||||
|
|
||||||
//This is for making an api call, collecting all post information and passing it to the apiHandler
|
|
||||||
var apiCaller = function(req, res, fields) {
|
|
||||||
res.header("Content-Type", "application/json; charset=utf-8");
|
|
||||||
|
|
||||||
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
|
|
||||||
|
|
||||||
//wrap the send function so we can log the response
|
|
||||||
//note: res._send seems to be already in use, so better use a "unique" name
|
|
||||||
res._____send = res.send;
|
|
||||||
res.send = function (response) {
|
|
||||||
response = JSON.stringify(response);
|
|
||||||
apiLogger.info("RESPONSE, " + req.params.func + ", " + response);
|
|
||||||
|
|
||||||
//is this a jsonp call, if yes, add the function call
|
|
||||||
if(req.query.jsonp)
|
|
||||||
response = req.query.jsonp + "(" + response + ")";
|
|
||||||
|
|
||||||
res._____send(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
//call the api handler
|
|
||||||
apiHandler.handle(req.params.func, fields, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.apiCaller = apiCaller;
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
//This is a api GET call, collect all post informations and pass it to the apiHandler
|
|
||||||
args.app.get('/api/1/:func', function (req, res) {
|
|
||||||
apiCaller(req, res, req.query)
|
|
||||||
});
|
|
||||||
|
|
||||||
//This is a api POST call, collect all post informations and pass it to the apiHandler
|
|
||||||
args.app.post('/api/1/:func', function(req, res) {
|
|
||||||
new formidable.IncomingForm().parse(req, function (err, fields, files) {
|
|
||||||
apiCaller(req, res, fields)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//The Etherpad client side sends information about how a disconnect happen
|
|
||||||
args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) {
|
|
||||||
new formidable.IncomingForm().parse(req, function(err, fields, files) {
|
|
||||||
console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
|
|
||||||
res.end("OK");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//The Etherpad client side sends information about client side javscript errors
|
|
||||||
args.app.post('/jserror', function(req, res) {
|
|
||||||
new formidable.IncomingForm().parse(req, function(err, fields, files) {
|
|
||||||
console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
|
|
||||||
res.end("OK");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
var os = require("os");
|
|
||||||
var db = require('../../db/DB');
|
|
||||||
|
|
||||||
|
|
||||||
exports.onShutdown = false;
|
|
||||||
exports.gracefulShutdown = function(err) {
|
|
||||||
if(err && err.stack) {
|
|
||||||
console.error(err.stack);
|
|
||||||
} else if(err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
//ensure there is only one graceful shutdown running
|
|
||||||
if(exports.onShutdown) return;
|
|
||||||
exports.onShutdown = true;
|
|
||||||
|
|
||||||
console.log("graceful shutdown...");
|
|
||||||
|
|
||||||
//stop the http server
|
|
||||||
exports.app.close();
|
|
||||||
|
|
||||||
//do the db shutdown
|
|
||||||
db.db.doShutdown(function() {
|
|
||||||
console.log("db sucessfully closed.");
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(function(){
|
|
||||||
process.exit(1);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
exports.app = args.app;
|
|
||||||
|
|
||||||
args.app.error(function(err, req, res, next){
|
|
||||||
res.send(500);
|
|
||||||
console.error(err.stack ? err.stack : err.toString());
|
|
||||||
exports.gracefulShutdown();
|
|
||||||
});
|
|
||||||
|
|
||||||
//connect graceful shutdown with sigint and uncaughtexception
|
|
||||||
if(os.type().indexOf("Windows") == -1) {
|
|
||||||
//sigint is so far not working on windows
|
|
||||||
//https://github.com/joyent/node/issues/1553
|
|
||||||
process.on('SIGINT', exports.gracefulShutdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('uncaughtException', exports.gracefulShutdown);
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
var hasPadAccess = require("../../padaccess");
|
|
||||||
var settings = require('../../utils/Settings');
|
|
||||||
var exportHandler = require('../../handler/ExportHandler');
|
|
||||||
var importHandler = require('../../handler/ImportHandler');
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) {
|
|
||||||
var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
|
|
||||||
//send a 404 if we don't support this filetype
|
|
||||||
if (types.indexOf(req.params.type) == -1) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if abiword is disabled, and this is a format we only support with abiword, output a message
|
|
||||||
if (settings.abiword == null &&
|
|
||||||
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
|
|
||||||
res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
hasPadAccess(req, res, function() {
|
|
||||||
exportHandler.doExport(req, res, req.params.pad, req.params.type);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//handle import requests
|
|
||||||
args.app.post('/p/:pad/import', function(req, res, next) {
|
|
||||||
//if abiword is disabled, skip handling this request
|
|
||||||
if(settings.abiword == null) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPadAccess(req, res, function() {
|
|
||||||
importHandler.doImport(req, res, req.params.pad);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
var async = require('async');
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var readOnlyManager = require("../../db/ReadOnlyManager");
|
|
||||||
var hasPadAccess = require("../../padaccess");
|
|
||||||
var exporthtml = require("../../utils/ExportHtml");
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
//serve read only pad
|
|
||||||
args.app.get('/ro/:id', function(req, res)
|
|
||||||
{
|
|
||||||
var html;
|
|
||||||
var padId;
|
|
||||||
var pad;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
//translate the read only pad to a padId
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
readOnlyManager.getPadId(req.params.id, function(err, _padId)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
padId = _padId;
|
|
||||||
|
|
||||||
//we need that to tell hasPadAcess about the pad
|
|
||||||
req.params.pad = padId;
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
//render the html document
|
|
||||||
function(callback)
|
|
||||||
{
|
|
||||||
//return if the there is no padId
|
|
||||||
if(padId == null)
|
|
||||||
{
|
|
||||||
callback("notfound");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPadAccess(req, res, function()
|
|
||||||
{
|
|
||||||
//render the html document
|
|
||||||
exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
html = _html;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err)
|
|
||||||
{
|
|
||||||
//throw any unexpected error
|
|
||||||
if(err && err != "notfound")
|
|
||||||
ERR(err);
|
|
||||||
|
|
||||||
if(err == "notfound")
|
|
||||||
res.send('404 - Not Found', 404);
|
|
||||||
else
|
|
||||||
res.send(html);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
var padManager = require('../../db/PadManager');
|
|
||||||
var url = require('url');
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
|
||||||
args.app.param('pad', function (req, res, next, padId) {
|
|
||||||
//ensure the padname is valid and the url doesn't end with a /
|
|
||||||
if(!padManager.isValidPadId(padId) || /\/$/.test(req.url))
|
|
||||||
{
|
|
||||||
res.send('Such a padname is forbidden', 404);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
padManager.sanitizePadId(padId, function(sanitizedPadId) {
|
|
||||||
//the pad id was sanitized, so we redirect to the sanitized version
|
|
||||||
if(sanitizedPadId != padId)
|
|
||||||
{
|
|
||||||
var real_url = req.url.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId);
|
|
||||||
var query = url.parse(req.url).query;
|
|
||||||
if ( query ) real_url += '?' + query;
|
|
||||||
res.header('Location', real_url);
|
|
||||||
res.send('You should be redirected to <a href="' + real_url + '">' + real_url + '</a>', 302);
|
|
||||||
}
|
|
||||||
//the pad id was fine, so just render it
|
|
||||||
else
|
|
||||||
{
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
var log4js = require('log4js');
|
|
||||||
var socketio = require('socket.io');
|
|
||||||
var settings = require('../../utils/Settings');
|
|
||||||
var socketIORouter = require("../../handler/SocketIORouter");
|
|
||||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
|
||||||
|
|
||||||
var padMessageHandler = require("../../handler/PadMessageHandler");
|
|
||||||
var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler");
|
|
||||||
|
|
||||||
var connect = require('connect');
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
//init socket.io and redirect all requests to the MessageHandler
|
|
||||||
var io = socketio.listen(args.app);
|
|
||||||
|
|
||||||
/* Require an express session cookie to be present, and load the
|
|
||||||
* session. See http://www.danielbaulig.de/socket-ioexpress for more
|
|
||||||
* info */
|
|
||||||
io.set('authorization', function (data, accept) {
|
|
||||||
if (!data.headers.cookie) return accept('No session cookie transmitted.', false);
|
|
||||||
data.cookie = connect.utils.parseCookie(data.headers.cookie);
|
|
||||||
data.sessionID = data.cookie.express_sid;
|
|
||||||
args.app.sessionStore.get(data.sessionID, function (err, session) {
|
|
||||||
if (err || !session) return accept('Bad session / session has expired', false);
|
|
||||||
data.session = new connect.middleware.session.Session(data, session);
|
|
||||||
accept(null, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//this is only a workaround to ensure it works with all browers behind a proxy
|
|
||||||
//we should remove this when the new socket.io version is more stable
|
|
||||||
io.set('transports', ['xhr-polling']);
|
|
||||||
|
|
||||||
var socketIOLogger = log4js.getLogger("socket.io");
|
|
||||||
io.set('logger', {
|
|
||||||
debug: function (str)
|
|
||||||
{
|
|
||||||
socketIOLogger.debug.apply(socketIOLogger, arguments);
|
|
||||||
},
|
|
||||||
info: function (str)
|
|
||||||
{
|
|
||||||
socketIOLogger.info.apply(socketIOLogger, arguments);
|
|
||||||
},
|
|
||||||
warn: function (str)
|
|
||||||
{
|
|
||||||
socketIOLogger.warn.apply(socketIOLogger, arguments);
|
|
||||||
},
|
|
||||||
error: function (str)
|
|
||||||
{
|
|
||||||
socketIOLogger.error.apply(socketIOLogger, arguments);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//minify socket.io javascript
|
|
||||||
if(settings.minify)
|
|
||||||
io.enable('browser client minification');
|
|
||||||
|
|
||||||
//Initalize the Socket.IO Router
|
|
||||||
socketIORouter.setSocketIO(io);
|
|
||||||
socketIORouter.addComponent("pad", padMessageHandler);
|
|
||||||
socketIORouter.addComponent("timeslider", timesliderMessageHandler);
|
|
||||||
|
|
||||||
hooks.callAll("socketio", {"app": args.app, "io": io});
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var eejs = require('ep_etherpad-lite/node/eejs');
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
|
|
||||||
//serve index.html under /
|
|
||||||
args.app.get('/', function(req, res)
|
|
||||||
{
|
|
||||||
res.send(eejs.require("ep_etherpad-lite/templates/index.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
//serve robots.txt
|
|
||||||
args.app.get('/robots.txt', function(req, res)
|
|
||||||
{
|
|
||||||
var filePath = path.normalize(__dirname + "/../../../static/robots.txt");
|
|
||||||
res.sendfile(filePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
//serve favicon.ico
|
|
||||||
args.app.get('/favicon.ico', function(req, res)
|
|
||||||
{
|
|
||||||
var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico");
|
|
||||||
res.sendfile(filePath, function(err)
|
|
||||||
{
|
|
||||||
//there is no custom favicon, send the default favicon
|
|
||||||
if(err)
|
|
||||||
{
|
|
||||||
filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
|
|
||||||
res.sendfile(filePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//serve pad.html under /p
|
|
||||||
args.app.get('/p/:pad', function(req, res, next)
|
|
||||||
{
|
|
||||||
res.send(eejs.require("ep_etherpad-lite/templates/pad.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
//serve timeslider.html under /p/$padname/timeslider
|
|
||||||
args.app.get('/p/:pad/timeslider', function(req, res, next)
|
|
||||||
{
|
|
||||||
res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
var path = require('path');
|
|
||||||
var minify = require('../../utils/Minify');
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
|
||||||
var CachingMiddleware = require('../../utils/caching_middleware');
|
|
||||||
var settings = require("../../utils/Settings");
|
|
||||||
var Yajsml = require('yajsml');
|
|
||||||
var fs = require("fs");
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var _ = require("underscore");
|
|
||||||
|
|
||||||
exports.expressCreateServer = function (hook_name, args, cb) {
|
|
||||||
// Cache both minified and static.
|
|
||||||
var assetCache = new CachingMiddleware;
|
|
||||||
args.app.all('/(javascripts|static)/*', assetCache.handle);
|
|
||||||
|
|
||||||
// Minify will serve static files compressed (minify enabled). It also has
|
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
|
||||||
args.app.all('/static/:filename(*)', minify.minify);
|
|
||||||
|
|
||||||
// Setup middleware that will package JavaScript files served by minify for
|
|
||||||
// CommonJS loader on the client-side.
|
|
||||||
var jsServer = new (Yajsml.Server)({
|
|
||||||
rootPath: 'javascripts/src/'
|
|
||||||
, rootURI: 'http://localhost:' + settings.port + '/static/js/'
|
|
||||||
, libraryPath: 'javascripts/lib/'
|
|
||||||
, libraryURI: 'http://localhost:' + settings.port + '/static/plugins/'
|
|
||||||
});
|
|
||||||
|
|
||||||
var StaticAssociator = Yajsml.associators.StaticAssociator;
|
|
||||||
var associations =
|
|
||||||
Yajsml.associators.associationsForSimpleMapping(minify.tar);
|
|
||||||
var associator = new StaticAssociator(associations);
|
|
||||||
jsServer.setAssociator(associator);
|
|
||||||
args.app.use(jsServer);
|
|
||||||
|
|
||||||
// serve plugin definitions
|
|
||||||
// not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js");
|
|
||||||
args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) {
|
|
||||||
|
|
||||||
var clientParts = _(plugins.parts)
|
|
||||||
.filter(function(part){ return _(part).has('client_hooks') });
|
|
||||||
|
|
||||||
var clientPlugins = {};
|
|
||||||
|
|
||||||
_(clientParts).chain()
|
|
||||||
.map(function(part){ return part.plugin })
|
|
||||||
.uniq()
|
|
||||||
.each(function(name){
|
|
||||||
clientPlugins[name] = _(plugins.plugins[name]).clone();
|
|
||||||
delete clientPlugins[name]['package'];
|
|
||||||
});
|
|
||||||
|
|
||||||
res.header("Content-Type","application/json; charset=utf-8");
|
|
||||||
res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts}));
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
var express = require('express');
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var httpLogger = log4js.getLogger("http");
|
|
||||||
var settings = require('../../utils/Settings');
|
|
||||||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
|
|
||||||
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
|
||||||
|
|
||||||
|
|
||||||
//checks for basic http auth
|
|
||||||
exports.basicAuth = function (req, res, next) {
|
|
||||||
var hookResultMangle = function (cb) {
|
|
||||||
return function (err, data) {
|
|
||||||
return cb(!err && data.length && data[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var authorize = function (cb) {
|
|
||||||
// Do not require auth for static paths...this could be a bit brittle
|
|
||||||
if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true);
|
|
||||||
|
|
||||||
if (req.path.indexOf('/admin') != 0) {
|
|
||||||
if (!settings.requireAuthentication) return cb(true);
|
|
||||||
if (!settings.requireAuthorization && req.session && req.session.user) return cb(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.session && req.session.user && req.session.user.is_admin) return cb(true);
|
|
||||||
|
|
||||||
hooks.aCallFirst("authorize", {req: req, res:res, next:next, resource: req.path}, hookResultMangle(cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
var authenticate = function (cb) {
|
|
||||||
// If auth headers are present use them to authenticate...
|
|
||||||
if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
|
|
||||||
var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":")
|
|
||||||
var username = userpass[0];
|
|
||||||
var password = userpass[1];
|
|
||||||
|
|
||||||
if (settings.users[username] != undefined && settings.users[username].password == password) {
|
|
||||||
settings.users[username].username = username;
|
|
||||||
req.session.user = settings.users[username];
|
|
||||||
return cb(true);
|
|
||||||
}
|
|
||||||
return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb));
|
|
||||||
}
|
|
||||||
hooks.aCallFirst("authenticate", {req: req, res:res, next:next}, hookResultMangle(cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Authentication OR authorization failed. */
|
|
||||||
var failure = function () {
|
|
||||||
return hooks.aCallFirst("authFailure", {req: req, res:res, next:next}, hookResultMangle(function (ok) {
|
|
||||||
if (ok) return;
|
|
||||||
/* No plugin handler for invalid auth. Return Auth required
|
|
||||||
* Headers, delayed for 1 second, if authentication failed
|
|
||||||
* before. */
|
|
||||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
|
||||||
if (req.headers.authorization) {
|
|
||||||
setTimeout(function () {
|
|
||||||
res.send('Authentication required', 401);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
res.send('Authentication required', 401);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* This is the actual authentication/authorization hoop. It is done in four steps:
|
|
||||||
|
|
||||||
1) Try to just access the thing
|
|
||||||
2) If not allowed using whatever creds are in the current session already, try to authenticate
|
|
||||||
3) If authentication using already supplied credentials succeeds, try to access the thing again
|
|
||||||
4) If all els fails, give the user a 401 to request new credentials
|
|
||||||
|
|
||||||
Note that the process could stop already in step 3 with a redirect to login page.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
authorize(function (ok) {
|
|
||||||
if (ok) return next();
|
|
||||||
authenticate(function (ok) {
|
|
||||||
if (!ok) return failure();
|
|
||||||
authorize(function (ok) {
|
|
||||||
if (ok) return next();
|
|
||||||
failure();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.expressConfigure = function (hook_name, args, cb) {
|
|
||||||
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
|
|
||||||
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
|
|
||||||
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
|
|
||||||
args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
|
|
||||||
args.app.use(express.cookieParser());
|
|
||||||
|
|
||||||
/* Do not let express create the session, so that we can retain a
|
|
||||||
* reference to it for socket.io to use. Also, set the key (cookie
|
|
||||||
* name) to a javascript identifier compatible string. Makes code
|
|
||||||
* handling it cleaner :) */
|
|
||||||
|
|
||||||
args.app.sessionStore = new express.session.MemoryStore();
|
|
||||||
args.app.use(express.session({store: args.app.sessionStore,
|
|
||||||
key: 'express_sid',
|
|
||||||
secret: apikey = randomString(32)}));
|
|
||||||
|
|
||||||
args.app.use(exports.basicAuth);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var securityManager = require('./db/SecurityManager');
|
|
||||||
|
|
||||||
//checks for padAccess
|
|
||||||
module.exports = function (req, res, callback) {
|
|
||||||
|
|
||||||
// FIXME: Why is this ever undefined??
|
|
||||||
if (req.cookies === undefined) req.cookies = {};
|
|
||||||
|
|
||||||
securityManager.checkAccess(req.params.pad, req.cookies.sessionid, req.cookies.token, req.cookies.password, function(err, accessObj) {
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//there is access, continue
|
|
||||||
if(accessObj.accessStatus == "grant") {
|
|
||||||
callback();
|
|
||||||
//no access
|
|
||||||
} else {
|
|
||||||
res.send("403 - Can't touch this", 403);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* This module is started with bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
|
|
||||||
* Static file Requests are answered directly from this module, Socket.IO messages are passed
|
|
||||||
* to MessageHandler and minfied requests are passed to minified.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var log4js = require('log4js');
|
|
||||||
var fs = require('fs');
|
|
||||||
var settings = require('./utils/Settings');
|
|
||||||
var db = require('./db/DB');
|
|
||||||
var async = require('async');
|
|
||||||
var express = require('express');
|
|
||||||
var path = require('path');
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
|
||||||
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
|
|
||||||
var npm = require("npm/lib/npm.js");
|
|
||||||
var _ = require("underscore");
|
|
||||||
|
|
||||||
//try to get the git version
|
|
||||||
var version = "";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var rootPath = path.resolve(npm.dir, '..');
|
|
||||||
var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8");
|
|
||||||
var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n"));
|
|
||||||
version = fs.readFileSync(refPath, "utf-8");
|
|
||||||
version = version.substring(0, 7);
|
|
||||||
console.log("Your Etherpad Lite git version is " + version);
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
console.warn("Can't get git version for server header\n" + e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
|
|
||||||
|
|
||||||
var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
|
|
||||||
|
|
||||||
//set loglevel
|
|
||||||
log4js.setGlobalLogLevel(settings.loglevel);
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
//initalize the database
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
db.init(callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins.update,
|
|
||||||
|
|
||||||
function (callback) {
|
|
||||||
console.info("Installed plugins: " + plugins.formatPlugins());
|
|
||||||
console.debug("Installed parts:\n" + plugins.formatParts());
|
|
||||||
console.debug("Installed hooks:\n" + plugins.formatHooks());
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
|
|
||||||
//initalize the http server
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
//create server
|
|
||||||
var app = express.createServer();
|
|
||||||
|
|
||||||
app.use(function (req, res, next) {
|
|
||||||
res.header("Server", serverName);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); });
|
|
||||||
|
|
||||||
hooks.callAll("expressCreateServer", {"app": app});
|
|
||||||
|
|
||||||
//let the server listen
|
|
||||||
app.listen(settings.port, settings.ip);
|
|
||||||
console.log("You can access your Etherpad-Lite instance at http://" + settings.ip + ":" + settings.port + "/");
|
|
||||||
if(!_.isEmpty(settings.users)){
|
|
||||||
console.log("The plugin admin page is at http://" + settings.ip + ":" + settings.port + "/admin/plugins");
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json");
|
|
||||||
}
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
]);
|
|
|
@ -1,147 +0,0 @@
|
||||||
/**
|
|
||||||
* Controls the communication with the Abiword application
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var util = require('util');
|
|
||||||
var spawn = require('child_process').spawn;
|
|
||||||
var async = require("async");
|
|
||||||
var settings = require("./Settings");
|
|
||||||
var os = require('os');
|
|
||||||
|
|
||||||
var doConvertTask;
|
|
||||||
|
|
||||||
//on windows we have to spawn a process for each convertion, cause the plugin abicommand doesn't exist on this platform
|
|
||||||
if(os.type().indexOf("Windows") > -1)
|
|
||||||
{
|
|
||||||
var stdoutBuffer = "";
|
|
||||||
|
|
||||||
doConvertTask = function(task, callback)
|
|
||||||
{
|
|
||||||
//span an abiword process to perform the conversion
|
|
||||||
var abiword = spawn(settings.abiword, ["--to=" + task.destFile, task.srcFile]);
|
|
||||||
|
|
||||||
//delegate the processing of stdout to another function
|
|
||||||
abiword.stdout.on('data', function (data)
|
|
||||||
{
|
|
||||||
//add data to buffer
|
|
||||||
stdoutBuffer+=data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
//append error messages to the buffer
|
|
||||||
abiword.stderr.on('data', function (data)
|
|
||||||
{
|
|
||||||
stdoutBuffer += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
//throw exceptions if abiword is dieing
|
|
||||||
abiword.on('exit', function (code)
|
|
||||||
{
|
|
||||||
if(code != 0) {
|
|
||||||
return callback("Abiword died with exit code " + code);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stdoutBuffer != "")
|
|
||||||
{
|
|
||||||
console.log(stdoutBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.convertFile = function(srcFile, destFile, type, callback)
|
|
||||||
{
|
|
||||||
doConvertTask({"srcFile": srcFile, "destFile": destFile, "type": type}, callback);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
//on unix operating systems, we can start abiword with abicommand and communicate with it via stdin/stdout
|
|
||||||
//thats much faster, about factor 10
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//spawn the abiword process
|
|
||||||
var abiword;
|
|
||||||
var stdoutCallback = null;
|
|
||||||
var spawnAbiword = function (){
|
|
||||||
abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]);
|
|
||||||
var stdoutBuffer = "";
|
|
||||||
var firstPrompt = true;
|
|
||||||
|
|
||||||
//append error messages to the buffer
|
|
||||||
abiword.stderr.on('data', function (data)
|
|
||||||
{
|
|
||||||
stdoutBuffer += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
//abiword died, let's restart abiword and return an error with the callback
|
|
||||||
abiword.on('exit', function (code)
|
|
||||||
{
|
|
||||||
spawnAbiword();
|
|
||||||
stdoutCallback("Abiword died with exit code " + code);
|
|
||||||
});
|
|
||||||
|
|
||||||
//delegate the processing of stdout to a other function
|
|
||||||
abiword.stdout.on('data',function (data)
|
|
||||||
{
|
|
||||||
//add data to buffer
|
|
||||||
stdoutBuffer+=data.toString();
|
|
||||||
|
|
||||||
//we're searching for the prompt, cause this means everything we need is in the buffer
|
|
||||||
if(stdoutBuffer.search("AbiWord:>") != -1)
|
|
||||||
{
|
|
||||||
//filter the feedback message
|
|
||||||
var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer;
|
|
||||||
|
|
||||||
//reset the buffer
|
|
||||||
stdoutBuffer = "";
|
|
||||||
|
|
||||||
//call the callback with the error message
|
|
||||||
//skip the first prompt
|
|
||||||
if(stdoutCallback != null && !firstPrompt)
|
|
||||||
{
|
|
||||||
stdoutCallback(err);
|
|
||||||
stdoutCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
firstPrompt = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
spawnAbiword();
|
|
||||||
|
|
||||||
doConvertTask = function(task, callback)
|
|
||||||
{
|
|
||||||
abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n");
|
|
||||||
|
|
||||||
//create a callback that calls the task callback and the caller callback
|
|
||||||
stdoutCallback = function (err)
|
|
||||||
{
|
|
||||||
callback();
|
|
||||||
console.log("queue continue");
|
|
||||||
task.callback(err);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//Queue with the converts we have to do
|
|
||||||
var queue = async.queue(doConvertTask, 1);
|
|
||||||
|
|
||||||
exports.convertFile = function(srcFile, destFile, type, callback)
|
|
||||||
{
|
|
||||||
queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback});
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
/**
|
|
||||||
* The CLI module handles command line parameters
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2012 Jordan Hollinger
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// An object containing the parsed command-line options
|
|
||||||
exports.argv = {};
|
|
||||||
|
|
||||||
var argv = process.argv.slice(2);
|
|
||||||
var arg, prevArg;
|
|
||||||
|
|
||||||
// Loop through args
|
|
||||||
for ( var i = 0; i < argv.length; i++ ) {
|
|
||||||
arg = argv[i];
|
|
||||||
|
|
||||||
// Override location of settings.json file
|
|
||||||
if ( prevArg == '--settings' || prevArg == '-s' ) {
|
|
||||||
exports.argv.settings = arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevArg = arg;
|
|
||||||
}
|
|
|
@ -1,345 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2011 Adrian Lang
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var async = require("async");
|
|
||||||
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
|
|
||||||
function getPadDokuWiki(pad, revNum, callback)
|
|
||||||
{
|
|
||||||
var atext = pad.atext;
|
|
||||||
var dokuwiki;
|
|
||||||
async.waterfall([
|
|
||||||
// fetch revision atext
|
|
||||||
|
|
||||||
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
if (revNum != undefined)
|
|
||||||
{
|
|
||||||
pad.getInternalRevisionAText(revNum, function (err, revisionAtext)
|
|
||||||
{
|
|
||||||
atext = revisionAtext;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// convert atext to dokuwiki text
|
|
||||||
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
dokuwiki = getDokuWikiFromAtext(pad, atext);
|
|
||||||
callback(null);
|
|
||||||
}],
|
|
||||||
// run final callback
|
|
||||||
|
|
||||||
|
|
||||||
function (err)
|
|
||||||
{
|
|
||||||
callback(err, dokuwiki);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDokuWikiFromAtext(pad, atext)
|
|
||||||
{
|
|
||||||
var apool = pad.apool();
|
|
||||||
var textLines = atext.text.slice(0, -1).split('\n');
|
|
||||||
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
|
||||||
|
|
||||||
var tags = ['======', '=====', '**', '//', '__', 'del>'];
|
|
||||||
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
|
||||||
var anumMap = {};
|
|
||||||
|
|
||||||
props.forEach(function (propName, i)
|
|
||||||
{
|
|
||||||
var propTrueNum = apool.putAttrib([propName, true], true);
|
|
||||||
if (propTrueNum >= 0)
|
|
||||||
{
|
|
||||||
anumMap[propTrueNum] = i;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLineDokuWiki(text, attribs)
|
|
||||||
{
|
|
||||||
var propVals = [false, false, false];
|
|
||||||
var ENTER = 1;
|
|
||||||
var STAY = 2;
|
|
||||||
var LEAVE = 0;
|
|
||||||
|
|
||||||
// Use order of tags (b/i/u) as order of nesting, for simplicity
|
|
||||||
// and decent nesting. For example,
|
|
||||||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
|
||||||
// becomes
|
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
|
||||||
var taker = Changeset.stringIterator(text);
|
|
||||||
var assem = Changeset.stringAssembler();
|
|
||||||
|
|
||||||
function emitOpenTag(i)
|
|
||||||
{
|
|
||||||
if (tags[i].indexOf('>') !== -1) {
|
|
||||||
assem.append('<');
|
|
||||||
}
|
|
||||||
assem.append(tags[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitCloseTag(i)
|
|
||||||
{
|
|
||||||
if (tags[i].indexOf('>') !== -1) {
|
|
||||||
assem.append('</');
|
|
||||||
}
|
|
||||||
assem.append(tags[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var urls = _findURLs(text);
|
|
||||||
|
|
||||||
var idx = 0;
|
|
||||||
|
|
||||||
function processNextChars(numChars)
|
|
||||||
{
|
|
||||||
if (numChars <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
|
|
||||||
idx += numChars;
|
|
||||||
|
|
||||||
while (iter.hasNext())
|
|
||||||
{
|
|
||||||
var o = iter.next();
|
|
||||||
var propChanged = false;
|
|
||||||
Changeset.eachAttribNumber(o.attribs, function (a)
|
|
||||||
{
|
|
||||||
if (a in anumMap)
|
|
||||||
{
|
|
||||||
var i = anumMap[a]; // i = 0 => bold, etc.
|
|
||||||
if (!propVals[i])
|
|
||||||
{
|
|
||||||
propVals[i] = ENTER;
|
|
||||||
propChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
propVals[i] = STAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
if (propVals[i] === true)
|
|
||||||
{
|
|
||||||
propVals[i] = LEAVE;
|
|
||||||
propChanged = true;
|
|
||||||
}
|
|
||||||
else if (propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
propVals[i] = true; // set it back
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// now each member of propVal is in {false,LEAVE,ENTER,true}
|
|
||||||
// according to what happens at start of span
|
|
||||||
if (propChanged)
|
|
||||||
{
|
|
||||||
// leaving bold (e.g.) also leaves italics, etc.
|
|
||||||
var left = false;
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
var v = propVals[i];
|
|
||||||
if (!left)
|
|
||||||
{
|
|
||||||
if (v === LEAVE)
|
|
||||||
{
|
|
||||||
left = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (v === true)
|
|
||||||
{
|
|
||||||
propVals[i] = STAY; // tag will be closed and re-opened
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = propVals.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (propVals[i] === LEAVE)
|
|
||||||
{
|
|
||||||
emitCloseTag(i);
|
|
||||||
propVals[i] = false;
|
|
||||||
}
|
|
||||||
else if (propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
emitCloseTag(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
if (propVals[i] === ENTER || propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
emitOpenTag(i);
|
|
||||||
propVals[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// propVals is now all {true,false} again
|
|
||||||
} // end if (propChanged)
|
|
||||||
var chars = o.chars;
|
|
||||||
if (o.lines)
|
|
||||||
{
|
|
||||||
chars--; // exclude newline at end of line, if present
|
|
||||||
}
|
|
||||||
var s = taker.take(chars);
|
|
||||||
|
|
||||||
assem.append(_escapeDokuWiki(s));
|
|
||||||
} // end iteration over spans in line
|
|
||||||
for (var i = propVals.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (propVals[i])
|
|
||||||
{
|
|
||||||
emitCloseTag(i);
|
|
||||||
propVals[i] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // end processNextChars
|
|
||||||
if (urls)
|
|
||||||
{
|
|
||||||
urls.forEach(function (urlData)
|
|
||||||
{
|
|
||||||
var startIndex = urlData[0];
|
|
||||||
var url = urlData[1];
|
|
||||||
var urlLength = url.length;
|
|
||||||
processNextChars(startIndex - idx);
|
|
||||||
assem.append('[[');
|
|
||||||
|
|
||||||
// Do not use processNextChars since a link does not contain syntax and
|
|
||||||
// needs no escaping
|
|
||||||
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + urlLength));
|
|
||||||
idx += urlLength;
|
|
||||||
assem.append(taker.take(iter.next().chars));
|
|
||||||
|
|
||||||
assem.append(']]');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
processNextChars(text.length - idx);
|
|
||||||
|
|
||||||
return assem.toString() + "\n";
|
|
||||||
} // end getLineDokuWiki
|
|
||||||
var pieces = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < textLines.length; i++)
|
|
||||||
{
|
|
||||||
var line = _analyzeLine(textLines[i], attribLines[i], apool);
|
|
||||||
var lineContent = getLineDokuWiki(line.text, line.aline);
|
|
||||||
|
|
||||||
if (line.listLevel && lineContent)
|
|
||||||
{
|
|
||||||
pieces.push(new Array(line.listLevel + 1).join(' ') + '* ');
|
|
||||||
}
|
|
||||||
pieces.push(lineContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pieces.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function _analyzeLine(text, aline, apool)
|
|
||||||
{
|
|
||||||
var line = {};
|
|
||||||
|
|
||||||
// identify list
|
|
||||||
var lineMarker = 0;
|
|
||||||
line.listLevel = 0;
|
|
||||||
if (aline)
|
|
||||||
{
|
|
||||||
var opIter = Changeset.opIterator(aline);
|
|
||||||
if (opIter.hasNext())
|
|
||||||
{
|
|
||||||
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
|
|
||||||
if (listType)
|
|
||||||
{
|
|
||||||
lineMarker = 1;
|
|
||||||
listType = /([a-z]+)([12345678])/.exec(listType);
|
|
||||||
if (listType)
|
|
||||||
{
|
|
||||||
line.listTypeName = listType[1];
|
|
||||||
line.listLevel = Number(listType[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lineMarker)
|
|
||||||
{
|
|
||||||
line.text = text.substring(1);
|
|
||||||
line.aline = Changeset.subattribution(aline, 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
line.text = text;
|
|
||||||
line.aline = aline;
|
|
||||||
}
|
|
||||||
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getPadDokuWikiDocument = function (padId, revNum, callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function (err, pad)
|
|
||||||
{
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPadDokuWiki(pad, revNum, callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _escapeDokuWiki(s)
|
|
||||||
{
|
|
||||||
s = s.replace(/(\/\/|\*\*|__)/g, '%%$1%%');
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// copied from ACE
|
|
||||||
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
|
|
||||||
var _REGEX_SPACE = /\s/;
|
|
||||||
var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')');
|
|
||||||
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g');
|
|
||||||
|
|
||||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
|
||||||
|
|
||||||
|
|
||||||
function _findURLs(text)
|
|
||||||
{
|
|
||||||
_REGEX_URL.lastIndex = 0;
|
|
||||||
var urls = null;
|
|
||||||
var execResult;
|
|
||||||
while ((execResult = _REGEX_URL.exec(text)))
|
|
||||||
{
|
|
||||||
urls = (urls || []);
|
|
||||||
var startIndex = execResult.index;
|
|
||||||
var url = execResult[0];
|
|
||||||
urls.push([startIndex, url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
}
|
|
|
@ -1,595 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright 2009 Google Inc.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var async = require("async");
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var padManager = require("../db/PadManager");
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var Security = require('ep_etherpad-lite/static/js/security');
|
|
||||||
|
|
||||||
function getPadPlainText(pad, revNum)
|
|
||||||
{
|
|
||||||
var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext());
|
|
||||||
var textLines = atext.text.slice(0, -1).split('\n');
|
|
||||||
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
|
||||||
var apool = pad.pool();
|
|
||||||
|
|
||||||
var pieces = [];
|
|
||||||
for (var i = 0; i < textLines.length; i++)
|
|
||||||
{
|
|
||||||
var line = _analyzeLine(textLines[i], attribLines[i], apool);
|
|
||||||
if (line.listLevel)
|
|
||||||
{
|
|
||||||
var numSpaces = line.listLevel * 2 - 1;
|
|
||||||
var bullet = '*';
|
|
||||||
pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push(line.text, '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pieces.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPadHTML(pad, revNum, callback)
|
|
||||||
{
|
|
||||||
var atext = pad.atext;
|
|
||||||
var html;
|
|
||||||
async.waterfall([
|
|
||||||
// fetch revision atext
|
|
||||||
|
|
||||||
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
if (revNum != undefined)
|
|
||||||
{
|
|
||||||
pad.getInternalRevisionAText(revNum, function (err, revisionAtext)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
atext = revisionAtext;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// convert atext to html
|
|
||||||
|
|
||||||
|
|
||||||
function (callback)
|
|
||||||
{
|
|
||||||
html = getHTMLFromAtext(pad, atext);
|
|
||||||
callback(null);
|
|
||||||
}],
|
|
||||||
// run final callback
|
|
||||||
|
|
||||||
|
|
||||||
function (err)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, html);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getPadHTML = getPadHTML;
|
|
||||||
|
|
||||||
function getHTMLFromAtext(pad, atext)
|
|
||||||
{
|
|
||||||
var apool = pad.apool();
|
|
||||||
var textLines = atext.text.slice(0, -1).split('\n');
|
|
||||||
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
|
||||||
|
|
||||||
var tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
|
||||||
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
|
||||||
var anumMap = {};
|
|
||||||
|
|
||||||
props.forEach(function (propName, i)
|
|
||||||
{
|
|
||||||
var propTrueNum = apool.putAttrib([propName, true], true);
|
|
||||||
if (propTrueNum >= 0)
|
|
||||||
{
|
|
||||||
anumMap[propTrueNum] = i;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLineHTML(text, attribs)
|
|
||||||
{
|
|
||||||
var propVals = [false, false, false];
|
|
||||||
var ENTER = 1;
|
|
||||||
var STAY = 2;
|
|
||||||
var LEAVE = 0;
|
|
||||||
|
|
||||||
// Use order of tags (b/i/u) as order of nesting, for simplicity
|
|
||||||
// and decent nesting. For example,
|
|
||||||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
|
||||||
// becomes
|
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
|
||||||
var taker = Changeset.stringIterator(text);
|
|
||||||
var assem = Changeset.stringAssembler();
|
|
||||||
|
|
||||||
var openTags = [];
|
|
||||||
function emitOpenTag(i)
|
|
||||||
{
|
|
||||||
openTags.unshift(i);
|
|
||||||
assem.append('<');
|
|
||||||
assem.append(tags[i]);
|
|
||||||
assem.append('>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitCloseTag(i)
|
|
||||||
{
|
|
||||||
openTags.shift();
|
|
||||||
assem.append('</');
|
|
||||||
assem.append(tags[i]);
|
|
||||||
assem.append('>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderdCloseTags(tags2close)
|
|
||||||
{
|
|
||||||
for(var i=0;i<openTags.length;i++)
|
|
||||||
{
|
|
||||||
for(var j=0;j<tags2close.length;j++)
|
|
||||||
{
|
|
||||||
if(tags2close[j] == openTags[i])
|
|
||||||
{
|
|
||||||
emitCloseTag(tags2close[j]);
|
|
||||||
i--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var urls = _findURLs(text);
|
|
||||||
|
|
||||||
var idx = 0;
|
|
||||||
|
|
||||||
function processNextChars(numChars)
|
|
||||||
{
|
|
||||||
if (numChars <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
|
|
||||||
idx += numChars;
|
|
||||||
|
|
||||||
while (iter.hasNext())
|
|
||||||
{
|
|
||||||
var o = iter.next();
|
|
||||||
var propChanged = false;
|
|
||||||
Changeset.eachAttribNumber(o.attribs, function (a)
|
|
||||||
{
|
|
||||||
if (a in anumMap)
|
|
||||||
{
|
|
||||||
var i = anumMap[a]; // i = 0 => bold, etc.
|
|
||||||
if (!propVals[i])
|
|
||||||
{
|
|
||||||
propVals[i] = ENTER;
|
|
||||||
propChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
propVals[i] = STAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
if (propVals[i] === true)
|
|
||||||
{
|
|
||||||
propVals[i] = LEAVE;
|
|
||||||
propChanged = true;
|
|
||||||
}
|
|
||||||
else if (propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
propVals[i] = true; // set it back
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// now each member of propVal is in {false,LEAVE,ENTER,true}
|
|
||||||
// according to what happens at start of span
|
|
||||||
if (propChanged)
|
|
||||||
{
|
|
||||||
// leaving bold (e.g.) also leaves italics, etc.
|
|
||||||
var left = false;
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
var v = propVals[i];
|
|
||||||
if (!left)
|
|
||||||
{
|
|
||||||
if (v === LEAVE)
|
|
||||||
{
|
|
||||||
left = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (v === true)
|
|
||||||
{
|
|
||||||
propVals[i] = STAY; // tag will be closed and re-opened
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags2close = [];
|
|
||||||
|
|
||||||
for (var i = propVals.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (propVals[i] === LEAVE)
|
|
||||||
{
|
|
||||||
//emitCloseTag(i);
|
|
||||||
tags2close.push(i);
|
|
||||||
propVals[i] = false;
|
|
||||||
}
|
|
||||||
else if (propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
//emitCloseTag(i);
|
|
||||||
tags2close.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orderdCloseTags(tags2close);
|
|
||||||
|
|
||||||
for (var i = 0; i < propVals.length; i++)
|
|
||||||
{
|
|
||||||
if (propVals[i] === ENTER || propVals[i] === STAY)
|
|
||||||
{
|
|
||||||
emitOpenTag(i);
|
|
||||||
propVals[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// propVals is now all {true,false} again
|
|
||||||
} // end if (propChanged)
|
|
||||||
var chars = o.chars;
|
|
||||||
if (o.lines)
|
|
||||||
{
|
|
||||||
chars--; // exclude newline at end of line, if present
|
|
||||||
}
|
|
||||||
|
|
||||||
var s = taker.take(chars);
|
|
||||||
|
|
||||||
//removes the characters with the code 12. Don't know where they come
|
|
||||||
//from but they break the abiword parser and are completly useless
|
|
||||||
s = s.replace(String.fromCharCode(12), "");
|
|
||||||
|
|
||||||
assem.append(_encodeWhitespace(Security.escapeHTML(s)));
|
|
||||||
} // end iteration over spans in line
|
|
||||||
|
|
||||||
var tags2close = [];
|
|
||||||
for (var i = propVals.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (propVals[i])
|
|
||||||
{
|
|
||||||
tags2close.push(i);
|
|
||||||
propVals[i] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orderdCloseTags(tags2close);
|
|
||||||
} // end processNextChars
|
|
||||||
if (urls)
|
|
||||||
{
|
|
||||||
urls.forEach(function (urlData)
|
|
||||||
{
|
|
||||||
var startIndex = urlData[0];
|
|
||||||
var url = urlData[1];
|
|
||||||
var urlLength = url.length;
|
|
||||||
processNextChars(startIndex - idx);
|
|
||||||
assem.append('<a href="' + Security.escapeHTMLAttribute(url) + '">');
|
|
||||||
processNextChars(urlLength);
|
|
||||||
assem.append('</a>');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
processNextChars(text.length - idx);
|
|
||||||
|
|
||||||
return _processSpaces(assem.toString());
|
|
||||||
} // end getLineHTML
|
|
||||||
var pieces = [];
|
|
||||||
|
|
||||||
// Need to deal with constraints imposed on HTML lists; can
|
|
||||||
// only gain one level of nesting at once, can't change type
|
|
||||||
// mid-list, etc.
|
|
||||||
// People might use weird indenting, e.g. skip a level,
|
|
||||||
// so we want to do something reasonable there. We also
|
|
||||||
// want to deal gracefully with blank lines.
|
|
||||||
// => keeps track of the parents level of indentation
|
|
||||||
var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
|
|
||||||
for (var i = 0; i < textLines.length; i++)
|
|
||||||
{
|
|
||||||
var line = _analyzeLine(textLines[i], attribLines[i], apool);
|
|
||||||
var lineContent = getLineHTML(line.text, line.aline);
|
|
||||||
|
|
||||||
if (line.listLevel)//If we are inside a list
|
|
||||||
{
|
|
||||||
// do list stuff
|
|
||||||
var whichList = -1; // index into lists or -1
|
|
||||||
if (line.listLevel)
|
|
||||||
{
|
|
||||||
whichList = lists.length;
|
|
||||||
for (var j = lists.length - 1; j >= 0; j--)
|
|
||||||
{
|
|
||||||
if (line.listLevel <= lists[j][0])
|
|
||||||
{
|
|
||||||
whichList = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line
|
|
||||||
{
|
|
||||||
lists.push([line.listLevel, line.listTypeName]);
|
|
||||||
if(line.listTypeName == "number")
|
|
||||||
{
|
|
||||||
pieces.push('<ol class="'+line.listTypeName+'"><li>', lineContent || '<br>');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push('<ul class="'+line.listTypeName+'"><li>', lineContent || '<br>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//the following code *seems* dead after my patch.
|
|
||||||
//I keep it just in case I'm wrong...
|
|
||||||
/*else if (whichList == -1)//means we are not inside a list
|
|
||||||
{
|
|
||||||
if (line.text)
|
|
||||||
{
|
|
||||||
console.log('trace 1');
|
|
||||||
// non-blank line, end all lists
|
|
||||||
if(line.listTypeName == "number")
|
|
||||||
{
|
|
||||||
pieces.push(new Array(lists.length + 1).join('</li></ol>'));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push(new Array(lists.length + 1).join('</li></ul>'));
|
|
||||||
}
|
|
||||||
lists.length = 0;
|
|
||||||
pieces.push(lineContent, '<br>');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log('trace 2');
|
|
||||||
pieces.push('<br><br>');
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
else//means we are getting closer to the lowest level of indentation
|
|
||||||
{
|
|
||||||
while (whichList < lists.length - 1)
|
|
||||||
{
|
|
||||||
if(lists[lists.length - 1][1] == "number")
|
|
||||||
{
|
|
||||||
pieces.push('</li></ol>');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push('</li></ul>');
|
|
||||||
}
|
|
||||||
lists.length--;
|
|
||||||
}
|
|
||||||
pieces.push('</li><li>', lineContent || '<br>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else//outside any list
|
|
||||||
{
|
|
||||||
while (lists.length > 0)//if was in a list: close it before
|
|
||||||
{
|
|
||||||
if(lists[lists.length - 1][1] == "number")
|
|
||||||
{
|
|
||||||
pieces.push('</li></ol>');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push('</li></ul>');
|
|
||||||
}
|
|
||||||
lists.length--;
|
|
||||||
}
|
|
||||||
pieces.push(lineContent, '<br>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var k = lists.length - 1; k >= 0; k--)
|
|
||||||
{
|
|
||||||
if(lists[k][1] == "number")
|
|
||||||
{
|
|
||||||
pieces.push('</li></ol>');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pieces.push('</li></ul>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pieces.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function _analyzeLine(text, aline, apool)
|
|
||||||
{
|
|
||||||
var line = {};
|
|
||||||
|
|
||||||
// identify list
|
|
||||||
var lineMarker = 0;
|
|
||||||
line.listLevel = 0;
|
|
||||||
if (aline)
|
|
||||||
{
|
|
||||||
var opIter = Changeset.opIterator(aline);
|
|
||||||
if (opIter.hasNext())
|
|
||||||
{
|
|
||||||
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
|
|
||||||
if (listType)
|
|
||||||
{
|
|
||||||
lineMarker = 1;
|
|
||||||
listType = /([a-z]+)([12345678])/.exec(listType);
|
|
||||||
if (listType)
|
|
||||||
{
|
|
||||||
line.listTypeName = listType[1];
|
|
||||||
line.listLevel = Number(listType[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lineMarker)
|
|
||||||
{
|
|
||||||
line.text = text.substring(1);
|
|
||||||
line.aline = Changeset.subattribution(aline, 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
line.text = text;
|
|
||||||
line.aline = aline;
|
|
||||||
}
|
|
||||||
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
|
|
||||||
{
|
|
||||||
padManager.getPad(padId, function (err, pad)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
var head =
|
|
||||||
(noDocType ? '' : '<!doctype html>\n') +
|
|
||||||
'<html lang="en">\n' + (noDocType ? '' : '<head>\n' +
|
|
||||||
'<meta charset="utf-8">\n' +
|
|
||||||
'<style> * { font-family: arial, sans-serif;\n' +
|
|
||||||
'font-size: 13px;\n' +
|
|
||||||
'line-height: 17px; }' +
|
|
||||||
'ul.indent { list-style-type: none; }' +
|
|
||||||
'ol { list-style-type: decimal; }' +
|
|
||||||
'ol ol { list-style-type: lower-latin; }' +
|
|
||||||
'ol ol ol { list-style-type: lower-roman; }' +
|
|
||||||
'ol ol ol ol { list-style-type: decimal; }' +
|
|
||||||
'ol ol ol ol ol { list-style-type: lower-latin; }' +
|
|
||||||
'ol ol ol ol ol ol{ list-style-type: lower-roman; }' +
|
|
||||||
'ol ol ol ol ol ol ol { list-style-type: decimal; }' +
|
|
||||||
'ol ol ol ol ol ol ol ol{ list-style-type: lower-latin; }' +
|
|
||||||
'</style>\n' + '</head>\n') +
|
|
||||||
'<body>';
|
|
||||||
|
|
||||||
var foot = '</body>\n</html>\n';
|
|
||||||
|
|
||||||
getPadHTML(pad, revNum, function (err, html)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
callback(null, head + html + foot);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _encodeWhitespace(s) {
|
|
||||||
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c)
|
|
||||||
{
|
|
||||||
return "&#" +c.charCodeAt(0) + ";"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// copied from ACE
|
|
||||||
|
|
||||||
|
|
||||||
function _processSpaces(s)
|
|
||||||
{
|
|
||||||
var doesWrap = true;
|
|
||||||
if (s.indexOf("<") < 0 && !doesWrap)
|
|
||||||
{
|
|
||||||
// short-cut
|
|
||||||
return s.replace(/ /g, ' ');
|
|
||||||
}
|
|
||||||
var parts = [];
|
|
||||||
s.replace(/<[^>]*>?| |[^ <]+/g, function (m)
|
|
||||||
{
|
|
||||||
parts.push(m);
|
|
||||||
});
|
|
||||||
if (doesWrap)
|
|
||||||
{
|
|
||||||
var endOfLine = true;
|
|
||||||
var beforeSpace = false;
|
|
||||||
// last space in a run is normal, others are nbsp,
|
|
||||||
// end of line is nbsp
|
|
||||||
for (var i = parts.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var p = parts[i];
|
|
||||||
if (p == " ")
|
|
||||||
{
|
|
||||||
if (endOfLine || beforeSpace) parts[i] = ' ';
|
|
||||||
endOfLine = false;
|
|
||||||
beforeSpace = true;
|
|
||||||
}
|
|
||||||
else if (p.charAt(0) != "<")
|
|
||||||
{
|
|
||||||
endOfLine = false;
|
|
||||||
beforeSpace = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// beginning of line is nbsp
|
|
||||||
for (var i = 0; i < parts.length; i++)
|
|
||||||
{
|
|
||||||
var p = parts[i];
|
|
||||||
if (p == " ")
|
|
||||||
{
|
|
||||||
parts[i] = ' ';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (p.charAt(0) != "<")
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var i = 0; i < parts.length; i++)
|
|
||||||
{
|
|
||||||
var p = parts[i];
|
|
||||||
if (p == " ")
|
|
||||||
{
|
|
||||||
parts[i] = ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// copied from ACE
|
|
||||||
var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
|
|
||||||
var _REGEX_SPACE = /\s/;
|
|
||||||
var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')');
|
|
||||||
var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g');
|
|
||||||
|
|
||||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
|
||||||
|
|
||||||
|
|
||||||
function _findURLs(text)
|
|
||||||
{
|
|
||||||
_REGEX_URL.lastIndex = 0;
|
|
||||||
var urls = null;
|
|
||||||
var execResult;
|
|
||||||
while ((execResult = _REGEX_URL.exec(text)))
|
|
||||||
{
|
|
||||||
urls = (urls || []);
|
|
||||||
var startIndex = execResult.index;
|
|
||||||
var url = execResult[0];
|
|
||||||
urls.push([startIndex, url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright Yaco Sistemas S.L. 2011.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var jsdom = require('jsdom-nocontextifiy').jsdom;
|
|
||||||
var log4js = require('log4js');
|
|
||||||
|
|
||||||
|
|
||||||
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
|
|
||||||
var contentcollector = require("ep_etherpad-lite/static/js/contentcollector");
|
|
||||||
|
|
||||||
function setPadHTML(pad, html, callback)
|
|
||||||
{
|
|
||||||
var apiLogger = log4js.getLogger("ImportHtml");
|
|
||||||
|
|
||||||
// Clean the pad. This makes the rest of the code easier
|
|
||||||
// by several orders of magnitude.
|
|
||||||
pad.setText("");
|
|
||||||
var padText = pad.text();
|
|
||||||
|
|
||||||
// Parse the incoming HTML with jsdom
|
|
||||||
var doc = jsdom(html.replace(/>\n+</g, '><'));
|
|
||||||
apiLogger.debug('html:');
|
|
||||||
apiLogger.debug(html);
|
|
||||||
|
|
||||||
// Convert a dom tree into a list of lines and attribute liens
|
|
||||||
// using the content collector object
|
|
||||||
var cc = contentcollector.makeContentCollector(true, null, pad.pool);
|
|
||||||
cc.collectContent(doc.childNodes[0]);
|
|
||||||
var result = cc.finish();
|
|
||||||
apiLogger.debug('Lines:');
|
|
||||||
var i;
|
|
||||||
for (i = 0; i < result.lines.length; i += 1)
|
|
||||||
{
|
|
||||||
apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]);
|
|
||||||
apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the new plain text and its attributes
|
|
||||||
var newText = result.lines.join('\n');
|
|
||||||
apiLogger.debug('newText:');
|
|
||||||
apiLogger.debug(newText);
|
|
||||||
var newAttribs = result.lineAttribs.join('|1+1') + '|1+1';
|
|
||||||
|
|
||||||
function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ )
|
|
||||||
{
|
|
||||||
var attribsIter = Changeset.opIterator(attribs);
|
|
||||||
var textIndex = 0;
|
|
||||||
var newTextStart = 0;
|
|
||||||
var newTextEnd = newText.length;
|
|
||||||
while (attribsIter.hasNext())
|
|
||||||
{
|
|
||||||
var op = attribsIter.next();
|
|
||||||
var nextIndex = textIndex + op.chars;
|
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd))
|
|
||||||
{
|
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
|
||||||
}
|
|
||||||
textIndex = nextIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new changeset with a helper builder object
|
|
||||||
var builder = Changeset.builder(1);
|
|
||||||
|
|
||||||
// assemble each line into the builder
|
|
||||||
eachAttribRun(newAttribs, function(start, end, attribs)
|
|
||||||
{
|
|
||||||
builder.insert(newText.substring(start, end), attribs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// the changeset is ready!
|
|
||||||
var theChangeset = builder.toString();
|
|
||||||
apiLogger.debug('The changeset: ' + theChangeset);
|
|
||||||
pad.appendRevision(theChangeset);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setPadHTML = setPadHTML;
|
|
|
@ -1,321 +0,0 @@
|
||||||
/**
|
|
||||||
* This Module manages all /minified/* requests. It controls the
|
|
||||||
* minification && compression of Javascript and CSS.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ERR = require("async-stacktrace");
|
|
||||||
var settings = require('./Settings');
|
|
||||||
var async = require('async');
|
|
||||||
var fs = require('fs');
|
|
||||||
var cleanCSS = require('clean-css');
|
|
||||||
var jsp = require("uglify-js").parser;
|
|
||||||
var pro = require("uglify-js").uglify;
|
|
||||||
var path = require('path');
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
|
|
||||||
var RequireKernel = require('require-kernel');
|
|
||||||
|
|
||||||
var ROOT_DIR = path.normalize(__dirname + "/../../static/");
|
|
||||||
var TAR_PATH = path.join(__dirname, 'tar.json');
|
|
||||||
var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
|
|
||||||
|
|
||||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
|
||||||
var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js';
|
|
||||||
exports.tar = {};
|
|
||||||
for (var key in tar) {
|
|
||||||
exports.tar[LIBRARY_PREFIX + '/' + key] =
|
|
||||||
tar[key].map(function (p) {return LIBRARY_PREFIX + '/' + p}).concat(
|
|
||||||
tar[key].map(function (p) {
|
|
||||||
return LIBRARY_PREFIX + '/' + p.replace(/\.js$/, '')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates the minifed javascript for the given minified name
|
|
||||||
* @param req the Express request
|
|
||||||
* @param res the Express response
|
|
||||||
*/
|
|
||||||
exports.minify = function(req, res, next)
|
|
||||||
{
|
|
||||||
var filename = req.params['filename'];
|
|
||||||
|
|
||||||
// No relative paths, especially if they may go up the file hierarchy.
|
|
||||||
filename = path.normalize(path.join(ROOT_DIR, filename));
|
|
||||||
if (filename.indexOf(ROOT_DIR) == 0) {
|
|
||||||
filename = filename.slice(ROOT_DIR.length);
|
|
||||||
filename = filename.replace(/\\/g, '/'); // Windows (safe generally?)
|
|
||||||
} else {
|
|
||||||
res.writeHead(404, {});
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle static files for plugins:
|
|
||||||
paths like "plugins/ep_myplugin/static/js/test.js"
|
|
||||||
are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
|
|
||||||
commonly ETHERPAD_ROOT/node_modules/ep_myplugin/static/js/test.js
|
|
||||||
*/
|
|
||||||
var match = filename.match(/^plugins\/([^\/]+)\/static\/(.*)/);
|
|
||||||
if (match) {
|
|
||||||
var pluginName = match[1];
|
|
||||||
var resourcePath = match[2];
|
|
||||||
var plugin = plugins.plugins[pluginName];
|
|
||||||
if (plugin) {
|
|
||||||
var pluginPath = plugin.package.realPath;
|
|
||||||
filename = path.relative(ROOT_DIR, pluginPath + '/static/' + resourcePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// What content type should this be?
|
|
||||||
// TODO: This should use a MIME module.
|
|
||||||
var contentType;
|
|
||||||
if (filename.match(/\.js$/)) {
|
|
||||||
contentType = "text/javascript";
|
|
||||||
} else if (filename.match(/\.css$/)) {
|
|
||||||
contentType = "text/css";
|
|
||||||
} else if (filename.match(/\.html$/)) {
|
|
||||||
contentType = "text/html";
|
|
||||||
} else if (filename.match(/\.txt$/)) {
|
|
||||||
contentType = "text/plain";
|
|
||||||
} else if (filename.match(/\.png$/)) {
|
|
||||||
contentType = "image/png";
|
|
||||||
} else if (filename.match(/\.gif$/)) {
|
|
||||||
contentType = "image/gif";
|
|
||||||
} else if (filename.match(/\.ico$/)) {
|
|
||||||
contentType = "image/x-icon";
|
|
||||||
} else {
|
|
||||||
contentType = "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
statFile(filename, function (error, date, exists) {
|
|
||||||
if (date) {
|
|
||||||
date = new Date(date);
|
|
||||||
res.setHeader('last-modified', date.toUTCString());
|
|
||||||
res.setHeader('date', (new Date()).toUTCString());
|
|
||||||
if (settings.maxAge !== undefined) {
|
|
||||||
var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000);
|
|
||||||
res.setHeader('expires', expiresDate.toUTCString());
|
|
||||||
res.setHeader('cache-control', 'max-age=' + settings.maxAge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
res.writeHead(500, {});
|
|
||||||
res.end();
|
|
||||||
} else if (!exists) {
|
|
||||||
res.writeHead(404, {});
|
|
||||||
res.end();
|
|
||||||
} else if (new Date(req.headers['if-modified-since']) >= date) {
|
|
||||||
res.writeHead(304, {});
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
if (req.method == 'HEAD') {
|
|
||||||
res.header("Content-Type", contentType);
|
|
||||||
res.writeHead(200, {});
|
|
||||||
res.end();
|
|
||||||
} else if (req.method == 'GET') {
|
|
||||||
getFileCompressed(filename, contentType, function (error, content) {
|
|
||||||
if(ERR(error, function(){
|
|
||||||
res.writeHead(500, {});
|
|
||||||
res.end();
|
|
||||||
})) return;
|
|
||||||
res.header("Content-Type", contentType);
|
|
||||||
res.writeHead(200, {});
|
|
||||||
res.write(content);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.writeHead(405, {'allow': 'HEAD, GET'});
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// find all includes in ace.js and embed them.
|
|
||||||
function getAceFile(callback) {
|
|
||||||
fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) {
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
// Find all includes in ace.js and embed them
|
|
||||||
var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi);
|
|
||||||
if (!settings.minify) {
|
|
||||||
founds = [];
|
|
||||||
}
|
|
||||||
// Always include the require kernel.
|
|
||||||
founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")');
|
|
||||||
|
|
||||||
data += ';\n';
|
|
||||||
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
|
|
||||||
|
|
||||||
// Request the contents of the included file on the server-side and write
|
|
||||||
// them into the file.
|
|
||||||
async.forEach(founds, function (item, callback) {
|
|
||||||
var filename = item.match(/"([^"]*)"/)[1];
|
|
||||||
var request = require('request');
|
|
||||||
|
|
||||||
var baseURI = 'http://localhost:' + settings.port
|
|
||||||
var resourceURI = baseURI + path.normalize(path.join('/static/', filename));
|
|
||||||
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
|
|
||||||
|
|
||||||
request(resourceURI, function (error, response, body) {
|
|
||||||
if (!error && response.statusCode == 200) {
|
|
||||||
data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = '
|
|
||||||
+ JSON.stringify(body || '') + ';\n';
|
|
||||||
} else {
|
|
||||||
// Silence?
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, function(error) {
|
|
||||||
callback(error, data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the existance of the file and get the last modification date.
|
|
||||||
function statFile(filename, callback) {
|
|
||||||
if (filename == 'js/ace.js') {
|
|
||||||
// Sometimes static assets are inlined into this file, so we have to stat
|
|
||||||
// everything.
|
|
||||||
lastModifiedDateOfEverything(function (error, date) {
|
|
||||||
callback(error, date, !error);
|
|
||||||
});
|
|
||||||
} else if (filename == 'js/require-kernel.js') {
|
|
||||||
callback(null, requireLastModified(), true);
|
|
||||||
} else {
|
|
||||||
fs.stat(ROOT_DIR + filename, function (error, stats) {
|
|
||||||
if (error) {
|
|
||||||
if (error.code == "ENOENT") {
|
|
||||||
// Stat the directory instead.
|
|
||||||
fs.stat(path.dirname(ROOT_DIR + filename), function (error, stats) {
|
|
||||||
if (error) {
|
|
||||||
if (error.code == "ENOENT") {
|
|
||||||
callback(null, null, false);
|
|
||||||
} else {
|
|
||||||
callback(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback(null, stats.mtime.getTime(), false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback(null, stats.mtime.getTime(), true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function lastModifiedDateOfEverything(callback) {
|
|
||||||
var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/'];
|
|
||||||
var latestModification = 0;
|
|
||||||
//go trough this two folders
|
|
||||||
async.forEach(folders2check, function(path, callback)
|
|
||||||
{
|
|
||||||
//read the files in the folder
|
|
||||||
fs.readdir(path, function(err, files)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//we wanna check the directory itself for changes too
|
|
||||||
files.push(".");
|
|
||||||
|
|
||||||
//go trough all files in this folder
|
|
||||||
async.forEach(files, function(filename, callback)
|
|
||||||
{
|
|
||||||
//get the stat data of this file
|
|
||||||
fs.stat(path + "/" + filename, function(err, stats)
|
|
||||||
{
|
|
||||||
if(ERR(err, callback)) return;
|
|
||||||
|
|
||||||
//get the modification time
|
|
||||||
var modificationTime = stats.mtime.getTime();
|
|
||||||
|
|
||||||
//compare the modification time to the highest found
|
|
||||||
if(modificationTime > latestModification)
|
|
||||||
{
|
|
||||||
latestModification = modificationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
});
|
|
||||||
}, function () {
|
|
||||||
callback(null, latestModification);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should be provided by the module, but until then, just use startup
|
|
||||||
// time.
|
|
||||||
var _requireLastModified = new Date();
|
|
||||||
function requireLastModified() {
|
|
||||||
return _requireLastModified.toUTCString();
|
|
||||||
}
|
|
||||||
function requireDefinition() {
|
|
||||||
return 'var require = ' + RequireKernel.kernelSource + ';\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileCompressed(filename, contentType, callback) {
|
|
||||||
getFile(filename, function (error, content) {
|
|
||||||
if (error || !content) {
|
|
||||||
callback(error, content);
|
|
||||||
} else {
|
|
||||||
if (settings.minify) {
|
|
||||||
if (contentType == 'text/javascript') {
|
|
||||||
try {
|
|
||||||
content = compressJS([content]);
|
|
||||||
} catch (error) {
|
|
||||||
// silence
|
|
||||||
}
|
|
||||||
} else if (contentType == 'text/css') {
|
|
||||||
content = compressCSS([content]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback(null, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFile(filename, callback) {
|
|
||||||
if (filename == 'js/ace.js') {
|
|
||||||
getAceFile(callback);
|
|
||||||
} else if (filename == 'js/require-kernel.js') {
|
|
||||||
callback(undefined, requireDefinition());
|
|
||||||
} else {
|
|
||||||
fs.readFile(ROOT_DIR + filename, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function compressJS(values)
|
|
||||||
{
|
|
||||||
var complete = values.join("\n");
|
|
||||||
var ast = jsp.parse(complete); // parse code and get the initial AST
|
|
||||||
ast = pro.ast_mangle(ast); // get a new AST with mangled names
|
|
||||||
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
|
|
||||||
return pro.gen_code(ast); // compressed code here
|
|
||||||
}
|
|
||||||
|
|
||||||
function compressCSS(values)
|
|
||||||
{
|
|
||||||
var complete = values.join("\n");
|
|
||||||
return cleanCSS.process(complete);
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* The Settings Modul reads the settings out of settings.json and provides
|
|
||||||
* this information to the other modules
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var fs = require("fs");
|
|
||||||
var os = require("os");
|
|
||||||
var path = require('path');
|
|
||||||
var argv = require('./Cli').argv;
|
|
||||||
var npm = require("npm/lib/npm.js");
|
|
||||||
|
|
||||||
/* Root path of the installation */
|
|
||||||
exports.root = path.normalize(path.join(npm.dir, ".."));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The IP ep-lite should listen to
|
|
||||||
*/
|
|
||||||
exports.ip = "0.0.0.0";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Port ep-lite should listen to
|
|
||||||
*/
|
|
||||||
exports.port = 9001;
|
|
||||||
/*
|
|
||||||
* The Type of the database
|
|
||||||
*/
|
|
||||||
exports.dbType = "dirty";
|
|
||||||
/**
|
|
||||||
* This setting is passed with dbType to ueberDB to set up the database
|
|
||||||
*/
|
|
||||||
exports.dbSettings = { "filename" : path.join(exports.root, "dirty.db") };
|
|
||||||
/**
|
|
||||||
* The default Text of a new pad
|
|
||||||
*/
|
|
||||||
exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A flag that requires any user to have a valid session (via the api) before accessing a pad
|
|
||||||
*/
|
|
||||||
exports.requireSession = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A flag that prevents users from creating new pads
|
|
||||||
*/
|
|
||||||
exports.editOnly = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Max age that responses will have (affects caching layer).
|
|
||||||
*/
|
|
||||||
exports.maxAge = 1000*60*60*6; // 6 hours
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A flag that shows if minification is enabled or not
|
|
||||||
*/
|
|
||||||
exports.minify = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The path of the abiword executable
|
|
||||||
*/
|
|
||||||
exports.abiword = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The log level of log4js
|
|
||||||
*/
|
|
||||||
exports.loglevel = "INFO";
|
|
||||||
|
|
||||||
/* This setting is used if you need authentication and/or
|
|
||||||
* authorization. Note: /admin always requires authentication, and
|
|
||||||
* either authorization by a module, or a user with is_admin set */
|
|
||||||
exports.requireAuthentication = false;
|
|
||||||
exports.requireAuthorization = false;
|
|
||||||
exports.users = {};
|
|
||||||
|
|
||||||
//checks if abiword is avaiable
|
|
||||||
exports.abiwordAvailable = function()
|
|
||||||
{
|
|
||||||
if(exports.abiword != null)
|
|
||||||
{
|
|
||||||
return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return "no";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover where the settings file lives
|
|
||||||
var settingsFilename = argv.settings || "settings.json";
|
|
||||||
if (settingsFilename.charAt(0) != '/') {
|
|
||||||
settingsFilename = path.normalize(path.join(root, settingsFilename));
|
|
||||||
}
|
|
||||||
|
|
||||||
var settingsStr
|
|
||||||
try{
|
|
||||||
//read the settings sync
|
|
||||||
settingsStr = fs.readFileSync(settingsFilename).toString();
|
|
||||||
} catch(e){
|
|
||||||
console.warn('No settings file found. Using defaults.');
|
|
||||||
settingsStr = '{}';
|
|
||||||
}
|
|
||||||
|
|
||||||
//remove all comments
|
|
||||||
settingsStr = settingsStr.replace(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/gm,"").replace(/#.*/g,"").replace(/\/\/.*/g,"");
|
|
||||||
|
|
||||||
//try to parse the settings
|
|
||||||
var settings;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
settings = JSON.parse(settingsStr);
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
console.error("There is a syntax error in your settings.json file");
|
|
||||||
console.error(e.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop trough the settings
|
|
||||||
for(var i in settings)
|
|
||||||
{
|
|
||||||
//test if the setting start with a low character
|
|
||||||
if(i.charAt(0).search("[a-z]") !== 0)
|
|
||||||
{
|
|
||||||
console.warn("Settings should start with a low character: '" + i + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
//we know this setting, so we overwrite it
|
|
||||||
if(exports[i] !== undefined)
|
|
||||||
{
|
|
||||||
exports[i] = settings[i];
|
|
||||||
}
|
|
||||||
//this setting is unkown, output a warning and throw it away
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.warn("Unkown Setting: '" + i + "'");
|
|
||||||
console.warn("This setting doesn't exist or it was removed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(exports.dbType === "dirty"){
|
|
||||||
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
/*
|
|
||||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var async = require('async');
|
|
||||||
var Buffer = require('buffer').Buffer;
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var zlib = require('zlib');
|
|
||||||
var util = require('util');
|
|
||||||
var settings = require('./Settings');
|
|
||||||
|
|
||||||
var CACHE_DIR = path.normalize(path.join(settings.root, 'var/'));
|
|
||||||
CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
|
||||||
|
|
||||||
var responseCache = {};
|
|
||||||
|
|
||||||
/*
|
|
||||||
This caches and compresses 200 and 404 responses to GET and HEAD requests.
|
|
||||||
TODO: Caching and compressing are solved problems, a middleware configuration
|
|
||||||
should replace this.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function CachingMiddleware() {
|
|
||||||
}
|
|
||||||
CachingMiddleware.prototype = new function () {
|
|
||||||
function handle(req, res, next) {
|
|
||||||
if (!(req.method == "GET" || req.method == "HEAD") || !CACHE_DIR) {
|
|
||||||
return next(undefined, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
var old_req = {};
|
|
||||||
var old_res = {};
|
|
||||||
|
|
||||||
var supportsGzip =
|
|
||||||
req.header('Accept-Encoding', '').indexOf('gzip') != -1;
|
|
||||||
|
|
||||||
var path = require('url').parse(req.url).path;
|
|
||||||
var cacheKey = (new Buffer(path)).toString('base64').replace(/[\/\+=]/g, '');
|
|
||||||
|
|
||||||
fs.stat(CACHE_DIR + 'minified_' + cacheKey, function (error, stats) {
|
|
||||||
var modifiedSince = (req.headers['if-modified-since']
|
|
||||||
&& new Date(req.headers['if-modified-since']));
|
|
||||||
var lastModifiedCache = !error && stats.mtime;
|
|
||||||
if (lastModifiedCache && responseCache[cacheKey]) {
|
|
||||||
req.headers['if-modified-since'] = lastModifiedCache.toUTCString();
|
|
||||||
} else {
|
|
||||||
delete req.headers['if-modified-since'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always issue get to downstream.
|
|
||||||
old_req.method = req.method;
|
|
||||||
req.method = 'GET';
|
|
||||||
|
|
||||||
var expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {})['expires']);
|
|
||||||
if (expirationDate > new Date()) {
|
|
||||||
// Our cached version is still valid.
|
|
||||||
return respond();
|
|
||||||
}
|
|
||||||
|
|
||||||
var _headers = {};
|
|
||||||
old_res.setHeader = res.setHeader;
|
|
||||||
res.setHeader = function (key, value) {
|
|
||||||
// Don't set cookies, see issue #707
|
|
||||||
if (key.toLowerCase() === 'set-cookie') return;
|
|
||||||
|
|
||||||
_headers[key.toLowerCase()] = value;
|
|
||||||
old_res.setHeader.call(res, key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
old_res.writeHead = res.writeHead;
|
|
||||||
res.writeHead = function (status, headers) {
|
|
||||||
var lastModified = (res.getHeader('last-modified')
|
|
||||||
&& new Date(res.getHeader('last-modified')));
|
|
||||||
|
|
||||||
res.writeHead = old_res.writeHead;
|
|
||||||
if (status == 200) {
|
|
||||||
// Update cache
|
|
||||||
var buffer = '';
|
|
||||||
|
|
||||||
Object.keys(headers || {}).forEach(function (key) {
|
|
||||||
res.setHeader(key, headers[key]);
|
|
||||||
});
|
|
||||||
headers = _headers;
|
|
||||||
|
|
||||||
old_res.write = res.write;
|
|
||||||
old_res.end = res.end;
|
|
||||||
res.write = function(data, encoding) {
|
|
||||||
buffer += data.toString(encoding);
|
|
||||||
};
|
|
||||||
res.end = function(data, encoding) {
|
|
||||||
async.parallel([
|
|
||||||
function (callback) {
|
|
||||||
var path = CACHE_DIR + 'minified_' + cacheKey;
|
|
||||||
fs.writeFile(path, buffer, function (error, stats) {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, function (callback) {
|
|
||||||
var path = CACHE_DIR + 'minified_' + cacheKey + '.gz';
|
|
||||||
zlib.gzip(buffer, function(error, content) {
|
|
||||||
if (error) {
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
fs.writeFile(path, content, function (error, stats) {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function () {
|
|
||||||
responseCache[cacheKey] = {statusCode: status, headers: headers};
|
|
||||||
respond();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else if (status == 304) {
|
|
||||||
// Nothing new changed from the cached version.
|
|
||||||
old_res.write = res.write;
|
|
||||||
old_res.end = res.end;
|
|
||||||
res.write = function(data, encoding) {};
|
|
||||||
res.end = function(data, encoding) { respond() };
|
|
||||||
} else {
|
|
||||||
res.writeHead(status, headers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
next(undefined, req, res);
|
|
||||||
|
|
||||||
// This handles read/write synchronization as well as its predecessor,
|
|
||||||
// which is to say, not at all.
|
|
||||||
// TODO: Implement locking on write or ditch caching of gzip and use
|
|
||||||
// existing middlewares.
|
|
||||||
function respond() {
|
|
||||||
req.method = old_req.method || req.method;
|
|
||||||
res.write = old_res.write || res.write;
|
|
||||||
res.end = old_res.end || res.end;
|
|
||||||
|
|
||||||
var headers = responseCache[cacheKey].headers;
|
|
||||||
var statusCode = responseCache[cacheKey].statusCode;
|
|
||||||
|
|
||||||
var pathStr = CACHE_DIR + 'minified_' + cacheKey;
|
|
||||||
if (supportsGzip && (headers['content-type'] || '').match(/^text\//)) {
|
|
||||||
pathStr = pathStr + '.gz';
|
|
||||||
headers['content-encoding'] = 'gzip';
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastModified = (headers['last-modified']
|
|
||||||
&& new Date(headers['last-modified']));
|
|
||||||
|
|
||||||
if (statusCode == 200 && lastModified <= modifiedSince) {
|
|
||||||
res.writeHead(304, headers);
|
|
||||||
res.end();
|
|
||||||
} else if (req.method == 'GET') {
|
|
||||||
var readStream = fs.createReadStream(pathStr);
|
|
||||||
res.writeHead(statusCode, headers);
|
|
||||||
util.pump(readStream, res);
|
|
||||||
} else {
|
|
||||||
res.writeHead(statusCode, headers);
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handle = handle;
|
|
||||||
}();
|
|
||||||
|
|
||||||
module.exports = CachingMiddleware;
|
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
This helper modules allows us to create different type of errors we can throw
|
|
||||||
*/
|
|
||||||
function customError(message, errorName)
|
|
||||||
{
|
|
||||||
this.name = errorName || "Error";
|
|
||||||
this.message = message;
|
|
||||||
|
|
||||||
var stackParts = new Error().stack.split("\n");
|
|
||||||
stackParts.splice(0,2);
|
|
||||||
stackParts.unshift(this.name + ": " + message);
|
|
||||||
|
|
||||||
this.stack = stackParts.join("\n");
|
|
||||||
}
|
|
||||||
customError.prototype = Error.prototype;
|
|
||||||
|
|
||||||
module.exports = customError;
|
|
|
@ -1,16 +0,0 @@
|
||||||
/**
|
|
||||||
* Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids
|
|
||||||
*/
|
|
||||||
var randomString = function randomString(len)
|
|
||||||
{
|
|
||||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
||||||
var randomstring = '';
|
|
||||||
for (var i = 0; i < len; i++)
|
|
||||||
{
|
|
||||||
var rnum = Math.floor(Math.random() * chars.length);
|
|
||||||
randomstring += chars.substring(rnum, rnum + 1);
|
|
||||||
}
|
|
||||||
return randomstring;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = randomString;
|
|
|
@ -1,69 +0,0 @@
|
||||||
{
|
|
||||||
"pad.js": [
|
|
||||||
"pad.js"
|
|
||||||
, "pad_utils.js"
|
|
||||||
, "pad_cookie.js"
|
|
||||||
, "pad_editor.js"
|
|
||||||
, "pad_editbar.js"
|
|
||||||
, "pad_docbar.js"
|
|
||||||
, "pad_modals.js"
|
|
||||||
, "ace.js"
|
|
||||||
, "collab_client.js"
|
|
||||||
, "pad_userlist.js"
|
|
||||||
, "pad_impexp.js"
|
|
||||||
, "pad_savedrevs.js"
|
|
||||||
, "pad_connectionstatus.js"
|
|
||||||
, "chat.js"
|
|
||||||
, "excanvas.js"
|
|
||||||
, "farbtastic.js"
|
|
||||||
]
|
|
||||||
, "timeslider.js": [
|
|
||||||
"timeslider.js"
|
|
||||||
, "colorutils.js"
|
|
||||||
, "draggable.js"
|
|
||||||
, "pad_utils.js"
|
|
||||||
, "pad_cookie.js"
|
|
||||||
, "pad_editor.js"
|
|
||||||
, "pad_editbar.js"
|
|
||||||
, "pad_docbar.js"
|
|
||||||
, "pad_modals.js"
|
|
||||||
, "pad_savedrevs.js"
|
|
||||||
, "pad_impexp.js"
|
|
||||||
, "AttributePool.js"
|
|
||||||
, "Changeset.js"
|
|
||||||
, "domline.js"
|
|
||||||
, "linestylefilter.js"
|
|
||||||
, "cssmanager.js"
|
|
||||||
, "broadcast.js"
|
|
||||||
, "broadcast_slider.js"
|
|
||||||
, "broadcast_revisions.js"
|
|
||||||
]
|
|
||||||
, "ace2_inner.js": [
|
|
||||||
"ace2_inner.js"
|
|
||||||
, "AttributePool.js"
|
|
||||||
, "Changeset.js"
|
|
||||||
, "ChangesetUtils.js"
|
|
||||||
, "skiplist.js"
|
|
||||||
, "virtual_lines.js"
|
|
||||||
, "cssmanager.js"
|
|
||||||
, "colorutils.js"
|
|
||||||
, "undomodule.js"
|
|
||||||
, "contentcollector.js"
|
|
||||||
, "changesettracker.js"
|
|
||||||
, "linestylefilter.js"
|
|
||||||
, "domline.js"
|
|
||||||
, "AttributeManager.js"
|
|
||||||
]
|
|
||||||
, "ace2_common.js": [
|
|
||||||
"ace2_common.js"
|
|
||||||
, "jquery.js"
|
|
||||||
, "rjquery.js"
|
|
||||||
, "underscore.js"
|
|
||||||
, "security.js"
|
|
||||||
, "json2.js"
|
|
||||||
, "pluginfw/plugins.js"
|
|
||||||
, "pluginfw/hooks.js"
|
|
||||||
, "pluginfw/async.js"
|
|
||||||
, "pluginfw/parent_require.js"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"name" : "ep_etherpad-lite",
|
|
||||||
"description" : "A Etherpad based on node.js",
|
|
||||||
"homepage" : "https://github.com/Pita/etherpad-lite",
|
|
||||||
"keywords" : ["etherpad", "realtime", "collaborative", "editor"],
|
|
||||||
"author" : "Peter 'Pita' Martischka <petermartischka@googlemail.com> - Primary Technology Ltd",
|
|
||||||
"contributors" : [
|
|
||||||
{ "name": "John McLear",
|
|
||||||
"name": "Hans Pinckaers",
|
|
||||||
"name": "Robin Buse" }
|
|
||||||
],
|
|
||||||
"dependencies" : {
|
|
||||||
"yajsml" : "1.1.3",
|
|
||||||
"request" : "2.9.100",
|
|
||||||
"require-kernel" : "1.0.5",
|
|
||||||
"resolve" : "0.2.1",
|
|
||||||
"socket.io" : "0.9.6",
|
|
||||||
"ueberDB" : "0.1.7",
|
|
||||||
"async" : "0.1.18",
|
|
||||||
"express" : "2.5.8",
|
|
||||||
"connect" : "1.8.7",
|
|
||||||
"clean-css" : "0.3.2",
|
|
||||||
"uglify-js" : "1.2.5",
|
|
||||||
"formidable" : "1.0.9",
|
|
||||||
"log4js" : "0.4.1",
|
|
||||||
"jsdom-nocontextifiy" : "0.2.10",
|
|
||||||
"async-stacktrace" : "0.0.2",
|
|
||||||
"npm" : "1.1.24",
|
|
||||||
"ejs" : "0.6.1",
|
|
||||||
"graceful-fs" : "1.1.5",
|
|
||||||
"slide" : "1.1.3",
|
|
||||||
"semver" : "1.0.13",
|
|
||||||
"underscore" : "1.3.1"
|
|
||||||
},
|
|
||||||
"bin": { "etherpad-lite": "./node/server.js" },
|
|
||||||
"devDependencies": {
|
|
||||||
"jshint" : "*"
|
|
||||||
},
|
|
||||||
"engines" : { "node" : ">=0.6.0",
|
|
||||||
"npm" : ">=1.0"
|
|
||||||
},
|
|
||||||
"version" : "1.0.0"
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font: 14px helvetica, sans-serif;
|
|
||||||
background: #ddd;
|
|
||||||
background: -webkit-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
|
||||||
background: -moz-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
|
||||||
background: -ms-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
|
||||||
background: -o-radial-gradient(circle,#aaa,#eee 60%) center fixed;
|
|
||||||
border-top: 8px solid rgba(51,51,51,.8);
|
|
||||||
}
|
|
||||||
#wrapper {
|
|
||||||
margin-top: 160px;
|
|
||||||
padding: 15px;
|
|
||||||
background: #fff;
|
|
||||||
opacity: .9;
|
|
||||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.3);
|
|
||||||
max-width: 700px;
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 0 0 7px 7px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 29px;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.separator {
|
|
||||||
margin: 10px 0;
|
|
||||||
height: 1px;
|
|
||||||
background: #aaa;
|
|
||||||
background: -webkit-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
||||||
background: -moz-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
||||||
background: -ms-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
||||||
background: -o-linear-gradient(left, #fff, #aaa 20%, #aaa 80%, #fff);
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
#inner {
|
|
||||||
width: 300px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
input[type="button"] {
|
|
||||||
padding: 4px 6px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
input[type="button"].do-install, input[type="button"].do-uninstall {
|
|
||||||
float: right;
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
input[type="button"]#do-search {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
input[type="text"] {
|
|
||||||
border-radius: 3px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
padding: 10px;
|
|
||||||
*padding: 0; /* IE7 hack */
|
|
||||||
width: 100%;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 3px;
|
|
||||||
border-spacing: 0;
|
|
||||||
width: 100%;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
table thead tr {
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.template {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.dialog {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 700px;
|
|
||||||
height: 500px;
|
|
||||||
margin-left: -350px;
|
|
||||||
margin-top: -250px;
|
|
||||||
border: 3px solid #999;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
.dialog .title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 2px;
|
|
||||||
border-bottom: 3px solid #999;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
height: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.dialog .title .close {
|
|
||||||
float: right;
|
|
||||||
padding: 1px 10px;
|
|
||||||
}
|
|
||||||
.dialog .history {
|
|
||||||
background: #222;
|
|
||||||
color: #eee;
|
|
||||||
position: absolute;
|
|
||||||
top: 41px;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 2px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
|
|
||||||
/* These CSS rules are included in both the outer and inner ACE iframe.
|
|
||||||
Also see inner.css, included only in the inner one.
|
|
||||||
*/
|
|
||||||
html { cursor: text; } /* in Safari, produces text cursor for whole doc (inc. below body) */
|
|
||||||
span { cursor: auto; }
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #acf;
|
|
||||||
}
|
|
||||||
::-moz-selection {
|
|
||||||
background: #acf;
|
|
||||||
}
|
|
||||||
|
|
||||||
a { cursor: pointer !important; }
|
|
||||||
|
|
||||||
ul, ol, li {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
ul { margin-left: 1.5em; }
|
|
||||||
ul ul { margin-left: 0 !important; }
|
|
||||||
ul.list-bullet1 { margin-left: 1.5em; }
|
|
||||||
ul.list-bullet2 { margin-left: 3em; }
|
|
||||||
ul.list-bullet3 { margin-left: 4.5em; }
|
|
||||||
ul.list-bullet4 { margin-left: 6em; }
|
|
||||||
ul.list-bullet5 { margin-left: 7.5em; }
|
|
||||||
ul.list-bullet6 { margin-left: 9em; }
|
|
||||||
ul.list-bullet7 { margin-left: 10.5em; }
|
|
||||||
ul.list-bullet8 { margin-left: 12em; }
|
|
||||||
|
|
||||||
ul { list-style-type: disc; }
|
|
||||||
ul.list-bullet1 { list-style-type: disc; }
|
|
||||||
ul.list-bullet2 { list-style-type: circle; }
|
|
||||||
ul.list-bullet3 { list-style-type: square; }
|
|
||||||
ul.list-bullet4 { list-style-type: disc; }
|
|
||||||
ul.list-bullet5 { list-style-type: circle; }
|
|
||||||
ul.list-bullet6 { list-style-type: square; }
|
|
||||||
ul.list-bullet7 { list-style-type: disc; }
|
|
||||||
ul.list-bullet8 { list-style-type: circle; }
|
|
||||||
|
|
||||||
ol.list-number1 { margin-left: 1.5em; }
|
|
||||||
ol.list-number2 { margin-left: 3em; }
|
|
||||||
ol.list-number3 { margin-left: 4.5em; }
|
|
||||||
ol.list-number4 { margin-left: 6em; }
|
|
||||||
ol.list-number5 { margin-left: 7.5em; }
|
|
||||||
ol.list-number6 { margin-left: 9em; }
|
|
||||||
ol.list-number7 { margin-left: 10.5em; }
|
|
||||||
ol.list-number8 { margin-left: 12em; }
|
|
||||||
|
|
||||||
ol { list-style-type: decimal; }
|
|
||||||
ol.list-number1 { list-style-type: decimal; }
|
|
||||||
ol.list-number2 { list-style-type: lower-latin; }
|
|
||||||
ol.list-number3 { list-style-type: lower-roman; }
|
|
||||||
ol.list-number4 { list-style-type: decimal; }
|
|
||||||
ol.list-number5 { list-style-type: lower-latin; }
|
|
||||||
ol.list-number6 { list-style-type: lower-roman; }
|
|
||||||
ol.list-number7 { list-style-type: decimal; }
|
|
||||||
ol.list-number8 { list-style-type: lower-latin; }
|
|
||||||
|
|
||||||
ul.list-indent1 { margin-left: 1.5em; }
|
|
||||||
ul.list-indent2 { margin-left: 3em; }
|
|
||||||
ul.list-indent3 { margin-left: 4.5em; }
|
|
||||||
ul.list-indent4 { margin-left: 6em; }
|
|
||||||
ul.list-indent5 { margin-left: 7.5em; }
|
|
||||||
ul.list-indent6 { margin-left: 9em; }
|
|
||||||
ul.list-indent7 { margin-left: 10.5em; }
|
|
||||||
ul.list-indent8 { margin-left: 12em; }
|
|
||||||
|
|
||||||
ul.list-indent1 { list-style-type: none; }
|
|
||||||
ul.list-indent2 { list-style-type: none; }
|
|
||||||
ul.list-indent3 { list-style-type: none; }
|
|
||||||
ul.list-indent4 { list-style-type: none; }
|
|
||||||
ul.list-indent5 { list-style-type: none; }
|
|
||||||
ul.list-indent6 { list-style-type: none; }
|
|
||||||
ul.list-indent7 { list-style-type: none; }
|
|
||||||
ul.list-indent8 { list-style-type: none; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#outerdocbody {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
body.grayedout { background-color: #eee !important }
|
|
||||||
|
|
||||||
#innerdocbody {
|
|
||||||
font-size: 12px; /* overridden by body.style */
|
|
||||||
font-family: monospace; /* overridden by body.style */
|
|
||||||
line-height: 16px; /* overridden by body.style */
|
|
||||||
}
|
|
||||||
|
|
||||||
body.doesWrap {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
#innerdocbody {
|
|
||||||
padding-top: 1px; /* important for some reason? */
|
|
||||||
padding-right: 10px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
padding-left: 1px /* prevents characters from looking chopped off in FF3 -- Removed because it added too much whitespace */;
|
|
||||||
overflow: hidden;
|
|
||||||
/* blank 1x1 gif, so that IE8 doesn't consider the body transparent */
|
|
||||||
background-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==);
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidediv {
|
|
||||||
font-size: 11px;
|
|
||||||
font-family: monospace;
|
|
||||||
line-height: 16px; /* overridden by sideDiv.style */
|
|
||||||
padding-top: 8px; /* EDIT_BODY_PADDING_TOP */
|
|
||||||
padding-right: 3px; /* LINE_NUMBER_PADDING_RIGHT - 1 */
|
|
||||||
position: absolute;
|
|
||||||
width: 20px; /* MIN_LINEDIV_WIDTH */
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
cursor: default;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidedivinner {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidedivdelayed { /* class set after sizes are set */
|
|
||||||
background-color: #eee;
|
|
||||||
color: #888 !important;
|
|
||||||
border-right: 1px solid #999;
|
|
||||||
}
|
|
||||||
.sidedivhidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#outerdocbody iframe {
|
|
||||||
display: block; /* codemirror says it suppresses bugs */
|
|
||||||
position: relative;
|
|
||||||
left: 32px; /* MIN_LINEDIV_WIDTH + LINE_NUMBER_PADDING_RIGHT + EDIT_BODY_PADDING_LEFT */
|
|
||||||
top: 7px; /* EDIT_BODY_PADDING_TOP - 1*/
|
|
||||||
border: 0;
|
|
||||||
width: 1px; /* changed programmatically */
|
|
||||||
height: 1px; /* changed programmatically */
|
|
||||||
}
|
|
||||||
|
|
||||||
#outerdocbody .hotrect {
|
|
||||||
border: 1px solid #999;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cause "body" area (e.g. where clicks are heard) to grow horizontally with text */
|
|
||||||
body.mozilla, body.safari {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.doesWrap {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.safari div {
|
|
||||||
/* prevents the caret from disappearing on the longest line of the doc */
|
|
||||||
padding-right: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#linemetricsdiv {
|
|
||||||
position: absolute;
|
|
||||||
left: -1000px;
|
|
||||||
top: -1000px;
|
|
||||||
color: white;
|
|
||||||
z-index: -1;
|
|
||||||
font-size: 12px; /* overridden by lineMetricsDiv.style */
|
|
||||||
font-family: monospace; /* overridden by lineMetricsDiv.style */
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlaysdiv { position: absolute; left: -1000px; top: -1000px; }
|
|
|
@ -1,995 +0,0 @@
|
||||||
*,
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.clear {
|
|
||||||
clear: both
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
font-size: 62.5%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
body,
|
|
||||||
textarea {
|
|
||||||
font-family: Helvetica, Arial, sans-serif
|
|
||||||
}
|
|
||||||
iframe {
|
|
||||||
position: absolute
|
|
||||||
}
|
|
||||||
#users {
|
|
||||||
background: #f7f7f7;
|
|
||||||
background: -webkit-linear-gradient( #F7F7F7,#EEE);
|
|
||||||
background: -moz-linear-gradient( #F7F7F7,#EEE);
|
|
||||||
background: -ms-linear-gradient( #F7F7F7,#EEE);
|
|
||||||
background: -o-linear-gradient( #F7F7F7,#EEE);
|
|
||||||
background: linear-gradient( #F7F7F7,#EEE);
|
|
||||||
width: 160px;
|
|
||||||
color: #fff;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
#otherusers {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
a img {
|
|
||||||
border: 0
|
|
||||||
}
|
|
||||||
/* menu */
|
|
||||||
.toolbar {
|
|
||||||
background: #f7f7f7;
|
|
||||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
overflow: hidden;
|
|
||||||
padding-top: 4px;
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
.toolbar ul {
|
|
||||||
position: relative;
|
|
||||||
list-style: none;
|
|
||||||
padding-right: 3px;
|
|
||||||
padding-left: 1px;
|
|
||||||
z-index: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
float: left
|
|
||||||
}
|
|
||||||
.toolbar ul.menu_right {
|
|
||||||
float: right
|
|
||||||
}
|
|
||||||
.toolbar ul li {
|
|
||||||
float: left;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
.toolbar ul li.separator {
|
|
||||||
border: inherit;
|
|
||||||
background: inherit;
|
|
||||||
visibility: hidden;
|
|
||||||
width: 0px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.toolbar ul li a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.toolbar ul li a:hover {
|
|
||||||
background: #fff;
|
|
||||||
background: -webkit-linear-gradient(#f4f4f4, #e4e4e4);
|
|
||||||
background: -moz-linear-gradient(#f4f4f4, #e4e4e4);
|
|
||||||
background: -o-linear-gradient(#f4f4f4, #e4e4e4);
|
|
||||||
background: -ms-linear-gradient(#f4f4f4, #e4e4e4);
|
|
||||||
background: linear-gradient(#f4f4f4, #e4e4e4);
|
|
||||||
}
|
|
||||||
.toolbar ul li a:active {
|
|
||||||
background: #eee;
|
|
||||||
background: -webkit-linear-gradient(#ddd, #fff);
|
|
||||||
background: -moz-linear-gradient(#ddd, #fff);
|
|
||||||
background: -o-linear-gradient(#ddd, #fff);
|
|
||||||
background: -ms-linear-gradient(#ddd, #fff);
|
|
||||||
background: linear-gradient(#ddd, #fff);
|
|
||||||
-webkit-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
|
||||||
-moz-box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
|
||||||
box-shadow: 0 0 8px rgba(0,0,0,.1) inset;
|
|
||||||
}
|
|
||||||
.toolbar ul li a {
|
|
||||||
background: #fff;
|
|
||||||
background: -webkit-linear-gradient(#fff, #f0f0f0);
|
|
||||||
background: -moz-linear-gradient(#fff, #f0f0f0);
|
|
||||||
background: -o-linear-gradient(#fff, #f0f0f0);
|
|
||||||
background: -ms-linear-gradient(#fff, #f0f0f0);
|
|
||||||
background: linear-gradient(#fff, #f0f0f0);
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #ccc;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
min-height: 18px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 4px 5px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
min-width: 18px;
|
|
||||||
}
|
|
||||||
.toolbar ul li a .buttonicon {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
.toolbar ul li a.grouped-left {
|
|
||||||
border-radius: 3px 0 0 3px;
|
|
||||||
}
|
|
||||||
.toolbar ul li a.grouped-middle {
|
|
||||||
border-radius: 0;
|
|
||||||
margin-left: -2px;
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
.toolbar ul li a.grouped-right {
|
|
||||||
border-radius: 0 3px 3px 0;
|
|
||||||
margin-left: -2px;
|
|
||||||
border-left: 0;
|
|
||||||
}
|
|
||||||
.toolbar ul li a.selected {
|
|
||||||
background: #eee !important;
|
|
||||||
background: -webkit-linear-gradient(#EEE, #F0F0F0) !important;
|
|
||||||
background: -moz-linear-gradient(#EEE, #F0F0F0) !important;
|
|
||||||
background: -o-linear-gradient(#EEE, #F0F0F0) !important;
|
|
||||||
background: -ms-linear-gradient(#EEE, #F0F0F0) !important;
|
|
||||||
background: linear-gradient(#EEE, #F0F0F0) !important;
|
|
||||||
}
|
|
||||||
.toolbar ul li select {
|
|
||||||
background: #fff;
|
|
||||||
padding: 4px;
|
|
||||||
line-height: 22px; /* fix for safari (win/mac) */
|
|
||||||
height: 28px; /* fix for chrome (mac) */
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#usericon a {
|
|
||||||
min-width: 30px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
#usericon a #online_count {
|
|
||||||
color: #777;
|
|
||||||
font-size: 10px;
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
#editorcontainer {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
top: 37px; /* + 1px border */
|
|
||||||
left: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
#editorcontainer iframe {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#editorloadingbox {
|
|
||||||
padding-top: 100px;
|
|
||||||
padding-bottom: 100px;
|
|
||||||
font-size: 2.5em;
|
|
||||||
color: #aaa;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 30px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
#editorcontainerbox {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#padpage {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#padmain {
|
|
||||||
margin-top: 0px;
|
|
||||||
position: absolute;
|
|
||||||
top: 63px !important;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
zoom: 1;
|
|
||||||
}
|
|
||||||
#padeditor {
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 0;
|
|
||||||
zoom: 1;
|
|
||||||
}
|
|
||||||
#myswatchbox {
|
|
||||||
position: absolute;
|
|
||||||
left: 5px;
|
|
||||||
top: 5px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border: 1px solid #000;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#myswatch {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: transparent; /*...initially*/
|
|
||||||
}
|
|
||||||
#mycolorpicker {
|
|
||||||
width: 232px;
|
|
||||||
height: 265px;
|
|
||||||
position: absolute;
|
|
||||||
left: -250px;
|
|
||||||
top: 0px;
|
|
||||||
z-index: 101;
|
|
||||||
display: none;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-top: 0;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
#mycolorpickersave {
|
|
||||||
left: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#mycolorpickercancel {
|
|
||||||
left: 85px
|
|
||||||
}
|
|
||||||
#mycolorpickersave,
|
|
||||||
#mycolorpickercancel {
|
|
||||||
background: #fff;
|
|
||||||
background: -webkit-linear-gradient(#fff, #ccc);
|
|
||||||
background: -moz-linear-gradient(#fff, #ccc);
|
|
||||||
background: -o-linear-gradient(#fff, #ccc);
|
|
||||||
background: -ms-linear-gradient(#fff, #ccc);
|
|
||||||
background: linear-gradient(#fff, #ccc);
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
-moz-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #000;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 4px;
|
|
||||||
top: 240px;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
#mycolorpickerpreview {
|
|
||||||
position: absolute;
|
|
||||||
left: 207px;
|
|
||||||
top: 240px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
padding: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #fff;
|
|
||||||
-webkit-border-radius: 5px;
|
|
||||||
-moz-border-radius: 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
#myusernameform {
|
|
||||||
margin-left: 35px
|
|
||||||
}
|
|
||||||
#myusernameedit {
|
|
||||||
font-size: 1.3em;
|
|
||||||
color: #fff;
|
|
||||||
padding: 3px;
|
|
||||||
height: 18px;
|
|
||||||
margin: 0;
|
|
||||||
border: 0;
|
|
||||||
width: 117px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
#myusernameform input.editable {
|
|
||||||
border: 1px solid #444
|
|
||||||
}
|
|
||||||
#myuser .myusernameedithoverable:hover {
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
#mystatusform {
|
|
||||||
margin-left: 35px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
#mystatusedit {
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #777;
|
|
||||||
font-style: italic;
|
|
||||||
display: none;
|
|
||||||
padding: 2px;
|
|
||||||
height: 14px;
|
|
||||||
margin: 0;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
width: 199px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
#myusernameform .editactive,
|
|
||||||
#myusernameform .editempty {
|
|
||||||
background: white;
|
|
||||||
border-left: 1px solid #c3c3c3;
|
|
||||||
border-top: 1px solid #c3c3c3;
|
|
||||||
border-right: 1px solid #e6e6e6;
|
|
||||||
border-bottom: 1px solid #e6e6e6;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
#myusernameform .editempty {
|
|
||||||
color: #333
|
|
||||||
}
|
|
||||||
#myswatchbox, #myusernameedit, #otheruserstable .swatch {
|
|
||||||
border: 1px solid #ccc !important;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
table#otheruserstable {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
#nootherusers {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #eee;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#nootherusers a {
|
|
||||||
color: #3C88FF
|
|
||||||
}
|
|
||||||
#otheruserstable td {
|
|
||||||
height: 26px;
|
|
||||||
vertical-align: middle;
|
|
||||||
padding: 0 2px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#otheruserstable .swatch {
|
|
||||||
border: 1px solid #000;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
.usertdswatch {
|
|
||||||
width: 1%
|
|
||||||
}
|
|
||||||
.usertdname {
|
|
||||||
font-size: 1.3em;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
.usertdstatus {
|
|
||||||
font-size: 1.1em;
|
|
||||||
font-style: italic;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.usertdactivity {
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
.usertdname input {
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
width: 80px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
.usertdname input.editactive,
|
|
||||||
.usertdname input.editempty {
|
|
||||||
background: white;
|
|
||||||
border-left: 1px solid #c3c3c3;
|
|
||||||
border-top: 1px solid #c3c3c3;
|
|
||||||
border-right: 1px solid #e6e6e6;
|
|
||||||
border-bottom: 1px solid #e6e6e6;
|
|
||||||
}
|
|
||||||
.usertdname input.editempty {
|
|
||||||
color: #888;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.modaldialog.cboxreconnecting .modaldialog-inner,
|
|
||||||
.modaldialog.cboxconnecting .modaldialog-inner {
|
|
||||||
background: url(../../static/img/connectingbar.gif) no-repeat center 60px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
.modaldialog.cboxreconnecting,
|
|
||||||
.modaldialog.cboxconnecting,
|
|
||||||
.modaldialog.cboxdisconnected {
|
|
||||||
background: #8FCDE0
|
|
||||||
}
|
|
||||||
.cboxdisconnected #connectionboxinner div {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_deleted #connectionboxinner #disconnected_deleted {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_looping #connectionboxinner #disconnected_looping {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise,
|
|
||||||
.cboxdisconnected_looping #connectionboxinner #reconnect_advise,
|
|
||||||
.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise,
|
|
||||||
.cboxdisconnected_unknown #connectionboxinner #reconnect_advise {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected div#reconnect_form {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected .disconnected h2 {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
.cboxdisconnected .disconnected .h2_disconnect {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_userdup .disconnected h2.h2_disconnect {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
.cboxdisconnected_userdup .disconnected h2.h2_userdup {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
.cboxdisconnected_unauth .disconnected h2.h2_disconnect {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
.cboxdisconnected_unauth .disconnected h2.h2_unauth {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
#connectionstatus {
|
|
||||||
position: absolute;
|
|
||||||
width: 37px;
|
|
||||||
height: 41px;
|
|
||||||
overflow: hidden;
|
|
||||||
right: 0;
|
|
||||||
z-index: 11;
|
|
||||||
}
|
|
||||||
#connectionboxinner .connecting {
|
|
||||||
margin-top: 20px;
|
|
||||||
font-size: 2.0em;
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.cboxconnecting #connectionboxinner .connecting {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
#connectionboxinner .disconnected h2 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
color: #333;
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
#connectionboxinner .disconnected p {
|
|
||||||
margin: 10px 10px;
|
|
||||||
font-size: 1.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#connectionboxinner .disconnected {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
.cboxdisconnected #connectionboxinner .disconnected {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
#connectionboxinner .reconnecting {
|
|
||||||
margin-top: 20px;
|
|
||||||
font-size: 1.6em;
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.cboxreconnecting #connectionboxinner .reconnecting {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
#reconnect_form button {
|
|
||||||
font-size: 12pt;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
#mainmodals {
|
|
||||||
z-index: 600; /* higher than the modals themselves: */
|
|
||||||
}
|
|
||||||
.modalfield {
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 1px;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
}
|
|
||||||
#mainmodals .editempty {
|
|
||||||
color: #aaa
|
|
||||||
}
|
|
||||||
.modaldialog {
|
|
||||||
position: absolute;
|
|
||||||
top: 100px;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -243px;
|
|
||||||
width: 485px;
|
|
||||||
display: none;
|
|
||||||
z-index: 501;
|
|
||||||
zoom: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #999;
|
|
||||||
}
|
|
||||||
.modaldialog .modaldialog-inner {
|
|
||||||
padding: 10pt
|
|
||||||
}
|
|
||||||
.modaldialog .modaldialog-hide {
|
|
||||||
float: right;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-image: url(static/img/sharebox4.gif);
|
|
||||||
display: block;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
background-position: -454px -6px;
|
|
||||||
margin-right: -5px;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
.modaldialog label,
|
|
||||||
.modaldialog h1 {
|
|
||||||
color: #222222;
|
|
||||||
font-size: 125%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.modaldialog th {
|
|
||||||
vertical-align: top;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#modaloverlay {
|
|
||||||
z-index: 500;
|
|
||||||
display: none;
|
|
||||||
background-repeat: repeat-both;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
* html #modaloverlay {
|
|
||||||
/* for IE 6+ */
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
|
||||||
filter: alpha(opacity=100);
|
|
||||||
opacity: 1; /* in case this is looked at */
|
|
||||||
background-image: none;
|
|
||||||
background-repeat: no-repeat; /* scale the image */
|
|
||||||
}
|
|
||||||
|
|
||||||
#chatbox {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 20px;
|
|
||||||
width: 180px;
|
|
||||||
height: 200px;
|
|
||||||
z-index: 400;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-left: 1px solid #999;
|
|
||||||
border-right: 1px solid #999;
|
|
||||||
border-top: 1px solid #999;
|
|
||||||
padding: 3px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#chattext {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid white;
|
|
||||||
-ms-overflow-y: scroll;
|
|
||||||
overflow-y: scroll;
|
|
||||||
font-size: 12px;
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
left: 0px;
|
|
||||||
top: 25px;
|
|
||||||
bottom: 25px;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
|
||||||
#chattext p {
|
|
||||||
padding: 3px;
|
|
||||||
-ms-overflow-x: hidden;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
#chatinputbox {
|
|
||||||
padding: 3px 2px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 0px;
|
|
||||||
left: 3px;
|
|
||||||
}
|
|
||||||
#chatlabel {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #555;
|
|
||||||
text-decoration: none;
|
|
||||||
margin-right: 3px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#chatinput {
|
|
||||||
border: 1px solid #BBBBBB;
|
|
||||||
width: 100%;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
#chaticon {
|
|
||||||
z-index: 400;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 20px;
|
|
||||||
padding: 5px;
|
|
||||||
border-left: 1px solid #999;
|
|
||||||
border-right: 1px solid #999;
|
|
||||||
border-top: 1px solid #999;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
background-color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#chaticon a {
|
|
||||||
text-decoration: none
|
|
||||||
}
|
|
||||||
#chatcounter {
|
|
||||||
color: #777;
|
|
||||||
font-size: 10px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#titlebar {
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #555;
|
|
||||||
position: relative;
|
|
||||||
bottom: 2px;
|
|
||||||
}
|
|
||||||
#titlelabel {
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 4px 0 0 4px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
#titlecross {
|
|
||||||
font-size: 25px;
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
float: right;
|
|
||||||
color: #333;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-left: 3px;
|
|
||||||
margin-right: 3px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.exporttype {
|
|
||||||
margin-top: 4px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
padding-left: 25px;
|
|
||||||
background-image: url("../../static/img/etherpad_lite_icons.png");
|
|
||||||
color: #333;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
#exporthtml {
|
|
||||||
background-position: 0px -299px
|
|
||||||
}
|
|
||||||
#exportplain {
|
|
||||||
background-position: 0px -395px
|
|
||||||
}
|
|
||||||
#exportword {
|
|
||||||
background-position: 0px -275px
|
|
||||||
}
|
|
||||||
#exportpdf {
|
|
||||||
background-position: 0px -371px
|
|
||||||
}
|
|
||||||
#exportopen {
|
|
||||||
background-position: 0px -347px
|
|
||||||
}
|
|
||||||
#exportdokuwiki {
|
|
||||||
background-position: 0px -459px
|
|
||||||
}
|
|
||||||
#importstatusball {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
#importarrow {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
#importmessagesuccess {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
#importsubmitinput {
|
|
||||||
height: 25px;
|
|
||||||
width: 85px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
#importstatusball {
|
|
||||||
height: 50px
|
|
||||||
}
|
|
||||||
#chatthrob {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 40px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 150px;
|
|
||||||
height: 40px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 200;
|
|
||||||
background-color: #000;
|
|
||||||
color: white;
|
|
||||||
background-color: rgb(0,0,0);
|
|
||||||
background-color: rgba(0,0,0,0.7);
|
|
||||||
padding: 10px;
|
|
||||||
-webkit-border-radius: 6px;
|
|
||||||
-moz-border-radius: 6px;
|
|
||||||
border-radius: 6px;
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
|
|
||||||
filter: alpha(opacity=80);
|
|
||||||
opacity: .8;
|
|
||||||
}
|
|
||||||
.buttonicon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-image: url('../../static/img/etherpad_lite_icons.png');
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.buttonicon-bold {
|
|
||||||
background-position: 0px -116px
|
|
||||||
}
|
|
||||||
.buttonicon-italic {
|
|
||||||
background-position: 0px 0px
|
|
||||||
}
|
|
||||||
.buttonicon-underline {
|
|
||||||
background-position: 0px -236px
|
|
||||||
}
|
|
||||||
.buttonicon-strikethrough {
|
|
||||||
background-position: 0px -200px
|
|
||||||
}
|
|
||||||
.buttonicon-insertorderedlist {
|
|
||||||
background-position: 0px -477px
|
|
||||||
}
|
|
||||||
.buttonicon-insertunorderedlist {
|
|
||||||
background-position: 0px -34px
|
|
||||||
}
|
|
||||||
.buttonicon-indent {
|
|
||||||
background-position: 0px -52px
|
|
||||||
}
|
|
||||||
.buttonicon-outdent {
|
|
||||||
background-position: 0px -134px
|
|
||||||
}
|
|
||||||
.buttonicon-undo {
|
|
||||||
background-position: 0px -255px
|
|
||||||
}
|
|
||||||
.buttonicon-redo {
|
|
||||||
background-position: 0px -166px
|
|
||||||
}
|
|
||||||
.buttonicon-clearauthorship {
|
|
||||||
background-position: 0px -86px
|
|
||||||
}
|
|
||||||
.buttonicon-settings {
|
|
||||||
background-position: 0px -436px
|
|
||||||
}
|
|
||||||
.buttonicon-import_export {
|
|
||||||
background-position: 0px -68px
|
|
||||||
}
|
|
||||||
.buttonicon-embed {
|
|
||||||
background-position: 0px -18px
|
|
||||||
}
|
|
||||||
.buttonicon-history {
|
|
||||||
background-position: 0px -218px
|
|
||||||
}
|
|
||||||
.buttonicon-chat {
|
|
||||||
background-position: 0px -102px;
|
|
||||||
}
|
|
||||||
.buttonicon-showusers {
|
|
||||||
background-position: 0px -183px;
|
|
||||||
}
|
|
||||||
.buttonicon-savedRevision {
|
|
||||||
background-position: 0px -493px
|
|
||||||
}
|
|
||||||
#focusprotector {
|
|
||||||
z-index: 100;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
background-color: white;
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=1)";
|
|
||||||
filter: alpha(opacity=1);
|
|
||||||
opacity: 0.01;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#online_count {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
.rtl {
|
|
||||||
direction: RTL
|
|
||||||
}
|
|
||||||
#chattext p {
|
|
||||||
word-wrap: break-word
|
|
||||||
}
|
|
||||||
/* fix for misaligned checkboxes */
|
|
||||||
input[type=checkbox] {
|
|
||||||
vertical-align: -1px
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
float: right
|
|
||||||
}
|
|
||||||
.popup {
|
|
||||||
font-size: 14px;
|
|
||||||
width: 450px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #f7f7f7;
|
|
||||||
background: -webkit-linear-gradient(#F7F7F7, #EEE);
|
|
||||||
background: -moz-linear-gradient(#F7F7F7, #EEE);
|
|
||||||
background: -ms-linear-gradient(#F7F7F7, #EEE);
|
|
||||||
background: -o-linear-gradient(#F7F7F7, #EEE);
|
|
||||||
background: linear-gradient(#F7F7F7, #EEE);
|
|
||||||
-webkit-box-shadow: 0 0 8px #888;
|
|
||||||
-moz-box-shadow: 0 0 8px #888;
|
|
||||||
box-shadow: 0 2px 4px #ddd;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
.popup input[type=text] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-ms-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.popup input[type=text], #users input[type=text] {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.popup a {
|
|
||||||
text-decoration: none
|
|
||||||
}
|
|
||||||
.popup h1 {
|
|
||||||
color: #555;
|
|
||||||
font-size: 18px
|
|
||||||
}
|
|
||||||
.popup h2 {
|
|
||||||
color: #777;
|
|
||||||
font-size: 15px
|
|
||||||
}
|
|
||||||
.popup p {
|
|
||||||
margin: 5px 0
|
|
||||||
}
|
|
||||||
.column {
|
|
||||||
float: left;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
#settings,
|
|
||||||
#importexport,
|
|
||||||
#embed,
|
|
||||||
#users {
|
|
||||||
position: absolute;
|
|
||||||
top: 36px;
|
|
||||||
right: 20px;
|
|
||||||
display: none;
|
|
||||||
z-index: 500;
|
|
||||||
}
|
|
||||||
.stickyChat {
|
|
||||||
background-color: #f1f1f1 !important;
|
|
||||||
right: 0px !important;
|
|
||||||
top: 37px;
|
|
||||||
-webkit-border-radius: 0px !important;
|
|
||||||
-moz-border-radius: 0px !important;
|
|
||||||
border-radius: 0px !important;
|
|
||||||
height: auto !important;
|
|
||||||
border: none !important;
|
|
||||||
border-left: 1px solid #ccc !important;
|
|
||||||
width: 185px !important;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 960px) {
|
|
||||||
.modaldialog {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 80%;
|
|
||||||
top: 40px;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.toolbar ul li.separator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.toolbar ul li a {
|
|
||||||
padding: 4px 1px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (min-device-width: 320px) and (max-device-width: 720px) {
|
|
||||||
#users {
|
|
||||||
top: 36px;
|
|
||||||
bottom: 40px;
|
|
||||||
border-radius: none;
|
|
||||||
}
|
|
||||||
#mycolorpicker {
|
|
||||||
left: -73px;
|
|
||||||
/* #mycolorpicker: width -#users: width */;
|
|
||||||
}
|
|
||||||
#editorcontainer {
|
|
||||||
margin-bottom: 33px
|
|
||||||
}
|
|
||||||
.toolbar ul.menu_right {
|
|
||||||
background: #f7f7f7;
|
|
||||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 32px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
.toolbar ul.menu_right > li:last-child {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.toolbar ul.menu_right > li a {
|
|
||||||
border-radius: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.toolbar ul li a.selected {
|
|
||||||
background: none !important
|
|
||||||
}
|
|
||||||
#chaticon, #timesliderlink {
|
|
||||||
display: none !important
|
|
||||||
}
|
|
||||||
.popup {
|
|
||||||
-webkit-border-radius: 0;
|
|
||||||
-moz-border-radius: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-ms-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#settings,
|
|
||||||
#importexport,
|
|
||||||
#embed {
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 33px;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.toolbar ul li .separator {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
#editorcontainerbox {
|
|
||||||
overflow: auto;
|
|
||||||
top: 40px;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
#padcontent {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
#timeslider-wrapper {
|
|
||||||
left: 0;
|
|
||||||
position: relative;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
#timeslider-left {
|
|
||||||
background-image: url(../../static/img/timeslider_left.png);
|
|
||||||
height: 63px;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 134px;
|
|
||||||
}
|
|
||||||
#timeslider-right {
|
|
||||||
background-image: url(../../static/img/timeslider_right.png);
|
|
||||||
height: 63px;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 155px;
|
|
||||||
}
|
|
||||||
#timeslider {
|
|
||||||
background-image: url(../../static/img/timeslider_background.png);
|
|
||||||
height: 63px;
|
|
||||||
margin: 0 9px;
|
|
||||||
}
|
|
||||||
#timeslider #timeslider-slider {
|
|
||||||
height: 61px;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#ui-slider-handle {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
background-image: url(../../static/img/crushed_current_location.png);
|
|
||||||
cursor: pointer;
|
|
||||||
height: 61px;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 13px;
|
|
||||||
}
|
|
||||||
#ui-slider-bar {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 35px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 148px;
|
|
||||||
position: relative;
|
|
||||||
top: 20px;
|
|
||||||
}
|
|
||||||
#playpause_button,
|
|
||||||
#playpause_button_icon {
|
|
||||||
height: 47px;
|
|
||||||
position: absolute;
|
|
||||||
width: 47px;
|
|
||||||
}
|
|
||||||
#playpause_button {
|
|
||||||
background-image: url(../../static/img/crushed_button_undepressed.png);
|
|
||||||
right: 77px;
|
|
||||||
top: 9px;
|
|
||||||
}
|
|
||||||
#playpause_button_icon {
|
|
||||||
background-image: url(../../static/img/play.png);
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.pause#playpause_button_icon {
|
|
||||||
background-image: url(../../static/img/pause.png)
|
|
||||||
}
|
|
||||||
#leftstar,
|
|
||||||
#rightstar,
|
|
||||||
#leftstep,
|
|
||||||
#rightstep {
|
|
||||||
background: url(../../static/img/stepper_buttons.png) 0 0 no-repeat;
|
|
||||||
height: 21px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
#leftstar {
|
|
||||||
background-position: 0 -44px;
|
|
||||||
right: 34px;
|
|
||||||
top: 8px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
#rightstar {
|
|
||||||
background-position: -29px -44px;
|
|
||||||
right: 5px;
|
|
||||||
top: 8px;
|
|
||||||
width: 29px;
|
|
||||||
}
|
|
||||||
#leftstep {
|
|
||||||
background-position: 0 -22px;
|
|
||||||
right: 34px;
|
|
||||||
top: 20px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
#rightstep {
|
|
||||||
background-position: -29px -22px;
|
|
||||||
right: 5px;
|
|
||||||
top: 20px;
|
|
||||||
width: 29px;
|
|
||||||
}
|
|
||||||
#timeslider .star {
|
|
||||||
background-image: url(../../static/img/star.png);
|
|
||||||
cursor: pointer;
|
|
||||||
height: 16px;
|
|
||||||
position: absolute;
|
|
||||||
top: 40px;
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
#timeslider #timer {
|
|
||||||
color: #fff;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 11px;
|
|
||||||
left: 7px;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
top: 9px;
|
|
||||||
width: 122px;
|
|
||||||
}
|
|
||||||
.topbarcenter,
|
|
||||||
#docbar {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
#padmain {
|
|
||||||
top: 0px !important
|
|
||||||
}
|
|
||||||
#editbarright {
|
|
||||||
float: right
|
|
||||||
}
|
|
||||||
#returnbutton {
|
|
||||||
color: #222;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 29px;
|
|
||||||
margin-top: 0;
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
|
||||||
#importexport .popup {
|
|
||||||
width: 185px
|
|
||||||
}
|
|
||||||
#importexport {
|
|
||||||
top: 118px;
|
|
||||||
width: 185px;
|
|
||||||
}
|
|
||||||
.timeslider-bar {
|
|
||||||
background: #f7f7f7;
|
|
||||||
background: -webkit-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -moz-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -o-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: -ms-linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
background: linear-gradient(#f7f7f7, #f1f1f1 80%);
|
|
||||||
overflow: hidden;
|
|
||||||
padding-top: 3px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.timeslider-bar #editbar {
|
|
||||||
border-bottom: none;
|
|
||||||
float: right;
|
|
||||||
width: 170px;
|
|
||||||
width: initial;
|
|
||||||
}
|
|
||||||
.timeslider-bar h1 {
|
|
||||||
margin: 5px
|
|
||||||
}
|
|
||||||
.timeslider-bar p {
|
|
||||||
margin: 5px
|
|
||||||
}
|
|
||||||
#timeslider-top {
|
|
||||||
width: 100%;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
#authorsList .author {
|
|
||||||
padding-left: 0.4em;
|
|
||||||
padding-right: 0.4em;
|
|
||||||
}
|
|
||||||
#authorsList .author-anonymous {
|
|
||||||
padding-left: 0.6em;
|
|
||||||
padding-right: 0.6em;
|
|
||||||
}
|
|
||||||
#padeditor {
|
|
||||||
position: static
|
|
||||||
}
|
|
||||||
/* lists */
|
|
||||||
.list-bullet2,
|
|
||||||
.list-indent2,
|
|
||||||
.list-number2 {
|
|
||||||
margin-left: 3em
|
|
||||||
}
|
|
||||||
.list-bullet3,
|
|
||||||
.list-indent3,
|
|
||||||
.list-number3 {
|
|
||||||
margin-left: 4.5em
|
|
||||||
}
|
|
||||||
.list-bullet4,
|
|
||||||
.list-indent4,
|
|
||||||
.list-number4 {
|
|
||||||
margin-left: 6em
|
|
||||||
}
|
|
||||||
.list-bullet5,
|
|
||||||
.list-indent5,
|
|
||||||
.list-number5 {
|
|
||||||
margin-left: 7.5em
|
|
||||||
}
|
|
||||||
.list-bullet6,
|
|
||||||
.list-indent6,
|
|
||||||
.list-number6 {
|
|
||||||
margin-left: 9em
|
|
||||||
}
|
|
||||||
.list-bullet7,
|
|
||||||
.list-indent7,
|
|
||||||
.list-number7 {
|
|
||||||
margin-left: 10.5em
|
|
||||||
}
|
|
||||||
.list-bullet8,
|
|
||||||
.list-indent8,
|
|
||||||
.list-number8 {
|
|
||||||
margin-left: 12em
|
|
||||||
}
|
|
||||||
/* unordered lists */
|
|
||||||
UL {
|
|
||||||
list-style-type: disc;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
}
|
|
||||||
UL UL {
|
|
||||||
margin-left: 0 !important
|
|
||||||
}
|
|
||||||
.list-bullet2,
|
|
||||||
.list-bullet5,
|
|
||||||
.list-bullet8 {
|
|
||||||
list-style-type: circle
|
|
||||||
}
|
|
||||||
.list-bullet3,
|
|
||||||
.list-bullet6 {
|
|
||||||
list-style-type: square
|
|
||||||
}
|
|
||||||
.list-indent1,
|
|
||||||
.list-indent2,
|
|
||||||
.list-indent3,
|
|
||||||
.list-indent5,
|
|
||||||
.list-indent5,
|
|
||||||
.list-indent6,
|
|
||||||
.list-indent7,
|
|
||||||
.list-indent8 {
|
|
||||||
list-style-type: none
|
|
||||||
}
|
|
||||||
/* ordered lists */
|
|
||||||
OL {
|
|
||||||
list-style-type: decimal;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
}
|
|
||||||
.list-number2,
|
|
||||||
.list-number5,
|
|
||||||
.list-number8 {
|
|
||||||
list-style-type: lower-latin
|
|
||||||
}
|
|
||||||
.list-number3,
|
|
||||||
.list-number6 {
|
|
||||||
list-style-type: lower-roman
|
|
||||||
}
|
|
||||||
/* IE 6/7 fixes */
|
|
||||||
* HTML #ui-slider-handle {
|
|
||||||
background-image: url(../../static/img/current_location.gif)
|
|
||||||
}
|
|
||||||
* HTML #timeslider .star {
|
|
||||||
background-image: url(../../static/img/star.gif)
|
|
||||||
}
|
|
||||||
* HTML #playpause_button_icon {
|
|
||||||
background-image: url(../../static/img/play.gif)
|
|
||||||
}
|
|
||||||
* HTML .pause#playpause_button_icon {
|
|
||||||
background-image: url(../../static/img/pause.gif)
|
|
||||||
}
|
|
3
src/static/custom/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
!*.template
|
|
|
@ -1,8 +0,0 @@
|
||||||
/*
|
|
||||||
custom css files are loaded after core css files. Simply use the same selector to override a style.
|
|
||||||
Example:
|
|
||||||
#editbar LI {border:1px solid #000;}
|
|
||||||
overrides
|
|
||||||
#editbar LI {border:1px solid #d5d5d5;}
|
|
||||||
from pad.css
|
|
||||||
*/
|
|
|
@ -1,6 +0,0 @@
|
||||||
function customStart()
|
|
||||||
{
|
|
||||||
//define your javascript here
|
|
||||||
//jquery is available - except index.js
|
|
||||||
//you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 697 B |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1,009 B |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 658 B |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 123 B |
Before Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 3.2 KiB |