diff --git a/CHANGELOG.md b/CHANGELOG.md index 5810ed25..613cd896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# v1.1.5 + * We updated to express v3 (please [make sure](https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x) your plugin works under express v3) + * `userColor` URL parameter which sets the initial author color + * Hooks for "padCreate", "padRemove", "padUpdate" and "padLoad" events + * Security patches concerning the handling of messages originating from clients + * Our database abstraction layer now natively supports couchDB, levelDB, mongoDB, postgres, and redis! + * We now provide a script helping you to migrate from dirtyDB to MySQL + * Support running Etherpad Lite behind IIS, using [iisnode](https://github.com/tjanczuk/iisnode/wiki) + * LibreJS Licensing information in headers of HTML templates + * Default port number to PORT env var, if port isn't specified in settings + * Fix for `convert.js` + * Raise upper char limit in chat to 999 characters + * Fixes for mobile layout + * Fixes for usage behind reverse proxy + * Improved documentation + * Fixed some opera style bugs + * Update npm and fix some bugs, this introduces + # v1.1 * Introduced Plugin framework * Many bugfixes diff --git a/Makefile b/Makefile index 01f30701..ab720f28 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,20 @@ -doc_dirs = doc $(wildcard doc/*/) -outdoc_dirs = out $(addprefix out/,$(doc_dirs)) doc_sources = $(wildcard doc/*/*.md) $(wildcard doc/*.md) outdoc_files = $(addprefix out/,$(doc_sources:.md=.html)) -docs: $(outdoc_files) +docassets = $(addprefix out/,$(wildcard doc/_assets/*)) + +VERSION = $(shell node -e "console.log( require('./src/package.json').version )") + +docs: $(outdoc_files) $(docassets) + +out/doc/_assets/%: doc/_assets/% + mkdir -p $(@D) + cp $< $@ out/doc/%.html: doc/%.md mkdir -p $(@D) node tools/doc/generate.js --format=html --template=doc/template.html $< > $@ + cat $@ | sed 's/__VERSION__/${VERSION}/' > $@ clean: rm -rf out/ diff --git a/README.plugins b/README.plugins deleted file mode 100644 index 72c45644..00000000 --- a/README.plugins +++ /dev/null @@ -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? \ No newline at end of file diff --git a/bin/convert.js b/bin/convert.js index ec792717..4bbdd667 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,9 +1,9 @@ var startTime = new Date().getTime(); var fs = require("fs"); -var ueberDB = require("ueberDB"); -var mysql = require("mysql"); -var async = require("async"); +var ueberDB = require("../src/node_modules/ueberDB"); +var mysql = require("../src/node_modules/ueberDB/node_modules/mysql"); +var async = require("../src/node_modules/async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var AttributePool = require("ep_etherpad-lite/static/js/AttributePool"); diff --git a/bin/migrateDirtyDBtoMySQL.js b/bin/migrateDirtyDBtoMySQL.js new file mode 100644 index 00000000..f2bc8efe --- /dev/null +++ b/bin/migrateDirtyDBtoMySQL.js @@ -0,0 +1,11 @@ +var dirty = require("../src/node_modules/ueberDB/node_modules/dirty")('var/dirty.db'); +var db = require("../src/node/db/DB"); + +db.init(function() { + db = db.db; + dirty.on("load", function() { + dirty.forEach(function(key, value) { + db.set(key, value); + }); + }); +}); diff --git a/doc/_assets/style.css b/doc/_assets/style.css new file mode 100644 index 00000000..fe1343af --- /dev/null +++ b/doc/_assets/style.css @@ -0,0 +1,44 @@ +body.apidoc { + width: 60%; + min-width: 10cm; + margin: 0 auto; +} + +#header { + background-color: #5a5; + padding: 10px; + color: #111; +} + +a, +a:active { + color: #272; +} +a:focus, +a:hover { + color: #050; +} + +#apicontent a.mark, +#apicontent a.mark:active { + float: right; + color: #BBB; + font-size: 0.7cm; + text-decoration: none; +} +#apicontent a.mark:focus, +#apicontent a.mark:hover { + color: #AAA; +} + +#apicontent code { + padding: 1px; + background-color: #EEE; + border-radius: 4px; + border: 1px solid #DDD; +} +#apicontent pre>code { + display: block; + overflow: auto; + padding: 5px; +} \ No newline at end of file diff --git a/doc/all.md b/doc/all.md index c0cbf369..f1e071a6 100644 --- a/doc/all.md +++ b/doc/all.md @@ -1,3 +1,4 @@ @include documentation @include api/api +@include plugins @include database diff --git a/doc/api/api.md b/doc/api/api.md index b96fa0c8..eb5bb9c9 100644 --- a/doc/api/api.md +++ b/doc/api/api.md @@ -1,7 +1,8 @@ @include embed_parameters @include http_api -@include hooks +@include hooks_overview @include hooks_client-side @include hooks_server-side @include editorInfo -@include changeset_library \ No newline at end of file +@include changeset_library +@include pluginfw \ No newline at end of file diff --git a/doc/api/hooks.md b/doc/api/hooks_overview.md similarity index 100% rename from doc/api/hooks.md rename to doc/api/hooks_overview.md diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 854b4339..c60d810e 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -49,7 +49,8 @@ Called from: src/node/server.js Things in context: -1. app - the main application object (helpful for adding new paths and such) +1. app - the main express application object (helpful for adding new paths and such) +2. server - the http server object This hook gets called after the application object has been created, but before it starts listening. This is similar to the expressConfigure hook, but it's not guaranteed that the application object will have all relevant configuration variables. @@ -64,6 +65,42 @@ This hook gets called upon the rendering of an ejs template block. For any speci Have a look at `src/templates/pad.html` and `src/templates/timeslider.html` to see which blocks are available. +## padCreate +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when a new pad was created. + +## padLoad +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when an pad was loaded. If a new pad was created and loaded this event will be emitted too. + +## padUpdate +Called from: src/node/db/Pad.js + +Things in context: + +1. pad - the pad instance + +This hook gets called when an existing pad was updated. + +## padRemove +Called from: src/node/db/Pad.js + +Things in context: + +1. padID + +This hook gets called when an existing pad was removed/deleted. + ## socketio Called from: src/node/hooks/express/socketio.js @@ -71,6 +108,7 @@ Things in context: 1. app - the application object 2. io - the socketio object +3. server - the http server object I have no idea what this is useful for, someone else will have to add this description. diff --git a/doc/api/http_api.md b/doc/api/http_api.md index 3afab498..990d1b49 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -62,7 +62,7 @@ Portal submits content into new blog post ### Request Format -The API is accessible via HTTP. HTTP Requests are in the format /api/$APIVERSION/$FUNCTIONNAME. Parameters are transmitted via HTTP GET. $APIVERSION is 1 +The API is accessible via HTTP. HTTP Requests are in the format /api/$APIVERSION/$FUNCTIONNAME. Parameters are transmitted via HTTP GET. $APIVERSION depends on the endpoints you want to use. The latest version is `1.1` ### Response Format Responses are valid JSON in the following format: @@ -116,43 +116,93 @@ Example usage: http://api.jquery.com/jQuery.getJSON/ ### Groups Pads can belong to a group. The padID of grouppads is starting with a groupID like g.asdfasdfasdfasdf$test -* **createGroup()** creates a new group

