diff --git a/.gitignore b/.gitignore
index 50cd6e212..31b7d2ea9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
node_modules
settings.json
-static/js/jquery.min.js
+static/js/jquery.js
+static/js/prefixfree.js
APIKEY.txt
bin/abiword.exe
bin/node.exe
@@ -8,4 +9,5 @@ etherpad-lite-win.zip
var/dirty.db
bin/convertSettings.json
*~
-*.patch
\ No newline at end of file
+*.patch
+*.DS_Store
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..a35c25535
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ 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.
diff --git a/README.md b/README.md
index ab241333f..84e866585 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
- Install the dependencies. We need gzip, git, curl, libssl develop libraries, python and gcc.
For Debian/Ubuntu apt-get install gzip git-core curl python libssl-dev build-essential
- For Fedora/CentOS yum install gzip git-core curl python openssl-dev && yum groupinstall "Development Tools"
+ For Fedora/CentOS yum install gzip git-core curl python openssl-devel && yum groupinstall "Development Tools"
- Install node.js
@@ -81,6 +81,8 @@ Here is the **[FAQ](https://github.com/Pita/etherpad-lite/wiki/FAQ)**
## 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
@@ -98,7 +100,7 @@ Look at this wiki pages:
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 here .
+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 .
@@ -111,11 +113,18 @@ You can join the [mailinglist](http://groups.google.com/group/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)
+[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
\ No newline at end of file
diff --git a/bin/checkPad.js b/bin/checkPad.js
index 9e0544415..356b07799 100644
--- a/bin/checkPad.js
+++ b/bin/checkPad.js
@@ -15,7 +15,8 @@ var log4js = require("log4js");
log4js.setGlobalLogLevel("INFO");
var async = require("async");
var db = require('../node/db/DB');
-var Changeset = require('../node/utils/Changeset');
+var CommonCode = require('../node/utils/common_code');
+var Changeset = CommonCode.require("/Changeset");
var padManager;
async.series([
diff --git a/bin/convert.js b/bin/convert.js
index d410f7a4a..c5dc535cd 100644
--- a/bin/convert.js
+++ b/bin/convert.js
@@ -1,10 +1,12 @@
+var CommonCode = require('../node/utils/common_code');
var startTime = new Date().getTime();
var fs = require("fs");
var ueberDB = require("ueberDB");
var mysql = require("mysql");
var async = require("async");
-var Changeset = require("../node/utils/Changeset");
-var AttributePoolFactory = require("../node/utils/AttributePoolFactory");
+var Changeset = CommonCode.require("/Changeset");
+var randomString = CommonCode.require('/pad_utils').randomString;
+var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
var settingsFile = process.argv[2];
var sqlOutputFile = process.argv[3];
@@ -450,18 +452,3 @@ function parsePage(array, pageStart, offsets, data, json)
start+=unitLength;
}
}
-
-/**
- * Generates a random String with the given length. Is needed to generate the Author Ids
- */
-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;
-}
diff --git a/bin/installDeps.sh b/bin/installDeps.sh
index a49c46b13..270ec98cd 100755
--- a/bin/installDeps.sh
+++ b/bin/installDeps.sh
@@ -16,7 +16,7 @@ hash curl > /dev/null 2>&1 || {
#Is node installed?
hash node > /dev/null 2>&1 || {
- echo "Please install node.js ( http://nodesjs.org )" >&2
+ echo "Please install node.js ( http://nodejs.org )" >&2
exit 1
}
@@ -33,10 +33,25 @@ if [ ! $(echo $NPM_VERSION | cut -d "." -f 1) = "1" ]; then
exit 1
fi
-#Does a settings.json exist? if no copy the template
-if [ ! -f "settings.json" ]; then
- echo "Copy the settings template to settings.json..."
- cp -v settings.json.template settings.json || exit 1
+#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..."
@@ -47,9 +62,9 @@ npm install || {
echo "Ensure jQuery is downloaded and up to date..."
DOWNLOAD_JQUERY="true"
-NEEDED_VERSION="1.7"
-if [ -f "static/js/jquery.min.js" ]; then
- VERSION=$(cat static/js/jquery.min.js | head -n 3 | grep -o "v[0-9].[0-9]");
+NEEDED_VERSION="1.7.1"
+if [ -f "static/js/jquery.js" ]; then
+ VERSION=$(cat 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"
@@ -57,7 +72,22 @@ if [ -f "static/js/jquery.min.js" ]; then
fi
if [ $DOWNLOAD_JQUERY = "true" ]; then
- curl -lo static/js/jquery.min.js http://code.jquery.com/jquery-$NEEDED_VERSION.min.js || exit 1
+ curl -lo static/js/jquery.js http://code.jquery.com/jquery-$NEEDED_VERSION.js || exit 1
+fi
+
+echo "Ensure prefixfree is downloaded and up to date..."
+DOWNLOAD_PREFIXFREE="true"
+NEEDED_VERSION="1.0.4"
+if [ -f "static/js/prefixfree.js" ]; then
+ VERSION=$(cat static/js/prefixfree.js | grep "PrefixFree" | grep -o "[0-9].[0-9].[0-9]");
+
+ if [ $VERSION = $NEEDED_VERSION ]; then
+ DOWNLOAD_PREFIXFREE="false"
+ fi
+fi
+
+if [ $DOWNLOAD_PREFIXFREE = "true" ]; then
+ curl -lo static/js/prefixfree.js -k https://raw.github.com/LeaVerou/prefixfree/master/prefixfree.js || exit 1
fi
#Remove all minified data to force node creating it new
diff --git a/bin/jshint.sh b/bin/jshint.sh
new file mode 100755
index 000000000..4dea73961
--- /dev/null
+++ b/bin/jshint.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+if [ -d "../bin" ]; then
+ cd "../"
+fi
+
+JSHINT=./node_modules/jshint/bin/hint
+
+$JSHINT ./node/
diff --git a/bin/loadTesting/README b/bin/loadTesting/README
new file mode 100644
index 000000000..297756f9d
--- /dev/null
+++ b/bin/loadTesting/README
@@ -0,0 +1,75 @@
+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 ');
+ 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 ###
diff --git a/bin/loadTesting/launcher.sh b/bin/loadTesting/launcher.sh
new file mode 100755
index 000000000..375b15444
--- /dev/null
+++ b/bin/loadTesting/launcher.sh
@@ -0,0 +1,16 @@
+#!/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
diff --git a/bin/loadTesting/loader.js b/bin/loadTesting/loader.js
new file mode 100644
index 000000000..ddcd0572d
--- /dev/null
+++ b/bin/loadTesting/loader.js
@@ -0,0 +1,20 @@
+var page = new WebPage(),
+ t, address;
+
+if (phantom.args.length === 0) {
+ console.log('Usage: loader.js ');
+ 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);
+
+}
diff --git a/bin/run.sh b/bin/run.sh
index a5245ff77..c409920e7 100755
--- a/bin/run.sh
+++ b/bin/run.sh
@@ -21,9 +21,9 @@ if [ "$(id -u)" -eq 0 ]; then
fi
#prepare the enviroment
-bin/installDeps.sh || exit 1
+bin/installDeps.sh $* || exit 1
#Move to the node folder and start
echo "start..."
cd "node"
-node server.js
+node server.js $*
diff --git a/node/README.md b/node/README.md
index 031810f6f..4b443289e 100644
--- a/node/README.md
+++ b/node/README.md
@@ -6,7 +6,7 @@
# Module name conventions
-Module file names starts with a capital letter and uses camelCase
+Module file names start with a capital letter and uses camelCase
# Where does it start?
diff --git a/node/db/API.js b/node/db/API.js
index 90bfd8f66..09cc95afc 100644
--- a/node/db/API.js
+++ b/node/db/API.js
@@ -293,7 +293,7 @@ Example returns:
exports.createPad = function(padID, text, callback)
{
//ensure there is no $ in the padID
- if(padID.indexOf("$") != -1)
+ if(padID && padID.indexOf("$") != -1)
{
callback(new customError("createPad can't create group pads","apierror"));
return;
@@ -360,7 +360,7 @@ Example returns:
exports.setPublicStatus = function(padID, publicStatus, callback)
{
//ensure this is a group pad
- if(padID.indexOf("$") == -1)
+ if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
return;
@@ -393,7 +393,7 @@ Example returns:
exports.getPublicStatus = function(padID, callback)
{
//ensure this is a group pad
- if(padID.indexOf("$") == -1)
+ if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the publicStatus of pads that belong to a group","apierror"));
return;
@@ -419,7 +419,7 @@ Example returns:
exports.setPassword = function(padID, password, callback)
{
//ensure this is a group pad
- if(padID.indexOf("$") == -1)
+ if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
return;
@@ -448,7 +448,7 @@ Example returns:
exports.isPasswordProtected = function(padID, callback)
{
//ensure this is a group pad
- if(padID.indexOf("$") == -1)
+ if(padID && padID.indexOf("$") == -1)
{
callback(new customError("You can only get/set the password of pads that belong to a group","apierror"));
return;
diff --git a/node/db/AuthorManager.js b/node/db/AuthorManager.js
index f4f42d112..9baf63475 100644
--- a/node/db/AuthorManager.js
+++ b/node/db/AuthorManager.js
@@ -18,9 +18,11 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
+var randomString = CommonCode.require('/pad_utils').randomString;
/**
* Checks if the author exists
@@ -177,18 +179,3 @@ exports.setAuthorName = function (author, name, callback)
{
db.setSub("globalAuthor:" + author, ["name"], name, callback);
}
-
-/**
- * Generates a random String with the given length. Is needed to generate the Author Ids
- */
-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;
-}
diff --git a/node/db/GroupManager.js b/node/db/GroupManager.js
index 473ea9b77..04c79cfae 100644
--- a/node/db/GroupManager.js
+++ b/node/db/GroupManager.js
@@ -18,8 +18,10 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
+var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
@@ -247,26 +249,15 @@ exports.listPads = function(groupID, callback)
//group exists, let's get the pads
else
{
- db.getSub("group:" + groupID, ["pads"], function(err, pads)
+ db.getSub("group:" + groupID, ["pads"], function(err, result)
{
if(ERR(err, callback)) return;
+ var pads = [];
+ for ( var padId in result ) {
+ pads.push(padId);
+ }
callback(null, {padIDs: pads});
});
}
});
}
-
-/**
- * Generates a random String with the given length. Is needed to generate the Author Ids
- */
-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;
-}
diff --git a/node/db/Pad.js b/node/db/Pad.js
index 7807d4643..40875effb 100644
--- a/node/db/Pad.js
+++ b/node/db/Pad.js
@@ -2,11 +2,11 @@
* The pad object, defined with joose
*/
-require('joose');
-
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
-var Changeset = require("../utils/Changeset");
-var AttributePoolFactory = require("../utils/AttributePoolFactory");
+var Changeset = CommonCode.require("/Changeset");
+var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var settings = require('../utils/Settings');
@@ -22,490 +22,451 @@ var crypto = require("crypto");
*/
exports.cleanText = function (txt) {
return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
-}
+};
-Class('Pad', {
- // these are the properties
- has : {
-
- atext : {
- is : 'rw', // readwrite
- init : function() { return Changeset.makeAText("\n"); } // first value
- }, // atext
-
- pool : {
- is: 'rw',
- init : function() { return AttributePoolFactory.createAttributePool(); },
- getterName : 'apool' // legacy
- }, // pool
-
- head : {
- is : 'rw',
- init : -1,
- getterName : 'getHeadRevisionNumber'
- }, // head
-
- chatHead : {
- is: 'rw',
- init: -1
- }, // chatHead
-
- publicStatus : {
- is: 'rw',
- init: false,
- getterName : 'getPublicStatus'
- }, //publicStatus
-
- passwordHash : {
- is: 'rw',
- init: null
- }, // passwordHash
-
- id : { is : 'r' }
- },
+var Pad = function Pad(id) {
- methods : {
-
- BUILD : function (id)
- {
- return {
- 'id' : id,
- }
- },
-
- appendRevision : function(aChangeset, author)
- {
- if(!author)
- author = '';
+ this.atext = Changeset.makeAText("\n");
+ this.pool = AttributePoolFactory.createAttributePool();
+ this.head = -1;
+ this.chatHead = -1;
+ this.publicStatus = false;
+ this.passwordHash = null;
+ this.id = id;
- 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);
- db.set("pad:"+this.id, {atext: this.atext,
- pool: this.pool.toJsonable(),
- head: this.head,
- chatHead: this.chatHead,
- publicStatus: this.publicStatus,
- passwordHash: this.passwordHash});
- }, //appendRevision
-
- getRevisionChangeset : function(revNum, callback)
+};
+
+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);
+ db.set("pad:"+this.id, {atext: this.atext,
+ pool: this.pool.toJsonable(),
+ head: this.head,
+ chatHead: this.chatHead,
+ publicStatus: this.publicStatus,
+ passwordHash: this.passwordHash});
+};
+
+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] != "")
{
- db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
- }, // getRevisionChangeset
-
- getRevisionAuthor : function(revNum, callback)
- {
- db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
- }, // getRevisionAuthor
-
- getRevisionDate : function(revNum, callback)
- {
- db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
- }, // getRevisionAuthor
-
- getAllAuthors : function()
+ 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)
{
- var authors = [];
-
- for(key in this.pool.numToAttrib)
- {
- if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
+ async.parallel([
+ //get the atext of the key revision
+ function (callback)
{
- authors.push(this.pool.numToAttrib[key][1]);
+ 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);
}
- }
-
- return authors;
+ ], callback);
},
-
- getInternalRevisionAText : function(targetRev, callback)
+ //apply all changesets to the key changeset
+ function(callback)
{
- var _this = this;
-
- var keyRev = this.getKeyRevisionNumber(targetRev);
- var atext;
- var changesets = [];
-
- //find out which changesets are needed
- var neededChangesets = [];
+ var apool = _this.apool();
var curRev = keyRev;
- while (curRev < targetRev)
+
+ while (curRev < targetRev)
{
curRev++;
- neededChangesets.push(curRev);
+ var cs = changesets[curRev];
+ atext = Changeset.applyToAText(cs, atext, apool);
}
-
- 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);
- });
- },
-
- getKeyRevisionNumber : function(revNum)
- {
- return Math.floor(revNum / 100) * 100;
- },
-
- text : function()
- {
- return this.atext.text;
- },
-
- setText : function(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);
- },
-
- appendChatMessage: function(text, userId, time)
- {
+
+ 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});
//save the new chat head
db.setSub("pad:"+this.id, ["chatHead"], this.chatHead);
- },
-
- getChatMessage: function(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)
+};
+
+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;
- callback(null, entry);
+ entry = _entry;
+ callback();
});
},
-
- getLastChatMessages: function(count, callback)
+ //add the authorName
+ function(callback)
{
- //return an empty array if there are no chat messages
- if(this.chatHead == -1)
+ //this chat message doesn't exist, return null
+ if(entry == null)
{
- callback(null, []);
+ callback();
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 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)
+ //get the authorName
+ authorManager.getAuthorName(entry.userId, function(err, authorName)
{
if(ERR(err, callback)) return;
+ entry.userName = authorName;
callback();
- })
- },
- //set in db
- setPublicStatus: function(publicStatus)
- {
- this.publicStatus = publicStatus;
- db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus);
- },
- setPassword: function(password)
- {
- this.passwordHash = password == null ? null : hash(password, generateSalt());
- db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
- },
- isCorrectPassword: function(password)
- {
- return compare(this.passwordHash, password)
- },
- isPasswordProtected: function()
- {
- return this.passwordHash != null;
+ });
}
- }, // methods
-});
+ ], 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 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;
+ db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus);
+};
+
+Pad.prototype.setPassword = function setPassword(password) {
+ this.passwordHash = password == null ? null : hash(password, generateSalt());
+ db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
+};
+
+Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
+ return compare(this.passwordHash, password);
+};
+
+Pad.prototype.isPasswordProtected = function isPasswordProtected() {
+ return this.passwordHash != null;
+};
/* Crypto helper methods */
@@ -518,18 +479,10 @@ function hash(password, salt)
function generateSalt()
{
- var len = 86;
- 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;
+ return randomString(86);
}
function compare(hashStr, password)
{
- return hash(password, hashStr.split("$")[1]) === hashStr;
+ return hash(password, hashStr.split("$")[1]) === hashStr;
}
diff --git a/node/db/PadManager.js b/node/db/PadManager.js
index 36c272637..4e3a31999 100644
--- a/node/db/PadManager.js
+++ b/node/db/PadManager.js
@@ -20,7 +20,7 @@
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
-require("../db/Pad");
+var Pad = require("../db/Pad").Pad;
var db = require("./DB").db;
/**
@@ -38,6 +38,15 @@ var globalPads = {
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
@@ -106,10 +115,43 @@ exports.doesPadExists = function(padId, callback)
db.get("pad:"+padId, function(err, value)
{
if(ERR(err, callback)) return;
- callback(null, value != null);
+ callback(null, value != null && value.atext);
});
}
+//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);
diff --git a/node/db/ReadOnlyManager.js b/node/db/ReadOnlyManager.js
index 73b3be9ec..e5dab99b4 100644
--- a/node/db/ReadOnlyManager.js
+++ b/node/db/ReadOnlyManager.js
@@ -18,9 +18,11 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
+var randomString = CommonCode.require('/pad_utils').randomString;
/**
* returns a read only id for a pad
@@ -70,18 +72,3 @@ exports.getPadId = function(readOnlyId, callback)
{
db.get("readonly2pad:" + readOnlyId, callback);
}
-
-/**
- * Generates a random String with the given length. Is needed to generate the read only ids
- */
-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;
-}
diff --git a/node/db/SecurityManager.js b/node/db/SecurityManager.js
index 52d5afcbe..33ab37d44 100644
--- a/node/db/SecurityManager.js
+++ b/node/db/SecurityManager.js
@@ -18,6 +18,7 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
@@ -25,6 +26,7 @@ var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
var settings = require("../utils/Settings")
+var randomString = CommonCode.require('/pad_utils').randomString;
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
diff --git a/node/db/SessionManager.js b/node/db/SessionManager.js
index a394f5442..c5af33c68 100644
--- a/node/db/SessionManager.js
+++ b/node/db/SessionManager.js
@@ -18,8 +18,10 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
+var randomString = CommonCode.require('/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
@@ -358,21 +360,6 @@ function listSessionsWithDBKey (dbkey, callback)
});
}
-/**
- * Generates a random String with the given length. Is needed to generate the SessionIDs
- */
-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;
-}
-
//checks if a number is an int
function is_int(value)
{
diff --git a/node/easysync_tests.js b/node/easysync_tests.js
index 5b73b7170..8e7398bea 100644
--- a/node/easysync_tests.js
+++ b/node/easysync_tests.js
@@ -20,8 +20,9 @@
* limitations under the License.
*/
-var Changeset = require('./utils/Changeset');
-var AttributePoolFactory = require("./utils/AttributePoolFactory");
+var CommonCode = require('./utils/common_code');
+var Changeset = CommonCode.require("/Changeset");
+var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
function random() {
this.nextInt = function (maxValue) {
diff --git a/node/handler/APIHandler.js b/node/handler/APIHandler.js
index 5c29f9e4c..a7f66151c 100644
--- a/node/handler/APIHandler.js
+++ b/node/handler/APIHandler.js
@@ -18,9 +18,12 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var fs = require("fs");
var api = require("../db/API");
+var padManager = require("../db/PadManager");
+var randomString = CommonCode.require('/pad_utils').randomString;
//ensure we have an apikey
var apikey = null;
@@ -95,7 +98,33 @@ exports.handle = function(functionName, fields, req, res)
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;idocument.domain = document.domain; var impexp = window.top.require('/pad_impexp').padimpexp.handleFrameCall('" + status + "'); ", 200);
});
}
diff --git a/node/handler/PadMessageHandler.js b/node/handler/PadMessageHandler.js
index d86528498..48f0aa988 100644
--- a/node/handler/PadMessageHandler.js
+++ b/node/handler/PadMessageHandler.js
@@ -18,17 +18,17 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
-var Changeset = require("../utils/Changeset");
-var AttributePoolFactory = require("../utils/AttributePoolFactory");
+var Changeset = CommonCode.require("/Changeset");
+var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
var authorManager = require("../db/AuthorManager");
var readOnlyManager = require("../db/ReadOnlyManager");
var settings = require('../utils/Settings');
var securityManager = require("../db/SecurityManager");
var log4js = require('log4js');
-var os = require("os");
var messageLogger = log4js.getLogger("message");
/**
@@ -517,7 +517,12 @@ exports.updatePadClients = function(pad, 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}});
@@ -539,7 +544,10 @@ exports.updatePadClients = function(pad, callback)
callback
);
- sessioninfos[session].rev = pad.getHeadRevisionNumber();
+ if(sessioninfos[session] != null)
+ {
+ sessioninfos[session].rev = pad.getHeadRevisionNumber();
+ }
},callback);
}
@@ -755,13 +763,6 @@ function handleClientReady(client, message)
var apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
- //check if abiword is avaiable
- var abiwordAvailable = settings.abiword != null ? "yes" : "no";
- if(settings.abiword != null && os.type().indexOf("Windows") != -1)
- {
- abiwordAvailable = "withoutPDF";
- }
-
var clientVars = {
"accountPrivs": {
"maxRevisions": 100
@@ -798,7 +799,7 @@ function handleClientReady(client, message)
"fullWidth": false,
"hideSidebar": false
},
- "abiwordAvailable": abiwordAvailable,
+ "abiwordAvailable": settings.abiwordAvailable(),
"hooks": {}
}
@@ -852,8 +853,19 @@ function handleClientReady(client, message)
//Run trough all sessions of this pad
async.forEach(pad2sessions[message.padId], function(sessionID, callback)
{
- var sessionAuthorName, sessionAuthorColorId;
-
+ 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)
@@ -861,7 +873,7 @@ function handleClientReady(client, message)
async.parallel([
function(callback)
{
- authorManager.getAuthorColorId(sessioninfos[sessionID].author, function(err, value)
+ authorManager.getAuthorColorId(author, function(err, value)
{
if(ERR(err, callback)) return;
sessionAuthorColorId = value;
@@ -870,7 +882,7 @@ function handleClientReady(client, message)
},
function(callback)
{
- authorManager.getAuthorName(sessioninfos[sessionID].author, function(err, value)
+ authorManager.getAuthorName(author, function(err, value)
{
if(ERR(err, callback)) return;
sessionAuthorName = value;
@@ -885,7 +897,7 @@ function handleClientReady(client, message)
if(sessionID != client.id)
{
//Send this Session the Notification about the new user
- socketio.sockets.sockets[sessionID].json.send(messageToTheOtherUsers);
+ socket.json.send(messageToTheOtherUsers);
//Send the new User a Notification about this other user
var messageToNotifyTheClientAboutTheOthers = {
@@ -897,7 +909,7 @@ function handleClientReady(client, message)
"colorId": sessionAuthorColorId,
"name": sessionAuthorName,
"userAgent": "Anonymous",
- "userId": sessioninfos[sessionID].author
+ "userId": author
}
}
};
diff --git a/node/handler/TimesliderMessageHandler.js b/node/handler/TimesliderMessageHandler.js
index b3493d8cb..188068430 100644
--- a/node/handler/TimesliderMessageHandler.js
+++ b/node/handler/TimesliderMessageHandler.js
@@ -18,11 +18,13 @@
* limitations under the License.
*/
+var CommonCode = require('../utils/common_code');
var ERR = require("async-stacktrace");
var async = require("async");
var padManager = require("../db/PadManager");
-var Changeset = require("../utils/Changeset");
-var AttributePoolFactory = require("../utils/AttributePoolFactory");
+var Changeset = CommonCode.require("/Changeset");
+var AttributePoolFactory = CommonCode.require("/AttributePoolFactory");
+var settings = require('../utils/Settings');
var authorManager = require("../db/AuthorManager");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
@@ -160,6 +162,7 @@ function createTimesliderClientVars (padId, callback)
fullWidth: false,
disableRightBar: false,
initialChangesets: [],
+ abiwordAvailable: settings.abiwordAvailable(),
hooks: [],
initialStyledContents: {}
};
diff --git a/node/server.js b/node/server.js
index 461e82693..c5377d81b 100644
--- a/node/server.js
+++ b/node/server.js
@@ -31,6 +31,8 @@ var async = require('async');
var express = require('express');
var path = require('path');
var minify = require('./utils/Minify');
+var CachingMiddleware = require('./utils/caching_middleware');
+var Yajsml = require('yajsml');
var formidable = require('formidable');
var apiHandler;
var exportHandler;
@@ -45,8 +47,9 @@ var socketIORouter;
var version = "";
try
{
- var ref = fs.readFileSync("../.git/HEAD", "utf-8");
- var refPath = "../.git/" + ref.substring(5, ref.indexOf("\n"));
+ var rootPath = path.normalize(__dirname + "/../")
+ 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);
@@ -60,8 +63,7 @@ console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues")
var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)";
-//cache 6 hours
-exports.maxAge = 1000*60*60*6;
+exports.maxAge = settings.maxAge;
//set loglevel
log4js.setGlobalLogLevel(settings.loglevel);
@@ -77,7 +79,39 @@ async.waterfall([
{
//create server
var app = express.createServer();
+
+ app.use(function (req, res, next) {
+ res.header("Server", serverName);
+ next();
+ });
+
+ //redirects browser to the pad's sanitized url if needed. otherwise, renders the html
+ 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_path = req.path.replace(/^\/p\/[^\/]+/, './' + sanitizedPadId);
+ res.header('Location', real_path);
+ res.send('You should be redirected to ' + real_path + '', 302);
+ }
+ //the pad id was fine, so just render it
+ else
+ {
+ next();
+ }
+ });
+ }
+ });
+
//load modules that needs a initalized db
readOnlyManager = require("./db/ReadOnlyManager");
exporthtml = require("./utils/ExportHtml");
@@ -108,31 +142,26 @@ async.waterfall([
gracefulShutdown();
});
- //serve static files
- app.get('/static/*', function(req, res)
- {
- res.header("Server", serverName);
- var filePath = path.normalize(__dirname + "/.." +
- req.url.replace(/\.\./g, '').split("?")[0]);
- res.sendfile(filePath, { maxAge: exports.maxAge });
- });
-
- //serve minified files
- app.get('/minified/:id', function(req, res, next)
- {
- res.header("Server", serverName);
-
- var id = req.params.id;
-
- if(id == "pad.js" || id == "timeslider.js")
- {
- minify.minifyJS(req,res,id);
- }
- else
- {
- next();
- }
+ // Cache both minified and static.
+ var assetCache = new CachingMiddleware;
+ app.all('/(minified|static)/*', assetCache.handle);
+
+ // Minify will serve static files compressed (minify enabled). It also has
+ // file-specific hacks for ace/require-kernel/etc.
+ 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: 'minified/'
+ , rootURI: 'http://localhost:' + settings.port + '/static/js/'
});
+ var StaticAssociator = Yajsml.associators.StaticAssociator;
+ var associations =
+ Yajsml.associators.associationsForSimpleMapping(minify.tar);
+ var associator = new StaticAssociator(associations);
+ jsServer.setAssociator(associator);
+ app.use(jsServer);
//checks for padAccess
function hasPadAccess(req, res, callback)
@@ -177,8 +206,6 @@ async.waterfall([
//serve read only pad
app.get('/ro/:id', function(req, res)
{
- res.header("Server", serverName);
-
var html;
var padId;
var pad;
@@ -232,18 +259,10 @@ async.waterfall([
res.send(html);
});
});
-
+
//serve pad.html under /p
app.get('/p/:pad', function(req, res, next)
{
- //ensure the padname is valid and the url doesn't end with a /
- if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
- {
- res.send('Such a padname is forbidden', 404);
- return;
- }
-
- res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/pad.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
@@ -251,28 +270,13 @@ async.waterfall([
//serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/timeslider', function(req, res, next)
{
- //ensure the padname is valid and the url doesn't end with a /
- if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
- {
- res.send('Such a padname is forbidden', 404);
- return;
- }
-
- res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/timeslider.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve timeslider.html under /p/$padname/timeslider
- app.get('/p/:pad/export/:type', function(req, res, next)
+ app.get('/p/:pad/:rev?/export/:type', function(req, res, next)
{
- //ensure the padname is valid and the url doesn't end with a /
- if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
- {
- res.send('Such a padname is forbidden', 404);
- return;
- }
-
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)
@@ -290,7 +294,6 @@ async.waterfall([
}
res.header("Access-Control-Allow-Origin", "*");
- res.header("Server", serverName);
hasPadAccess(req, res, function()
{
@@ -301,22 +304,13 @@ async.waterfall([
//handle import requests
app.post('/p/:pad/import', function(req, res, next)
{
- //ensure the padname is valid and the url doesn't end with a /
- if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
- {
- res.send('Such a padname is forbidden', 404);
- return;
- }
-
//if abiword is disabled, skip handling this request
if(settings.abiword == null)
{
next();
return;
}
-
- res.header("Server", serverName);
-
+
hasPadAccess(req, res, function()
{
importHandler.doImport(req, res, req.params.pad);
@@ -326,8 +320,8 @@ async.waterfall([
var apiLogger = log4js.getLogger("API");
//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("Server", serverName);
+ var apiCaller = function(req, res, fields)
+ {
res.header("Content-Type", "application/json; charset=utf-8");
apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields));
@@ -388,7 +382,6 @@ async.waterfall([
//serve index.html under /
app.get('/', function(req, res)
{
- res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/index.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
@@ -396,7 +389,6 @@ async.waterfall([
//serve robots.txt
app.get('/robots.txt', function(req, res)
{
- res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/robots.txt");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
@@ -404,7 +396,6 @@ async.waterfall([
//serve favicon.ico
app.get('/favicon.ico', function(req, res)
{
- res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/custom/favicon.ico");
res.sendfile(filePath, { maxAge: exports.maxAge }, function(err)
{
diff --git a/node/utils/Abiword.js b/node/utils/Abiword.js
index c53a6ab37..27138e648 100644
--- a/node/utils/Abiword.js
+++ b/node/utils/Abiword.js
@@ -53,7 +53,7 @@ if(os.type().indexOf("Windows") > -1)
abiword.on('exit', function (code)
{
if(code != 0) {
- throw "Abiword died with exit code " + code;
+ return callback("Abiword died with exit code " + code);
}
if(stdoutBuffer != "")
@@ -75,52 +75,54 @@ if(os.type().indexOf("Windows") > -1)
else
{
//spawn the abiword process
- var abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]);
-
- //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)
- {
- throw "Abiword died with exit code " + code;
- });
-
- //delegate the processing of stdout to a other function
- abiword.stdout.on('data',onAbiwordStdout);
-
+ var abiword;
var stdoutCallback = null;
- var stdoutBuffer = "";
- var firstPrompt = true;
+ var spawnAbiword = function (){
+ abiword = spawn(settings.abiword, ["--plugin", "AbiCommand"]);
+ var stdoutBuffer = "";
+ var firstPrompt = true;
- function onAbiwordStdout(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)
+ //append error messages to the buffer
+ abiword.stderr.on('data', function (data)
{
- //filter the feedback message
- var err = stdoutBuffer.search("OK") != -1 ? null : stdoutBuffer;
+ 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();
- //reset the buffer
- stdoutBuffer = "";
-
- //call the callback with the error message
- //skip the first prompt
- if(stdoutCallback != null && !firstPrompt)
+ //we're searching for the prompt, cause this means everything we need is in the buffer
+ if(stdoutBuffer.search("AbiWord:>") != -1)
{
- stdoutCallback(err);
- stdoutCallback = null;
+ //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;
}
-
- firstPrompt = false;
- }
+ });
}
+ spawnAbiword();
doConvertTask = function(task, callback)
{
@@ -130,6 +132,7 @@ else
stdoutCallback = function (err)
{
callback();
+ console.log("queue continue");
task.callback(err);
};
}
diff --git a/node/utils/Cli.js b/node/utils/Cli.js
new file mode 100644
index 000000000..0c7947e91
--- /dev/null
+++ b/node/utils/Cli.js
@@ -0,0 +1,38 @@
+/**
+ * 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;
+}
diff --git a/node/utils/ExportDokuWiki.js b/node/utils/ExportDokuWiki.js
index 48e4b2915..abe6d3471 100644
--- a/node/utils/ExportDokuWiki.js
+++ b/node/utils/ExportDokuWiki.js
@@ -15,7 +15,8 @@
*/
var async = require("async");
-var Changeset = require("./Changeset");
+var CommonCode = require('./common_code');
+var Changeset = CommonCode.require("/Changeset");
var padManager = require("../db/PadManager");
function getPadDokuWiki(pad, revNum, callback)
diff --git a/node/utils/ExportHtml.js b/node/utils/ExportHtml.js
index 46ed980a5..afeafd3a9 100644
--- a/node/utils/ExportHtml.js
+++ b/node/utils/ExportHtml.js
@@ -14,10 +14,12 @@
* limitations under the License.
*/
+var CommonCode = require('./common_code');
var async = require("async");
-var Changeset = require("./Changeset");
+var Changeset = CommonCode.require("/Changeset");
var padManager = require("../db/PadManager");
var ERR = require("async-stacktrace");
+var Security = CommonCode.require('/security');
function getPadPlainText(pad, revNum)
{
@@ -269,7 +271,7 @@ function getHTMLFromAtext(pad, atext)
//from but they break the abiword parser and are completly useless
s = s.replace(String.fromCharCode(12), "");
- assem.append(_escapeHTML(s));
+ assem.append(_encodeWhitespace(Security.escapeHTML(s)));
} // end iteration over spans in line
var tags2close = [];
@@ -292,7 +294,7 @@ function getHTMLFromAtext(pad, atext)
var url = urlData[1];
var urlLength = url.length;
processNextChars(startIndex - idx);
- assem.append('');
+ assem.append('');
processNextChars(urlLength);
assem.append('');
});
@@ -309,13 +311,14 @@ function getHTMLFromAtext(pad, atext)
// 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 || lists.length > 0)
+
+ if (line.listLevel)//If we are inside a list
{
// do list stuff
var whichList = -1; // index into lists or -1
@@ -331,41 +334,89 @@ function getHTMLFromAtext(pad, atext)
}
}
- if (whichList >= lists.length)
+ if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line
{
lists.push([line.listLevel, line.listTypeName]);
- pieces.push('- ', lineContent || '
');
+ if(line.listTypeName == "number")
+ {
+ pieces.push('- ', lineContent || '
');
+ }
+ else
+ {
+ pieces.push('- ', lineContent || '
');
+ }
}
- else if (whichList == -1)
+ //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
- pieces.push(new Array(lists.length + 1).join('
'));
+ if(line.listTypeName == "number")
+ {
+ pieces.push(new Array(lists.length + 1).join('
'));
+ }
+ else
+ {
+ pieces.push(new Array(lists.length + 1).join('
'));
+ }
lists.length = 0;
pieces.push(lineContent, '
');
}
else
{
+ console.log('trace 2');
pieces.push('
');
}
- }
- else
+ }*/
+ else//means we are getting closer to the lowest level of indentation
{
while (whichList < lists.length - 1)
{
- pieces.push('
');
+ if(lists[lists.length - 1][1] == "number")
+ {
+ pieces.push('
');
+ }
+ else
+ {
+ pieces.push('');
+ }
lists.length--;
}
pieces.push('', lineContent || '
');
}
}
- else
+ 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('');
+ }
+ else
+ {
+ pieces.push('');
+ }
+ lists.length--;
+ }
pieces.push(lineContent, '
');
}
}
- pieces.push(new Array(lists.length + 1).join(''));
+
+ for (var k = lists.length - 1; k >= 0; k--)
+ {
+ if(lists[k][1] == "number")
+ {
+ pieces.push('');
+ }
+ else
+ {
+ pieces.push('');
+ }
+ }
return pieces.join('');
}
@@ -415,7 +466,24 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
{
if(ERR(err, callback)) return;
- var head = (noDocType ? '' : '\n') + '\n' + (noDocType ? '' : '\n' + '\n' + '\n' + '\n') + '';
+ var head =
+ (noDocType ? '' : '\n') +
+ '\n' + (noDocType ? '' : '\n' +
+ '\n' +
+ '\n' + '\n') +
+ '';
var foot = '\n\n';
@@ -427,24 +495,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
});
}
-function _escapeHTML(s)
-{
- var re = /[&<>]/g;
- if (!re.MAP)
- {
- // persisted across function calls!
- re.MAP = {
- '&': '&',
- '<': '<',
- '>': '>',
- };
- }
-
- s = s.replace(re, function (c)
- {
- return re.MAP[c];
- });
-
+function _encodeWhitespace(s) {
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c)
{
return "" +c.charCodeAt(0) + ";"
diff --git a/node/utils/ImportHtml.js b/node/utils/ImportHtml.js
index 1b0bcaea7..ce8663697 100644
--- a/node/utils/ImportHtml.js
+++ b/node/utils/ImportHtml.js
@@ -17,9 +17,10 @@
var jsdom = require('jsdom-nocontextifiy').jsdom;
var log4js = require('log4js');
-var Changeset = require("./Changeset");
-var contentcollector = require("./contentcollector");
-var map = require("../../static/js/ace2_common.js").map;
+var CommonCode = require('../utils/common_code');
+var Changeset = CommonCode.require("/Changeset");
+var contentcollector = CommonCode.require("/contentcollector");
+var map = CommonCode.require("/ace2_common").map;
function setPadHTML(pad, html, callback)
{
diff --git a/node/utils/Minify.js b/node/utils/Minify.js
index fd1dd0793..a49195a7b 100644
--- a/node/utils/Minify.js
+++ b/node/utils/Minify.js
@@ -27,285 +27,257 @@ var cleanCSS = require('clean-css');
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
var path = require('path');
-var Buffer = require('buffer').Buffer;
-var gzip = require('gzip');
+var RequireKernel = require('require-kernel');
var server = require('../server');
-var os = require('os');
-var padJS = ["jquery.min.js", "pad_utils.js", "plugins.js", "undo-xpopup.js", "json2.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", "pad2.js", "jquery-ui.js", "chat.js", "excanvas.js", "farbtastic.js"];
+var ROOT_DIR = path.normalize(__dirname + "/../../static/");
+var TAR_PATH = path.join(__dirname, 'tar.json');
+var tar = JSON.parse(fs.readFileSync(TAR_PATH, 'utf8'));
-var timesliderJS = ["jquery.min.js", "plugins.js", "undo-xpopup.js", "json2.js", "colorutils.js", "draggable.js", "pad_utils.js", "pad_cookie.js", "pad_editor.js", "pad_editbar.js", "pad_docbar.js", "pad_modals.js", "easysync2_client.js", "domline_client.js", "linestylefilter_client.js", "cssmanager_client.js", "broadcast.js", "broadcast_slider.js", "broadcast_revisions.js"];
+// Rewrite tar to include modules with no extensions and proper rooted paths.
+exports.tar = {};
+for (var key in tar) {
+ exports.tar['/' + key] =
+ tar[key].map(function (p) {return '/' + p}).concat(
+ tar[key].map(function (p) {return '/' + p.replace(/\.js$/, '')})
+ );
+}
/**
* creates the minifed javascript for the given minified name
* @param req the Express request
* @param res the Express response
*/
-exports.minifyJS = function(req, res, jsFilename)
+exports.minify = function(req, res, next)
{
- res.header("Content-Type","text/javascript");
-
- //choose the js files we need
- if(jsFilename == "pad.js")
- {
- jsFiles = padJS;
+ 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;
}
- else if(jsFilename == "timeslider.js")
- {
- jsFiles = timesliderJS;
+
+ // 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";
}
- else
- {
- throw new Error("there is no profile for creating " + name);
- }
-
- //minifying is enabled
- if(settings.minify)
- {
- var fileValues = {};
- var embeds = {};
- var latestModification = 0;
-
- async.series([
- //find out the highest modification date
- function(callback)
- {
- var folders2check = ["../static/css","../static/js"];
-
- //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);
- });
- }, callback);
- },
- function(callback)
- {
- //check the modification time of the minified js
- fs.stat("../var/minified_" + jsFilename, function(err, stats)
- {
- if(err && err.code != "ENOENT")
- {
- ERR(err, callback);
- return;
- }
-
- //there is no minfied file or there new changes since this file was generated, so continue generating this file
- if((err && err.code == "ENOENT") || stats.mtime.getTime() < latestModification)
- {
- callback();
- }
- //the minified file is still up to date, stop minifying
- else
- {
- callback("stop");
- }
+
+ 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 (server.maxAge) {
+ var expiresDate = new Date((new Date()).getTime()+server.maxAge*1000);
+ res.setHeader('expires', expiresDate.toUTCString());
+ res.setHeader('cache-control', 'max-age=' + server.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)) return;
+ res.header("Content-Type", contentType);
+ res.writeHead(200, {});
+ res.write(content);
+ res.end();
});
- },
- //load all js files
- function (callback)
- {
- async.forEach(jsFiles, function (item, callback)
- {
- fs.readFile("../static/js/" + item, "utf-8", function(err, data)
- {
- if(ERR(err, callback)) return;
- fileValues[item] = data;
- callback();
- });
- }, callback);
- },
- //find all includes in ace.js and embed them
- function(callback)
- {
- //if this is not the creation of pad.js, skip this part
- if(jsFilename != "pad.js")
- {
- callback();
- return;
+ } 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
+
+ request(baseURI + path.normalize(path.join('/static/', filename)), function (error, response, body) {
+ if (!error && response.statusCode == 200) {
+ data += 'Ace2Editor.EMBEDED[' + JSON.stringify(filename) + '] = '
+ + JSON.stringify(body || '') + ';\n';
+ } else {
+ // Silence?
}
-
- var founds = fileValues["ace.js"].match(/\$\$INCLUDE_[a-zA-Z_]+\([a-zA-Z0-9.\/_"]+\)/gi);
-
- //go trough all includes
- async.forEach(founds, function (item, callback)
- {
- var filename = item.match(/"[^"]*"/g)[0].substr(1);
- filename = filename.substr(0,filename.length-1);
-
- var type = item.match(/INCLUDE_[A-Z]+/g)[0].substr("INCLUDE_".length);
-
- var quote = item.search("_Q") != -1;
-
- //read the included file
- fs.readFile(filename, "utf-8", function(err, data)
- {
- if(ERR(err, callback)) return;
-
- //compress the file
- if(type == "JS")
- {
- embeds[item] = "
-
-
New Pad
or create/open a Pad with the name
-
-
+=======
+ // start the custom js
+ if (typeof customStart == "function") customStart();
+
+
+>>>>>>> 6fd73ecfda27fd6639c30a4c05c6149d66f669ee