diff options
author | Stefan <mu.stefan@googlemail.com> | 2015-04-11 12:10:37 +0200 |
---|---|---|
committer | Stefan <mu.stefan@googlemail.com> | 2015-04-11 12:10:37 +0200 |
commit | aa0d14c7d71da3d3c4f123e1577848c026bccf0b (patch) | |
tree | 19cc760c2aa04ab57eb23e500dc442c0a901a7d1 /src/node | |
parent | 573a912e4f1b481fca8f3c8146972e78f76278e2 (diff) | |
parent | cc34f4e325830f798321b8152095c4dccd6b465f (diff) | |
download | etherpad-lite-aa0d14c7d71da3d3c4f123e1577848c026bccf0b.zip |
Merge branch 'master' of git://github.com/ether/etherpad-lite into create_pad_special_characters
Diffstat (limited to 'src/node')
38 files changed, 1229 insertions, 686 deletions
diff --git a/src/node/db/API.js b/src/node/db/API.js index 79f5fbeb..97d5162d 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -263,7 +263,7 @@ exports.getText = function(padID, rev, callback) { if(ERR(err, callback)) return; - data = {text: atext.text}; + var data = {text: atext.text}; callback(null, data); }) @@ -368,7 +368,7 @@ exports.getHTML = function(padID, rev, callback) if(ERR(err, callback)) return; html = "<!DOCTYPE HTML><html><body>" +html; // adds HTML head html += "</body></html>"; - data = {html: html}; + var data = {html: html}; callback(null, data); }); } @@ -380,7 +380,7 @@ exports.getHTML = function(padID, rev, callback) if(ERR(err, callback)) return; html = "<!DOCTYPE HTML><html><body>" +html; // adds HTML head html += "</body></html>"; - data = {html: html}; + var data = {html: html}; callback(null, data); }); } @@ -410,11 +410,16 @@ exports.setHTML = function(padID, html, callback) if(ERR(err, callback)) return; // add a new changeset with the new html to the pad - importHtml.setPadHTML(pad, cleanText(html), callback); - - //update the clients on the pad - padMessageHandler.updatePadClients(pad, callback); - + importHtml.setPadHTML(pad, cleanText(html), function(e){ + if(e){ + callback(new customError("HTML is malformed","apierror")); + return; + }else{ + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + return; + } + }); }); } @@ -427,8 +432,8 @@ getChatHistory(padId, start, end), returns a part of or the whole chat-history o Example returns: -{"code":0,"message":"ok","data":{"messages":[{"text":"foo","userId":"a.foo","time":1359199533759,"userName":"test"}, - {"text":"bar","userId":"a.foo","time":1359199534622,"userName":"test"}]}} +{"code":0,"message":"ok","data":{"messages":[{"text":"foo","authorID":"a.foo","time":1359199533759,"userName":"test"}, + {"text":"bar","authorID":"a.foo","time":1359199534622,"userName":"test"}]}} {code: 1, message:"start is higher or equal to the current chatHead", data: null} @@ -489,6 +494,33 @@ exports.getChatHistory = function(padID, start, end, callback) }); } +/** +appendChatMessage(padID, text, authorID, time), creates a chat message for the pad id, time is a timestamp + +Example returns: + +{code: 0, message:"ok", data: null +{code: 1, message:"padID does not exist", data: null} +*/ +exports.appendChatMessage = function(padID, text, authorID, time, callback) +{ + //text is required + if(typeof text != "string") + { + callback(new customError("text is no string","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.appendChatMessage(text, authorID, parseInt(time)); + callback(); + }); +} + /*****************/ /**PAD FUNCTIONS */ /*****************/ @@ -513,6 +545,117 @@ exports.getRevisionsCount = function(padID, callback) } /** +getSavedRevisionsCount(padID) returns the number of saved revisions of this pad + +Example returns: + +{code: 0, message:"ok", data: {savedRevisions: 42}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getSavedRevisionsCount = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + callback(null, {savedRevisions: pad.getSavedRevisionsNumber()}); + }); +} + +/** +listSavedRevisions(padID) returns the list of saved revisions of this pad + +Example returns: + +{code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.listSavedRevisions = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + callback(null, {savedRevisions: pad.getSavedRevisionsList()}); + }); +} + +/** +saveRevision(padID) returns the list of saved revisions of this pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.saveRevision = function(padID, rev, callback) +{ + //check if rev is set + if(typeof rev == "function") + { + callback = rev; + rev = undefined; + } + + //check if rev is a number + if(rev !== undefined && typeof rev != "number") + { + //try to parse the number + if(!isNaN(parseInt(rev))) + { + rev = parseInt(rev); + } + else + { + callback(new customError("rev is not a number", "apierror")); + return; + } + } + + //ensure this is not a negativ number + if(rev !== undefined && rev < 0) + { + callback(new customError("rev is a negativ number","apierror")); + return; + } + + //ensure this is not a float value + if(rev !== undefined && !is_int(rev)) + { + callback(new customError("rev is a float value","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + //the client asked for a special revision + if(rev !== undefined) + { + //check if this is a valid revision + if(rev > pad.getHeadRevisionNumber()) + { + callback(new customError("rev is higher than the head revision of the pad","apierror")); + return; + } + } else { + rev = pad.getHeadRevisionNumber(); + } + + authorManager.createAuthor('API', function(err, author) { + if(ERR(err, callback)) return; + + pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); + callback(); + }); + }); +} + +/** getLastEdited(padID) returns the timestamp of the last revision of the pad Example returns: @@ -584,6 +727,117 @@ exports.deletePad = function(padID, callback) pad.remove(callback); }); } +/** + restoreRevision(padID, [rev]) Restores revision from past as new changeset + + Example returns: + + {code:0, message:"ok", data:null} + {code: 1, message:"padID does not exist", data: null} + */ +exports.restoreRevision = function (padID, rev, callback) +{ + var Changeset = require("ep_etherpad-lite/static/js/Changeset"); + var padMessage = require("ep_etherpad-lite/node/handler/PadMessageHandler.js"); + + //check if rev is a number + if (rev !== undefined && typeof rev != "number") + { + //try to parse the number + if (!isNaN(parseInt(rev))) + { + rev = parseInt(rev); + } + else + { + callback(new customError("rev is not a number", "apierror")); + return; + } + } + + //ensure this is not a negativ number + if (rev !== undefined && rev < 0) + { + callback(new customError("rev is a negativ number", "apierror")); + return; + } + + //ensure this is not a float value + if (rev !== undefined && !is_int(rev)) + { + callback(new customError("rev is a float value", "apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function (err, pad) + { + if (ERR(err, callback)) return; + + + //check if this is a valid revision + if (rev > pad.getHeadRevisionNumber()) + { + callback(new customError("rev is higher than the head revision of the pad", "apierror")); + return; + } + + pad.getInternalRevisionAText(rev, function (err, atext) + { + if (ERR(err, callback)) return; + + var oldText = pad.text(); + atext.text += "\n"; + function eachAttribRun(attribs, func) + { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = 0; + var newTextEnd = atext.text.length; + while (attribsIter.hasNext()) + { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) + { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; + } + } + + // create a new changeset with a helper builder object + var builder = Changeset.builder(oldText.length); + + // assemble each line into the builder + eachAttribRun(atext.attribs, function (start, end, attribs) + { + builder.insert(atext.text.substring(start, end), attribs); + }); + + var lastNewlinePos = oldText.lastIndexOf('\n'); + if (lastNewlinePos < 0) + { + builder.remove(oldText.length - 1, 0); + } else + { + builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); + builder.remove(oldText.length - lastNewlinePos - 1, 0); + } + + var changeset = builder.toString(); + + //append the changeset + pad.appendRevision(changeset); + // + padMessage.updatePadClients(pad, function () + { + }); + callback(null, null); + }); + + }); +}; /** copyPad(sourceID, destinationID[, force=false]) copies a pad. If force is true, diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 5ba608e9..e0f569ef 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -21,7 +21,6 @@ var ERR = require("async-stacktrace"); var db = require("./DB").db; -var async = require("async"); var customError = require("../utils/customError"); var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 4670696a..53847600 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -54,6 +54,21 @@ Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() { return this.head; }; +Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() { + return this.savedRevisions.length; +}; + +Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() { + var savedRev = new Array(); + for(var rev in this.savedRevisions){ + savedRev.push(this.savedRevisions[rev].revNum); + } + savedRev.sort(function(a, b) { + return a - b; + }); + return savedRev; +}; + Pad.prototype.getPublicStatus = function getPublicStatus() { return this.publicStatus; }; @@ -135,7 +150,7 @@ Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) { Pad.prototype.getAllAuthors = function getAllAuthors() { var authors = []; - for(key in this.pool.numToAttrib) + for(var key in this.pool.numToAttrib) { if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "") { @@ -461,7 +476,6 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { // if the pad exists, we should abort, unless forced. function(callback) { - console.log("destinationID", destinationID, force); padManager.doesPadExists(destinationID, function (err, exists) { if(ERR(err, callback)) return; @@ -470,9 +484,9 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { { if (!force) { - console.log("erroring out without force"); + console.error("erroring out without force"); callback(new customError("destinationID already exists","apierror")); - console.log("erroring out without force - after"); + console.error("erroring out without force - after"); return; } else // exists and forcing @@ -521,12 +535,9 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { function(callback) { var revHead = _this.head; - //console.log(revHead); for(var i=0;i<=revHead;i++) { db.get("pad:"+sourceID+":revs:"+i, function (err, rev) { - //console.log("HERE"); - if (ERR(err, callback)) return; db.set("pad:"+destinationID+":revs:"+i, rev); }); @@ -538,10 +549,8 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { function(callback) { var authorIDs = _this.getAllAuthors(); - authorIDs.forEach(function (authorID) { - console.log("authors"); authorManager.addPad(authorID, destinationID); }); @@ -555,7 +564,9 @@ Pad.prototype.copy = function copy(destinationID, force, callback) { if(destGroupID) db.setSub("group:" + destGroupID, ["pads", destinationID], 1); // Initialize the new pad (will update the listAllPads cache) - padManager.getPad(destinationID, null, callback) + setTimeout(function(){ + padManager.getPad(destinationID, null, callback) // this runs too early. + },10); } // series ], function(err) @@ -690,7 +701,7 @@ Pad.prototype.isPasswordProtected = function isPasswordProtected() { 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){ + if(this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum){ return; } } diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index df3c3826..6fae57ff 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -20,7 +20,6 @@ var ERR = require("async-stacktrace"); -var db = require("./DB").db; var async = require("async"); var authorManager = require("./AuthorManager"); var padManager = require("./PadManager"); diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 71315adc..f8000e47 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -351,7 +351,15 @@ function listSessionsWithDBKey (dbkey, callback) { exports.getSessionInfo(sessionID, function(err, sessionInfo) { - if(ERR(err, callback)) return; + if (err == "apierror: sessionID does not exist") + { + console.warn("Found bad session " + sessionID + " in " + dbkey + "."); + } + else if(ERR(err, callback)) + { + return; + } + sessions[sessionID] = sessionInfo; callback(); }); diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 52a504f1..5c45ddb3 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -5,8 +5,6 @@ */ var Store = require('ep_etherpad-lite/node_modules/connect/lib/middleware/session/store'), - utils = require('ep_etherpad-lite/node_modules/connect/lib/utils'), - Session = require('ep_etherpad-lite/node_modules/connect/lib/middleware/session/session'), db = require('ep_etherpad-lite/node/db/DB').db, log4js = require('ep_etherpad-lite/node_modules/log4js'), messageLogger = log4js.getLogger("SessionStore"); diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js index 48185d80..30f5a442 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.js @@ -71,7 +71,7 @@ exports.begin_define_block = function (name) { } exports.end_define_block = function () { - content = exports.end_capture(); + var content = exports.end_capture(); return content; } diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 273a58a6..b4d24201 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -345,10 +345,109 @@ var version = , "getChatHistory" : ["padID", "start", "end"] , "getChatHead" : ["padID"] } +, "1.2.11": + { "createGroup" : [] + , "createGroupIfNotExistsFor" : ["groupMapper"] + , "deleteGroup" : ["groupID"] + , "listPads" : ["groupID"] + , "listAllPads" : [] + , "createDiffHTML" : ["padID", "startRev", "endRev"] + , "createPad" : ["padID", "text"] + , "createGroupPad" : ["groupID", "padName", "text"] + , "createAuthor" : ["name"] + , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] + , "listPadsOfAuthor" : ["authorID"] + , "createSession" : ["groupID", "authorID", "validUntil"] + , "deleteSession" : ["sessionID"] + , "getSessionInfo" : ["sessionID"] + , "listSessionsOfGroup" : ["groupID"] + , "listSessionsOfAuthor" : ["authorID"] + , "getText" : ["padID", "rev"] + , "setText" : ["padID", "text"] + , "getHTML" : ["padID", "rev"] + , "setHTML" : ["padID", "html"] + , "getAttributePool" : ["padID"] + , "getRevisionsCount" : ["padID"] + , "getSavedRevisionsCount" : ["padID"] + , "listSavedRevisions" : ["padID"] + , "saveRevision" : ["padID", "rev"] + , "getRevisionChangeset" : ["padID", "rev"] + , "getLastEdited" : ["padID"] + , "deletePad" : ["padID"] + , "copyPad" : ["sourceID", "destinationID", "force"] + , "movePad" : ["sourceID", "destinationID", "force"] + , "getReadOnlyID" : ["padID"] + , "getPadID" : ["roID"] + , "setPublicStatus" : ["padID", "publicStatus"] + , "getPublicStatus" : ["padID"] + , "setPassword" : ["padID", "password"] + , "isPasswordProtected" : ["padID"] + , "listAuthorsOfPad" : ["padID"] + , "padUsersCount" : ["padID"] + , "getAuthorName" : ["authorID"] + , "padUsers" : ["padID"] + , "sendClientsMessage" : ["padID", "msg"] + , "listAllGroups" : [] + , "checkToken" : [] + , "getChatHistory" : ["padID"] + , "getChatHistory" : ["padID", "start", "end"] + , "getChatHead" : ["padID"] + , "restoreRevision" : ["padID", "rev"] + } +, "1.2.12": + { "createGroup" : [] + , "createGroupIfNotExistsFor" : ["groupMapper"] + , "deleteGroup" : ["groupID"] + , "listPads" : ["groupID"] + , "listAllPads" : [] + , "createDiffHTML" : ["padID", "startRev", "endRev"] + , "createPad" : ["padID", "text"] + , "createGroupPad" : ["groupID", "padName", "text"] + , "createAuthor" : ["name"] + , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] + , "listPadsOfAuthor" : ["authorID"] + , "createSession" : ["groupID", "authorID", "validUntil"] + , "deleteSession" : ["sessionID"] + , "getSessionInfo" : ["sessionID"] + , "listSessionsOfGroup" : ["groupID"] + , "listSessionsOfAuthor" : ["authorID"] + , "getText" : ["padID", "rev"] + , "setText" : ["padID", "text"] + , "getHTML" : ["padID", "rev"] + , "setHTML" : ["padID", "html"] + , "getAttributePool" : ["padID"] + , "getRevisionsCount" : ["padID"] + , "getSavedRevisionsCount" : ["padID"] + , "listSavedRevisions" : ["padID"] + , "saveRevision" : ["padID", "rev"] + , "getRevisionChangeset" : ["padID", "rev"] + , "getLastEdited" : ["padID"] + , "deletePad" : ["padID"] + , "copyPad" : ["sourceID", "destinationID", "force"] + , "movePad" : ["sourceID", "destinationID", "force"] + , "getReadOnlyID" : ["padID"] + , "getPadID" : ["roID"] + , "setPublicStatus" : ["padID", "publicStatus"] + , "getPublicStatus" : ["padID"] + , "setPassword" : ["padID", "password"] + , "isPasswordProtected" : ["padID"] + , "listAuthorsOfPad" : ["padID"] + , "padUsersCount" : ["padID"] + , "getAuthorName" : ["authorID"] + , "padUsers" : ["padID"] + , "sendClientsMessage" : ["padID", "msg"] + , "listAllGroups" : [] + , "checkToken" : [] + , "appendChatMessage" : ["padID", "text", "authorID", "time"] + , "getChatHistory" : ["padID"] + , "getChatHistory" : ["padID", "start", "end"] + , "getChatHead" : ["padID"] + , "restoreRevision" : ["padID", "rev"] + } }; // set the latest available API version here -exports.latestApiVersion = '1.2.10'; +exports.latestApiVersion = '1.2.12'; // exports the versions so it can be used by the new Swagger endpoint exports.version = version; @@ -404,6 +503,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res) if(fields["apikey"] != apikey.trim()) { + res.statusCode = 401; res.send({code: 4, message: "no or wrong API Key", data: null}); return; } diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 5bedcce2..0654deb4 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -4,6 +4,7 @@ /* * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * 2014 John McLear (Etherpad Foundation / McLear Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +22,7 @@ var ERR = require("async-stacktrace"); var exporthtml = require("../utils/ExportHtml"); var exporttxt = require("../utils/ExportTxt"); -var exportdokuwiki = require("../utils/ExportDokuWiki"); -var padManager = require("../db/PadManager"); +var exportEtherpad = require("../utils/ExportEtherpad"); var async = require("async"); var fs = require("fs"); var settings = require('../utils/Settings'); @@ -54,14 +54,20 @@ exports.doExport = function(req, res, padId, type) // if fileName is set then set it to the padId, note that fileName is returned as an array. if(hookFileName.length) fileName = hookFileName; - //tell the browser that this is a downloadable file res.attachment(fileName + "." + type); //if this is a plain text export, we can do this directly // We have to over engineer this because tabs are stored as attributes and not plain text - - if(type == "txt") + if(type == "etherpad"){ + exportEtherpad.getPadRaw(padId, function(err, pad){ + if(!err){ + res.send(pad); + // return; + } + }); + } + else if(type == "txt") { var txt; var randNum; @@ -129,26 +135,6 @@ exports.doExport = function(req, res, padId, type) if(err && err != "stop") ERR(err); }) } - else if(type == 'dokuwiki') - { - var randNum; - var srcFile, destFile; - - async.series([ - //render the dokuwiki document - function(callback) - { - exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki) - { - res.send(dokuwiki); - callback("stop"); - }); - }, - ], function(err) - { - if(err && err != "stop") throw err; - }); - } else { var html; @@ -172,8 +158,12 @@ exports.doExport = function(req, res, padId, type) //if this is a html export, we can send this from here directly if(type == "html") { - res.send(html); - callback("stop"); + // do any final changes the plugin might want to make cake + hooks.aCallFirst("exportHTMLSend", html, function(err, newHTML){ + if(newHTML.length) html = newHTML; + res.send(html); + callback("stop"); + }); } else //write the html export to a file { diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 60fa5ffb..2dad8b3d 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -5,6 +5,7 @@ /* * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * 2012 Iván Eixarch + * 2014 John McLear (Etherpad Foundation / McLear Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +30,7 @@ var ERR = require("async-stacktrace") , formidable = require('formidable') , os = require("os") , importHtml = require("../utils/ImportHtml") + , importEtherpad = require("../utils/ImportEtherpad") , log4js = require("log4js") , hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); @@ -53,7 +55,8 @@ exports.doImport = function(req, res, padId) var srcFile, destFile , pad , text - , importHandledByPlugin; + , importHandledByPlugin + , directDatabaseAccess; var randNum = Math.floor(Math.random()*0xFFFFFFFF); @@ -83,7 +86,7 @@ exports.doImport = function(req, res, padId) //this allows us to accept source code files like .c or .java function(callback) { var fileEnding = path.extname(srcFile).toLowerCase() - , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm"] + , knownFileEndings = [".txt", ".doc", ".docx", ".pdf", ".odt", ".html", ".htm", ".etherpad"] , fileEndingKnown = (knownFileEndings.indexOf(fileEnding) > -1); //if the file ending is known, continue as normal @@ -92,9 +95,14 @@ exports.doImport = function(req, res, padId) } //we need to rename this file with a .txt ending else { - var oldSrcFile = srcFile; - srcFile = path.join(path.dirname(srcFile),path.basename(srcFile, fileEnding)+".txt"); - fs.rename(oldSrcFile, srcFile, callback); + if(settings.allowUnknownFileEnds === true){ + var oldSrcFile = srcFile; + srcFile = path.join(path.dirname(srcFile),path.basename(srcFile, fileEnding)+".txt"); + fs.rename(oldSrcFile, srcFile, callback); + }else{ + console.warn("Not allowing unknown file type to be imported", fileEnding); + callback("uploadFailed"); + } } }, function(callback){ @@ -111,11 +119,38 @@ exports.doImport = function(req, res, padId) } }); }, + function(callback) { + var fileEnding = path.extname(srcFile).toLowerCase() + var fileIsEtherpad = (fileEnding === ".etherpad"); + + if(fileIsEtherpad){ + // we do this here so we can see if the pad has quit ea few edits + padManager.getPad(padId, function(err, _pad){ + var headCount = _pad.head; + if(headCount >= 10){ + apiLogger.warn("Direct database Import attempt of a pad that already has content, we wont be doing this") + return callback("padHasData"); + }else{ + fs.readFile(srcFile, "utf8", function(err, _text){ + directDatabaseAccess = true; + importEtherpad.setPadRaw(padId, _text, function(err){ + callback(); + }); + }); + } + }); + }else{ + callback(); + } + }, //convert file to html function(callback) { - if(!importHandledByPlugin){ + if(!importHandledByPlugin || !directDatabaseAccess){ var fileEnding = path.extname(srcFile).toLowerCase(); var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm"); + var fileIsTXT = (fileEnding === ".txt"); + if (fileIsTXT) abiword = false; // Don't use abiword for text files + // See https://github.com/ether/etherpad-lite/issues/2572 if (abiword && !fileIsHTML) { abiword.convertFile(srcFile, destFile, "htm", function(err) { //catch convert errors @@ -136,24 +171,28 @@ exports.doImport = function(req, res, padId) }, function(callback) { - if (!abiword) { - // Read the file with no encoding for raw buffer access. - fs.readFile(destFile, function(err, buf) { - if (err) throw err; - var isAscii = true; - // Check if there are only ascii chars in the uploaded file - for (var i=0, len=buf.length; i<len; i++) { - if (buf[i] > 240) { - isAscii=false; - break; + if (!abiword){ + if(!directDatabaseAccess) { + // Read the file with no encoding for raw buffer access. + fs.readFile(destFile, function(err, buf) { + if (err) throw err; + var isAscii = true; + // Check if there are only ascii chars in the uploaded file + for (var i=0, len=buf.length; i<len; i++) { + if (buf[i] > 240) { + isAscii=false; + break; + } } - } - if (isAscii) { - callback(); - } else { - callback("uploadFailed"); - } - }); + if (isAscii) { + callback(); + } else { + callback("uploadFailed"); + } + }); + }else{ + callback(); + } } else { callback(); } @@ -170,66 +209,101 @@ exports.doImport = function(req, res, padId) //read the text function(callback) { - fs.readFile(destFile, "utf8", function(err, _text){ - if(ERR(err, callback)) return; - text = _text; - // Title needs to be stripped out else it appends it to the pad.. - text = text.replace("<title>", "<!-- <title>"); - text = text.replace("</title>","</title>-->"); + if(!directDatabaseAccess){ + fs.readFile(destFile, "utf8", function(err, _text){ + if(ERR(err, callback)) return; + text = _text; + // Title needs to be stripped out else it appends it to the pad.. + text = text.replace("<title>", "<!-- <title>"); + text = text.replace("</title>","</title>-->"); - //node on windows has a delay on releasing of the file lock. - //We add a 100ms delay to work around this - if(os.type().indexOf("Windows") > -1){ - setTimeout(function() {callback();}, 100); - } else { - callback(); - } - }); + //node on windows has a delay on releasing of the file lock. + //We add a 100ms delay to work around this + if(os.type().indexOf("Windows") > -1){ + setTimeout(function() {callback();}, 100); + } else { + callback(); + } + }); + }else{ + callback(); + } }, //change text of the pad and broadcast the changeset function(callback) { - var fileEnding = path.extname(srcFile).toLowerCase(); - if (abiword || fileEnding == ".htm" || fileEnding == ".html") { - try{ - importHtml.setPadHTML(pad, text); - }catch(e){ - apiLogger.warn("Error importing, possibly caused by malformed HTML"); + if(!directDatabaseAccess){ + var fileEnding = path.extname(srcFile).toLowerCase(); + if (abiword || fileEnding == ".htm" || fileEnding == ".html") { + importHtml.setPadHTML(pad, text, function(e){ + if(e) apiLogger.warn("Error importing, possibly caused by malformed HTML"); + }); + } else { + pad.setText(text); } - } else { - pad.setText(text); } - padMessageHandler.updatePadClients(pad, callback); + + // Load the Pad into memory then brodcast updates to all clients + padManager.unloadPad(padId); + padManager.getPad(padId, function(err, _pad){ + var pad = _pad; + padManager.unloadPad(padId); + // direct Database Access means a pad user should perform a switchToPad + // and not attempt to recieve updated pad data.. + if(!directDatabaseAccess){ + padMessageHandler.updatePadClients(pad, function(){ + callback(); + }); + }else{ + callback(); + } + }); + }, //clean up temporary files function(callback) { - //for node < 0.7 compatible - var fileExists = fs.exists || path.exists; - async.parallel([ - function(callback){ - fileExists (srcFile, function(exist) { (exist)? fs.unlink(srcFile, callback): callback(); }); - }, - function(callback){ - fileExists (destFile, function(exist) { (exist)? fs.unlink(destFile, callback): callback(); }); - } - ], callback); + if(!directDatabaseAccess){ + //for node < 0.7 compatible + var fileExists = fs.exists || path.exists; + async.parallel([ + function(callback){ + fileExists (srcFile, function(exist) { (exist)? fs.unlink(srcFile, callback): callback(); }); + }, + function(callback){ + fileExists (destFile, function(exist) { (exist)? fs.unlink(destFile, callback): callback(); }); + } + ], callback); + }else{ + callback(); + } } ], function(err) { - var status = "ok"; //check for known errors and replace the status - if(err == "uploadFailed" || err == "convertFailed") + if(err == "uploadFailed" || err == "convertFailed" || err == "padHasData") { status = err; err = null; } ERR(err); - + //close the connection - res.send("<head><script type='text/javascript' src='../../static/js/jquery.js'></script><script type='text/javascript' src='../../static/js/jquery_browser.js'></script></head><script>$(window).load(function(){if ( (!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf(\"1.8.\") == 0)) ){document.domain = document.domain;}var impexp = window.parent.padimpexp.handleFrameCall('" + status + "');})</script>", 200); + res.send( + "<head> \ + <script type='text/javascript' src='../../static/js/jquery.js'></script> \ + </head> \ + <script> \ + $(window).load(function(){ \ + if(navigator.userAgent.indexOf('MSIE') === -1){ \ + document.domain = document.domain; \ + } \ + var impexp = window.parent.padimpexp.handleFrameCall('" + directDatabaseAccess +"', '" + status + "'); \ + }) \ + </script>" + , 200); }); } diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e1ac994e..c210ab2b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -37,6 +37,7 @@ var _ = require('underscore'); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var channels = require("channels"); var stats = require('../stats'); +var remoteAddress = require("../utils/RemoteAddress").remoteAddress; /** * A associative array that saves informations about a session @@ -93,8 +94,18 @@ exports.handleConnect = function(client) */ exports.kickSessionsFromPad = function(padID) { + if(typeof socketio.sockets['clients'] !== 'function') + return; + //skip if there is nobody on this pad - if(socketio.sockets.clients(padID).length == 0) + var roomClients = [], room = socketio.sockets.adapter.rooms[padID]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + + if(roomClients.length == 0) return; //disconnect everyone from this pad @@ -115,14 +126,16 @@ exports.handleDisconnect = function(client) //if this connection was already etablished with a handshake, send a disconnect message to the others if(session && session.author) { - client.get('remoteAddress', function(er, ip) { - //Anonymize the IP address if IP logging is disabled - if(settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } - accessLogger.info('[LEAVE] Pad "'+session.padId+'": Author "'+session.author+'" on client '+client.id+' with IP "'+ip+'" left the pad') - }) + // Get the IP address from our persistant object + var ip = remoteAddress[client.id]; + + // Anonymize the IP address if IP logging is disabled + if(settings.disableIPlogging) { + ip = 'ANONYMOUS'; + } + + accessLogger.info('[LEAVE] Pad "'+session.padId+'": Author "'+session.author+'" on client '+client.id+' with IP "'+ip+'" left the pad') //get the author color out of the db authorManager.getAuthorColorId(session.author, function(err, color) @@ -220,6 +233,8 @@ exports.handleMessage = function(client, message) } else { messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type); } + } else if(message.type == "SWITCH_TO_PAD") { + handleSwitchToPad(client, message); } else { messageLogger.warn("Dropped message, unknown Message Type " + message.type); } @@ -233,18 +248,7 @@ exports.handleMessage = function(client, message) { // client tried to auth for the first time (first msg from the client) if(message.type == "CLIENT_READY") { - // Remember this information since we won't - // have the cookie in further socket.io messages. - // This information will be used to check if - // the sessionId of this connection is still valid - // since it could have been deleted by the API. - sessioninfos[client.id].auth = - { - sessionID: message.sessionID, - padID: message.padId, - token : message.token, - password: message.password - }; + createSessionInfo(client, message); } // Note: message.sessionID is an entirely different kind of @@ -253,11 +257,10 @@ exports.handleMessage = function(client, message) // FIXME: Use a hook instead // FIXME: Allow to override readwrite access with readonly - // FIXME: A message might arrive but wont have an auth object, this is obviously bad so we should deny it // Simulate using the load testing tool if(!sessioninfos[client.id].auth){ console.error("Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.") - callback(); + return; }else{ var auth = sessioninfos[client.id].auth; var checkAccessCallback = function(err, statusObject) @@ -493,14 +496,19 @@ function handleSuggestUserName(client, message) return; } - var padId = sessioninfos[client.id].padId, - clients = socketio.sockets.clients(padId); + var padId = sessioninfos[client.id].padId; + var roomClients = [], room = socketio.sockets.adapter.rooms[padId]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } //search the author and send him this message - for(var i = 0; i < clients.length; i++) { - var session = sessioninfos[clients[i].id]; + for(var i = 0; i < roomClients.length; i++) { + var session = sessioninfos[roomClients[i].id]; if(session && session.author == message.data.payload.unnamedId) { - clients[i].json.send(message); + roomClients[i].json.send(message); break; } } @@ -648,12 +656,17 @@ function handleUserChanges(data, cb) , op while(iterator.hasNext()) { op = iterator.next() - if(op.opcode != '+') continue; + + //+ can add text with attribs + //= can change or add attribs + //- can have attribs, but they are discarded and don't show up in the attribs - but do show up in the pool + op.attribs.split('*').forEach(function(attr) { if(!attr) return attr = wireApool.getAttrib(attr) if(!attr) return - if('author' == attr[0] && attr[1] != thisSession.author) throw new Error("Trying to submit changes as another author in changeset "+changeset); + //the empty author is used in the clearAuthorship functionality so this should be the only exception + if('author' == attr[0] && (attr[1] != thisSession.author && attr[1] != '')) throw new Error("Trying to submit changes as another author in changeset "+changeset); }) } @@ -694,6 +707,14 @@ function handleUserChanges(data, cb) // and can be applied after "c". try { + // a changeset can be based on an old revision with the same changes in it + // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES + // of that revision + if(baseRev+1 == r && c == changeset) { + client.json.send({disconnect:"badChangeset"}); + stats.meter('failedChangesets').mark(); + return callback(new Error("Won't apply USER_CHANGES, because it contains an already accepted changeset")); + } changeset = Changeset.follow(c, changeset, false, apool); }catch(e){ client.json.send({disconnect:"badChangeset"}); @@ -724,7 +745,16 @@ function handleUserChanges(data, cb) return callback(new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length)); } - pad.appendRevision(changeset, thisSession.author); + try + { + pad.appendRevision(changeset, thisSession.author); + } + catch(e) + { + client.json.send({disconnect:"badChangeset"}); + stats.meter('failedChangesets').mark(); + return callback(e) + } var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { @@ -753,7 +783,13 @@ function handleUserChanges(data, cb) exports.updatePadClients = function(pad, callback) { //skip this step if noone is on this pad - var roomClients = socketio.sockets.clients(pad.id); + var roomClients = [], room = socketio.sockets.adapter.rooms[pad.id]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + if(roomClients.length==0) return callback(); @@ -766,10 +802,8 @@ exports.updatePadClients = function(pad, callback) var revCache = {}; //go trough all sessions on this pad - async.forEach(roomClients, function(client, callback) - { + async.forEach(roomClients, function(client, callback){ var sid = client.id; - //https://github.com/caolan/async#whilst //send them all new changesets async.whilst( @@ -816,10 +850,10 @@ exports.updatePadClients = function(pad, callback) client.json.send(wireMsg); } - - sessioninfos[sid].time = currentTime; - sessioninfos[sid].rev = r; - + if(sessioninfos[sid]){ + sessioninfos[sid].time = currentTime; + sessioninfos[sid].rev = r; + } callback(null); } ], callback); @@ -875,6 +909,48 @@ function _correctMarkersInPad(atext, apool) { return builder.toString(); } +function handleSwitchToPad(client, message) +{ + // clear the session and leave the room + var currentSession = sessioninfos[client.id]; + var padId = currentSession.padId; + var roomClients = [], room = socketio.sockets.adapter.rooms[padId]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + + for(var i = 0; i < roomClients.length; i++) { + var sinfo = sessioninfos[roomClients[i].id]; + if(sinfo && sinfo.author == currentSession.author) { + // fix user's counter, works on page refresh or if user closes browser window and then rejoins + sessioninfos[roomClients[i].id] = {}; + roomClients[i].leave(padId); + } + } + + // start up the new pad + createSessionInfo(client, message); + handleClientReady(client, message); +} + +function createSessionInfo(client, message) +{ + // Remember this information since we won't + // have the cookie in further socket.io messages. + // This information will be used to check if + // the sessionId of this connection is still valid + // since it could have been deleted by the API. + sessioninfos[client.id].auth = + { + sessionID: message.sessionID, + padID: message.padId, + token : message.token, + password: message.password + }; +} + /** * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad @@ -998,6 +1074,11 @@ function handleClientReady(client, message) { authorManager.getAuthor(authorId, function(err, author) { + if(!author && !err) + { + messageLogger.error("There is no author for authorId:", authorId); + return callback(); + } if(ERR(err, callback)) return; historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients) callback(); @@ -1015,7 +1096,13 @@ function handleClientReady(client, message) return callback(); //Check if this author is already on the pad, if yes, kick the other sessions! - var roomClients = socketio.sockets.clients(padIds.padId); + var roomClients = [], room = socketio.sockets.adapter.rooms[pad.id]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + for(var i = 0; i < roomClients.length; i++) { var sinfo = sessioninfos[roomClients[i].id]; if(sinfo && sinfo.author == author) { @@ -1032,19 +1119,19 @@ function handleClientReady(client, message) sessioninfos[client.id].readonly = padIds.readonly; //Log creation/(re-)entering of a pad - client.get('remoteAddress', function(er, ip) { - //Anonymize the IP address if IP logging is disabled - if(settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } + var ip = remoteAddress[client.id]; - if(pad.head > 0) { - accessLogger.info('[ENTER] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" entered the pad'); - } - else if(pad.head == 0) { - accessLogger.info('[CREATE] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" created the pad'); - } - }) + //Anonymize the IP address if IP logging is disabled + if(settings.disableIPlogging) { + ip = 'ANONYMOUS'; + } + + if(pad.head > 0) { + accessLogger.info('[ENTER] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" entered the pad'); + } + else if(pad.head == 0) { + accessLogger.info('[CREATE] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" created the pad'); + } //If this is a reconnect, we don't have to send the client the ClientVars again if(message.reconnect == true) @@ -1165,7 +1252,14 @@ function handleClientReady(client, message) client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); //Run trough all sessions of this pad - async.forEach(socketio.sockets.clients(padIds.padId), function(roomClient, callback) + var roomClients = [], room = socketio.sockets.adapter.rooms[pad.id]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + + async.forEach(roomClients, function(roomClient, callback) { var author; @@ -1540,10 +1634,15 @@ function composePadChangesets(padId, startNum, endNum, callback) changeset = changesets[startNum]; var pool = pad.apool(); - for(var r=startNum+1;r<endNum;r++) - { - var cs = changesets[r]; - changeset = Changeset.compose(changeset, cs, pool); + try { + for(var r=startNum+1;r<endNum;r++) { + var cs = changesets[r]; + changeset = Changeset.compose(changeset, cs, pool); + } + } catch(e){ + // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 + console.warn("failed to compose cs in pad:",padId," startrev:",startNum," current rev:",r); + return callback(e); } callback(null); @@ -1561,8 +1660,16 @@ function composePadChangesets(padId, startNum, endNum, callback) * Get the number of users in a pad */ exports.padUsersCount = function (padID, callback) { + + var roomClients = [], room = socketio.sockets.adapter.rooms[padID]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + callback(null, { - padUsersCount: socketio.sockets.clients(padID).length + padUsersCount: roomClients.length }); } @@ -1572,7 +1679,14 @@ exports.padUsersCount = function (padID, callback) { exports.padUsers = function (padID, callback) { var result = []; - async.forEach(socketio.sockets.clients(padID), function(roomClient, callback) { + var roomClients = [], room = socketio.sockets.adapter.rooms[padID]; + if (room) { + for (var id in room) { + roomClients.push(socketio.sockets.adapter.nsp.connected[id]); + } + } + + async.forEach(roomClients, function(roomClient, callback) { var s = sessioninfos[roomClient.id]; if(s) { authorManager.getAuthor(s.author, function(err, author) { diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index b3e046d2..0a7361f4 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -24,6 +24,7 @@ var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); var readOnlyManager = require("../db/ReadOnlyManager"); +var remoteAddress = require("../utils/RemoteAddress").remoteAddress; var settings = require('../utils/Settings'); /** @@ -56,11 +57,15 @@ exports.setSocketIO = function(_socket) { socket.sockets.on('connection', function(client) { + + // Broken: See http://stackoverflow.com/questions/4647348/send-message-to-specific-client-with-socket-io-and-node-js + // Fixed by having a persistant object, ideally this would actually be in the database layer + // TODO move to database layer if(settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined){ - client.set('remoteAddress', client.handshake.headers['x-forwarded-for']); + remoteAddress[client.id] = client.handshake.headers['x-forwarded-for']; } else{ - client.set('remoteAddress', client.handshake.address.address); + remoteAddress[client.id] = client.handshake.address; } var clientAuthorized = false; diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index c6573c80..bf849419 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -10,24 +10,11 @@ var server; var serverName; exports.createServer = function () { - //try to get the git version - var version = ""; - try - { - var rootPath = path.resolve(npm.dir, '..'); - var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8"); - var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n")); - version = fs.readFileSync(refPath, "utf-8"); - version = version.substring(0, 7); - console.log("Your Etherpad git version is " + version); - } - catch(e) - { - console.warn("Can't get git version for server header\n" + e.message) - } console.log("Report bugs at https://github.com/ether/etherpad-lite/issues") - serverName = "Etherpad " + version + " (http://etherpad.org)"; + serverName = "Etherpad " + settings.getGitCommit() + " (http://etherpad.org)"; + + console.log("Your Etherpad version is " + settings.getEpVersion() + " (" + settings.getGitCommit() + ")"); exports.restartServer(); @@ -38,7 +25,6 @@ exports.createServer = function () { else{ console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); } - } exports.restartServer = function () { @@ -56,7 +42,7 @@ exports.restartServer = function () { console.log( "SSL -- server key file: " + settings.ssl.key ); console.log( "SSL -- Certificate Authority's certificate file: " + settings.ssl.cert ); - options = { + var options = { key: fs.readFileSync( settings.ssl.key ), cert: fs.readFileSync( settings.ssl.cert ) }; diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index d8f19bba..1ae8d7b5 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -1,10 +1,9 @@ -var path = require('path'); var eejs = require('ep_etherpad-lite/node/eejs'); +var settings = require('ep_etherpad-lite/node/utils/Settings'); var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); var _ = require('underscore'); var semver = require('semver'); -var async = require('async'); exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/admin/plugins', function(req, res) { @@ -14,18 +13,25 @@ exports.expressCreateServer = function (hook_name, args, cb) { search_results: {}, errors: [], }; - res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins.html", render_args) ); }); args.app.get('/admin/plugins/info', function(req, res) { - res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", {}) ); + var gitCommit = settings.getGitCommit(); + var epVersion = settings.getEpVersion(); + res.send( eejs.require("ep_etherpad-lite/templates/admin/plugins-info.html", + { + gitCommit: gitCommit, + epVersion: epVersion + }) + ); }); } exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/pluginfw/installer"); io.on('connection', function (socket) { - if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; + + if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; socket.on("getInstalled", function (query) { // send currently installed plugins @@ -85,7 +91,7 @@ exports.socketio = function (hook_name, args, cb) { socket.on("install", function (plugin_name) { installer.install(plugin_name, function (er) { if(er) console.warn(er) - socket.emit("finished:install", {plugin: plugin_name, error: er? er.message : null}); + socket.emit("finished:install", {plugin: plugin_name, code: er? er.code : null, error: er? er.message : null}); }); }); @@ -107,4 +113,4 @@ function sortPluginList(plugins, property, /*ASC?*/dir) { // a must be equal to b return 0; }) -}
\ No newline at end of file +} diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 2a48d289..4986f093 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -1,7 +1,5 @@ -var path = require('path'); var eejs = require('ep_etherpad-lite/node/eejs'); var settings = require('ep_etherpad-lite/node/utils/Settings'); -var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var fs = require('fs'); @@ -22,7 +20,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/settings"); io.on('connection', function (socket) { - if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; + + if (!socket.conn.request.session || !socket.conn.request.session.user || !socket.conn.request.session.user.is_admin) return; socket.on("load", function (query) { fs.readFile('settings.json', 'utf8', function (err,data) { diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js index f5a3e5a1..f3f05163 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.js @@ -5,7 +5,7 @@ var importHandler = require('../../handler/ImportHandler'); exports.expressCreateServer = function (hook_name, args, cb) { args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { - var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; + var types = ["pdf", "doc", "txt", "html", "odt", "etherpad"]; //send a 404 if we don't support this filetype if (types.indexOf(req.params.type) == -1) { next(); diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js index 9a0a52bf..d60d3863 100644 --- a/src/node/hooks/express/padreadonly.js +++ b/src/node/hooks/express/padreadonly.js @@ -10,7 +10,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { { var html; var padId; - var pad; async.series([ //translate the read only pad to a padId diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 524bab3d..35d6d074 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -1,6 +1,5 @@ -var log4js = require('log4js'); -var socketio = require('socket.io'); var settings = require('../../utils/Settings'); +var socketio = require('socket.io'); var socketIORouter = require("../../handler/SocketIORouter"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var webaccess = require("ep_etherpad-lite/node/hooks/express/webaccess"); @@ -11,14 +10,25 @@ var connect = require('connect'); exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler - var io = socketio.listen(args.server); + // there shouldn't be a browser that isn't compatible to all + // transports in this list at once + // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling + var io = socketio({ + transports: settings.socketTransportProtocols + }).listen(args.server); /* Require an express session cookie to be present, and load the * session. See http://www.danielbaulig.de/socket-ioexpress for more * info */ - io.set('authorization', function (data, accept) { - if (!data.headers.cookie) return accept('No session cookie transmitted.', false); + io.use(function(socket, accept) { + var data = socket.request; + // Use a setting if we want to allow load Testing + if(!data.headers.cookie && settings.loadTest){ + accept(null, true); + }else{ + if (!data.headers.cookie) return accept('No session cookie transmitted.', false); + } // Use connect's cookie parser, because it knows how to parse signed cookies connect.cookieParser(webaccess.secret)(data, {}, function(err){ if(err) { @@ -36,35 +46,17 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); }); - // there shouldn't be a browser that isn't compatible to all - // transports in this list at once - // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling - io.set('transports', settings.socketTransportProtocols ); - - var socketIOLogger = log4js.getLogger("socket.io"); - io.set('logger', { - debug: function (str) - { - socketIOLogger.debug.apply(socketIOLogger, arguments); - }, - info: function (str) - { - socketIOLogger.info.apply(socketIOLogger, arguments); - }, - warn: function (str) - { - socketIOLogger.warn.apply(socketIOLogger, arguments); - }, - error: function (str) - { - socketIOLogger.error.apply(socketIOLogger, arguments); - }, - }); + // var socketIOLogger = log4js.getLogger("socket.io"); + // Debug logging now has to be set at an environment level, this is stupid. + // https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0 + // This debug logging environment is set in Settings.js //minify socket.io javascript - if(settings.minify) - io.enable('browser client minification'); - + // Due to a shitty decision by the SocketIO team minification is + // no longer available, details available at: + // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 + // if(settings.minify) io.enable('browser client minification'); + //Initalize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent("pad", padMessageHandler); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index 7d654c1b..e5a2bff0 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -1,11 +1,8 @@ -var path = require('path'); var minify = require('../../utils/Minify'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var CachingMiddleware = require('../../utils/caching_middleware'); var settings = require("../../utils/Settings"); -var Yajsml = require('yajsml'); -var fs = require("fs"); -var ERR = require("async-stacktrace"); +var Yajsml = require('etherpad-yajsml'); var _ = require("underscore"); exports.expressCreateServer = function (hook_name, args, cb) { diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js index e8daa61c..f606eb88 100644 --- a/src/node/hooks/express/swagger.js +++ b/src/node/hooks/express/swagger.js @@ -1,4 +1,3 @@ -var log4js = require('log4js'); var express = require('express'); var apiHandler = require('../../handler/APIHandler'); var apiCaller = require('./apicalls').apiCaller; @@ -285,6 +284,10 @@ var API = { } }, "response": {"chatHead":{"type":"Message"}} + }, + "appendChatMessage": { + "func": "appendChatMessage", + "description": "appends a chat message" } } }; @@ -356,7 +359,17 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.use(basePath, subpath); - swagger.setAppHandler(subpath); + //hack! + var swagger_temp = swagger + swagger = swagger.createNew(subpath); + swagger.params = swagger_temp.params + swagger.queryParam = swagger_temp.queryParam + swagger.pathParam = swagger_temp.pathParam + swagger.bodyParam = swagger_temp.bodyParam + swagger.formParam = swagger_temp.formParam + swagger.headerParam = swagger_temp.headerParam + swagger.error = swagger_temp.error + //swagger.setAppHandler(subpath); swagger.addModels(swaggerModels); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 3157d68e..151c99fa 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -23,6 +23,10 @@ exports.expressCreateServer = function (hook_name, args, cb) { }); + + // path.join seems to normalize by default, but we'll just be explicit + var rootTestFolder = path.normalize(path.join(npm.root, "../tests/frontend/")); + var url2FilePath = function(url){ var subPath = url.substr("/tests/frontend".length); if (subPath == ""){ @@ -30,8 +34,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { } subPath = subPath.split("?")[0]; - var filePath = path.normalize(npm.root + "/../tests/frontend/") - filePath += subPath.replace("..", ""); + var filePath = path.normalize(path.join(rootTestFolder, subPath)); + // make sure we jail the paths to the test folder, otherwise serve index + if (filePath.indexOf(rootTestFolder) !== 0) { + filePath = path.join(rootTestFolder, "index.html"); + } return filePath; } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 6998853f..b798f2c7 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -2,7 +2,6 @@ var express = require('express'); var log4js = require('log4js'); var httpLogger = log4js.getLogger("http"); var settings = require('../../utils/Settings'); -var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); var ueberStore = require('../../db/SessionStore'); var stats = require('ep_etherpad-lite/node/stats') diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js index 62631b93..67815659 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.js @@ -1,7 +1,6 @@ var languages = require('languages4translatewiki') , fs = require('fs') , path = require('path') - , express = require('express') , _ = require('underscore') , npm = require('npm') , plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins.js').plugins diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js index 5f12bd97..1d9ac5d3 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.js @@ -18,7 +18,6 @@ * limitations under the License. */ -var util = require('util'); var spawn = require('child_process').spawn; var async = require("async"); var settings = require("./Settings"); diff --git a/src/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js deleted file mode 100644 index f5d2d177..00000000 --- a/src/node/utils/ExportDokuWiki.js +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Copyright 2011 Adrian Lang - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var async = require("async"); - -var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); - -function getPadDokuWiki(pad, revNum, callback) -{ - var atext = pad.atext; - var dokuwiki; - async.waterfall([ - // fetch revision atext - - - function (callback) - { - if (revNum != undefined) - { - pad.getInternalRevisionAText(revNum, function (err, revisionAtext) - { - atext = revisionAtext; - callback(err); - }); - } - else - { - callback(null); - } - }, - - // convert atext to dokuwiki text - - function (callback) - { - dokuwiki = getDokuWikiFromAtext(pad, atext); - callback(null); - }], - // run final callback - - - function (err) - { - callback(err, dokuwiki); - }); -} - -function getDokuWikiFromAtext(pad, atext) -{ - var apool = pad.apool(); - var textLines = atext.text.slice(0, -1).split('\n'); - var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - - var tags = ['======', '=====', '**', '//', '__', 'del>']; - var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - var anumMap = {}; - - props.forEach(function (propName, i) - { - var propTrueNum = apool.putAttrib([propName, true], true); - if (propTrueNum >= 0) - { - anumMap[propTrueNum] = i; - } - }); - - function getLineDokuWiki(text, attribs) - { - var propVals = [false, false, false]; - var ENTER = 1; - var STAY = 2; - var LEAVE = 0; - - // Use order of tags (b/i/u) as order of nesting, for simplicity - // and decent nesting. For example, - // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i> - // becomes - // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i> - var taker = Changeset.stringIterator(text); - var assem = Changeset.stringAssembler(); - - function emitOpenTag(i) - { - if (tags[i].indexOf('>') !== -1) { - assem.append('<'); - } - assem.append(tags[i]); - } - - function emitCloseTag(i) - { - if (tags[i].indexOf('>') !== -1) { - assem.append('</'); - } - assem.append(tags[i]); - } - - var urls = _findURLs(text); - - var idx = 0; - - function processNextChars(numChars) - { - if (numChars <= 0) - { - return; - } - - var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); - idx += numChars; - - while (iter.hasNext()) - { - var o = iter.next(); - var propChanged = false; - Changeset.eachAttribNumber(o.attribs, function (a) - { - if (a in anumMap) - { - var i = anumMap[a]; // i = 0 => bold, etc. - if (!propVals[i]) - { - propVals[i] = ENTER; - propChanged = true; - } - else - { - propVals[i] = STAY; - } - } - }); - for (var i = 0; i < propVals.length; i++) - { - if (propVals[i] === true) - { - propVals[i] = LEAVE; - propChanged = true; - } - else if (propVals[i] === STAY) - { - propVals[i] = true; // set it back - } - } - // now each member of propVal is in {false,LEAVE,ENTER,true} - // according to what happens at start of span - if (propChanged) - { - // leaving bold (e.g.) also leaves italics, etc. - var left = false; - for (var i = 0; i < propVals.length; i++) - { - var v = propVals[i]; - if (!left) - { - if (v === LEAVE) - { - left = true; - } - } - else - { - if (v === true) - { - propVals[i] = STAY; // tag will be closed and re-opened - } - } - } - - for (var i = propVals.length - 1; i >= 0; i--) - { - if (propVals[i] === LEAVE) - { - emitCloseTag(i); - propVals[i] = false; - } - else if (propVals[i] === STAY) - { - emitCloseTag(i); - } - } - for (var i = 0; i < propVals.length; i++) - { - if (propVals[i] === ENTER || propVals[i] === STAY) - { - emitOpenTag(i); - propVals[i] = true; - } - } - // propVals is now all {true,false} again - } // end if (propChanged) - var chars = o.chars; - if (o.lines) - { - chars--; // exclude newline at end of line, if present - } - var s = taker.take(chars); - - assem.append(_escapeDokuWiki(s)); - } // end iteration over spans in line - for (var i = propVals.length - 1; i >= 0; i--) - { - if (propVals[i]) - { - emitCloseTag(i); - propVals[i] = false; - } - } - } // end processNextChars - if (urls) - { - urls.forEach(function (urlData) - { - var startIndex = urlData[0]; - var url = urlData[1]; - var urlLength = url.length; - processNextChars(startIndex - idx); - assem.append('[['); - - // Do not use processNextChars since a link does not contain syntax and - // needs no escaping - var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + urlLength)); - idx += urlLength; - assem.append(taker.take(iter.next().chars)); - - assem.append(']]'); - }); - } - processNextChars(text.length - idx); - - return assem.toString() + "\n"; - } // end getLineDokuWiki - var pieces = []; - - for (var i = 0; i < textLines.length; i++) - { - var line = _analyzeLine(textLines[i], attribLines[i], apool); - var lineContent = getLineDokuWiki(line.text, line.aline); - - if (line.listLevel && lineContent) - { - if (line.listTypeName == "number") - { - pieces.push(new Array(line.listLevel + 1).join(' ') + ' - '); - } else { - pieces.push(new Array(line.listLevel + 1).join(' ') + '* '); - } - } - pieces.push(lineContent); - } - - return pieces.join(''); -} - -function _analyzeLine(text, aline, apool) -{ - var line = {}; - - // identify list - var lineMarker = 0; - line.listLevel = 0; - if (aline) - { - var opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) - { - var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); - if (listType) - { - lineMarker = 1; - listType = /([a-z]+)([12345678])/.exec(listType); - if (listType) - { - line.listTypeName = listType[1]; - line.listLevel = Number(listType[2]); - } - } - } - } - if (lineMarker) - { - line.text = text.substring(1); - line.aline = Changeset.subattribution(aline, 1); - } - else - { - line.text = text; - line.aline = aline; - } - - return line; -} - -exports.getPadDokuWikiDocument = function (padId, revNum, callback) -{ - padManager.getPad(padId, function (err, pad) - { - if (err) - { - callback(err); - return; - } - - getPadDokuWiki(pad, revNum, callback); - }); -}; - -function _escapeDokuWiki(s) -{ - s = s.replace(/(\/\/|\*\*|__)/g, '%%$1%%'); - return s; -} - -// copied from ACE -var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; -var _REGEX_SPACE = /\s/; -var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); -var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); - -// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] - - -function _findURLs(text) -{ - _REGEX_URL.lastIndex = 0; - var urls = null; - var execResult; - while ((execResult = _REGEX_URL.exec(text))) - { - urls = (urls || []); - var startIndex = execResult.index; - var url = execResult[0]; - urls.push([startIndex, url]); - } - - return urls; -} diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js new file mode 100644 index 00000000..46ae0d7a --- /dev/null +++ b/src/node/utils/ExportEtherpad.js @@ -0,0 +1,79 @@ +/** + * 2014 John McLear (Etherpad Foundation / McLear Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +var async = require("async"); +var db = require("../db/DB").db; +var ERR = require("async-stacktrace"); + +exports.getPadRaw = function(padId, callback){ + async.waterfall([ + function(cb){ + + // Get the Pad + db.findKeys("pad:"+padId, null, function(err,padcontent){ + if(!err){ + cb(err, padcontent); + } + }) + }, + function(padcontent,cb){ + + // Get the Pad available content keys + db.findKeys("pad:"+padId+":*", null, function(err,records){ + if(!err){ + for (var key in padcontent) { records.push(padcontent[key]);} + cb(err, records); + } + }) + }, + function(records, cb){ + var data = {}; + + async.forEachSeries(Object.keys(records), function(key, r){ + + // For each piece of info about a pad. + db.get(records[key], function(err, entry){ + data[records[key]] = entry; + + // Get the Pad Authors + if(entry.pool && entry.pool.numToAttrib){ + var authors = entry.pool.numToAttrib; + async.forEachSeries(Object.keys(authors), function(k, c){ + if(authors[k][0] === "author"){ + var authorId = authors[k][1]; + + // Get the author info + db.get("globalAuthor:"+authorId, function(e, authorEntry){ + if(authorEntry && authorEntry.padIDs) authorEntry.padIDs = padId; + if(!e) data["globalAuthor:"+authorId] = authorEntry; + }); + + } + // console.log("authorsK", authors[k]); + c(null); + }); + } + r(null); // callback; + }); + }, function(err){ + cb(err, data); + }) + } + ], function(err, data){ + callback(null, data); + }); +} diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index 136896f0..297c2d7a 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -18,12 +18,7 @@ * limitations under the License. */ -var async = require("async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); -var padManager = require("../db/PadManager"); -var ERR = require("async-stacktrace"); -var Security = require('ep_etherpad-lite/static/js/security'); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); exports.getPadPlainText = function(pad, revNum){ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext()); @@ -60,7 +55,7 @@ exports._analyzeLine = function(text, aline, apool){ var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); if (listType){ lineMarker = 1; - listType = /([a-z]+)([12345678])/.exec(listType); + listType = /([a-z]+)([0-9]+)/.exec(listType); if (listType){ line.listTypeName = listType[1]; line.listLevel = Number(listType[2]); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 01920da7..9e1ba124 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -30,8 +30,6 @@ function getPadHTML(pad, revNum, callback) var html; async.waterfall([ // fetch revision atext - - function (callback) { if (revNum != undefined) @@ -78,6 +76,14 @@ function getHTMLFromAtext(pad, atext, authorColors) var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + + hooks.aCallAll("exportHtmlAdditionalTags", pad, function(err, newProps){ + newProps.forEach(function (propName, i){ + tags.push(propName); + props.push(propName); + }); + }); + // holds a map of used styling attributes (*1, *2, etc) in the apool // and maps them to an index in props // *3:2 -> the attribute *3 means strong @@ -297,10 +303,12 @@ function getHTMLFromAtext(pad, atext, authorColors) // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...] + var listLevels = [] for (var i = 0; i < textLines.length; i++) { var line = _analyzeLine(textLines[i], attribLines[i], apool); var lineContent = getLineHTML(line.text, line.aline); + listLevels.push(line.listLevel) if (line.listLevel)//If we are inside a list { @@ -320,13 +328,27 @@ function getHTMLFromAtext(pad, atext, authorColors) if (whichList >= lists.length)//means we are on a deeper level of indentation than the previous line { + if(lists.length > 0){ + pieces.push('</li>') + } lists.push([line.listLevel, line.listTypeName]); + + // if there is a previous list we need to open x tags, where x is the difference of the levels + // if there is no previous list we need to open x tags, where x is the wanted level + var toOpen = lists.length > 1 ? line.listLevel - lists[lists.length - 2][0] - 1 : line.listLevel - 1 + if(line.listTypeName == "number") { + if(toOpen > 0){ + pieces.push(new Array(toOpen + 1).join('<ol>')) + } pieces.push('<ol class="'+line.listTypeName+'"><li>', lineContent || '<br>'); } else { + if(toOpen > 0){ + pieces.push(new Array(toOpen + 1).join('<ul>')) + } pieces.push('<ul class="'+line.listTypeName+'"><li>', lineContent || '<br>'); } } @@ -355,44 +377,50 @@ function getHTMLFromAtext(pad, atext, authorColors) pieces.push('<br><br>'); } }*/ - else//means we are getting closer to the lowest level of indentation + else//means we are getting closer to the lowest level of indentation or are at the same level { - while (whichList < lists.length - 1) - { + var toClose = lists.length > 0 ? listLevels[listLevels.length - 2] - line.listLevel : 0 + if( toClose > 0){ + pieces.push('</li>') if(lists[lists.length - 1][1] == "number") { - pieces.push('</li></ol>'); + pieces.push(new Array(toClose+1).join('</ol>')) + pieces.push('<li>', lineContent || '<br>'); } else { - pieces.push('</li></ul>'); + pieces.push(new Array(toClose+1).join('</ul>')) + pieces.push('<li>', lineContent || '<br>'); } - lists.length--; + lists = lists.slice(0,whichList+1) + } else { + pieces.push('</li><li>', lineContent || '<br>'); } - pieces.push('</li><li>', lineContent || '<br>'); } } - else//outside any list + else//outside any list, need to close line.listLevel of lists { - while (lists.length > 0)//if was in a list: close it before - { - if(lists[lists.length - 1][1] == "number") - { + if(lists.length > 0){ + if(lists[lists.length - 1][1] == "number"){ pieces.push('</li></ol>'); - } - else - { + pieces.push(new Array(listLevels[listLevels.length - 2]).join('</ol>')) + } else { pieces.push('</li></ul>'); + pieces.push(new Array(listLevels[listLevels.length - 2]).join('</ul>')) } - lists.length--; } - var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport", - { + lists = [] + + var context = { line: line, + lineContent: lineContent, apool: apool, attribLine: attribLines[i], text: textLines[i] - }, " ", " ", ""); + } + + var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport", context, " ", " ", ""); + if (lineContentFromHook) { pieces.push(lineContentFromHook, ''); @@ -425,37 +453,120 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) { if(ERR(err, callback)) return; - var head = - (noDocType ? '' : '<!doctype html>\n') + - '<html lang="en">\n' + (noDocType ? '' : '<head>\n' + - '<title>' + Security.escapeHTML(padId) + '</title>\n' + - '<meta charset="utf-8">\n' + - '<style> * { font-family: arial, sans-serif;\n' + - 'font-size: 13px;\n' + - 'line-height: 17px; }' + - 'ul.indent { list-style-type: none; }' + - 'ol { list-style-type: decimal; }' + - 'ol ol { list-style-type: lower-latin; }' + - 'ol ol ol { list-style-type: lower-roman; }' + - 'ol ol ol ol { list-style-type: decimal; }' + - 'ol ol ol ol ol { list-style-type: lower-latin; }' + - 'ol ol ol ol ol ol{ list-style-type: lower-roman; }' + - 'ol ol ol ol ol ol ol { list-style-type: decimal; }' + - 'ol ol ol ol ol ol ol ol{ list-style-type: lower-latin; }' + - '</style>\n' + '</head>\n') + - '<body>'; - - var foot = '</body>\n</html>\n'; - - getPadHTML(pad, revNum, function (err, html) - { - if(ERR(err, callback)) return; - callback(null, head + html + foot); + var stylesForExportCSS = ""; + // Include some Styles into the Head for Export + hooks.aCallAll("stylesForExport", padId, function(err, stylesForExport){ + stylesForExport.forEach(function(css){ + stylesForExportCSS += css; + }); + // Core inclusion of head etc. + var head = + (noDocType ? '' : '<!doctype html>\n') + + '<html lang="en">\n' + (noDocType ? '' : '<head>\n' + + '<title>' + Security.escapeHTML(padId) + '</title>\n' + + '<meta charset="utf-8">\n' + + '<style> * { font-family: arial, sans-serif;\n' + + 'font-size: 13px;\n' + + 'line-height: 17px; }' + + 'ul.indent { list-style-type: none; }' + + + 'ol { list-style-type: none; padding-left:0;}' + + 'body > ol { counter-reset: first second third fourth fifth sixth seventh eigth ninth tenth eleventh twelth thirteenth fourteenth fifteenth sixteenth; }' + + 'ol > li:before {' + + 'content: counter(first) ". " ;'+ + 'counter-increment: first;}' + + + 'ol > ol > li:before {' + + 'content: counter(first) "." counter(second) ". " ;'+ + 'counter-increment: second;}' + + + 'ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) ". ";'+ + 'counter-increment: third;}' + + + 'ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) ". ";'+ + 'counter-increment: fourth;}' + + + 'ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) ". ";'+ + 'counter-increment: fifth;}' + + + 'ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) ". ";'+ + 'counter-increment: sixth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) ". ";'+ + 'counter-increment: seventh;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) ". ";'+ + 'counter-increment: eigth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) ". ";'+ + 'counter-increment: ninth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) ". ";'+ + 'counter-increment: tenth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) ". ";'+ + 'counter-increment: eleventh;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) ". ";'+ + 'counter-increment: twelth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) ". ";'+ + 'counter-increment: thirteenth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) ". ";'+ + 'counter-increment: fourteenth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) ". ";'+ + 'counter-increment: fifteenth;}' + + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before {' + + 'content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) "." counter(sixthteenth) ". ";'+ + 'counter-increment: sixthteenth;}' + + + 'ol{ text-indent: 0px; }' + + 'ol > ol{ text-indent: 10px; }' + + 'ol > ol > ol{ text-indent: 20px; }' + + 'ol > ol > ol > ol{ text-indent: 30px; }' + + 'ol > ol > ol > ol > ol{ text-indent: 40px; }' + + 'ol > ol > ol > ol > ol > ol{ text-indent: 50px; }' + + 'ol > ol > ol > ol > ol > ol > ol{ text-indent: 60px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 70px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 80px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 90px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 100px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 110px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol { text-indent: 120px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 130px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 140px; }' + + 'ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol{ text-indent: 150px; }' + + + stylesForExportCSS + + '</style>\n' + '</head>\n') + + '<body>'; + var foot = '</body>\n</html>\n'; + + getPadHTML(pad, revNum, function (err, html) + { + if(ERR(err, callback)) return; + callback(null, head + html + foot); + }); }); }); }; - // copied from ACE var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; var _REGEX_SPACE = /\s/; diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index f0b62743..a6bec4a5 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -22,9 +22,6 @@ var async = require("async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); -var Security = require('ep_etherpad-lite/static/js/security'); -var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); -var getPadPlainText = require('./ExportHelper').getPadPlainText; var _analyzeLine = require('./ExportHelper')._analyzeLine; // This is slightly different than the HTML method as it passes the output to getTXTFromAText @@ -82,7 +79,6 @@ function getTXTFromAtext(pad, atext, authorColors) var textLines = atext.text.slice(0, -1).split('\n'); var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); - var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; var anumMap = {}; var css = ""; @@ -110,7 +106,6 @@ function getTXTFromAtext(pad, atext, authorColors) // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i> var taker = Changeset.stringIterator(text); var assem = Changeset.stringAssembler(); - var openTags = []; var idx = 0; @@ -250,7 +245,6 @@ function getTXTFromAtext(pad, atext, authorColors) // so we want to do something reasonable there. We also // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...] for (var i = 0; i < textLines.length; i++) { var line = _analyzeLine(textLines[i], attribLines[i], apool); diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js new file mode 100644 index 00000000..37863bff --- /dev/null +++ b/src/node/utils/ImportEtherpad.js @@ -0,0 +1,83 @@ +/** + * 2014 John McLear (Etherpad Foundation / McLear Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var log4js = require('log4js'); +var async = require("async"); +var db = require("../db/DB").db; + +exports.setPadRaw = function(padId, records, callback){ + records = JSON.parse(records); + + // !! HACK !! + // If you have a really large pad it will cause a Maximum Range Stack crash + // This is a temporary patch for that so things are kept stable. + var recordCount = Object.keys(records).length; + if(recordCount >= 50000){ + console.warn("Etherpad file is too large to import.. We need to fix this. See https://github.com/ether/etherpad-lite/issues/2524"); + return callback("tooLarge", false); + } + + async.eachSeries(Object.keys(records), function(key, cb){ + var value = records[key] + + if(!value){ + cb(); // null values are bad. + } + + // Author data + if(value.padIDs){ + // rewrite author pad ids + value.padIDs[padId] = 1; + var newKey = key; + + // Does this author already exist? + db.get(key, function(err, author){ + if(author){ + // Yes, add the padID to the author.. + if( Object.prototype.toString.call(author) === '[object Array]'){ + author.padIDs.push(padId); + } + value = author; + }else{ + // No, create a new array with the author info in + value.padIDs = [padId]; + } + }); + + // Not author data, probably pad data + }else{ + // we can split it to look to see if its pad data + var oldPadId = key.split(":"); + + // we know its pad data.. + if(oldPadId[0] === "pad"){ + + // so set the new pad id for the author + oldPadId[1] = padId; + + // and create the value + var newKey = oldPadId.join(":"); // create the new key + } + + } + // Write the value to the server + db.set(newKey, value); + + cb(); + }, function(){ + callback(null, true); + }); +} diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 48188dfd..33fd91c6 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -14,23 +14,22 @@ * limitations under the License. */ -var jsdom = require('jsdom-nocontextifiy').jsdom; var log4js = require('log4js'); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var contentcollector = require("ep_etherpad-lite/static/js/contentcollector"); +var cheerio = require("cheerio"); function setPadHTML(pad, html, callback) { var apiLogger = log4js.getLogger("ImportHtml"); - // Parse the incoming HTML with jsdom - try{ - var doc = jsdom(html.replace(/>\n+</g, '><')); - }catch(e){ - apiLogger.warn("Error importing, possibly caused by malformed HTML"); - var doc = jsdom("<html><body><div>Error during import, possibly malformed HTML</div></body></html>"); - } + var $ = cheerio.load(html); + + // Appends a line break, used by Etherpad to ensure a caret is available + // below the last line of an import + $('body').append("<p></p>"); + var doc = $('html')[0]; apiLogger.debug('html:'); apiLogger.debug(html); @@ -38,10 +37,10 @@ function setPadHTML(pad, html, callback) // using the content collector object var cc = contentcollector.makeContentCollector(true, null, pad.pool); try{ // we use a try here because if the HTML is bad it will blow up - cc.collectContent(doc.childNodes[0]); + cc.collectContent(doc); }catch(e){ apiLogger.warn("HTML was not properly formed", e); - return; // We don't process the HTML because it was bad.. + return callback(e); // We don't process the HTML because it was bad.. } var result = cc.finish(); @@ -92,6 +91,7 @@ function setPadHTML(pad, html, callback) apiLogger.debug('The changeset: ' + theChangeset); pad.setText(""); pad.appendRevision(theChangeset); + callback(null); } exports.setPadHTML = setPadHTML; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 58d08b30..ba45ab75 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -23,12 +23,12 @@ var ERR = require("async-stacktrace"); var settings = require('./Settings'); var async = require('async'); var fs = require('fs'); -var cleanCSS = require('clean-css'); +var CleanCSS = require('clean-css'); var jsp = require("uglify-js").parser; var pro = require("uglify-js").uglify; var path = require('path'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); -var RequireKernel = require('require-kernel'); +var RequireKernel = require('etherpad-require-kernel'); var urlutil = require('url'); var ROOT_DIR = path.normalize(__dirname + "/../../static/"); @@ -145,7 +145,6 @@ function minify(req, res, next) filename = path.normalize(path.join(ROOT_DIR, filename)); if (filename.indexOf(ROOT_DIR) == 0) { filename = filename.slice(ROOT_DIR.length); - filename = filename.replace(/\\/g, '/'); // Windows (safe generally?) } else { res.writeHead(404, {}); res.end(); @@ -261,7 +260,6 @@ function getAceFile(callback) { // them into the file. async.forEach(founds, function (item, callback) { var filename = item.match(/"([^"]*)"/)[1]; - var request = require('request'); var baseURI = 'http://localhost:' + settings.port; var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); @@ -411,7 +409,8 @@ function compressJS(values) function compressCSS(values) { var complete = values.join("\n"); - return cleanCSS.process(complete); + var minimized = new CleanCSS().minify(complete).styles; + return minimized; } exports.minify = minify; diff --git a/src/node/utils/RemoteAddress.js b/src/node/utils/RemoteAddress.js new file mode 100644 index 00000000..86a4a5b2 --- /dev/null +++ b/src/node/utils/RemoteAddress.js @@ -0,0 +1 @@ +exports.remoteAddress = {}; diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index c455617b..7e0e6c5a 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -27,7 +27,7 @@ var npm = require("npm/lib/npm.js"); var jsonminify = require("jsonminify"); var log4js = require("log4js"); var randomString = require("./randomstring"); - +var suppressDisableMsg = " -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n"; /* Root path of the installation */ exports.root = path.normalize(path.join(npm.dir, "..")); @@ -55,6 +55,11 @@ exports.ip = "0.0.0.0"; exports.port = process.env.PORT || 9001; /** + * Should we suppress Error messages from being in Pad Contents + */ +exports.suppressErrorsInPadText = false; + +/** * The SSL signed server key and the Certificate Authority's own certificate * default case: ep-lite does *not* use SSL. A signed server key is not required in this case. */ @@ -95,7 +100,7 @@ exports.toolbar = { ["showusers"] ], timeslider: [ - ["timeslider_export", "timeslider_returnToPad"] + ["timeslider_export", "timeslider_settings", "timeslider_returnToPad"] ] } @@ -130,6 +135,11 @@ exports.minify = true; exports.abiword = null; /** + * Should we support none natively supported file types on import? + */ +exports.allowUnknownFileEnds = true; + +/** * The log level of log4js */ exports.loglevel = "INFO"; @@ -139,6 +149,11 @@ exports.loglevel = "INFO"; */ exports.disableIPlogging = false; +/** + * Disable Load Testing + */ +exports.loadTest = false; + /* * log4js appender configuration */ @@ -174,6 +189,29 @@ exports.abiwordAvailable = function() } }; +// Provide git version if available +exports.getGitCommit = function() { + var version = ""; + try + { + var rootPath = path.resolve(npm.dir, '..'); + var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8"); + var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n")); + version = fs.readFileSync(refPath, "utf-8"); + version = version.substring(0, 7); + } + catch(e) + { + console.warn("Can't get git version for server header\n" + e.message) + } + return version; +} + +// Return etherpad version from package.json +exports.getEpVersion = function() { + return require('ep_etherpad-lite/package.json').version; +} + exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = argv.settings || "settings.json"; @@ -228,17 +266,45 @@ exports.reloadSettings = function reloadSettings() { log4js.configure(exports.logconfig);//Configure the logging appenders log4js.setGlobalLogLevel(exports.loglevel);//set loglevel + process.env['DEBUG'] = 'socket.io:' + exports.loglevel; // Used by SocketIO for Debug log4js.replaceConsole(); + if(exports.abiword){ + // Check abiword actually exists + if(exports.abiword != null) + { + fs.exists(exports.abiword, function(exists) { + if (!exists) { + var abiwordError = "Abiword does not exist at this path, check your settings file"; + if(!exports.suppressErrorsInPadText){ + exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; + } + console.error(abiwordError); + exports.abiword = null; + } + }); + } + } + if(!exports.sessionKey){ // If the secretKey isn't set we also create yet another unique value here exports.sessionKey = randomString(32); - console.warn("You need to set a sessionKey value in settings.json, this will allow your users to reconnect to your Etherpad Instance if your instance restarts"); + var sessionWarning = "You need to set a sessionKey value in settings.json, this will allow your users to reconnect to your Etherpad Instance if your instance restarts"; + if(!exports.suppressErrorsInPadText){ + exports.defaultPadText = exports.defaultPadText + "\nWarning: " + sessionWarning + suppressDisableMsg; + } + console.warn(sessionWarning); } if(exports.dbType === "dirty"){ - console.warn("DirtyDB is used. This is fine for testing but not recommended for production."); + var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production."; + if(!exports.suppressErrorsInPadText){ + exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg; + } + console.warn(dirtyWarning); } }; // initially load settings exports.reloadSettings(); + + diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index d30dc398..97134356 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -19,7 +19,6 @@ var Buffer = require('buffer').Buffer; var fs = require('fs'); var path = require('path'); var zlib = require('zlib'); -var util = require('util'); var settings = require('./Settings'); var semver = require('semver'); diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 88fa5cba..24d5bb0c 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -101,8 +101,12 @@ PadDiff.prototype._createClearStartAtext = function(rev, callback){ return callback(err); } + try { //apply the clearAuthorship changeset var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool); + } catch(err) { + return callback(err) + } callback(null, newAText); }); @@ -209,10 +213,14 @@ PadDiff.prototype._createDiffAtext = function(callback) { if(superChangeset){ var deletionChangeset = self._createDeletionChangeset(superChangeset,atext,self._pad.pool); - //apply the superChangeset, which includes all addings - atext = Changeset.applyToAText(superChangeset,atext,self._pad.pool); - //apply the deletionChangeset, which adds a deletions - atext = Changeset.applyToAText(deletionChangeset,atext,self._pad.pool); + try { + //apply the superChangeset, which includes all addings + atext = Changeset.applyToAText(superChangeset,atext,self._pad.pool); + //apply the deletionChangeset, which adds a deletions + atext = Changeset.applyToAText(deletionChangeset,atext,self._pad.pool); + } catch(err) { + return callback(err) + } } callback(err, atext); diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 70001f8f..05d764a7 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -2,6 +2,7 @@ "pad.js": [ "pad.js" , "pad_utils.js" + , "browser.js" , "pad_cookie.js" , "pad_editor.js" , "pad_editbar.js" @@ -24,6 +25,7 @@ , "colorutils.js" , "draggable.js" , "pad_utils.js" + , "browser.js" , "pad_cookie.js" , "pad_editor.js" , "pad_editbar.js" @@ -42,6 +44,7 @@ ] , "ace2_inner.js": [ "ace2_inner.js" + , "browser.js" , "AttributePool.js" , "Changeset.js" , "ChangesetUtils.js" @@ -58,6 +61,7 @@ ] , "ace2_common.js": [ "ace2_common.js" + , "browser.js" , "jquery.js" , "rjquery.js" , "$async.js" diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js index e8d02dd6..07b86496 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.js @@ -4,7 +4,6 @@ var _ = require("underscore") , tagAttributes , tag - , defaultButtons , Button , ButtonsGroup , Separator @@ -100,12 +99,14 @@ _.extend(Button.prototype, { }; return tag("li", liAttributes, tag("a", { "class": this.grouping, "data-l10n-id": this.attributes.localizationId }, - tag("span", { "class": " "+ this.attributes.class }) + tag("button", { "class": " "+ this.attributes.class, "data-l10n-id": this.attributes.localizationId }) ) ); } }); + + SelectButton = function (attributes) { this.attributes = attributes; this.options = []; @@ -122,8 +123,7 @@ _.extend(SelectButton.prototype, Button.prototype, { }, select: function (attributes) { - var self = this - , options = []; + var options = []; _.each(this.options, function (opt) { var a = _.extend({ @@ -210,6 +210,12 @@ module.exports = { class: "buttonicon buttonicon-import_export" }, + timeslider_settings: { + command: "settings", + localizationId: "pad.toolbar.settings.title", + class: "buttonicon buttonicon-settings" + }, + timeslider_returnToPad: { command: "timeslider_returnToPad", localizationId: "timeslider.toolbar.returnbutton", |