*Example returns:* +#### createGroup() + * API >= 1 + +creates a new group + +*Example returns:* * `{code: 0, message:"ok", data: {groupID: g.s8oes9dhwrvt0zif}}` -* **createGroupIfNotExistsFor(groupMapper)** this functions helps you to map your application group ids to etherpad lite group ids

*Example returns:* +#### createGroupIfNotExistsFor(groupMapper) + * API >= 1 + +this functions helps you to map your application group ids to etherpad lite group ids + +*Example returns:* * `{code: 0, message:"ok", data: {groupID: g.s8oes9dhwrvt0zif}}` -* **deleteGroup(groupID)** deletes a group

*Example returns:* +#### deleteGroup(groupID) + * API >= 1 + +deletes a group + +*Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"groupID does not exist", data: null}` -* **listPads(groupID)** returns all pads of this group

*Example returns:* +#### listPads(groupID) + * API >= 1 + +returns all pads of this group + +*Example returns:* * `{code: 0, message:"ok", data: {padIDs : ["g.s8oes9dhwrvt0zif$test", "g.s8oes9dhwrvt0zif$test2"]}` * `{code: 1, message:"groupID does not exist", data: null}` -* **createGroupPad(groupID, padName [, text])** creates a new pad in this group

*Example returns:* +#### createGroupPad(groupID, padName [, text]) + * API >= 1 + +creates a new pad in this group + +*Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"pad does already exist", data: null}` * `{code: 1, message:"groupID does not exist", data: null}` -* **listAllGroups()** lists all existing groups

*Example returns:* +#### listAllGroups() + * API >= 1 + +lists all existing groups + +*Example returns:* * `{code: 0, message:"ok", data: {groupIDs: ["g.mKjkmnAbSMtCt8eL", "g.3ADWx6sbGuAiUmCy"]}}` * `{code: 0, message:"ok", data: {groupIDs: []}}` ### Author These authors are bound to the attributes the users choose (color and name). -* **createAuthor([name])** creates a new author

*Example returns:* +#### createAuthor([name]) + * API >= 1 + +creates a new author + +*Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif"}}` -* **createAuthorIfNotExistsFor(authorMapper [, name])** this functions helps you to map your application author ids to etherpad lite author ids

*Example returns:* +#### createAuthorIfNotExistsFor(authorMapper [, name]) + * API >= 1 + +this functions helps you to map your application author ids to etherpad lite author ids + +*Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif"}}` -* **listPadsOfAuthor(authorID)** returns an array of all pads this author contributed to

*Example returns:* +#### listPadsOfAuthor(authorID) + * API >= 1 + +returns an array of all pads this author contributed to + +*Example returns:* * `{code: 0, message:"ok", data: {padIDs: ["g.s8oes9dhwrvt0zif$test", "g.s8oejklhwrvt0zif$foo"]}}` * `{code: 1, message:"authorID does not exist", data: null}` -* **getAuthorName(authorID)** Returns the Author Name of the author

*Example returns:* +#### getAuthorName(authorID) + * API >= 1.1 + +Returns the Author Name of the author + +*Example returns:* * `{code: 0, message:"ok", data: {authorName: "John McLear"}}` -> can't be deleted cause this would involve scanning all the pads where this author was @@ -160,25 +210,50 @@ These authors are bound to the attributes the users choose (color and name). ### Session Sessions can be created between a group and an author. This allows an author to access more than one group. The sessionID will be set as a cookie to the client and is valid until a certain date. The session cookie can also contain multiple comma-seperated sessionIDs, allowing a user to edit pads in different groups at the same time. Only users with a valid session for this group, can access group pads. You can create a session after you authenticated the user at your web application, to give them access to the pads. You should save the sessionID of this session and delete it after the user logged out. -* **createSession(groupID, authorID, validUntil)** creates a new session. validUntil is an unix timestamp in seconds

