diff options
author | Matthias Bartelmeß <mba@fourplusone.de> | 2012-04-05 21:47:59 +0200 |
---|---|---|
committer | Matthias Bartelmeß <mba@fourplusone.de> | 2012-04-05 21:47:59 +0200 |
commit | 9e042ee942a4a3cce7fc89270833b72fd6bfb74a (patch) | |
tree | fe1115383b869cc73528465a55c60670bf5b343d /src | |
parent | 35c0a38a01e65981cb03bd67a717693ec7549b9d (diff) | |
parent | be5aec7e20e7abeb1da0df3f836974178e960667 (diff) | |
download | etherpad-lite-9e042ee942a4a3cce7fc89270833b72fd6bfb74a.zip |
Merge branch 'develop' into timeslider_authors
Conflicts:
src/static/css/pad.css
src/templates/timeslider.html
Diffstat (limited to 'src')
42 files changed, 1566 insertions, 1377 deletions
diff --git a/src/ep.json b/src/ep.json index 59cbf3aa..6bc77735 100644 --- a/src/ep.json +++ b/src/ep.json @@ -8,7 +8,9 @@ { "name": "apicalls", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/apicalls:expressCreateServer" } }, { "name": "importexport", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport:expressCreateServer" } }, { "name": "errorhandling", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling:expressCreateServer" } }, - { "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } } - + { "name": "socketio", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio:expressCreateServer" } }, + { "name": "adminplugins", "hooks": { + "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer", + "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } } ] } diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index c384f172..b4a39c17 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -15,6 +15,11 @@ var padManager = require("./PadManager"); var padMessageHandler = require("../handler/PadMessageHandler"); var readOnlyManager = require("./ReadOnlyManager"); var crypto = require("crypto"); +var randomString = require("../utils/randomstring"); + +//serialization/deserialization attributes +var attributeBlackList = ["id"]; +var jsonableList = ["pool"]; /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces @@ -34,7 +39,7 @@ var Pad = function Pad(id) { this.publicStatus = false; this.passwordHash = null; this.id = id; - + this.savedRevisions = []; }; exports.Pad = Pad; @@ -75,15 +80,28 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) { 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}); + db.set("pad:"+this.id+":revs:"+newRev, newRevData); + this.saveToDatabase(); }; +//save all attributes to the database +Pad.prototype.saveToDatabase = function saveToDatabase(){ + var dbObject = {}; + + for(var attr in this){ + if(typeof this[attr] === "function") continue; + if(attributeBlackList.indexOf(attr) !== -1) continue; + + dbObject[attr] = this[attr]; + + if(jsonableList.indexOf(attr) !== -1){ + dbObject[attr] = dbObject[attr].toJsonable(); + } + } + + db.set("pad:"+this.id, dbObject); +} + Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) { db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback); }; @@ -200,11 +218,10 @@ Pad.prototype.setText = function setText(newText) { }; 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); + this.chatHead++; + //save the chat entry in the database + db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time}); + this.saveToDatabase(); }; Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) { @@ -324,27 +341,14 @@ Pad.prototype.init = function init(text, callback) { //if this pad exists, load it if(value != null) { - _this.head = value.head; - _this.atext = value.atext; - _this.pool = _this.pool.fromJsonable(value.pool); - - //ensure we have a local chatHead variable - if(value.chatHead != null) - _this.chatHead = value.chatHead; - else - _this.chatHead = -1; - - //ensure we have a local publicStatus variable - if(value.publicStatus != null) - _this.publicStatus = value.publicStatus; - else - _this.publicStatus = false; - - //ensure we have a local passwordHash variable - if(value.passwordHash != null) - _this.passwordHash = value.passwordHash; - else - _this.passwordHash = null; + //copy all attr. To a transfrom via fromJsonable if necassary + for(var attr in value){ + if(jsonableList.indexOf(attr) !== -1){ + _this[attr] = _this[attr].fromJsonable(value[attr]); + } else { + _this[attr] = value[attr]; + } + } } //this pad doesn't exist, so create it else @@ -452,12 +456,12 @@ Pad.prototype.remove = function remove(callback) { //set in db Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) { this.publicStatus = publicStatus; - db.setSub("pad:"+this.id, ["publicStatus"], this.publicStatus); + this.saveToDatabase(); }; Pad.prototype.setPassword = function setPassword(password) { this.passwordHash = password == null ? null : hash(password, generateSalt()); - db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); + this.saveToDatabase(); }; Pad.prototype.isCorrectPassword = function isCorrectPassword(password) { @@ -468,6 +472,31 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() { return this.passwordHash != null; }; +Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) { + //if this revision is already saved, return silently + for(var i in this.savedRevisions){ + if(this.savedRevisions.revNum === revNum){ + return; + } + } + + //build the saved revision object + var savedRevision = {}; + savedRevision.revNum = revNum; + savedRevision.savedById = savedById; + savedRevision.label = label || "Revision " + revNum; + savedRevision.timestamp = new Date().getTime(); + savedRevision.id = randomString(10); + + //save this new saved revision + this.savedRevisions.push(savedRevision); + this.saveToDatabase(); +}; + +Pad.prototype.getSavedRevisions = function getSavedRevisions() { + return this.savedRevisions; +}; + /* Crypto helper methods */ function hash(password, salt) diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 4e3a3199..5f08b1b1 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -115,7 +115,13 @@ exports.doesPadExists = function(padId, callback) db.get("pad:"+padId, function(err, value) { if(ERR(err, callback)) return; - callback(null, value != null && value.atext); + if(value != null && value.atext){ + callback(null, true); + } + else + { + callback(null, false); + } }); } diff --git a/src/node/eejs/examples/bar.ejs b/src/node/eejs/examples/bar.ejs new file mode 100644 index 00000000..6a2cc4ba --- /dev/null +++ b/src/node/eejs/examples/bar.ejs @@ -0,0 +1,9 @@ +a +<% e.begin_block("bar"); %> + A + <% e.begin_block("foo"); %> + XX + <% e.end_block(); %> + B +<% e.end_block(); %> +b diff --git a/src/node/eejs/examples/foo.ejs b/src/node/eejs/examples/foo.ejs new file mode 100644 index 00000000..daee5f8e --- /dev/null +++ b/src/node/eejs/examples/foo.ejs @@ -0,0 +1,7 @@ +<% e.inherit("./bar.ejs"); %> + +<% e.begin_define_block("foo"); %> + YY + <% e.super(); %> + ZZ +<% e.end_define_block(); %> diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js new file mode 100644 index 00000000..90c69e59 --- /dev/null +++ b/src/node/eejs/index.js @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Basic usage: + * + * require("./index").require("./examples/foo.ejs") + */ + +var ejs = require("ejs"); +var fs = require("fs"); +var path = require("path"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); + +exports.info = { + buf_stack: [], + block_stack: [], + blocks: {}, + file_stack: [], +}; + +exports._init = function (b, recursive) { + exports.info.buf_stack.push(exports.info.buf); + exports.info.buf = b; +} + +exports._exit = function (b, recursive) { + exports.info.file_stack[exports.info.file_stack.length-1].inherit.forEach(function (item) { + exports._require(item.name, item.args); + }); + exports.info.buf = exports.info.buf_stack.pop(); +} + +exports.begin_capture = function() { + exports.info.buf_stack.push(exports.info.buf.concat()); + exports.info.buf.splice(0, exports.info.buf.length); +} + +exports.end_capture = function () { + var res = exports.info.buf.join(""); + exports.info.buf.splice.apply( + exports.info.buf, + [0, exports.info.buf.length].concat(exports.info.buf_stack.pop())); + return res; +} + +exports.begin_define_block = function (name) { + if (typeof exports.info.blocks[name] == "undefined") + exports.info.blocks[name] = {}; + exports.info.block_stack.push(name); + exports.begin_capture(); +} + +exports.super = function () { + exports.info.buf.push('<!eejs!super!>'); +} + +exports.end_define_block = function () { + content = exports.end_capture(); + var name = exports.info.block_stack.pop(); + if (typeof exports.info.blocks[name].content == "undefined") + exports.info.blocks[name].content = content; + else if (typeof exports.info.blocks[name].content.indexOf('<!eejs!super!>')) + exports.info.blocks[name].content = exports.info.blocks[name].content.replace('<!eejs!super!>', content); + + return exports.info.blocks[name].content; +} + +exports.end_block = function () { + var name = exports.info.block_stack[exports.info.block_stack.length-1]; + var args = {content: exports.end_define_block()}; + hooks.callAll("eejsBlock_" + name, args); + exports.info.buf.push(args.content); +} + +exports.begin_block = exports.begin_define_block; + +exports.inherit = function (name, args) { + exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args}); +} + +exports.require = function (name, args) { + if (args == undefined) args = {}; + + if ((name.indexOf("./") == 0 || name.indexOf("../") == 0) && exports.info.file_stack.length) { + name = path.join(path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path), name); + } + var ejspath = require.resolve(name) + + args.e = exports; + args.require = require; + var template = '<% e._init(buf); %>' + fs.readFileSync(ejspath).toString() + '<% e._exit(); %>'; + + exports.info.file_stack.push({path: ejspath, inherit: []}); + var res = ejs.render(template, args); + exports.info.file_stack.pop(); + + return res; +} + +exports._require = function (name, args) { + exports.info.buf.push(exports.require(name, args)); +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c3ee231c..866edeb0 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -191,6 +191,11 @@ exports.handleMessage = function(client, message) handleChatMessage(client, message); } else if(message.type == "COLLABROOM" && + message.data.type == "SAVE_REVISION") + { + handleSaveRevisionMessage(client, message); + } + else if(message.type == "COLLABROOM" && message.data.type == "CLIENT_MESSAGE" && message.data.payload.type == "suggestUserName") { @@ -204,6 +209,23 @@ exports.handleMessage = function(client, message) } /** + * Handles a save revision message + * @param client the client that send this message + * @param message the message from the client + */ +function handleSaveRevisionMessage(client, message){ + var padId = session2pad[client.id]; + var userId = sessioninfos[client.id].author; + + padManager.getPad(padId, function(err, pad) + { + if(ERR(err)) return; + + pad.addSavedRevision(pad.head, userId); + }); +} + +/** * Handles a Chat Message * @param client the client that send this message * @param message the message from the client diff --git a/src/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js index da859779..a6cf8f4d 100644 --- a/src/node/handler/TimesliderMessageHandler.js +++ b/src/node/handler/TimesliderMessageHandler.js @@ -166,6 +166,7 @@ function createTimesliderClientVars (padId, callback) hooks: [], initialStyledContents: {} }; + var pad; var initialChangesets = []; @@ -180,6 +181,12 @@ function createTimesliderClientVars (padId, callback) callback(); }); }, + //get all saved revisions and add them + function(callback) + { + clientVars.savedRevisions = pad.getSavedRevisions(); + callback(); + }, //get all authors and add them to function(callback) { diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js new file mode 100644 index 00000000..fa7e7077 --- /dev/null +++ b/src/node/hooks/express/adminplugins.js @@ -0,0 +1,51 @@ +var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); +var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); +var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/admin/plugins', function(req, res) { + var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + var render_args = { + plugins: plugins.plugins, + search_results: {}, + errors: [], + }; + + res.send(eejs.require( + "ep_etherpad-lite/templates/admin/plugins.html", + render_args), {}); + }); +} + +exports.socketio = function (hook_name, args, cb) { + var io = args.io.of("/pluginfw/installer"); + io.on('connection', function (socket) { + socket.on("load", function (query) { + socket.emit("installed-results", {results: plugins.plugins}); + }); + + socket.on("search", function (query) { + socket.emit("progress", {progress:0, message:'Fetching results...'}); + installer.search(query, function (progress) { + if (progress.results) + socket.emit("search-result", progress); + socket.emit("progress", progress); + }); + }); + + socket.on("install", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."}); + installer.install(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + + socket.on("uninstall", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."}); + installer.uninstall(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + }); +} diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index 13cfd821..474f475e 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -1,32 +1,32 @@ var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); exports.expressCreateServer = function (hook_name, args, cb) { //serve index.html under / args.app.get('/', function(req, res) { - var filePath = path.normalize(__dirname + "/../../../static/index.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/index.html")); }); //serve robots.txt args.app.get('/robots.txt', function(req, res) { var filePath = path.normalize(__dirname + "/../../../static/robots.txt"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.sendfile(filePath); }); //serve favicon.ico args.app.get('/favicon.ico', function(req, res) { var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico"); - res.sendfile(filePath, { maxAge: exports.maxAge }, function(err) + res.sendfile(filePath, function(err) { //there is no custom favicon, send the default favicon if(err) { filePath = path.normalize(__dirname + "/../../../static/favicon.ico"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.sendfile(filePath); } }); }); @@ -34,15 +34,13 @@ exports.expressCreateServer = function (hook_name, args, cb) { //serve pad.html under /p args.app.get('/p/:pad', function(req, res, next) { - var filePath = path.normalize(__dirname + "/../../../static/pad.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/pad.html")); }); //serve timeslider.html under /p/$padname/timeslider args.app.get('/p/:pad/timeslider', function(req, res, next) { - var filePath = path.normalize(__dirname + "/../../../static/timeslider.html"); - res.sendfile(filePath, { maxAge: exports.maxAge }); + res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html")); }); }
\ No newline at end of file diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index 9209967c..f284e478 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -6,6 +6,7 @@ var settings = require("../../utils/Settings"); var Yajsml = require('yajsml'); var fs = require("fs"); var ERR = require("async-stacktrace"); +var _ = require("underscore"); exports.expressCreateServer = function (hook_name, args, cb) { // Cache both minified and static. @@ -35,8 +36,22 @@ exports.expressCreateServer = function (hook_name, args, cb) { // serve plugin definitions // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { + + var clientParts = _(plugins.parts) + .filter(function(part){ return _(part).has('client_hooks') }); + + var clientPlugins = {}; + + _(clientParts).chain() + .map(function(part){ return part.plugin }) + .uniq() + .each(function(name){ + clientPlugins[name] = _(plugins.plugins[name]).clone(); + delete clientPlugins[name]['package']; + }); + res.header("Content-Type","application/json; charset=utf-8"); - res.write(JSON.stringify({"plugins": plugins.plugins, "parts": plugins.parts})); + res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); res.end(); }); } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 8e9f967a..d0e28737 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -6,14 +6,30 @@ var settings = require('../../utils/Settings'); //checks for basic http auth exports.basicAuth = function (req, res, next) { - if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { - // fetch login and password - if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() == settings.httpAuth) { - next(); - return; + + // When handling HTTP-Auth, an undefined password will lead to no authorization at all + var pass = settings.httpAuth || ''; + + if (req.path.indexOf('/admin') == 0) { + var pass = settings.adminHttpAuth; + + } + + // Just pass if password is an empty string + if (pass === '') { + return next(); + } + + + // If a password has been set and auth headers are present... + if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + // ...check login and password + if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { + return next(); } } + // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); if (req.headers.authorization) { setTimeout(function () { @@ -25,8 +41,7 @@ exports.basicAuth = function (req, res, next) { } exports.expressConfigure = function (hook_name, args, cb) { - // Activate http basic auth if it has been defined in settings.json - if(settings.httpAuth != null) args.app.use(exports.basicAuth); + args.app.use(exports.basicAuth); // 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. diff --git a/src/node/server.js b/src/node/server.js index 8e6d6fa0..bc4333cd 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -51,10 +51,6 @@ 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, by default -var hour = 60*60; -exports.maxAge = settings.maxAge || 6 * hour; - //set loglevel log4js.setGlobalLogLevel(settings.loglevel); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index f569d4b9..c5996565 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -29,7 +29,6 @@ var pro = require("uglify-js").uglify; var path = require('path'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var RequireKernel = require('require-kernel'); -var server = require('../server'); var ROOT_DIR = path.normalize(__dirname + "/../../static/"); var TAR_PATH = path.join(__dirname, 'tar.json'); @@ -109,10 +108,10 @@ exports.minify = function(req, res, next) 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); + if (settings.maxAge !== undefined) { + var expiresDate = new Date((new Date()).getTime()+settings.maxAge*1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', 'max-age=' + server.maxAge); + res.setHeader('cache-control', 'max-age=' + settings.maxAge); } } @@ -132,7 +131,10 @@ exports.minify = function(req, res, next) res.end(); } else if (req.method == 'GET') { getFileCompressed(filename, contentType, function (error, content) { - if(ERR(error)) return; + if(ERR(error, function(){ + res.writeHead(500, {}); + res.end(); + })) return; res.header("Content-Type", contentType); res.writeHead(200, {}); res.write(content); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 24237de4..12fcc55c 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -85,6 +85,11 @@ exports.loglevel = "INFO"; */ exports.httpAuth = null; +/** + * Http basic auth, with "user:password" format + */ +exports.adminHttpAuth = null; + //checks if abiword is avaiable exports.abiwordAvailable = function() { diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index b8b7e1f1..70d5a08c 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -18,13 +18,11 @@ var async = require('async'); var Buffer = require('buffer').Buffer; var fs = require('fs'); var path = require('path'); -var server = require('../server'); var zlib = require('zlib'); var util = require('util'); +var settings = require('./Settings'); -var ROOT_DIR = path.normalize(__dirname + "/../"); -var CACHE_DIR = path.normalize(ROOT_DIR + '../../var/'); -console.log(CACHE_DIR) +var CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined; var responseCache = {}; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js new file mode 100644 index 00000000..4c1bba24 --- /dev/null +++ b/src/node/utils/randomstring.js @@ -0,0 +1,16 @@ +/** + * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids + */ +var randomString = function randomString(len) +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var randomstring = ''; + for (var i = 0; i < len; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; +}; + +module.exports = randomString; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index f4af3b18..14c93f5c 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -22,7 +22,6 @@ , "chat.js" , "excanvas.js" , "farbtastic.js" - , "prefixfree.js" ] , "timeslider.js": [ "jquery.js" diff --git a/src/package.json b/src/package.json index 6253ecb1..83441da0 100644 --- a/src/package.json +++ b/src/package.json @@ -23,7 +23,11 @@ "log4js" : "0.4.1", "jsdom-nocontextifiy" : "0.2.10", "async-stacktrace" : "0.0.2", - "npm" : "1.1", + "npm" : "1.1", + "ejs" : "0.6.1", + "graceful-fs" : "1.1.5", + "slide" : "1.1.3", + "semver" : "1.0.13", "underscore" : "1.3.1" }, "devDependencies": { diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 0131c719..4df79857 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -24,7 +24,7 @@ a img } /* menu */ -#editbar ul +.toolbar ul { position: relative; list-style: none; @@ -35,19 +35,22 @@ a img } -#editbar +.toolbar { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); border-bottom: 1px solid #ccc; - height: 32px; overflow: hidden; padding-top: 3px; width: 100%; white-space: nowrap; + position: absolute; + left: 0; + right: 0; + height: 32px; } -#editbar ul li +.toolbar ul li { background: #fff; background: linear-gradient(#fff, #f0f0f0); @@ -62,52 +65,52 @@ a img width: 18px; } -#editbar ul li a +.toolbar ul li a { text-decoration: none; color: #ccc; position: absolute; } -#editbar ul li a span +.toolbar ul li a span { position: relative; top:-2px } -#editbar ul li:hover { +.toolbar ul li:hover { background: #fff; background: linear-gradient(#f4f4f4, #e4e4e4); } -#editbar ul li:active { +.toolbar ul li:active { background: #eee; background: linear-gradient(#ddd, #fff); box-shadow: 0 0 8px rgba(0,0,0,.1) inset; } -#editbar ul li.separator +.toolbar ul li.separator { border: inherit; background: inherit; visibility:hidden; width: 0px; } -#editbar ul li a +.toolbar ul li a { display: block; } -#editbar ul li a img +.toolbar ul li a img { padding: 1px; } -#editbar ul +.toolbar ul { float: left; } -#editbar ul#menu_right +.toolbar ul.menu_right { float: right; } @@ -320,7 +323,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } z-index: 10; } -#editbarsavetable +.toolbarsavetable { position:absolute; top: 6px; @@ -328,7 +331,7 @@ a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; } height: 24px; } -#editbarsavetable td, #editbartable td +.toolbarsavetable td, .toolbartable td { white-space: nowrap; } @@ -686,14 +689,15 @@ a#topbarmaximize { background: url(static/img/maximize_maximized.png); } -#editbarinner h1 { +.toolbarinner h1 { line-height: 29px; font-size: 16px; padding-left: 6pt; margin-top: 0; + white-space: nowrap; } -#editbarinner h1 a { +.toolbarinner h1 a { font-size: 12px; } @@ -1032,6 +1036,9 @@ margin-top: 1px; background-position: 0px -183px; display: inline-block; } +.buttonicon-savedRevision { + background-position: 0px -493px +} #usericon { @@ -1171,13 +1178,13 @@ input[type=checkbox] { } @media screen and (max-width: 600px) { - #editbar ul li { + .toolbar ul li { padding: 4px 1px; } } @media only screen and (min-device-width: 320px) and (max-device-width: 720px) { - #editbar ul li { + .toolbar ul li { padding: 4px 3px; } #users { @@ -1192,7 +1199,7 @@ input[type=checkbox] { #editorcontainer { margin-bottom: 33px; } - #editbar ul#menu_right { + .toolbar ul.menu_right { background: #f7f7f7; background: linear-gradient(#f7f7f7, #f1f1f1 80%); width: 100%; @@ -1202,7 +1209,7 @@ input[type=checkbox] { bottom: 0; border-top: 1px solid #ccc; } - #editbar ul#menu_right li:last-child { + .toolbar ul.menu_right li:last-child { height: 24px; border-radius: 0; margin-top: 0; @@ -1224,7 +1231,7 @@ input[type=checkbox] { border-top-right-radius: 0; border-right: none; } - #editbar ul li a span { + .toolbar ul li a span { top: -3px; } #usericonback { @@ -1233,10 +1240,10 @@ input[type=checkbox] { #qrcode { display: none; } - #editbar ul#menu_right li:not(:last-child) { + .toolbar ul.menu_right li:not(:last-child) { display: block; } - #editbar ul#menu_right > li { + .toolbar ul.menu_right > li { background: none; border: none; margin-top: 4px; @@ -1265,4 +1272,4 @@ input[type=checkbox] { #online_count { line-height: 24px; } -}
\ No newline at end of file +} diff --git a/src/static/css/timeslider.css b/src/static/css/timeslider.css index 2179c940..03e97048 100644 --- a/src/static/css/timeslider.css +++ b/src/static/css/timeslider.css @@ -45,10 +45,10 @@ #leftstar, #rightstar, #leftstep, #rightstep {background:url(../../static/img/stepper_buttons.png) 0 0 no-repeat; height:21px; overflow:hidden; position:absolute;} -#leftstar {background-position:0 44px; right:34px; top:8px; width:30px;} -#rightstar {background-position:29px 44px; right:5px; top:8px; width:29px;} -#leftstep {background-position:0 22px; right:34px; top:20px; width:30px;} -#rightstep {background-position:29px 22px; right:5px; top:20px; width:29px;} +#leftstar {background-position:0 -44px; right:34px; top:8px; width:30px;} +#rightstar {background-position:-29px -44px; right:5px; top:8px; width:29px;} +#leftstep {background-position:0 -22px; right:34px; top:20px; width:30px;} +#rightstep {background-position:-29px -22px; right:5px; top:20px; width:29px;} #timeslider .star { background-image:url(../../static/img/star.png); @@ -75,8 +75,11 @@ #padmain {top:0px !important;} #editbarright {float:right;} #returnbutton {color:#222; font-size:16px; line-height:29px; margin-top:0; padding-right:6px;} -#importexport {top:118px;} #importexport .popup {width:185px;} +#importexport{ + top:118px; + width:185px; +} .timeslider-bar diff --git a/src/static/img/etherpad_lite_icons.png b/src/static/img/etherpad_lite_icons.png Binary files differindex cadf5ed2..27867d42 100644 --- a/src/static/img/etherpad_lite_icons.png +++ b/src/static/img/etherpad_lite_icons.png diff --git a/src/static/img/star.png b/src/static/img/star.png Binary files differnew file mode 100644 index 00000000..e0c7099e --- /dev/null +++ b/src/static/img/star.png diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 685d45df..4dfcc64e 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -233,14 +233,16 @@ require.setGlobalKeyPath("require");\n\ iframeHTML.push(doctype); iframeHTML.push("<html><head>"); + iframeHTML.push('<script type="text/javascript" src="../static/js/jquery.js"></script>'); + + hooks.callAll("aceInitInnerdocbodyHead", { + iframeHTML: iframeHTML + }); // For compatability's sake transform in and out. for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.stringify(iframeHTML[i]); } - hooks.callAll("aceInitInnerdocbodyHead", { - iframeHTML: iframeHTML - }); for (var i = 0, ii = iframeHTML.length; i < ii; i++) { iframeHTML[i] = JSON.parse(iframeHTML[i]); } @@ -262,6 +264,11 @@ require.setGlobalKeyPath("require");\n\ // Inject my plugins into my child. iframeHTML.push('\ <script type="text/javascript">\ + parent_req = require("./pluginfw/parent_require.js");\ + parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/hooks");\ + parent_req.getRequirementFromParent(require, "ep_etherpad-lite/static/js/pluginfw/plugins");\ + parent_req.getRequirementFromParent(require, "./pluginfw/hooks");\ + parent_req.getRequirementFromParent(require, "./pluginfw/plugins");\ require.define("/plugins", null);\n\ require.define("/plugins.js", function (require, exports, module) {\ module.exports = require("ep_etherpad-lite/static/js/plugins");\ diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index a2c4afeb..f8393d0b 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -844,7 +844,7 @@ function Ace2Inner(){ var cmdArgs = Array.prototype.slice.call(arguments, 1); if (CMDS[cmd]) { - inCallStack(cmd, function() + inCallStackIfNecessary(cmd, function() { fastIncorp(9); CMDS[cmd].apply(CMDS, cmdArgs); @@ -854,7 +854,7 @@ function Ace2Inner(){ function replaceRange(start, end, text) { - inCallStack('replaceRange', function() + inCallStackIfNecessary('replaceRange', function() { fastIncorp(9); performDocumentReplaceRange(start, end, text); @@ -1155,7 +1155,7 @@ function Ace2Inner(){ return; } - inCallStack("idleWorkTimer", function() + inCallStackIfNecessary("idleWorkTimer", function() { var isTimeUp = newTimeLimit(250); @@ -2043,6 +2043,7 @@ function Ace2Inner(){ return [lineNum, col]; } } + editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; function createDomLineEntry(lineString) { @@ -2328,6 +2329,7 @@ function Ace2Inner(){ var cs = builder.toString(); performDocumentApplyChangeset(cs); } + editorInfo.ace_performDocumentApplyAttributesToRange = performDocumentApplyAttributesToRange; function buildKeepToStartOfRange(builder, start) { @@ -2853,6 +2855,7 @@ function Ace2Inner(){ currentCallStack.selectionAffected = true; } } + editorInfo.ace_performSelectionChange = performSelectionChange; // Change the abstract representation of the document to have a different selection. // Should not rely on the line representation. Should not affect the DOM. @@ -3280,7 +3283,7 @@ function Ace2Inner(){ function handleClick(evt) { - inCallStack("handleClick", function() + inCallStackIfNecessary("handleClick", function() { idleWorkTimer.atMost(200); }); @@ -3602,7 +3605,7 @@ function Ace2Inner(){ var stopped = false; - inCallStack("handleKeyEvent", function() + inCallStackIfNecessary("handleKeyEvent", function() { if (type == "keypress" || (isTypeForSpecialKey && keyCode == 13 /*return*/ )) @@ -4689,7 +4692,7 @@ function Ace2Inner(){ } // click below the body - inCallStack("handleOuterClick", function() + inCallStackIfNecessary("handleOuterClick", function() { // put caret at bottom of doc fastIncorp(11); @@ -4726,6 +4729,54 @@ function Ace2Inner(){ else $(elem).removeClass(elem, className); } + function setup() + { + doc = document; // defined as a var in scope outside + inCallStackIfNecessary("setup", function() + { + var body = doc.getElementById("innerdocbody"); + root = body; // defined as a var in scope outside + if (browser.mozilla) addClass(root, "mozilla"); + if (browser.safari) addClass(root, "safari"); + if (browser.msie) addClass(root, "msie"); + if (browser.msie) + { + // cache CSS background images + try + { + doc.execCommand("BackgroundImageCache", false, true); + } + catch (e) + { /* throws an error in some IE 6 but not others! */ + } + } + setClassPresence(root, "authorColors", true); + setClassPresence(root, "doesWrap", doesWrap); + + initDynamicCSS(); + + enforceEditability(); + + // set up dom and rep + while (root.firstChild) root.removeChild(root.firstChild); + var oneEntry = createDomLineEntry(""); + doRepLineSplice(0, rep.lines.length(), [oneEntry]); + insertDomLines(null, [oneEntry.domInfo], null); + rep.alines = Changeset.splitAttributionLines( + Changeset.makeAttribution("\n"), "\n"); + + bindTheEventHandlers(); + + }); + + scheduler.setTimeout(function() + { + parent.readyFunc(); // defined in code that sets up the inner iframe + }, 0); + + isSetUp = true; + } + function focus() { window.focus(); diff --git a/src/static/js/json2.js b/src/static/js/json2.js index 663f932c..d72b4be9 100644 --- a/src/static/js/json2.js +++ b/src/static/js/json2.js @@ -465,7 +465,7 @@ if (!JSON) } // If the text is not JSON parseable, then a SyntaxError is thrown. - throw new SyntaxError('JSON.parse'); + throw new SyntaxError('JSON.parse: ' + text); }; } }()); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index d19cface..426eb089 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -31,7 +31,6 @@ require('./farbtastic'); require('./excanvas'); JSON = require('./json2'); require('./undo-xpopup'); -require('./prefixfree'); var chat = require('./chat').chat; var getCollabClient = require('./collab_client').getCollabClient; @@ -42,7 +41,7 @@ var padeditbar = require('./pad_editbar').padeditbar; var padeditor = require('./pad_editor').padeditor; var padimpexp = require('./pad_impexp').padimpexp; var padmodals = require('./pad_modals').padmodals; -var padsavedrevs = require('./pad_savedrevs').padsavedrevs; +var padsavedrevs = require('./pad_savedrevs'); var paduserlist = require('./pad_userlist').paduserlist; var padutils = require('./pad_utils').padutils; @@ -50,6 +49,50 @@ var createCookie = require('./pad_utils').createCookie; var readCookie = require('./pad_utils').readCookie; var randomString = require('./pad_utils').randomString; +var hooks = require('./pluginfw/hooks'); + +function createCookie(name, value, days, path) +{ + if (days) + { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "; expires=" + date.toGMTString(); + } + else var expires = ""; + + if(!path) + path = "/"; + + document.cookie = name + "=" + value + expires + "; path=" + path; +} + +function readCookie(name) +{ + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) + { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function randomString() +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var string_length = 20; + var randomstring = ''; + for (var i = 0; i < string_length; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return "t." + randomstring; +} + function getParams() { var params = getUrlVars() @@ -457,7 +500,7 @@ var pad = { guestPolicy: pad.padOptions.guestPolicy }, this); padimpexp.init(this); - padsavedrevs.init(clientVars.initialRevisionList, this); + padsavedrevs.init(this); padeditor.init(postAceInit, pad.padOptions.view || {}, this); @@ -491,6 +534,7 @@ var pad = { if(padcookie.getPref("showAuthorshipColors") == false){ pad.changeViewOption('showAuthorColors', false); } + hooks.aCallAll("postAceInit"); } }, dispose: function() diff --git a/src/static/js/pad_docbar.js b/src/static/js/pad_docbar.js index 08bbb0c4..c5858126 100644 --- a/src/static/js/pad_docbar.js +++ b/src/static/js/pad_docbar.js @@ -449,7 +449,7 @@ var paddocbar = (function() handleResizePage: function() { // Side-step circular reference. This should be injected. - var padsavedrevs = require('./pad_savedrevs').padsavedrevs; + var padsavedrevs = require('./pad_savedrevs'); padsavedrevs.handleResizePage(); }, hideLaterIfNoOtherInteraction: function() diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 0cfd1f20..7e8cf96e 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -22,7 +22,7 @@ var padutils = require('./pad_utils').padutils; var padeditor = require('./pad_editor').padeditor; -var padsavedrevs = require('./pad_savedrevs').padsavedrevs; +var padsavedrevs = require('./pad_savedrevs'); function indexOf(array, value) { for (var i = 0, ii = array.length; i < ii; i++) { @@ -131,7 +131,7 @@ var padeditbar = (function() { self.toogleDropDown("importexport"); } - else if (cmd == 'save') + else if (cmd == 'savedRevision') { padsavedrevs.saveNow(); } diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js index 12f83aeb..b8a4ea0e 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.js @@ -87,8 +87,6 @@ var padeditor = (function() return defaultValue; } - self.ace.setProperty("showsauthorcolors", !settings.noColors); - self.ace.setProperty("rtlIsTrue", settings.rtlIsTrue); var v; @@ -100,6 +98,8 @@ var padeditor = (function() v = getOption('showAuthorColors', true); self.ace.setProperty("showsauthorcolors", v); padutils.setCheckbox($("#options-colorscheck"), v); + // Override from parameters + self.ace.setProperty("showsauthorcolors", !settings.noColors); v = getOption('useMonospaceFont', false); self.ace.setProperty("textface", (v ? "monospace" : "Arial, sans-serif")); diff --git a/src/static/js/pad_savedrevs.js b/src/static/js/pad_savedrevs.js index 2a0f4fde..bf4f941c 100644 --- a/src/static/js/pad_savedrevs.js +++ b/src/static/js/pad_savedrevs.js @@ -1,11 +1,5 @@ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. + * Copyright 2012 Peter 'Pita' Martischka * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,507 +14,13 @@ * limitations under the License. */ -var padutils = require('./pad_utils').padutils; -var paddocbar = require('./pad_docbar').paddocbar; - -var padsavedrevs = (function() -{ - - function reversedCopy(L) - { - var L2 = L.slice(); - L2.reverse(); - return L2; - } - - function makeRevisionBox(revisionInfo, rnum) - { - var box = $('<div class="srouterbox">' + '<div class="srinnerbox">' + '<a href="javascript:void(0)" class="srname"><!-- --></a>' + '<div class="sractions"><a class="srview" href="javascript:void(0)" target="_blank">view</a> | <a class="srrestore" href="javascript:void(0)">restore</a></div>' + '<div class="srtime"><!-- --></div>' + '<div class="srauthor"><!-- --></div>' + '<img class="srtwirly" src="static/img/misc/status-ball.gif">' + '</div></div>'); - setBoxLabel(box, revisionInfo.label); - setBoxTimestamp(box, revisionInfo.timestamp); - box.find(".srauthor").html("by " + padutils.escapeHtml(revisionInfo.savedBy)); - var viewLink = '/ep/pad/view/' + pad.getPadId() + '/' + revisionInfo.id; - box.find(".srview").attr('href', viewLink); - var restoreLink = 'javascript:void(require('+JSON.stringify(module.id)+').padsavedrevs.restoreRevision(' + JSON.stringify(rnum) + ');'; - box.find(".srrestore").attr('href', restoreLink); - box.find(".srname").click(function(evt) - { - editRevisionLabel(rnum, box); - }); - return box; - } - - function setBoxLabel(box, label) - { - box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label); - } - - function setBoxTimestamp(box, timestamp) - { - box.find(".srtime").html(padutils.escapeHtml( - padutils.timediff(new Date(timestamp)))); - } - - function getNthBox(n) - { - return $("#savedrevisions .srouterbox").eq(n); - } - - function editRevisionLabel(rnum, box) - { - var input = $('<input type="text" class="srnameedit"/>'); - box.find(".srnameedit").remove(); // just in case - var label = box.find(".srname"); - input.width(label.width()); - input.height(label.height()); - input.css('top', label.position().top); - input.css('left', label.position().left); - label.after(input); - label.css('opacity', 0); - - function endEdit() - { - input.remove(); - label.css('opacity', 1); - } - var rev = currentRevisionList[rnum]; - var oldLabel = rev.label; - input.blur(function() - { - var newLabel = input.val(); - if (newLabel && newLabel != oldLabel) - { - relabelRevision(rnum, newLabel); - } - endEdit(); - }); - input.val(rev.label).focus().select(); - padutils.bindEnterAndEscape(input, function onEnter() - { - input.blur(); - }, function onEscape() - { - input.val('').blur(); - }); - } - - function relabelRevision(rnum, newLabel) - { - var rev = currentRevisionList[rnum]; - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevisionlabel', - data: { - userId: pad.getUserId(), - padId: pad.getPadId(), - revId: rev.id, - newLabel: newLabel - }, - success: success, - error: error - }); - - function success(text) - { - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'revisionLabel', - revisionList: reversedCopy(currentRevisionList), - savedBy: pad.getUserName(), - newLabel: newLabel - }); - } - - function error(e) - { - alert("Oops! There was an error saving that revision label. Please try again later."); - } - } - - var currentRevisionList = []; - - function setRevisionList(newRevisionList, noAnimation) - { - // deals with changed labels and new added revisions - for (var i = 0; i < currentRevisionList.length; i++) - { - var a = currentRevisionList[i]; - var b = newRevisionList[i]; - if (b.label != a.label) - { - setBoxLabel(getNthBox(i), b.label); - } - } - for (var j = currentRevisionList.length; j < newRevisionList.length; j++) - { - var newBox = makeRevisionBox(newRevisionList[j], j); - $("#savedrevs-scrollinner").append(newBox); - newBox.css('left', j * REVISION_BOX_WIDTH); - } - var newOnes = (newRevisionList.length > currentRevisionList.length); - currentRevisionList = newRevisionList; - if (newOnes) - { - setDesiredScroll(getMaxScroll()); - if (noAnimation) - { - setScroll(desiredScroll); - } - - if (!noAnimation) - { - var nameOfLast = currentRevisionList[currentRevisionList.length - 1].label; - displaySavedTip(nameOfLast); - } - } - } - - function refreshRevisionList() - { - for (var i = 0; i < currentRevisionList.length; i++) - { - var r = currentRevisionList[i]; - var box = getNthBox(i); - setBoxTimestamp(box, r.timestamp); - } - } - - var savedTipAnimator = padutils.makeShowHideAnimator(function(state) - { - if (state == -1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'block'); - } - else if (state == 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state == 1) - { - $("#revision-notifier").css('opacity', 0).css('display', 'none'); - } - else if (state < 0) - { - $("#revision-notifier").css('opacity', 1); - } - else if (state > 0) - { - $("#revision-notifier").css('opacity', 1 - state); - } - }, false, 25, 300); - - function displaySavedTip(text) - { - $("#revision-notifier .name").html(padutils.escapeHtml(text)); - savedTipAnimator.show(); - padutils.cancelActions("hide-revision-notifier"); - var hideLater = padutils.getCancellableAction("hide-revision-notifier", function() - { - savedTipAnimator.hide(); - }); - window.setTimeout(hideLater, 3000); - } - - var REVISION_BOX_WIDTH = 120; - var curScroll = 0; // distance between left of revisions and right of view - var desiredScroll = 0; - - function getScrollWidth() - { - return REVISION_BOX_WIDTH * currentRevisionList.length; - } - - function getViewportWidth() - { - return $("#savedrevs-scrollouter").width(); - } - - function getMinScroll() - { - return Math.min(getViewportWidth(), getScrollWidth()); - } - - function getMaxScroll() - { - return getScrollWidth(); - } - - function setScroll(newScroll) - { - curScroll = newScroll; - $("#savedrevs-scrollinner").css('right', newScroll); - updateScrollArrows(); - } - - function setDesiredScroll(newDesiredScroll, dontUpdate) - { - desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(), newDesiredScroll)); - if (!dontUpdate) - { - updateScroll(); - } - } - - function updateScroll() - { - updateScrollArrows(); - scrollAnimator.scheduleAnimation(); - } - - function updateScrollArrows() - { - $("#savedrevs-scrollleft").toggleClass("disabledscrollleft", desiredScroll <= getMinScroll()); - $("#savedrevs-scrollright").toggleClass("disabledscrollright", desiredScroll >= getMaxScroll()); - } - var scrollAnimator = padutils.makeAnimationScheduler(function() - { - setDesiredScroll(desiredScroll, true); // re-clamp - if (Math.abs(desiredScroll - curScroll) < 1) - { - setScroll(desiredScroll); - return false; - } - else - { - setScroll(curScroll + (desiredScroll - curScroll) * 0.5); - return true; - } - }, 50, 2); - - var isSaving = false; - - function setIsSaving(v) - { - isSaving = v; - rerenderButton(); - } - - function haveReachedRevLimit() - { - var mv = pad.getPrivilege('maxRevisions'); - return (!(mv < 0 || mv > currentRevisionList.length)); - } - - function rerenderButton() - { - if (isSaving || (!pad.isFullyConnected()) || haveReachedRevLimit()) - { - $("#savedrevs-savenow").css('opacity', 0.75); - } - else - { - $("#savedrevs-savenow").css('opacity', 1); - } - } - - var scrollRepeatTimer = null; - var scrollStartTime = 0; - - function setScrollRepeatTimer(dir) - { - clearScrollRepeatTimer(); - scrollStartTime = +new Date; - scrollRepeatTimer = window.setTimeout(function f() - { - if (!scrollRepeatTimer) - { - return; - } - self.scroll(dir); - var scrollTime = (+new Date) - scrollStartTime; - var delay = (scrollTime > 2000 ? 50 : 300); - scrollRepeatTimer = window.setTimeout(f, delay); - }, 300); - $(document).bind('mouseup', clearScrollRepeatTimer); - } - - function clearScrollRepeatTimer() - { - if (scrollRepeatTimer) - { - window.clearTimeout(scrollRepeatTimer); - scrollRepeatTimer = null; - } - $(document).unbind('mouseup', clearScrollRepeatTimer); - } - - var pad = undefined; - var self = { - init: function(initialRevisions, _pad) - { - pad = _pad; - self.newRevisionList(initialRevisions, true); - - $("#savedrevs-savenow").click(function() - { - self.saveNow(); - }); - $("#savedrevs-scrollleft").mousedown(function() - { - self.scroll('left'); - setScrollRepeatTimer('left'); - }); - $("#savedrevs-scrollright").mousedown(function() - { - self.scroll('right'); - setScrollRepeatTimer('right'); - }); - $("#savedrevs-close").click(function() - { - paddocbar.setShownPanel(null); - }); - - // update "saved n minutes ago" times - window.setInterval(function() - { - refreshRevisionList(); - }, 60 * 1000); - }, - restoreRevision: function(rnum) - { - var rev = currentRevisionList[rnum]; - var warning = ("Restoring this revision will overwrite the current" + " text of the pad. " + "Are you sure you want to continue?"); - var hidePanel = paddocbar.hideLaterIfNoOtherInteraction(); - var box = getNthBox(rnum); - if (confirm(warning)) - { - box.find(".srtwirly").show(); - $.ajax( - { - type: 'get', - url: '/ep/pad/getrevisionatext', - data: { - padId: pad.getPadId(), - revId: rev.id - }, - success: success, - error: error - }); - } - - function success(resultJson) - { - untwirl(); - var result = JSON.parse(resultJson); - padeditor.restoreRevisionText(result); - window.setTimeout(function() - { - hidePanel(); - }, 0); - } - - function error(e) - { - untwirl(); - alert("Oops! There was an error retreiving the text (revNum= " + rev.revNum + "; padId=" + pad.getPadId()); - } - - function untwirl() - { - box.find(".srtwirly").hide(); - } - }, - showReachedLimit: function() - { - alert("Sorry, you do not have privileges to save more than " + pad.getPrivilege('maxRevisions') + " revisions."); - }, - newRevisionList: function(lst, noAnimation) - { - // server gives us list with newest first; - // we want chronological order - var L = reversedCopy(lst); - setRevisionList(L, noAnimation); - rerenderButton(); - }, - saveNow: function() - { - if (isSaving) - { - return; - } - if (!pad.isFullyConnected()) - { - return; - } - if (haveReachedRevLimit()) - { - self.showReachedLimit(); - return; - } - setIsSaving(true); - var savedBy = pad.getUserName() || "unnamed"; - pad.callWhenNotCommitting(submitSave); - - function submitSave() - { - $.ajax( - { - type: 'post', - url: '/ep/pad/saverevision', - data: { - padId: pad.getPadId(), - savedBy: savedBy, - savedById: pad.getUserId(), - revNum: pad.getCollabRevisionNumber() - }, - success: success, - error: error - }); - } - - function success(text) - { - setIsSaving(false); - var newRevisionList = JSON.parse(text); - self.newRevisionList(newRevisionList); - pad.sendClientMessage( - { - type: 'newRevisionList', - revisionList: newRevisionList, - savedBy: savedBy - }); - } +var pad; - function error(e) - { - setIsSaving(false); - alert("Oops! The server failed to save the revision. Please try again later."); - } - }, - handleResizePage: function() - { - updateScrollArrows(); - }, - handleIsFullyConnected: function(isConnected) - { - rerenderButton(); - }, - scroll: function(dir) - { - var minScroll = getMinScroll(); - var maxScroll = getMaxScroll(); - if (dir == 'left') - { - if (desiredScroll > minScroll) - { - var n = Math.floor((desiredScroll - 1 - minScroll) / REVISION_BOX_WIDTH); - setDesiredScroll(Math.max(0, n) * REVISION_BOX_WIDTH + minScroll); - } - } - else if (dir == 'right') - { - if (desiredScroll < maxScroll) - { - var n = Math.floor((maxScroll - desiredScroll - 1) / REVISION_BOX_WIDTH); - setDesiredScroll(maxScroll - Math.max(0, n) * REVISION_BOX_WIDTH); - } - } - } - }; - return self; -}()); +exports.saveNow = function(){ + pad.collabClient.sendMessage({"type": "SAVE_REVISION"}); + alert("This revision is now marked as a saved revision"); +} -exports.padsavedrevs = padsavedrevs; +exports.init = function(_pad){ + pad = _pad; +} diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 9c04023f..c4cd5aeb 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -10,12 +10,18 @@ if (plugins.isClient) { _ = require("underscore"); } +exports.bubbleExceptions = true + var hookCallWrapper = function (hook, hook_name, args, cb) { if (cb === undefined) cb = function (x) { return x; }; - try { + if (exports.bubbleExceptions) { return hook.hook_fn(hook_name, args, cb); - } catch (ex) { - console.error([hook_name, hook.part.full_name, ex.stack || ex]); + } else { + try { + return hook.hook_fn(hook_name, args, cb); + } catch (ex) { + console.error([hook_name, hook.part.full_name, ex.stack || ex]); + } } } @@ -36,6 +42,7 @@ exports.flatten = function (lst) { } exports.callAll = function (hook_name, args) { + if (!args) args = {}; if (plugins.hooks[hook_name] === undefined) return []; return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) { return hookCallWrapper(hook, hook_name, args); @@ -43,26 +50,31 @@ exports.callAll = function (hook_name, args) { } exports.aCallAll = function (hook_name, args, cb) { - if (plugins.hooks[hook_name] === undefined) cb([]); + if (!args) args = {}; + if (!cb) cb = function () {}; + if (plugins.hooks[hook_name] === undefined) return cb(null, []); async.map( plugins.hooks[hook_name], function (hook, cb) { hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); }); }, function (err, res) { - cb(exports.flatten(res)); + cb(null, exports.flatten(res)); } ); } exports.callFirst = function (hook_name, args) { + if (!args) args = {}; if (plugins.hooks[hook_name][0] === undefined) return []; return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args)); } exports.aCallFirst = function (hook_name, args, cb) { - if (plugins.hooks[hook_name][0] === undefined) cb([]); - hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(exports.flatten(res)); }); + if (!args) args = {}; + if (!cb) cb = function () {}; + if (plugins.hooks[hook_name][0] === undefined) return cb(null, []); + hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(null, exports.flatten(res)); }); } exports.callAllStr = function(hook_name, args, sep, pre, post) { diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js new file mode 100644 index 00000000..127a95aa --- /dev/null +++ b/src/static/js/pluginfw/installer.js @@ -0,0 +1,76 @@ +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 withNpm = function (npmfn, cb) { + npm.load({}, function (er) { + if (er) return cb({progress:1, error:er}); + npm.on("log", function (message) { + cb({progress: 0.5, message:message.msg + ": " + message.pref}); + }); + npmfn(function (er, data) { + if (er) return cb({progress:1, error:er.code + ": " + er.path}); + if (!data) data = {}; + data.progress = 1; + data.message = "Done."; + cb(data); + }); + }); +} + +// All these functions call their callback multiple times with +// {progress:[0,1], message:STRING, error:object}. They will call it +// with progress = 1 at least once, and at all times will either +// message or error be present, not both. It can be called multiple +// times for all values of propgress except for 1. + +exports.uninstall = function(plugin_name, cb) { + withNpm( + function (cb) { + npm.commands.uninstall([plugin_name], function (er) { + if (er) return cb(er); + hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) { + if (er) return cb(er); + plugins.update(cb); + }); + }); + }, + cb + ); +}; + +exports.install = function(plugin_name, cb) { + withNpm( + function (cb) { + npm.commands.install([plugin_name], function (er) { + if (er) return cb(er); + hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) { + if (er) return cb(er); + plugins.update(cb); + }); + }); + }, + cb + ); +}; + +exports.search = function(pattern, cb) { + withNpm( + function (cb) { + registry.get( + "/-/all", null, 600, false, true, + function (er, data) { + if (er) return cb(er); + var res = {}; + for (key in data) { + if (key.indexOf(plugins.prefix) == 0 && key.indexOf(pattern) != -1) + res[key] = data[key]; + } + cb(null, {results:res}); + } + ); + }, + cb + ); +}; diff --git a/src/static/js/pluginfw/parent_require.js b/src/static/js/pluginfw/parent_require.js new file mode 100644 index 00000000..d7f6190d --- /dev/null +++ b/src/static/js/pluginfw/parent_require.js @@ -0,0 +1,37 @@ +/** + * This module allows passing require modules instances to + * embedded iframes in a page. + * For example, if a page has the "plugins" module initialized, + * it is important to use exactly the same "plugins" instance + * inside iframes as well. Otherwise, plugins cannot save any + * state. + */ + + +/** + * Instructs the require object that when a reqModuleName module + * needs to be loaded, that it iterates through the parents of the + * current window until it finds one who can execute "require" + * statements and asks it to perform require on reqModuleName. + * + * @params requireDefObj Require object which supports define + * statements. This object is accessible after loading require-kernel. + * @params reqModuleName Module name e.g. (ep_etherpad-lite/static/js/plugins) + */ +exports.getRequirementFromParent = function(requireDefObj, reqModuleName) { + requireDefObj.define(reqModuleName, function(require, exports, module) { + var t = parent; + var max = 0; // make sure I don't go up more than 10 times + while (typeof(t) != "undefined") { + max++; + if (max==10) + break; + if (typeof(t.require) != "undefined") { + module.exports = t.require(reqModuleName); + return; + } + t = t.parent; + } + }); + +} diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index aa2dfafb..db0b92e4 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -4,7 +4,7 @@ var _; if (!exports.isClient) { var npm = require("npm/lib/npm.js"); - var readInstalled = require("npm/lib/utils/read-installed.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"); @@ -14,10 +14,9 @@ if (!exports.isClient) { var util = require("util"); _ = require("underscore"); }else{ - var $, jQuery + var $, jQuery; $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$; _ = require("ep_etherpad-lite/static/js/underscore"); - } exports.prefix = 'ep_'; @@ -31,15 +30,15 @@ exports.ensure = function (cb) { exports.update(cb); else cb(); -} +}; exports.formatPlugins = function () { return _.keys(exports.plugins).join(", "); -} +}; exports.formatParts = function () { return _.map(exports.parts, function (part) { return part.full_name; }).join("\n"); -} +}; exports.formatHooks = function () { var res = []; @@ -49,33 +48,39 @@ exports.formatHooks = function () { }); }); return res.join("\n"); -} +}; -exports.loadFn = function (path) { +exports.loadFn = function (path, hookName) { var x = path.split(":"); var fn = require(x[0]); - _.each(x[1].split("."), function (name) { + var functionName = x[1] ? x[1] : hookName; + + _.each(functionName.split("."), function (name) { fn = fn[name]; }); return fn; -} +}; exports.extractHooks = function (parts, hook_set_name) { var hooks = {}; _.each(parts,function (part) { - _.chain(part[hook_set_name] || {}).keys().each(function (hook_name) { + _.chain(part[hook_set_name] || {}) + .keys() + .each(function (hook_name) { if (hooks[hook_name] === undefined) hooks[hook_name] = []; + + var hook_fn_name = part[hook_set_name][hook_name]; - var hook_fn = exports.loadFn(part[hook_set_name][hook_name]); + var hook_fn = exports.loadFn(hook_fn_name, hook_name); if (hook_fn) { hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part}); } else { - console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]); + console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]); } }); }); return hooks; -} +}; if (exports.isClient) { @@ -90,7 +95,7 @@ if (exports.isClient) { console.error("Failed to load plugin-definitions: " + err); cb(); }); - } + }; } else { exports.update = function (cb) { @@ -104,15 +109,15 @@ exports.update = function (cb) { exports.loadPlugin(packages, plugin_name, plugins, parts, cb); }, function (err) { - exports.plugins = plugins; + exports.plugins = plugins; exports.parts = exports.sortParts(parts); exports.hooks = exports.extractHooks(exports.parts, "hooks"); - exports.loaded = true; + exports.loaded = true; cb(err); } ); }); -} + }; exports.getPackages = function (cb) { // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that @@ -122,44 +127,50 @@ exports.getPackages = function (cb) { var packages = {}; function flatten(deps) { _.chain(deps).keys().each(function (name) { - if (name.indexOf(exports.prefix) == 0) { - packages[name] = deps[name]; - } - if (deps[name].dependencies !== undefined) - flatten(deps[name].dependencies); - delete deps[name].dependencies; + if (name.indexOf(exports.prefix) === 0) { + packages[name] = _.clone(deps[name]); + // Delete anything that creates loops so that the plugin + // list can be sent as JSON to the web client + delete packages[name].dependencies; + delete packages[name].parent; + } + + if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); }); } - flatten([data]); + + var tmp = {}; + tmp[data.name] = data; + flatten(tmp); cb(null, packages); }); -} + }; -exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { + exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) { var plugin_path = path.resolve(packages[plugin_name].path, "ep.json"); fs.readFile( plugin_path, function (er, data) { if (er) { - console.error("Unable to load plugin definition file " + plugin_path); + console.error("Unable to load plugin definition file " + plugin_path); return cb(); } try { var plugin = JSON.parse(data); - plugin.package = packages[plugin_name]; - plugins[plugin_name] = plugin; - _.each(plugin.parts, function (part) { - part.plugin = plugin_name; - part.full_name = plugin_name + "/" + part.name; - parts[part.full_name] = part; - }); + plugin['package'] = packages[plugin_name]; + plugins[plugin_name] = plugin; + _.each(plugin.parts, function (part) { + part.plugin = plugin_name; + part.full_name = plugin_name + "/" + part.name; + parts[part.full_name] = part; + }); } catch (ex) { - console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); + console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString()); } cb(); } ); -} + }; exports.partsToParentChildList = function (parts) { var res = []; @@ -175,7 +186,7 @@ exports.partsToParentChildList = function (parts) { } }); return res; -} +}; // Used only in Node, so no need for _ diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js new file mode 100644 index 00000000..cc03b357 --- /dev/null +++ b/src/static/js/pluginfw/read-installed.js @@ -0,0 +1,324 @@ +// A copy of npm/lib/utils/read-installed.js +// that is hacked to not cache everything :) + +// Walk through the file-system "database" of installed +// packages, and create a data object related to the +// installed versions of each package. + +/* +This will traverse through all node_modules folders, +resolving the dependencies object to the object corresponding to +the package that meets that dep, or just the version/range if +unmet. + +Assuming that you had this folder structure: + +/path/to ++-- package.json { name = "root" } +`-- node_modules + +-- foo {bar, baz, asdf} + | +-- node_modules + | +-- bar { baz } + | `-- baz + `-- asdf + +where "foo" depends on bar, baz, and asdf, bar depends on baz, +and bar and baz are bundled with foo, whereas "asdf" is at +the higher level (sibling to foo), you'd get this object structure: + +{ <package.json data> +, path: "/path/to" +, parent: null +, dependencies: + { foo : + { version: "1.2.3" + , path: "/path/to/node_modules/foo" + , parent: <Circular: root> + , dependencies: + { bar: + { parent: <Circular: foo> + , path: "/path/to/node_modules/foo/node_modules/bar" + , version: "2.3.4" + , dependencies: { baz: <Circular: foo.dependencies.baz> } + } + , baz: { ... } + , asdf: <Circular: asdf> + } + } + , asdf: { ... } + } +} + +Unmet deps are left as strings. +Extraneous deps are marked with extraneous:true +deps that don't meet a requirement are marked with invalid:true + +to READ(packagefolder, parentobj, name, reqver) +obj = read package.json +installed = ./node_modules/* +if parentobj is null, and no package.json + obj = {dependencies:{<installed>:"*"}} +deps = Object.keys(obj.dependencies) +obj.path = packagefolder +obj.parent = parentobj +if name, && obj.name !== name, obj.invalid = true +if reqver, && obj.version !satisfies reqver, obj.invalid = true +if !reqver && parentobj, obj.extraneous = true +for each folder in installed + obj.dependencies[folder] = READ(packagefolder+node_modules+folder, + obj, folder, obj.dependencies[folder]) +# walk tree to find unmet deps +for each dep in obj.dependencies not in installed + r = obj.parent + while r + if r.dependencies[dep] + if r.dependencies[dep].verion !satisfies obj.dependencies[dep] + WARN + r.dependencies[dep].invalid = true + obj.dependencies[dep] = r.dependencies[dep] + r = null + else r = r.parent +return obj + + +TODO: +1. Find unmet deps in parent directories, searching as node does up +as far as the left-most node_modules folder. +2. Ignore anything in node_modules that isn't a package folder. + +*/ + + +var npm = require("npm/lib/npm.js") + , fs = require("graceful-fs") + , 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") + +module.exports = readInstalled + +function readInstalled (folder, cb) { + /* This is where we clear the cache, these three lines are all the + * new code there is */ + rpSeen = {}; + riSeen = []; + var fuSeen = []; + + var d = npm.config.get("depth") + readInstalled_(folder, null, null, null, 0, d, function (er, obj) { + if (er) return cb(er) + // now obj has all the installed things, where they're installed + // figure out the inheritance links, now that the object is built. + resolveInheritance(obj) + cb(null, obj) + }) +} + +var rpSeen = {} +function readInstalled_ (folder, parent, name, reqver, depth, maxDepth, cb) { + //console.error(folder, name) + + var installed + , obj + , real + , link + + fs.readdir(path.resolve(folder, "node_modules"), function (er, i) { + // error indicates that nothing is installed here + if (er) i = [] + installed = i.filter(function (f) { return f.charAt(0) !== "." }) + next() + }) + + readJson(path.resolve(folder, "package.json"), function (er, data) { + obj = copy(data) + + if (!parent) { + obj = obj || true + er = null + } + return next(er) + }) + + fs.lstat(folder, function (er, st) { + if (er) { + if (!parent) real = true + return next(er) + } + fs.realpath(folder, function (er, rp) { + //console.error("realpath(%j) = %j", folder, rp) + real = rp + if (st.isSymbolicLink()) link = rp + next(er) + }) + }) + + var errState = null + , called = false + function next (er) { + if (errState) return + if (er) { + errState = er + return cb(null, []) + } + //console.error('next', installed, obj && typeof obj, name, real) + if (!installed || !obj || !real || called) return + called = true + if (rpSeen[real]) return cb(null, rpSeen[real]) + if (obj === true) { + obj = {dependencies:{}, path:folder} + installed.forEach(function (i) { obj.dependencies[i] = "*" }) + } + if (name && obj.name !== name) obj.invalid = true + obj.realName = name || obj.name + obj.dependencies = obj.dependencies || {} + + // "foo":"http://blah" is always presumed valid + if (reqver + && semver.validRange(reqver) + && !semver.satisfies(obj.version, reqver)) { + obj.invalid = true + } + + if (parent + && !(name in parent.dependencies) + && !(name in (parent.devDependencies || {}))) { + obj.extraneous = true + } + obj.path = obj.path || folder + obj.realPath = real + obj.link = link + if (parent && !obj.link) obj.parent = parent + rpSeen[real] = obj + obj.depth = depth + if (depth >= maxDepth) return cb(null, obj) + asyncMap(installed, function (pkg, cb) { + var rv = obj.dependencies[pkg] + if (!rv && obj.devDependencies) rv = obj.devDependencies[pkg] + readInstalled_( path.resolve(folder, "node_modules/"+pkg) + , obj, pkg, obj.dependencies[pkg], depth + 1, maxDepth + , cb ) + }, function (er, installedData) { + if (er) return cb(er) + installedData.forEach(function (dep) { + obj.dependencies[dep.realName] = dep + }) + + // any strings here are unmet things. however, if it's + // optional, then that's fine, so just delete it. + if (obj.optionalDependencies) { + Object.keys(obj.optionalDependencies).forEach(function (dep) { + if (typeof obj.dependencies[dep] === "string") { + delete obj.dependencies[dep] + } + }) + } + return cb(null, obj) + }) + } +} + +// starting from a root object, call findUnmet on each layer of children +var riSeen = [] +function resolveInheritance (obj) { + if (typeof obj !== "object") return + if (riSeen.indexOf(obj) !== -1) return + riSeen.push(obj) + if (typeof obj.dependencies !== "object") { + obj.dependencies = {} + } + Object.keys(obj.dependencies).forEach(function (dep) { + findUnmet(obj.dependencies[dep]) + }) + Object.keys(obj.dependencies).forEach(function (dep) { + resolveInheritance(obj.dependencies[dep]) + }) +} + +// find unmet deps by walking up the tree object. +// No I/O +var fuSeen = [] +function findUnmet (obj) { + if (fuSeen.indexOf(obj) !== -1) return + fuSeen.push(obj) + //console.error("find unmet", obj.name, obj.parent && obj.parent.name) + var deps = obj.dependencies = obj.dependencies || {} + //console.error(deps) + Object.keys(deps) + .filter(function (d) { return typeof deps[d] === "string" }) + .forEach(function (d) { + //console.error("find unmet", obj.name, d, deps[d]) + var r = obj.parent + , found = null + while (r && !found && typeof deps[d] === "string") { + // if r is a valid choice, then use that. + found = r.dependencies[d] + if (!found && r.realName === d) found = r + + if (!found) { + r = r.link ? null : r.parent + continue + } + if ( typeof deps[d] === "string" + && !semver.satisfies(found.version, deps[d])) { + // the bad thing will happen + log.warn(obj.path + " requires "+d+"@'"+deps[d] + +"' but will load\n" + +found.path+",\nwhich is version "+found.version + ,"unmet dependency") + found.invalid = true + } + deps[d] = found + } + + }) + log.verbose([obj._id], "returning") + return obj +} + +function copy (obj) { + if (!obj || typeof obj !== 'object') return obj + if (Array.isArray(obj)) return obj.map(copy) + + var o = {} + for (var i in obj) o[i] = copy(obj[i]) + return o +} + +if (module === require.main) { + var util = require("util") + console.error("testing") + + var called = 0 + readInstalled(process.cwd(), function (er, map) { + console.error(called ++) + if (er) return console.error(er.stack || er.message) + cleanup(map) + console.error(util.inspect(map, true, 10, true)) + }) + + var seen = [] + function cleanup (map) { + if (seen.indexOf(map) !== -1) return + seen.push(map) + for (var i in map) switch (i) { + case "_id": + case "path": + case "extraneous": case "invalid": + case "dependencies": case "name": + continue + default: delete map[i] + } + var dep = map.dependencies +// delete map.dependencies + if (dep) { +// map.dependencies = dep + for (var i in dep) if (typeof dep[i] === "object") { + cleanup(dep[i]) + } + } + return map + } +} diff --git a/src/static/js/prefixfree.js b/src/static/js/prefixfree.js deleted file mode 100644 index b5b23466..00000000 --- a/src/static/js/prefixfree.js +++ /dev/null @@ -1,419 +0,0 @@ -/** - * StyleFix 1.0.2 - * @author Lea Verou - * MIT license - */ - -(function(){ - -if(!window.addEventListener) { - return; -} - -var self = window.StyleFix = { - link: function(link) { - try { - // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets - if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { - return; - } - } - catch(e) { - return; - } - - var url = link.href || link.getAttribute('data-href'), - base = url.replace(/[^\/]+$/, ''), - parent = link.parentNode, - xhr = new XMLHttpRequest(); - - xhr.open('GET', url); - - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - var css = xhr.responseText; - - if(css && link.parentNode) { - css = self.fix(css, true, link); - - // Convert relative URLs to absolute, if needed - if(base) { - css = css.replace(/url\(('?|"?)(.+?)\1\)/gi, function($0, quote, url) { - if(!/^([a-z]{3,10}:|\/|#)/i.test(url)) { // If url not absolute & not a hash - // May contain sequences like /../ and /./ but those DO work - return 'url("' + base + url + '")'; - } - - return $0; - }); - - // behavior URLs shoudn’t be converted (Issue #19) - css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + base, 'gi'), '$1'); - } - - var style = document.createElement('style'); - style.textContent = css; - style.media = link.media; - style.disabled = link.disabled; - style.setAttribute('data-href', link.getAttribute('href')); - - parent.insertBefore(style, link); - parent.removeChild(link); - } - } - }; - - xhr.send(null); - - link.setAttribute('data-inprogress', ''); - }, - - styleElement: function(style) { - var disabled = style.disabled; - - style.textContent = self.fix(style.textContent, true, style); - - style.disabled = disabled; - }, - - styleAttribute: function(element) { - var css = element.getAttribute('style'); - - css = self.fix(css, false, element); - - element.setAttribute('style', css); - }, - - process: function() { - // Linked stylesheets - $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); - - // Inline stylesheets - $('style').forEach(StyleFix.styleElement); - - // Inline styles - $('[style]').forEach(StyleFix.styleAttribute); - }, - - register: function(fixer, index) { - (self.fixers = self.fixers || []) - .splice(index === undefined? self.fixers.length : index, 0, fixer); - }, - - fix: function(css, raw) { - for(var i=0; i<self.fixers.length; i++) { - css = self.fixers[i](css, raw) || css; - } - - return css; - }, - - camelCase: function(str) { - return str.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); }).replace('-',''); - }, - - deCamelCase: function(str) { - return str.replace(/[A-Z]/g, function($0) { return '-' + $0.toLowerCase() }); - } -}; - -/************************************** - * Process styles - **************************************/ -(function(){ - setTimeout(function(){ - $('link[rel="stylesheet"]').forEach(StyleFix.link); - }, 10); - - document.addEventListener('DOMContentLoaded', StyleFix.process, false); -})(); - -function $(expr, con) { - return [].slice.call((con || document).querySelectorAll(expr)); -} - -})(); - -/** - * PrefixFree 1.0.4 - * @author Lea Verou - * MIT license - */ -(function(root, undefined){ - -if(!window.StyleFix || !window.getComputedStyle) { - return; -} - -var self = window.PrefixFree = { - prefixCSS: function(css, raw) { - var prefix = self.prefix; - - function fix(what, before, after, replacement) { - what = self[what]; - - if(what.length) { - var regex = RegExp(before + '(' + what.join('|') + ')' + after, 'gi'); - - css = css.replace(regex, replacement); - } - } - - fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2('); - fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3'); - fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:'); - - // Prefix properties *inside* values (issue #8) - if (self.properties.length) { - var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); - - fix('valueProperties', '\\b', ':(.+?);', function($0) { - return $0.replace(regex, prefix + "$1") - }); - } - - if(raw) { - fix('selectors', '', '\\b', self.prefixSelector); - fix('atrules', '@', '\\b', '@' + prefix + '$1'); - } - - // Fix double prefixing - css = css.replace(RegExp('-' + prefix, 'g'), '-'); - - return css; - }, - - // Warning: prefixXXX functions prefix no matter what, even if the XXX is supported prefix-less - prefixSelector: function(selector) { - return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) - }, - - prefixProperty: function(property, camelCase) { - var prefixed = self.prefix + property; - - return camelCase? StyleFix.camelCase(prefixed) : prefixed; - } -}; - -/************************************** - * Properties - **************************************/ -(function() { - var prefixes = {}, - properties = [], - shorthands = {}, - style = getComputedStyle(document.documentElement, null), - dummy = document.createElement('div').style; - - // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. - var iterate = function(property) { - if(property.charAt(0) === '-') { - properties.push(property); - - var parts = property.split('-'), - prefix = parts[1]; - - // Count prefix uses - prefixes[prefix] = ++prefixes[prefix] || 1; - - // This helps determining shorthands - while(parts.length > 3) { - parts.pop(); - - var shorthand = parts.join('-'); - - if(supported(shorthand) && properties.indexOf(shorthand) === -1) { - properties.push(shorthand); - } - } - } - }, - supported = function(property) { - return StyleFix.camelCase(property) in dummy; - } - - // Some browsers have numerical indices for the properties, some don't - if(style.length > 0) { - for(var i=0; i<style.length; i++) { - iterate(style[i]) - } - } - else { - for(var property in style) { - iterate(StyleFix.deCamelCase(property)); - } - } - - // Find most frequently used prefix - var highest = {uses:0}; - for(var prefix in prefixes) { - var uses = prefixes[prefix]; - - if(highest.uses < uses) { - highest = {prefix: prefix, uses: uses}; - } - } - - self.prefix = '-' + highest.prefix + '-'; - self.Prefix = StyleFix.camelCase(self.prefix); - - self.properties = []; - - // Get properties ONLY supported with a prefix - for(var i=0; i<properties.length; i++) { - var property = properties[i]; - - if(property.indexOf(self.prefix) === 0) { // we might have multiple prefixes, like Opera - var unprefixed = property.slice(self.prefix.length); - - if(!supported(unprefixed)) { - self.properties.push(unprefixed); - } - } - } - - // IE fix - if(self.Prefix == 'Ms' - && !('transform' in dummy) - && !('MsTransform' in dummy) - && ('msTransform' in dummy)) { - self.properties.push('transform', 'transform-origin'); - } - - self.properties.sort(); -})(); - -/************************************** - * Values - **************************************/ -(function() { -// Values that might need prefixing -var functions = { - 'linear-gradient': { - property: 'backgroundImage', - params: 'red, teal' - }, - 'calc': { - property: 'width', - params: '1px + 5%' - }, - 'element': { - property: 'backgroundImage', - params: '#foo' - } -}; - - -functions['repeating-linear-gradient'] = -functions['repeating-radial-gradient'] = -functions['radial-gradient'] = -functions['linear-gradient']; - -var keywords = { - 'initial': 'color', - 'zoom-in': 'cursor', - 'zoom-out': 'cursor', - 'box': 'display', - 'flexbox': 'display', - 'inline-flexbox': 'display' -}; - -self.functions = []; -self.keywords = []; - -var style = document.createElement('div').style; - -function supported(value, property) { - style[property] = ''; - style[property] = value; - - return !!style[property]; -} - -for (var func in functions) { - var test = functions[func], - property = test.property, - value = func + '(' + test.params + ')'; - - if (!supported(value, property) - && supported(self.prefix + value, property)) { - // It's supported, but with a prefix - self.functions.push(func); - } -} - -for (var keyword in keywords) { - var property = keywords[keyword]; - - if (!supported(keyword, property) - && supported(self.prefix + keyword, property)) { - // It's supported, but with a prefix - self.keywords.push(keyword); - } -} - -})(); - -/************************************** - * Selectors and @-rules - **************************************/ -(function() { - -var -selectors = { - ':read-only': null, - ':read-write': null, - ':any-link': null, - '::selection': null -}, - -atrules = { - 'keyframes': 'name', - 'viewport': null, - 'document': 'regexp(".")' -}; - -self.selectors = []; -self.atrules = []; - -var style = root.appendChild(document.createElement('style')); - -function supported(selector) { - style.textContent = selector + '{}'; // Safari 4 has issues with style.innerHTML - - return !!style.sheet.cssRules.length; -} - -for(var selector in selectors) { - var test = selector + (selectors[selector]? '(' + selectors[selector] + ')' : ''); - - if(!supported(test) && supported(self.prefixSelector(test))) { - self.selectors.push(selector); - } -} - -for(var atrule in atrules) { - var test = atrule + ' ' + (atrules[atrule] || ''); - - if(!supported('@' + test) && supported('@' + self.prefix + test)) { - self.atrules.push(atrule); - } -} - -root.removeChild(style); - -})(); - -// Properties that accept properties as their value -self.valueProperties = [ - 'transition', - 'transition-property' -] - -// Add class for current prefix -root.className += ' ' + self.prefix; - -StyleFix.register(self.prefixCSS); - - -})(document.documentElement); diff --git a/src/static/pad.html b/src/static/pad.html deleted file mode 100644 index 95a5b98f..00000000 --- a/src/static/pad.html +++ /dev/null @@ -1,284 +0,0 @@ -<!doctype html> -<html> - - <title>Etherpad Lite</title> - - <meta charset="utf-8"> - <meta name="robots" content="noindex, nofollow"> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> - - <link href="../static/css/pad.css" rel="stylesheet"> - <link href="../static/custom/pad.css" rel="stylesheet"> - <style title="dynamicsyntax"></style> - - <!-- head and body had been removed intentionally --> - - <div id="editbar"> - <ul id="menu_left"> - <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> - <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> - </li> - <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> - <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> - </li> - <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > - <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> - </li> - <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> - <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> - </li> - <li class="separator"></li> - <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> - <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> - </li> - <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> - <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> - </li> - <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> - <a class="buttonicon buttonicon-indent" title="Indent"></a> - </li> - <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> - <a class="buttonicon buttonicon-outdent" title="Unindent"></a> - </li> - <li class="separator"></li> - <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> - <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> - </li> - <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> - <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> - </li> - <li class="separator"></li> - <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> - <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> - </li> - </ul> - <ul id="menu_right"> - <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> - <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> - </li> - <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> - <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> - </li> - <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > - <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> - </li> - <li class="separator"></li> - <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> - <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> - </li> - <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> - <span class="buttonicon buttonicon-showusers" id="usericonback"></span> - <span id="online_count">1</span> - </li> - </ul> - </div> - - <div id="users"> - <div id="connectionstatus"></div> - <div id="myuser"> - <div id="mycolorpicker"> - <div id="colorpicker"></div> - <button id="mycolorpickersave">Save</button> - <button id="mycolorpickercancel">Cancel</button> - <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> - </div> - <div id="myswatchbox"><div id="myswatch"></div></div> - <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> - <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> - </div> - <div id="otherusers"> - <div id="guestprompts"></div> - <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> - <tr><td></td></tr> - </table> - <div id="nootherusers"></div> - </div> - <div id="userlistbuttonarea"></div> - </div> - - <div id="editorcontainerbox"> - <div id="editorcontainer"></div> - <div id="editorloadingbox">Loading...</div> - </div> - - <div id="settingsmenu" class="popup"> - <h1>Pad settings</h1> - <div class="column"> - <h2>My view</h2> - <p> - <input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();"> - <label for="options-stickychat">Chat always on screen</label> - </p> - <p> - <input type="checkbox" id="options-colorscheck"> - <label for="options-colorscheck">Authorship colors</label> - </p> - <p> - <input type="checkbox" id="options-linenoscheck" checked> - <label for="options-linenoscheck">Line numbers</label> - </p> - <p> - Font type: - <select id="viewfontmenu"> - <option value="normal">Normal</option> - <option value="monospace">Monospaced</option> - </select> - </p> - </div> - <div class="column"> - <h2>Global view</h2> - <p>Currently nothing.</p> - <p class="note">These options affect everyone viewing this pad.</p> - </div> - </div> - - <div id="importexport" class="popup"> - <div class="column"> - <h2>Import from text file, HTML, PDF, Word, ODT or RTF</h2><br> - <form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data"> - <div class="importformdiv" id="importformfilediv"> - <input type="file" name="file" size="15" id="importfileinput"> - <div class="importmessage" id="importmessagefail"></div> - </div> - <div id="import"></div> - <div class="importmessage" id="importmessagesuccess">Successful!</div> - <div class="importformdiv" id="importformsubmitdiv"> - <input type="hidden" name="padId" value="blpmaXT35R"> - <span class="nowrap"> - <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> - <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> - <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> - </span> - </div> - </form> - </div> - <div class="column"> - <h2>Export current pad as</h2> - <a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a> - <a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a> - <a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a> - <a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a> - <a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a> - <a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a> - <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> - </div> - </div> - - <div id="embed" class="popup"> - <div id="embedreadonly" class="right"> - <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"> - <label for="readonlyinput">Read only</label> - </div> - <h1>Share this pad</h1> - <div id="linkcode"> - <h2>Link</h2> - <input id="linkinput" type="text" value=""> - </div> - <br> - <div id="embedcode"> - <h2>Embed URL</h2> - <input id="embedinput" type="text" value=""> - </div> - <br> - <div id="qrcode"> - <h2>QR code</h2> - <div id="qr_center"><img id="embedreadonlyqr"></div> - </div> - </div> - - <div id="chatthrob"></div> - - <div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;"> - <span id="chatlabel">Chat</span> - <span class="buttonicon buttonicon-chat"></span> - <span id="chatcounter">0</span> - </div> - - <div id="chatbox"> - <div id="titlebar"><span id ="titlelabel">Chat</span><a id="titlecross" onClick="chat.hide();return false;">- </a></div> - <div id="chattext" class="authorColors"></div> - <div id="chatinputbox"> - <form> - <input id="chatinput" type="text" maxlength="140"> - </form> - </div> - </div> - - <div id="focusprotector"> </div> - - <div id="modaloverlay"> - <div id="modaloverlay-inner"></div> - </div> - - <div id="mainmodals"> - <div id="connectionbox" class="modaldialog"> - <div id="connectionboxinner" class="modaldialog-inner"> - <div class="connecting">Connecting...</div> - <div class="reconnecting">Reestablishing connection...</div> - <div class="disconnected"> - <h2 class="h2_disconnect">Disconnected.</h2> - <h2 class="h2_userdup">Opened in another window.</h2> - <h2 class="h2_unauth">No Authorization.</h2> - <div id="disconnected_looping"> - <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> - </div> - <div id="disconnected_initsocketfail"> - <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> - </div> - <div id="disconnected_userdup"> - <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> - </div> - <div id="disconnected_unknown"> - <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> - </div> - <div id="disconnected_slowcommit"> - <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> - </div> - <div id="disconnected_unauth"> - <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> - </div> - <div id="disconnected_deleted"> - <p>This pad was deleted.</p> - </div> - <div id="reconnect_advise"> - <p>If this continues to happen, please let us know</p> - </div> - <div id="reconnect_form"> - <button id="forcereconnect">Reconnect Now</button> - </div> - </div> - </div> - <form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;"> - <input type="hidden" class="padId" name="padId"> - <input type="hidden" class="diagnosticInfo" name="diagnosticInfo"> - <input type="hidden" class="missedChanges" name="missedChanges"> - </form> - </div> - - </div> - - <script type="text/javascript" src="../static/js/require-kernel.js"></script> - <script type="text/javascript" src="../static/js/jquery.js"></script> - <script type="text/javascript" src="../socket.io/socket.io.js"></script> - <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> - <script type="text/javascript"> - var clientVars = {}; - (function () { - require.setRootURI("../javascripts/src"); - require.setLibraryURI("../javascripts/lib"); - require.setGlobalKeyPath("require"); - - var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); - plugins.update(function () { - require('ep_etherpad-lite/static/js/pad').init(); - }); - - /* TODO: These globals shouldn't exist. */ - pad = require('ep_etherpad-lite/static/js/pad').pad; - chat = require('ep_etherpad-lite/static/js/chat').chat; - padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; - padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; - }()); - </script> - -</html> diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html new file mode 100644 index 00000000..7dcb6fa3 --- /dev/null +++ b/src/templates/admin/plugins.html @@ -0,0 +1,217 @@ +<html> + <head> + <title>Plugin manager</title> + <style> + table { + border-collapse: collapse; + } + td, th { + border: 1px solid black; + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + padding-bottom: 2px; + } + .template { + display: none; + } + .dialog { + display: none; + position: absolute; + left: 50%; + top: 50%; + width: 700px; + height: 500px; + margin-left: -350px; + margin-top: -250px; + border: 3px solid #999999; + background: #eeeeee; + } + .dialog .title { + margin: 0; + padding: 2px; + border-bottom: 3px solid #999999; + font-size: 24px; + line-height: 24px; + height: 24px; + overflow: hidden; + } + .dialog .title .close { + float: right; + } + .dialog .history { + background: #222222; + color: #eeeeee; + position: absolute; + top: 41px; + bottom: 10px; + left: 10px; + right: 10px; + padding: 2px; + overflow: auto; + } + </style> + <script src="../../static/js/jquery.js"></script> + <script src="../../socket.io/socket.io.js"></script> + <script> + $(document).ready(function () { + var socket = io.connect().of("/pluginfw/installer"); + + var doUpdate = false; + + function updateHandlers() { + $("#progress.dialog .close").unbind('click').click(function () { + $("#progress.dialog").hide(); + }); + + $("#do-search").unbind('click').click(function () { + if ($("#search-query")[0].value != "") + socket.emit("search", $("#search-query")[0].value); + }); + + $(".do-install").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("install", row.find(".name").html()); + }); + + $(".do-uninstall").unbind('click').click(function (e) { + var row = $(e.target).closest("tr"); + doUpdate = true; + socket.emit("uninstall", row.find(".name").html()); + }); + } + + updateHandlers(); + + socket.on('progress', function (data) { + $("#progress.dialog .close").hide(); + $("#progress.dialog").show(); + var message = "Unknown status"; + if (data.message) { + message = "<span class='status'>" + data.message.toString() + "</span>"; + } + if (data.error) { + message = "<span class='error'>" + data.error.toString() + "<span>"; + } + $("#progress.dialog .message").html(message); + $("#progress.dialog .history").append("<div>" + message + "</div>"); + + if (data.progress >= 1) { + if (data.error) { + $("#progress.dialog .close").show(); + } else { + if (doUpdate) { + doUpdate = false; + socket.emit("load"); + } + $("#progress.dialog").hide(); + } + } + }); + + socket.on('search-result', function (data) { + $("#search-results *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#search-result-template").clone(); + + for (attr in plugin) { + row.find("." + attr).html(plugin[attr]); + } + $("#search-results").append(row); + } + updateHandlers(); + }); + + socket.on('installed-results', function (data) { + $("#installed-plugins *").remove(); + for (plugin_name in data.results) { + var plugin = data.results[plugin_name]; + var row = $("#installed-plugin-template").clone(); + + for (attr in plugin.package) { + row.find("." + attr).html(plugin.package[attr]); + } + $("#installed-plugins").append(row); + } + updateHandlers(); + }); + + socket.emit("load"); + + }); + </script> + </head> + <body> + <% if (errors.length) { %> + <div class="errors"> + <% errors.forEach(function (item) { %> + <div class="error"><%= item.toString() %></div> + <% }) %> + </div> + <% } %> + + + <h1>Installed plugins</h1> + <table> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <td></td> + </tr> + </thead> + <tbody class="template"> + <tr id="installed-plugin-template"> + <td class="name"></td> + <td class="description"></td> + <td class="actions"> + <input type="button" value="I" class="do-uninstall"> + </td> + </tr> + </tbody> + <tbody id="installed-plugins"> + </tbody> + </table> + + + + <h1>Search for plugins to install</h1> + <form> + <input type="text" name="search" value="" id="search-query"> + <input type="button" value="S" id="do-search"> + </form> + <table> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <td></td> + </tr> + </thead> + <tbody class="template"> + <tr id="search-result-template"> + <td class="name"></td> + <td class="description"></td> + <td class="actions"> + <input type="button" value="I" class="do-install"> + </td> + </tr> + </tbody> + <tbody id="search-results"> + </tbody> + </table> + + + <div id="progress" class="dialog"> + <h1 class="title"> + Please wait: <span class="message"></span> + <input type="button" class="close" value="Close"> + </h1> + + <div class="history"></div> + </div> + + </body> +</html> diff --git a/src/static/index.html b/src/templates/index.html index 58f68801..58f68801 100644 --- a/src/static/index.html +++ b/src/templates/index.html diff --git a/src/templates/pad.html b/src/templates/pad.html new file mode 100644 index 00000000..e589fb55 --- /dev/null +++ b/src/templates/pad.html @@ -0,0 +1,307 @@ +<% + var settings = require("ep_etherpad-lite/node/utils/Settings"); +%> +<!doctype html> +<html> + + <title>Etherpad Lite</title> + + <meta charset="utf-8"> + <meta name="robots" content="noindex, nofollow"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> + + <% e.begin_block("styles"); %> + <link href="../static/css/pad.css" rel="stylesheet"> + <link href="../static/custom/pad.css" rel="stylesheet"> + <style title="dynamicsyntax"></style> + <% e.end_block(); %> + + <!-- head and body had been removed intentionally --> + + <div id="editbar" class="toolbar"> + <ul class="menu_left"> + <% e.begin_block("editbarMenuLeft"); %> + <li id="bold" onClick="window.pad&&pad.editbarClick('bold');return false"> + <a class="buttonicon buttonicon-bold" title="Bold (ctrl-B)"></a> + </li> + <li id="italic" onClick="window.pad&&pad.editbarClick('italic'); return false;"> + <a class="buttonicon buttonicon-italic" title="Italics (ctrl-I)"></a> + </li> + <li id="underline" onClick="window.pad&&pad.editbarClick('underline');return false;" > + <a class="buttonicon buttonicon-underline" title="Underline (ctrl-U)"></a> + </li> + <li id="strikethrough" onClick="window.pad&&pad.editbarClick('strikethrough');return false;"> + <a class="buttonicon buttonicon-strikethrough" title="Strikethrough"></a> + </li> + <li class="separator"></li> + <li id="oderedlist" onClick="window.pad&&pad.editbarClick('insertorderedlist');return false;"> + <a class="buttonicon buttonicon-insertorderedlist" title="Toggle Ordered List"></a> + </li> + <li id="unoderedlist" onClick="window.pad&&pad.editbarClick('insertunorderedlist');return false;"> + <a class="buttonicon buttonicon-insertunorderedlist" title="Toggle Bullet List"></a> + </li> + <li id="indent" onClick="window.pad&&pad.editbarClick('indent');return false;"> + <a class="buttonicon buttonicon-indent" title="Indent"></a> + </li> + <li id="outdent" onClick="window.pad&&pad.editbarClick('outdent');return false;"> + <a class="buttonicon buttonicon-outdent" title="Unindent"></a> + </li> + <li class="separator"></li> + <li id="undo" onClick="window.pad&&pad.editbarClick('undo');return false;"> + <a class="buttonicon buttonicon-undo" title="Undo (ctrl-Z)"></a> + </li> + <li id="redo" onClick="window.pad&&pad.editbarClick('redo');return false;"> + <a class="buttonicon buttonicon-redo" title="Redo (ctrl-Y)"></a> + </li> + <li class="separator"></li> + <li id="clearAuthorship" onClick="window.pad&&pad.editbarClick('clearauthorship');return false;"> + <a class="buttonicon buttonicon-clearauthorship" title="Clear Authorship Colors"></a> + </li> + <% e.end_block(); %> + </ul> + <ul class="menu_right"> + <% e.begin_block("editbarMenuRight"); %> + <li onClick="window.pad&&pad.editbarClick('savedRevision');return false;"> + <a id="settingslink" title="Mark this revision as a saved revision"> + <div class="buttonicon buttonicon-savedRevision"></div> + </a> + </li> + <li id="settingslink" onClick="window.pad&&pad.editbarClick('settings');return false;"> + <a class="buttonicon buttonicon-settings" id="settingslink" title="Settings of this pad"></a> + </li> + <li id="importexportlink" onClick="window.pad&&pad.editbarClick('import_export');return false;"> + <a class="buttonicon buttonicon-import_export" id="exportlink" title="Import/Export from/to different document formats"></a> + </li> + <li id="embedlink" onClick="window.pad&&pad.editbarClick('embed');return false;" > + <a class="buttonicon buttonicon-embed" id="embedlink" title="Share and Embed this pad"></a> + </li> + <li class="separator"></li> + <li id="timesliderlink" onClick="document.location = document.location.pathname+ '/timeslider'"> + <a class="buttonicon buttonicon-history" title="Show the history of this pad"></a> + </li> + <li id="usericon" onClick="window.pad&&pad.editbarClick('showusers');return false;" title="Show connected users"> + <span class="buttonicon buttonicon-showusers" id="usericonback"></span> + <span id="online_count">1</span> + </li> + <% e.end_block(); %> + </ul> + </div> + + <div id="users"> + <div id="connectionstatus"></div> + <div id="myuser"> + <div id="mycolorpicker"> + <div id="colorpicker"></div> + <button id="mycolorpickersave">Save</button> + <button id="mycolorpickercancel">Cancel</button> + <span id="mycolorpickerpreview" class="myswatchboxhoverable"></span> + </div> + <div id="myswatchbox"><div id="myswatch"></div></div> + <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled"></div> + <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled"></div> + </div> + <div id="otherusers"> + <div id="guestprompts"></div> + <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0"> + <tr><td></td></tr> + </table> + <div id="nootherusers"></div> + </div> + <div id="userlistbuttonarea"></div> + </div> + + <div id="editorcontainerbox"> + <div id="editorcontainer"></div> + <div id="editorloadingbox">Loading...</div> + </div> + + <div id="settingsmenu" class="popup"> + <h1>Pad settings</h1> + <div class="column"> + <h2>My view</h2> + <p> + <input type="checkbox" id="options-stickychat" onClick="chat.stickToScreen();"> + <label for="options-stickychat">Chat always on screen</label> + </p> + <p> + <input type="checkbox" id="options-colorscheck"> + <label for="options-colorscheck">Authorship colors</label> + </p> + <p> + <input type="checkbox" id="options-linenoscheck" checked> + <label for="options-linenoscheck">Line numbers</label> + </p> + <p> + Font type: + <select id="viewfontmenu"> + <option value="normal">Normal</option> + <option value="monospace">Monospaced</option> + </select> + </p> + </div> + <div class="column"> + <h2>Global view</h2> + <p>Currently nothing.</p> + <p class="note">These options affect everyone viewing this pad.</p> + </div> + </div> + + <div id="importexport" class="popup"> + <div class="column"> + <h2>Import from text file, HTML, PDF, Word, ODT or RTF</h2><br> + <form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data"> + <div class="importformdiv" id="importformfilediv"> + <input type="file" name="file" size="15" id="importfileinput"> + <div class="importmessage" id="importmessagefail"></div> + </div> + <div id="import"></div> + <div class="importmessage" id="importmessagesuccess">Successful!</div> + <div class="importformdiv" id="importformsubmitdiv"> + <input type="hidden" name="padId" value="blpmaXT35R"> + <span class="nowrap"> + <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput"> + <img alt="" id="importstatusball" src="../static/img/loading.gif" align="top"> + <img alt="" id="importarrow" src="../static/img/leftarrow.png" align="top"> + </span> + </div> + </form> + </div> + <div class="column"> + <h2>Export current pad as</h2> + <a id="exporthtmla" target="_blank" class="exportlink"><div class="exporttype" id="exporthtml">HTML</div></a> + <a id="exportplaina" target="_blank" class="exportlink"><div class="exporttype" id="exportplain">Plain text</div></a> + <a id="exportworda" target="_blank" class="exportlink"><div class="exporttype" id="exportword">Microsoft Word</div></a> + <a id="exportpdfa" target="_blank" class="exportlink"><div class="exporttype" id="exportpdf">PDF</div></a> + <a id="exportopena" target="_blank" class="exportlink"><div class="exporttype" id="exportopen">OpenDocument</div></a> + <a id="exportdokuwikia" target="_blank" class="exportlink"><div class="exporttype" id="exportdokuwiki">DokuWiki text</div></a> + <a id="exportwordlea" target="_blank" onClick="padimpexp.export2Wordle();return false;" class="exportlink"><div class="exporttype" id="exportwordle">Wordle</div></a> + </div> + </div> + + <div id="embed" class="popup"> + <div id="embedreadonly" class="right"> + <input type="checkbox" id="readonlyinput" onClick="padeditbar.setEmbedLinks();"> + <label for="readonlyinput">Read only</label> + </div> + <h1>Share this pad</h1> + <div id="linkcode"> + <h2>Link</h2> + <input id="linkinput" type="text" value=""> + </div> + <br> + <div id="embedcode"> + <h2>Embed URL</h2> + <input id="embedinput" type="text" value=""> + </div> + <br> + <div id="qrcode"> + <h2>QR code</h2> + <div id="qr_center"><img id="embedreadonlyqr"></div> + </div> + </div> + + <div id="chatthrob"></div> + + <div id="chaticon" title="Open the chat for this pad" onclick="chat.show();return false;"> + <span id="chatlabel">Chat</span> + <span class="buttonicon buttonicon-chat"></span> + <span id="chatcounter">0</span> + </div> + + <div id="chatbox"> + <div id="titlebar"><span id ="titlelabel">Chat</span><a id="titlecross" onClick="chat.hide();return false;">- </a></div> + <div id="chattext" class="authorColors"></div> + <div id="chatinputbox"> + <form> + <input id="chatinput" type="text" maxlength="140"> + </form> + </div> + </div> + + <div id="focusprotector"> </div> + + <div id="modaloverlay"> + <div id="modaloverlay-inner"></div> + </div> + + <div id="mainmodals"> + <% e.begin_block("modals"); %> + <div id="connectionbox" class="modaldialog"> + <div id="connectionboxinner" class="modaldialog-inner"> + <div class="connecting">Connecting...</div> + <div class="reconnecting">Reestablishing connection...</div> + <div class="disconnected"> + <h2 class="h2_disconnect">Disconnected.</h2> + <h2 class="h2_userdup">Opened in another window.</h2> + <h2 class="h2_unauth">No Authorization.</h2> + <div id="disconnected_looping"> + <p><b>We're having trouble talking to the EtherPad lite synchronization server.</b> You may be connecting through an incompatible firewall or proxy server.</p> + </div> + <div id="disconnected_initsocketfail"> + <p><b>We were unable to connect to the EtherPad lite synchronization server.</b> This may be due to an incompatibility with your web browser or internet connection.</p> + </div> + <div id="disconnected_userdup"> + <p><b>You seem to have opened this pad in another browser window.</b> If you'd like to use this window instead, you can reconnect.</p> + </div> + <div id="disconnected_unknown"> + <p><b>Lost connection with the EtherPad lite synchronization server.</b> This may be due to a loss of network connectivity.</p> + </div> + <div id="disconnected_slowcommit"> + <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p> + </div> + <div id="disconnected_unauth"> + <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p> + </div> + <div id="disconnected_deleted"> + <p>This pad was deleted.</p> + </div> + <div id="reconnect_advise"> + <p>If this continues to happen, please let us know</p> + </div> + <div id="reconnect_form"> + <button id="forcereconnect">Reconnect Now</button> + </div> + </div> + </div> + <form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;"> + <input type="hidden" class="padId" name="padId"> + <input type="hidden" class="diagnosticInfo" name="diagnosticInfo"> + <input type="hidden" class="missedChanges" name="missedChanges"> + </form> + </div> + <% e.end_block(); %> + </div> + + <% e.begin_block("scripts"); %> + <script type="text/javascript" src="../static/js/require-kernel.js"></script> + <script type="text/javascript" src="../static/js/jquery.js"></script> + <script type="text/javascript" src="../socket.io/socket.io.js"></script> + <% if (settings.minify) { %> + <script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define"></script> + <% } %> + <script type="text/javascript"> + var clientVars = {}; + (function () { + <% if (settings.minify) { %> + require.setRootURI("../javascripts/src"); + require.setLibraryURI("../javascripts/lib"); + require.setGlobalKeyPath("require"); + <% } else { %> + require.setRootURI("../static/js"); + require.setLibraryURI("../static/plugins"); + <% } %> + + var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + plugins.update(function () { + require('ep_etherpad-lite/static/js/pad').init(); + }); + + /* TODO: These globals shouldn't exist. */ + pad = require('ep_etherpad-lite/static/js/pad').pad; + chat = require('ep_etherpad-lite/static/js/chat').chat; + padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar; + padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; + }()); + </script> + <% e.end_block(); %> +</html> diff --git a/src/static/timeslider.html b/src/templates/timeslider.html index 3c909ede..12867b7a 100644 --- a/src/static/timeslider.html +++ b/src/templates/timeslider.html @@ -63,7 +63,7 @@ </div> <div class="timeslider-bar"> - <div class="editbarright" id="editbar"> + <div class="editbarright toolbar" id="editbar"> <ul> <li onClick="window.padeditbar.toolbarClick('import_export');return false;"> <a id="exportlink" title="Export to different document formats"> |