*Example returns:* +#### createSession(groupID, authorID, validUntil) + * API >= 1 + +creates a new session. validUntil is an unix timestamp in seconds + +*Example returns:* * `{code: 0, message:"ok", data: {sessionID: "s.s8oes9dhwrvt0zif"}}` * `{code: 1, message:"groupID doesn't exist", data: null}` * `{code: 1, message:"authorID doesn't exist", data: null}` * `{code: 1, message:"validUntil is in the past", data: null}` -* **deleteSession(sessionID)** deletes a session

*Example returns:* +#### deleteSession(sessionID) + * API >= 1 + +deletes a session + +*Example returns:* * `{code: 1, message:"ok", data: null}` * `{code: 1, message:"sessionID does not exist", data: null}` -* **getSessionInfo(sessionID)** returns informations about a session

*Example returns:* +#### getSessionInfo(sessionID) + * API >= 1 + +returns informations about a session + +*Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` * `{code: 1, message:"sessionID does not exist", data: null}` -* **listSessionsOfGroup(groupID)** returns all sessions of a group

*Example returns:* +#### listSessionsOfGroup(groupID) + * API >= 1 + +returns all sessions of a group + +*Example returns:* * `{"code":0,"message":"ok","data":{"s.oxf2ras6lvhv2132":{"groupID":"g.s8oes9dhwrvt0zif","authorID":"a.akf8finncvomlqva","validUntil":2312905480}}}` * `{code: 1, message:"groupID does not exist", data: null}` -* **listSessionsOfAuthor(authorID)** returns all sessions of an author

*Example returns:* +#### listSessionsOfAuthor(authorID) + * API >= 1 + +returns all sessions of an author + +*Example returns:* * `{"code":0,"message":"ok","data":{"s.oxf2ras6lvhv2132":{"groupID":"g.s8oes9dhwrvt0zif","authorID":"a.akf8finncvomlqva","validUntil":2312905480}}}` * `{code: 1, message:"authorID does not exist", data: null}` @@ -186,69 +261,149 @@ Sessions can be created between a group and an author. This allows an author to Pad content can be updated and retrieved through the API -* **getText(padID, [rev])** returns the text of a pad

*Example returns:* +#### getText(padID, [rev]) + * API >= 1 + +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}` -* **setText(padID, text)** sets the text of a pad

*Example returns:* +#### setText(padID, text) + * API >= 1 + +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}` -* **getHTML(padID, [rev])** returns the text of a pad formatted as HTML

*Example returns:* +#### getHTML(padID, [rev]) + * API >= 1 + +returns the text of a pad formatted as HTML + +*Example returns:* * `{code: 0, message:"ok", data: {html:"Welcome Text
More Text"}}` * `{code: 1, message:"padID does not exist", data: null}` ### Pad Group pads are normal pads, but with the name schema GROUPID$PADNAME. A security manager controls access of them and its forbidden for normal pads to include a $ in the name. -* **createPad(padID [, text])** creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**.

*Example returns:* +#### createPad(padID [, text]) + * API >= 1 + +creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**. + +*Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"pad does already exist", data: null}` -* **getRevisionsCount(padID)** returns the number of revisions of this pad

*Example returns:* +#### getRevisionsCount(padID) + * API >= 1 + +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}` -* **padUsersCount(padID)** returns the number of user that are currently editing this pad

*Example returns:* +#### padUsersCount(padID) + * API >= 1 + +returns the number of user that are currently editing this pad + +*Example returns:* * `{code: 0, message:"ok", data: {padUsersCount: 5}}` -* **padUsers(padID)** returns the list of users that are currently editing this pad

*Example returns:* +#### padUsers(padID) + * API >= 1.1 + +returns the list of users that are currently editing this pad + +*Example returns:* * `{code: 0, message:"ok", data: {padUsers: [{colorId:"#c1a9d9","name":"username1","timestamp":1345228793126},{"colorId":"#d9a9cd","name":"Hmmm","timestamp":1345228796042}]}}` * `{code: 0, message:"ok", data: {padUsers: []}}` -* **deletePad(padID)** deletes a pad

*Example returns:* +#### deletePad(padID) + * API >= 1 + +deletes a pad + +*Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"padID does not exist", data: null}` -* **getReadOnlyID(padID)** returns the read only link of a pad

*Example returns:* +#### getReadOnlyID(padID) + * API >= 1 + +returns the read only link of a pad + +*Example returns:* * `{code: 0, message:"ok", data: {readOnlyID: "r.s8oes9dhwrvt0zif"}}` * `{code: 1, message:"padID does not exist", data: null}` -* **setPublicStatus(padID, publicStatus)** sets a boolean for the public status of a pad

*Example returns:* +#### setPublicStatus(padID, publicStatus) + * API >= 1 + +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}` -* **getPublicStatus(padID)** return true of false

*Example returns:* +#### getPublicStatus(padID) + * API >= 1 + +return true of false + +*Example returns:* * `{code: 0, message:"ok", data: {publicStatus: true}}` * `{code: 1, message:"padID does not exist", data: null}` -* **setPassword(padID, password)** returns ok or a error message

*Example returns:* +#### setPassword(padID, password) + * API >= 1 + +returns ok or a error message + +*Example returns:* * `{code: 0, message:"ok", data: null}` * `{code: 1, message:"padID does not exist", data: null}` -* **isPasswordProtected(padID)** returns true or false

*Example returns:* +#### isPasswordProtected(padID) + * API >= 1 + +returns true or false + +*Example returns:* * `{code: 0, message:"ok", data: {passwordProtection: true}}` * `{code: 1, message:"padID does not exist", data: null}` -* **listAuthorsOfPad(padID)** returns an array of authors who contributed to this pad

*Example returns:* +#### listAuthorsOfPad(padID) + * API >= 1 + +returns an array of authors who contributed to this pad + +*Example returns:* * `{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}` * `{code: 1, message:"padID does not exist", data: null}` -* **getLastEdited(padID)** returns the timestamp of the last revision of the pad

*Example returns:* +#### getLastEdited(padID) + * API >= 1 + +returns the timestamp of the last revision of the pad + +*Example returns:* * `{code: 0, message:"ok", data: {lastEdited: 1340815946602}}` * `{code: 1, message:"padID does not exist", data: null}` -* **sendClientsMessage(padID, msg)** sends a custom message of type `msg` to the pad

*Example returns:* +#### sendClientsMessage(padID, msg) + * API >= 1.1 + +sends a custom message of type `msg` to the pad + +*Example returns:* * `{code: 0, message:"ok", data: {}}` * `{code: 1, message:"padID does not exist", data: null}` diff --git a/doc/api/pluginfw.md b/doc/api/pluginfw.md new file mode 100644 index 00000000..2189c74c --- /dev/null +++ b/doc/api/pluginfw.md @@ -0,0 +1,14 @@ +# Plugin Framework +`require("ep_etherpad-lite/static/js/plugingfw/plugins")` + +## plugins.update +`require("ep_etherpad-lite/static/js/plugingfw/plugins").update()` will use npm to list all installed modules and read their ep.json files, registering the contained hooks. +A hook registration is a pairs of a hook name and a function reference (filename for require() plus function name) + +## hooks.callAll +`require("ep_etherpad-lite/static/js/plugingfw/hooks").callAll("hook_name", {argname:value})` will call all hook functions registered for `hook_name` with `{argname:value}`. + +## hooks.aCallAll +? + +## ... diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 00000000..3717c111 --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,106 @@ +# Plugins +Etherpad-Lite allows you to extend its functionality with plugins. A plugin registers hooks (functions) for certain events (thus certain features) in Etherpad-lite to execute its own functionality based on these events. + +Publicly available plugins can be found in the npm registry (see ). Etherpad-lite's naming convention for plugins is to prefix your plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install plugins from npm, using `npm install ep_flubberworm` in etherpad-lite's root directory. + +You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will list all installed plugins and those available on npm. It even provides functionality to search through all available plugins. + +## Folder structure +A basic plugin usually has the following folder structure: +``` +ep_/ + | static/ + | templates/ + + ep.json + + package.json +``` +If your plugin includes client-side hooks, put them in `static/js/`. If you're adding in CSS or image files, you should put those files in `static/css/ `and `static/image/`, respectively, and templates go into `templates/`. + +A Standard directory structure like this makes it easier to navigate through your code. That said, do note, that this is not actually *required* to make your plugin run. + +## Plugin definition +Your plugin definition goes into `ep.json`. In this file you register your hooks, indicate the parts of your plugin and the order of execution. (A documentation of all available events to hook into can be found in chapter [hooks](#all_hooks).) + +A hook registration is a pairs of a hook name and a function reference (filename to require() + exported function name) + +```json +{ + "parts": [ + { + "name": "nameThisPartHoweverYouWant", + "hooks": { + "authenticate" : "ep_/:FUNCTIONNAME1", + "expressCreateServer": "ep_/:FUNCTIONNAME2" + }, + "client_hooks": { + "acePopulateDOMLine": "ep_plugin/:FUNCTIONNAME3" + } + } + ] +} +``` + +Etherpad-lite will expect the part of the hook definition before the colon to be a javascript file and will try to require it. The part after the colon is expected to be a valid function identifier of that module. So, you have to export your hooks, using [`module.exports`](http://nodejs.org/docs/latest/api/modules.html#modules_modules) and register it in `ep.json` as `ep_/path/to/:FUNCTIONNAME`. +You can omit the `FUNCTIONNAME` part, if the exported function has got the same name as the hook. So `"authorize" : "ep_flubberworm/foo"` will call the function `exports.authorize` in `ep_flubberworm/foo.js` + +### Client hooks and server hooks +There are server hooks, which will be executed on the server (e.g. `expressCreateServer`), and there are client hooks, which are executed on the client (e.g. `acePopulateDomLine`). Be sure to not make assumptions about the environment your code is running in, e.g. don't try to access `process`, if you know your code will be run on the client, and likewise, don't try to access `window` on the server... + +### Parts +As your plugins become more and more complex, you will find yourself in the need to manage dependencies between plugins. E.g. you want the hooks of a certain plugin to be executed before (or after) yours. You can also manage these dependencies in your plugin definition file `ep.json`: + +```javascript +{ + "parts": [ + { + "name": "onepart", + "pre": [], + "post": ["ep_onemoreplugin/partone"] + "hooks": { + "storeBar": "ep_monospace/plugin:storeBar", + "getFoo": "ep_monospace/plugin:getFoo", + } + }, + { + "name": "otherpart", + "pre": ["ep_my_example/somepart", "ep_otherplugin/main"], + "post": [], + "hooks": { + "someEvent": "ep_my_example/otherpart:someEvent", + "another": "ep_my_example/otherpart:another" + } + } + ] +} +``` + +Usually a plugin will add only one functionality at a time, so it will probably only use one `part` definition to register its hooks. However, sometimes you have to put different (unrelated) functionalities into one plugin. For this you will want use parts, so other plugins can depend on them. + +#### pre/post +The `"pre"` and `"post"` definitions, affect the order in which parts of a plugin are executed. This ensures that plugins and their hooks are executed in the correct order. + +`"pre"` lists parts that must be executed *before* the defining part. `"post"` lists parts that must be executed *after* the defining part. + +You can, on a basic level, think of this as double-ended dependency listing. If you have a dependency on another plugin, you can make sure it loads before yours by putting it in `"pre"`. If you are setting up things that might need to be used by a plugin later, you can ensure proper order by putting it in `"post"`. + +Note that it would be far more sane to use `"pre"` in almost any case, but if you want to change config variables for another plugin, or maybe modify its environment, `"post"` could definitely be useful. + +Also, note that dependencies should *also* be listed in your package.json, so they can be `npm install`'d automagically when your plugin gets installed. + +## Package definition +Your plugin must also contain a [package definition file](http://npmjs.org/doc/json.html), called package.json, in the project root - this file contains various metadata relevant to your plugin, such as the name and version number, author, project hompage, contributors, a short description, etc. If you publish your plugin on npm, these metadata are used for package search etc., but it's necessary for Etherpad-lite plugins, even if you don't publish your plugin. + +```json +{ + "name": "ep_PLUGINNAME", + "version": "0.0.1", + "description": "DESCRIPTION", + "author": "USERNAME (REAL NAME) ", + "contributors": [], + "dependencies": {"MODULE": "0.3.20"}, + "engines": { "node": ">= 0.6.0"} +} +``` + +## Templates +If your plugin adds or modifies the front end HTML (e.g. adding buttons or changing their functions), you should put the necessary HTML code for such operations in `templates/`, in files of type ".ejs", since Etherpad-Lite uses EJS for HTML templating. See the following link for more information about EJS: . \ No newline at end of file diff --git a/doc/template.html b/doc/template.html index 2eb93987..6416da94 100644 --- a/doc/template.html +++ b/doc/template.html @@ -2,12 +2,12 @@ - __SECTION__ Etherpad-Lite Manual & Documentation - + __SECTION__ - Etherpad Lite v__VERSION__ Manual & Documentation +
diff --git a/settings.json.template b/settings.json.template index 7d175a34..087c710c 100644 --- a/settings.json.template +++ b/settings.json.template @@ -27,7 +27,7 @@ */ //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", + "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\nGet involved with Etherpad at http:\/\/etherpad.org\n", /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */ "requireSession" : false, diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 2a6625c8..28b2dd91 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -141,22 +141,6 @@ exports.getAuthor = function (author, callback) db.get("globalAuthor:" + author, callback); } -/** - * Returns the Author Name of the author - * @param {String} author The id of the author - * @param {Function} callback callback(err, name) - */ - -exports.getAuthorName = function (authorID, callback) -{ - db.getSub("globalAuthor:" + author, ["name"], callback); - console.log(authorID); - db.getSub("globalAuthor:" + authorID, ["name"], function(err, authorName){ - if(ERR(err, callback)) return; - callback(null, {authorName: authorName}); - }); -} - /** diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index ad2d59f3..dba791fd 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -16,6 +16,7 @@ var padMessageHandler = require("../handler/PadMessageHandler"); var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); var randomString = require("../utils/randomstring"); +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //serialization/deserialization attributes var attributeBlackList = ["id"]; @@ -86,6 +87,12 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { // set the author to pad if(author) authorManager.addPad(author, this.id); + + if (this.head == 0) { + hooks.callAll("padCreate", {'pad':this}); + } else { + hooks.callAll("padUpdate", {'pad':this}); + } }; //save all attributes to the database @@ -368,6 +375,7 @@ Pad.prototype.init = function init(text, callback) { _this.appendRevision(firstChangeset, ''); } + hooks.callAll("padLoad", {'pad':_this}); callback(null); }); }; @@ -467,6 +475,7 @@ Pad.prototype.remove = function remove(callback) { { db.remove("pad:"+padID); padManager.unloadPad(padID); + hooks.callAll("padRemove", {'padID':padID}); callback(); } ], function(err) diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 1f4a6f2c..eb3f6188 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,4 +1,5 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var http = require('http'); var express = require('express'); var settings = require('../utils/Settings'); var fs = require('fs'); @@ -42,22 +43,24 @@ exports.createServer = function () { } exports.restartServer = function () { + if (server) { console.log("Restarting express server"); server.close(); } - server = express.createServer(); + var app = express(); // New syntax for express v3 + server = http.createServer(app); - server.use(function (req, res, next) { + app.use(function (req, res, next) { res.header("Server", serverName); next(); }); - server.configure(function() { - hooks.callAll("expressConfigure", {"app": server}); + app.configure(function() { + hooks.callAll("expressConfigure", {"app": app}); }); - hooks.callAll("expressCreateServer", {"app": server}); + hooks.callAll("expressCreateServer", {"app": app, "server": server}); server.listen(settings.port, settings.ip); -} \ No newline at end of file +} diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index fc274a07..97a0d602 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -12,14 +12,10 @@ exports.expressCreateServer = function (hook_name, args, cb) { errors: [], }; - res.send(eejs.require( - "ep_etherpad-lite/templates/admin/plugins.html", - render_args), {}); + res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args) ); }); args.app.get('/admin/plugins/info', function(req, res) { - res.send(eejs.require( - "ep_etherpad-lite/templates/admin/plugins-info.html", - {}), {}); + res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {}) ); }); } diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js index 4f5dad4f..3c595683 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.js @@ -16,9 +16,6 @@ exports.gracefulShutdown = function(err) { console.log("graceful shutdown..."); - //stop the http server - exports.app.close(); - //do the db shutdown db.db.doShutdown(function() { console.log("db sucessfully closed."); @@ -35,10 +32,14 @@ exports.gracefulShutdown = function(err) { 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()); - }); + // Handle errors + args.app.use(function(err, req, res, next){ + // if an error occurs Connect will pass it down + // through these "error-handling" middleware + // allowing you to respond however you like + res.send(500, { error: 'Sorry, something bad happened!' }); + console.error(err.stack? err.stack : err.toString()); + }) //connect graceful shutdown with sigint and uncaughtexception if(os.type().indexOf("Windows") == -1) { diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 60ece0ad..af5cbed3 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -56,7 +56,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { ERR(err); if(err == "notfound") - res.send('404 - Not Found', 404); + res.send(404, '404 - Not Found'); else res.send(html); }); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 24ec2c3d..29782b69 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -7,7 +7,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { //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); + res.send(404, 'Such a padname is forbidden'); } else { @@ -19,7 +19,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { var query = url.parse(req.url).query; if ( query ) real_url += '?' + query; res.header('Location', real_url); - res.send('You should be redirected to ' + real_url + '', 302); + res.send(302, 'You should be redirected to ' + real_url + ''); } //the pad id was fine, so just render it else diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 4f780cb0..546ba2af 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -3,6 +3,7 @@ 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 webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); var padMessageHandler = require("../../handler/PadMessageHandler"); @@ -10,19 +11,28 @@ 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); + var io = socketio.listen(args.server); /* 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); + + // Use connect's cookie parser, because it knows how to parse signed cookies + connect.cookieParser(webaccess.secret)(data, {}, function(err){ + if(err) { + console.error(err); + accept("Couldn't parse request cookies. ", false); + return; + } + + data.sessionID = data.signedCookies.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); + }); }); }); @@ -62,5 +72,5 @@ exports.expressCreateServer = function (hook_name, args, cb) { socketIORouter.setSocketIO(io); socketIORouter.addComponent("pad", padMessageHandler); - hooks.callAll("socketio", {"app": args.app, "io": io}); + hooks.callAll("socketio", {"app": args.app, "io": io, "server": args.server}); } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index ffced047..41bf3880 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -56,10 +56,10 @@ exports.basicAuth = function (req, res, next) { res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); if (req.headers.authorization) { setTimeout(function () { - res.send('Authentication required', 401); + res.send(401, 'Authentication required'); }, 1000); } else { - res.send('Authentication required', 401); + res.send(401, 'Authentication required'); } })); } @@ -88,14 +88,13 @@ exports.basicAuth = function (req, res, next) { }); } -var secret = null; +exports.secret = null; 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 @@ -104,13 +103,14 @@ exports.expressConfigure = function (hook_name, args, cb) { if (!exports.sessionStore) { exports.sessionStore = new express.session.MemoryStore(); - secret = randomString(32); + exports.secret = randomString(32); } + + args.app.use(express.cookieParser(exports.secret)); args.app.sessionStore = exports.sessionStore; args.app.use(express.session({store: args.app.sessionStore, - key: 'express_sid', - secret: secret})); + key: 'express_sid' })); args.app.use(exports.basicAuth); } diff --git a/src/node/padaccess.js b/src/node/padaccess.js index a3d1df33..4388ab94 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.js @@ -15,7 +15,7 @@ module.exports = function (req, res, callback) { callback(); //no access } else { - res.send("403 - Can't touch this", 403); + res.send(403, "403 - Can't touch this"); } }); } diff --git a/src/node/server.js b/src/node/server.js index cca76c1f..327fa166 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -21,27 +21,49 @@ * limitations under the License. */ -var log4js = require('log4js'); -var settings = require('./utils/Settings'); -var db = require('./db/DB'); -var async = require('async'); -var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); -var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); +var log4js = require('log4js') + , async = require('async') + ; + +// set up logger +log4js.replaceConsole(); + +var settings + , db + , plugins + , hooks; var npm = require("npm/lib/npm.js"); -hooks.plugins = plugins; - -//set loglevel -log4js.setGlobalLogLevel(settings.loglevel); - async.waterfall([ + // load npm + function(callback) { + npm.load({}, function(er) { + callback(er) + }) + }, + + // load everything + function(callback) { + settings = require('./utils/Settings'); + db = require('./db/DB'); + plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + hooks.plugins = plugins; + + //set loglevel + log4js.setGlobalLogLevel(settings.loglevel); + callback(); + }, + //initalize the database function (callback) { db.init(callback); }, - plugins.update, + function(callback) { + plugins.update(callback) + }, function (callback) { console.info("Installed plugins: " + plugins.formatPlugins()); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index dd34ac5e..3d7894d5 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -37,7 +37,7 @@ exports.ip = "0.0.0.0"; /** * The Port ep-lite should listen to */ -exports.port = 9001; +exports.port = process.env.PORT || 9001; /* * The Type of the database */ diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index 1f533673..c6b23713 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -48,7 +48,7 @@ CachingMiddleware.prototype = new function () { var old_res = {}; var supportsGzip = - req.header('Accept-Encoding', '').indexOf('gzip') != -1; + (req.get('Accept-Encoding') || '').indexOf('gzip') != -1; var path = require('url').parse(req.url).path; var cacheKey = (new Buffer(path)).toString('base64').replace(/[\/\+=]/g, ''); diff --git a/src/package.json b/src/package.json index a4097dc2..67e40238 100644 --- a/src/package.json +++ b/src/package.json @@ -5,9 +5,10 @@ "keywords" : ["etherpad", "realtime", "collaborative", "editor"], "author" : "Peter 'Pita' Martischka - Primary Technology Ltd", "contributors" : [ - { "name": "John McLear", - "name": "Hans Pinckaers", - "name": "Robin Buse" } + { "name": "John McLear" }, + { "name": "Hans Pinckaers" }, + { "name": "Robin Buse" }, + { "name": "Marcel Klehr" } ], "dependencies" : { "yajsml" : "1.1.6", @@ -15,17 +16,18 @@ "require-kernel" : "1.0.5", "resolve" : "0.2.x", "socket.io" : "0.9.x", - "ueberDB" : "0.1.7", + "ueberDB" : "0.1.8", "async" : "0.1.22", - "express" : "2.5.x", - "connect" : "1.x", + "express" : "3.x", + "connect" : "2.4.x", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9", - "log4js" : "0.4.1", + "log4js" : "0.5.x", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1.24", + "npm" : "1.1.x", + "npm-registry-client" : "0.2.10", "ejs" : "0.6.1", "graceful-fs" : "1.1.5", "slide" : "1.1.3", @@ -42,5 +44,5 @@ "engines" : { "node" : ">=0.6.0", "npm" : ">=1.0" }, - "version" : "1.1.2" + "version" : "1.1.5" } diff --git a/src/static/css/pad.css b/src/static/css/pad.css index df9dde14..5ee6b3c5 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -783,6 +783,23 @@ input[type=checkbox] { padding: 4px 1px } } +@media screen and (max-width: 400px){ + #editorcontainer { + top: 68px; + } + #editbar { + height: 62px; + } + .toolbar ul.menu_right { + float: left; + margin-top:2px; + } + .popup { + width:100%; + max-width:300px; + top: 72px !important; + } +} @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { #users { top: 36px; diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css index 5f24a4cf..1dd0ca91 100644 --- a/src/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -59,7 +59,7 @@ cursor: pointer; height: 35px; margin-left: 5px; - margin-right: 148px; + margin-right: 150px; position: relative; top: 20px; } @@ -292,4 +292,4 @@ OL { } * HTML .pause#playpause_button_icon { background-image: url(../../static/img/pause.gif) -} \ No newline at end of file +} diff --git a/src/static/js/ace.js b/src/static/js/ace.js index e50f75c7..83ad9447 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -122,6 +122,11 @@ function Ace2Editor() return info.ace_getDebugProperty(prop); }; + editor.getInInternationalComposition = function() + { + return info.ace_getInInternationalComposition(); + }; + // prepareUserChangeset: // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes // to the latest base text into a Changeset, which is returned (as a string if encodeAsString). diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 652a3d25..2e56b950 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1173,7 +1173,7 @@ function Ace2Inner(){ //if (! top.BEFORE) top.BEFORE = []; //top.BEFORE.push(magicdom.root.dom.innerHTML); //if (! isEditable) return; // and don't reschedule - if (window.parent.parent.inInternationalComposition) + if (inInternationalComposition) { // don't do idle input incorporation during international input composition idleWorkTimer.atLeast(500); @@ -3729,7 +3729,7 @@ function Ace2Inner(){ thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!window.parent.parent.inInternationalComposition)) + if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { if (type != "keyup" || !incorpIfQuick()) { @@ -4589,9 +4589,24 @@ function Ace2Inner(){ } } + + var inInternationalComposition = false; function handleCompositionEvent(evt) { - window.parent.parent.handleCompositionEvent(evt); + // international input events, fired in FF3, at least; allow e.g. Japanese input + if (evt.type == "compositionstart") + { + inInternationalComposition = true; + } + else if (evt.type == "compositionend") + { + inInternationalComposition = false; + } + } + + editorInfo.ace_getInInternationalComposition = function () + { + return inInternationalComposition; } function bindTheEventHandlers() diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index d149b256..b700fc49 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -111,7 +111,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) function handleUserChanges() { - if (window.parent.parent.inInternationalComposition) return; + if (editor.getInInternationalComposition()) return; if ((!getSocket()) || channelState == "CONNECTING") { if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) @@ -288,7 +288,7 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) var apool = msg.apool; // When inInternationalComposition, msg pushed msgQueue. - if (msgQueue.length > 0 || window.parent.parent.inInternationalComposition) { + if (msgQueue.length > 0 || editor.getInInternationalComposition()) { if (msgQueue.length > 0) oldRev = msgQueue[msgQueue.length - 1].newRev; else oldRev = rev; @@ -358,6 +358,14 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) { var userInfo = msg.userInfo; var id = userInfo.userId; + + // Avoid a race condition when setting colors. If our color was set by a + // query param, ignore our own "new user" message's color value. + if (id === initialUserInfo.userId && initialUserInfo.globalUserColor) + { + msg.userInfo.colorId = initialUserInfo.globalUserColor; + } + if (userSet[id]) { diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js index 5fbefb4d..74a2e463 100644 --- a/src/static/js/colorutils.js +++ b/src/static/js/colorutils.js @@ -24,6 +24,13 @@ var colorutils = {}; +// Check that a given value is a css hex color value, e.g. +// "#ffffff" or "#fff" +colorutils.isCssHex = function(cssColor) +{ + return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); +} + // "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] colorutils.css2triple = function(cssColor) { diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 6a75de43..dd4fd1e5 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -311,7 +311,7 @@ function makeContentCollector(collectStyles, browser, apool, domInterface, class ['insertorder', 'first'] ].concat( _.map(state.lineAttributes,function(value,key){ - if (window.console) console.log([key, value]) + if (typeof(window)!= 'undefined' && window.console) console.log([key, value]) return [key, value]; }) ); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 737f5dc6..89777040 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -43,6 +43,7 @@ var padmodals = require('./pad_modals').padmodals; var padsavedrevs = require('./pad_savedrevs'); var paduserlist = require('./pad_userlist').paduserlist; var padutils = require('./pad_utils').padutils; +var colorutils = require('./colorutils').colorutils; var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; @@ -50,22 +51,6 @@ var randomString = require('./pad_utils').randomString; var hooks = require('./pluginfw/hooks'); -window.inInternationalComposition = false; -var inInternationalComposition = window.inInternationalComposition; - -window.handleCompositionEvent = function handleCompositionEvent(evt) - { - // international input events, fired in FF3, at least; allow e.g. Japanese input - if (evt.type == "compositionstart") - { - this.inInternationalComposition = true; - } - else if (evt.type == "compositionend") - { - this.inInternationalComposition = false; - } - } - function createCookie(name, value, days, path) { if (days) @@ -114,6 +99,7 @@ function getParams() var showControls = params["showControls"]; var showChat = params["showChat"]; var userName = params["userName"]; + var userColor = params["userColor"]; var showLineNumbers = params["showLineNumbers"]; var useMonospaceFont = params["useMonospaceFont"]; var IsnoColors = params["noColors"]; @@ -162,6 +148,11 @@ function getParams() // If the username is set as a parameter we should set a global value that we can call once we have initiated the pad. settings.globalUserName = decodeURIComponent(userName); } + if(userColor) + // If the userColor is set as a parameter, set a global value to use once we have initiated the pad. + { + settings.globalUserColor = decodeURIComponent(userColor); + } if(rtl) { if(rtl == "true") @@ -363,6 +354,14 @@ function handshake() pad.myUserInfo.name = settings.globalUserName; $('#myusernameedit').attr({"value":settings.globalUserName}); // Updates the current users UI } + if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) + { + + // Add a 'globalUserColor' property to myUserInfo, so collabClient knows we have a query parameter. + pad.myUserInfo.globalUserColor = settings.globalUserColor; + pad.notifyChangeColor(settings.globalUserColor); // Updates pad.myUserInfo.colorId + paduserlist.setMyUserInfo(pad.myUserInfo); + } } //This handles every Message after the clientVars else @@ -1025,6 +1024,7 @@ var settings = { , noColors: false , useMonospaceFontGlobal: false , globalUserName: false +, globalUserColor: false , rtlIsTrue: false }; diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index 09f3d79f..5a9e7b9b 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -85,11 +85,13 @@ var padeditor = (function() if (value == "false") return false; return defaultValue; } - self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue); var v; + v = getOption('rtlIsTrue', false); + self.ace.setProperty("rtlIsTrue", v); + v = getOption('showLineNumbers', true); self.ace.setProperty("showslinenumbers", v); padutils.setCheckbox($("#options-linenoscheck"), v); diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index e7c6fb80..d668e549 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,7 +1,12 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm"); -var registry = require("npm/lib/utils/npm-registry-client/index.js"); +var RegClient = require("npm-registry-client") + +var registry = new RegClient( +{ registry: "http://registry.npmjs.org" +, cache: npm.cache } +); var withNpm = function (npmfn, final, cb) { npm.load({}, function (er) { @@ -72,7 +77,7 @@ exports.search = function(query, cache, cb) { cb(null, exports.searchCache); } else { registry.get( - "/-/all", null, 600, false, true, + "/-/all", 600, false, true, function (er, data) { if (er) return cb(er); exports.searchCache = data; diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 12ba94a2..e02c1331 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,7 +1,5 @@ var npm = require("npm/lib/npm.js"); var readInstalled = require("./read-installed.js"); -var relativize = require("npm/lib/utils/relativize.js"); -var readJson = require("npm/lib/utils/read-json.js"); var path = require("path"); var async = require("async"); var fs = require("fs"); diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js index cc03b357..800ee32c 100644 --- a/src/static/js/pluginfw/read-installed.js +++ b/src/static/js/pluginfw/read-installed.js @@ -94,8 +94,21 @@ var npm = require("npm/lib/npm.js") , path = require("path") , asyncMap = require("slide").asyncMap , semver = require("semver") - , readJson = require("npm/lib/utils/read-json.js") - , log = require("npm/lib/utils/log.js") + , log = require("log4js").getLogger('pluginfw') + +function readJson(file, callback) { + fs.readFile(file, function(er, buf) { + if(er) { + callback(er); + return; + } + try { + callback( null, JSON.parse(buf.toString()) ) + } catch(er) { + callback(er) + } + }) +} module.exports = readInstalled @@ -274,7 +287,7 @@ function findUnmet (obj) { } }) - log.verbose([obj._id], "returning") + log.debug([obj._id], "returning") return obj } diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index e630bde0..5203e57b 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -144,13 +144,12 @@ function handleClientVars(message) require('./pad_impexp').padimpexp.init(); //change export urls when the slider moves - var export_rev_regex = /(\/\d+)?\/export/ BroadcastSlider.onSlider(function(revno) { // export_links is a jQuery Array, so .each is allowed. export_links.each(function() { - this.setAttribute('href', this.href.replace(export_rev_regex, '/' + revno + '/export')); + this.setAttribute('href', this.href.replace( /(.+?)\/\w+\/(\d+\/)?export/ , '$1/' + padId + '/' + revno + '/export')); }); }); @@ -159,6 +158,7 @@ function handleClientVars(message) { fireWhenAllScriptsAreLoaded[i](); } + $("#ui-slider-handle").css('left', $("#ui-slider-bar").width() - 2); } exports.baseURL = ''; diff --git a/src/templates/index.html b/src/templates/index.html index 4a45d6a5..cdd9346d 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -2,6 +2,29 @@ Etherpad Lite + @@ -9,9 +32,11 @@