diff --git a/.gitignore b/.gitignore index 325e11ca..29835e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules settings.json static/js/jquery.min.js +APIKEY.txt bin/abiword.exe bin/node.exe etherpad-lite-win.zip -var/dirty.db \ No newline at end of file +var/dirty.db diff --git a/doc/database.md b/doc/database.md new file mode 100644 index 00000000..2d43d6f1 --- /dev/null +++ b/doc/database.md @@ -0,0 +1,69 @@ +# Database structure + +## Used so far + +### pad:$PADID +Saves all informations about pads + +* **atext** - the latest attributed text +* **pool** - the attribute pool +* **head** - the number of the latest revision +* **chatHead** - the number of the latest chat entry + +*planed:* + +* **public** - flag that disables security for this pad +* **passwordHash** - string that contains a bcrypt hashed password for this pad + +### pad:$PADID:revs:$REVNUM +Saves a revision $REVNUM of pad $PADID + +* **meta** + * **author** - the autorID of this revision + * **timestamp** - the timestamp of when this revision was created +* **changeset** - the changeset of this revision + +### pad:$PADID:chat:$CHATNUM +Saves a chatentry with num $CHATNUM of pad $PADID + +* **text** - the text of this chat entry +* **userId** - the autorID of this chat entry +* **time** - the timestamp of this chat entry + +### pad2readonly:$PADID +Translates a padID to a readonlyID +### readonly2pad:$READONLYID +Translates a readonlyID to a padID +### token2author:$TOKENID +Translates a token to an authorID +### globalAuthor:$AUTHORID +Information about an author + +* **name** - the name of this author as shown in the pad +* **colorID** - the colorID of this author as shown in the pad + +## Planed + +### mapper2group:$MAPPER +Maps an external application identifier to an internal group +### mapper2author:$MAPPER +Maps an external application identifier to an internal author +### group:$GROUPID +a group of pads + +* **pads** - object with pad names in it, values are 1 +### session:$SESSIONID +a session between an author and a group + +* **groupID** - the groupID the session belongs too +* **authorID** - the authorID the session belongs too +* **validUntil** - the timestamp until this session is valid + +### author2sessions:$AUTHORID +saves the sessions of an author + +* **sessionsIDs** - object with sessionIDs in it, values are 1 + +### group2sessions:$GROUPID + +* **sessionsIDs** - object with sessionIDs in it, values are 1 diff --git a/node/db/API.js b/node/db/API.js new file mode 100644 index 00000000..1fd1bb7b --- /dev/null +++ b/node/db/API.js @@ -0,0 +1,432 @@ +/** + * This module provides all API functions + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var padManager = require("./PadManager"); +var padMessageHandler = require("../handler/PadMessageHandler"); +var readOnlyManager = require("./ReadOnlyManager"); +var groupManager = require("./GroupManager"); +var authorManager = require("./AuthorManager"); +var sessionManager = require("./SessionManager"); +var async = require("async"); + +/**********************/ +/**GROUP FUNCTIONS*****/ +/**********************/ + +exports.createGroup = groupManager.createGroup; +exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; +exports.deleteGroup = groupManager.deleteGroup; +exports.listPads = groupManager.listPads; +exports.createGroupPad = groupManager.createGroupPad; + +/**********************/ +/**AUTHOR FUNCTIONS****/ +/**********************/ + +exports.createAuthor = authorManager.createAuthor; +exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; + +/**********************/ +/**SESSION FUNCTIONS***/ +/**********************/ + +exports.createSession = sessionManager.createSession; +exports.deleteSession = sessionManager.deleteSession; +exports.getSessionInfo = sessionManager.getSessionInfo; +exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; +exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; + +/************************/ +/**PAD CONTENT FUNCTIONS*/ +/************************/ + +/** +getText(padID, [rev]) returns the text of a pad + +Example returns: + +{code: 0, message:"ok", data: {text:"Welcome Text"}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getText = function(padID, rev, callback) +{ + //check if rev is set + if(typeof rev == "function") + { + callback = rev; + rev = undefined; + } + + //check if rev is a number + if(rev !== undefined && typeof rev != "number") + { + //try to parse the number + if(!isNaN(parseInt(rev))) + { + rev = parseInt(rev); + } + else + { + callback({stop: "rev is not a number"}); + return; + } + } + + //ensure this is not a negativ number + if(rev !== undefined && rev < 0) + { + callback({stop: "rev is a negativ number"}); + return; + } + + //ensure this is not a float value + if(rev !== undefined && !is_int(rev)) + { + callback({stop: "rev is a float value"}); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + //the client asked for a special revision + if(rev !== undefined) + { + //check if this is a valid revision + if(rev > pad.getHeadRevisionNumber()) + { + callback({stop: "rev is higher than the head revision of the pad"}); + return; + } + + //get the text of this revision + pad.getInternalRevisionAText(rev, function(err, atext) + { + if(!err) + { + data = {text: atext.text}; + } + + callback(err, data); + }) + } + //the client wants the latest text, lets return it to him + else + { + callback(null, {"text": pad.text()}); + } + }); +} + +/** +setText(padID, text) sets the text of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +{code: 1, message:"text too long", data: null} +*/ +exports.setText = function(padID, text, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + //set the text + pad.setText(text); + + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + }); +} + +/*****************/ +/**PAD FUNCTIONS */ +/*****************/ + +/** +getRevisionsCount(padID) returns the number of revisions of this pad + +Example returns: + +{code: 0, message:"ok", data: {revisions: 56}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getRevisionsCount = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + callback(null, {revisions: pad.getHeadRevisionNumber()}); + }); +} + +/** +createPad(padName [, text]) creates a new pad in this group + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"pad does already exist", data: null} +*/ +exports.createPad = function(padID, text, callback) +{ + //ensure there is no $ in the padID + if(padID.indexOf("$") != -1) + { + callback({stop: "createPad can't create group pads"}); + return; + } + + //create pad + getPadSafe(padID, false, text, function(err) + { + callback(err); + }); +} + +/** +deletePad(padID) deletes a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.deletePad = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + + }); +} + +/** +getReadOnlyLink(padID) returns the read only link of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getReadOnlyID = function(padID, callback) +{ + //we don't need the pad object, but this function does all the security stuff for us + getPadSafe(padID, true, function(err) + { + if(err) + { + callback(err); + return; + } + + //get the readonlyId + readOnlyManager.getReadOnlyId(padID, function(err, readOnlyId) + { + callback(err, {readOnlyID: readOnlyId}); + }); + }); +} + +/** +setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.setPublicStatus = function(padID, publicStatus, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + //convert string to boolean + if(typeof publicStatus == "string") + publicStatus = publicStatus == "true" ? true : false; + + //set the password + pad.setPublicStatus(publicStatus); + + callback(); + }); +} + +/** +getPublicStatus(padID) return true of false + +Example returns: + +{code: 0, message:"ok", data: {publicStatus: true}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.getPublicStatus = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + callback(null, {publicStatus: pad.getPublicStatus()}); + }); +} + +/** +setPassword(padID, password) returns ok or a error message + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.setPassword = function(padID, password, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + //set the password + pad.setPassword(password); + + callback(); + }); +} + +/** +isPasswordProtected(padID) returns true or false + +Example returns: + +{code: 0, message:"ok", data: {passwordProtection: true}} +{code: 1, message:"padID does not exist", data: null} +*/ +exports.isPasswordProtected = function(padID, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + callback(null, {isPasswordProtected: pad.isPasswordProtected()}); + }); +} + +/******************************/ +/** INTERNAL HELPER FUNCTIONS */ +/******************************/ + +//checks if a number is an int +function is_int(value) +{ + return (parseFloat(value) == parseInt(value)) && !isNaN(value) +} + +//gets a pad safe +function getPadSafe(padID, shouldExist, text, callback) +{ + if(typeof text == "function") + { + callback = text; + text = null; + } + + //check if padID is a string + if(typeof padID != "string") + { + callback({stop: "padID is not a string"}); + return; + } + + //check if the padID maches the requirements + if(!padManager.isValidPadId(padID)) + { + callback({stop: "padID did not match requirements"}); + return; + } + + //check if the pad exists + padManager.doesPadExists(padID, function(err, exists) + { + //error + if(err) + { + callback(err); + } + //does not exist, but should + else if(exists == false && shouldExist == true) + { + callback({stop: "padID does not exist"}); + } + //does exists, but shouldn't + else if(exists == true && shouldExist == false) + { + callback({stop: "padID does already exist"}); + } + //pad exists, let's get it + else + { + padManager.getPad(padID, text, callback); + } + }); +} diff --git a/node/db/AuthorManager.js b/node/db/AuthorManager.js index 6a9064ea..958e2c16 100644 --- a/node/db/AuthorManager.js +++ b/node/db/AuthorManager.js @@ -3,7 +3,7 @@ */ /* - * 2011 Peter 'Pita' Martischka + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,94 +22,122 @@ var db = require("./DB").db; var async = require("async"); /** - * Returns the Author Id for a token. If the token is unkown, - * it creates a author for the token + * Checks if the author exists + */ +exports.doesAuthorExists = function (authorID, callback) +{ + //check if the database entry of this author exists + db.get("globalAuthor:" + authorID, function (err, author) + { + callback(err, author != null); + }); +} + +/** + * Returns the AuthorID for a token. * @param {String} token The token * @param {Function} callback callback (err, author) - * The callback function that is called when the result is here */ exports.getAuthor4Token = function (token, callback) -{ - var author; - - async.series([ - //try to get the author for this token - function(callback) - { - db.get("token2author:" + token, function (err, _author) - { - author = _author; - callback(err); - }); - }, - function(callback) - { - //there is no author with this token, so create one - if(author == null) - { - createAuthor(token, function(err, _author) - { - author = _author; - callback(err); - }); - } - //there is a author with this token - else - { - //check if there is also an author object for this token, if not, create one - db.get("globalAuthor:" + author, function(err, authorObject) - { - if(authorObject == null) - { - createAuthor(token, function(err, _author) - { - author = _author; - callback(err); - }); - } - //the author exists, update the timestamp of this author - else - { - db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime()); - callback(); - } - }); - } - } - ], function(err) +{ + mapAuthorWithDBKey("token2author", token, function(err, author) { - callback(err, author); + //return only the sub value authorID + callback(err, author ? author.authorID : author); + }); +} + +/** + * Returns the AuthorID for a mapper. + * @param {String} token The mapper + * @param {Function} callback callback (err, author) + */ +exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback) +{ + mapAuthorWithDBKey("mapper2author", authorMapper, function(err, author) + { + //error? + if(err) + { + callback(err); + return; + } + + //set the name of this author + if(name) + exports.setAuthorName(author.authorID, name); + + //return the authorID + callback(null, author); + }); +} + +/** + * Returns the AuthorID for a mapper. We can map using a mapperkey, + * so far this is token2author and mapper2author + * @param {String} mapperkey The database key name for this mapper + * @param {String} mapper The mapper + * @param {Function} callback callback (err, author) + */ +function mapAuthorWithDBKey (mapperkey, mapper, callback) +{ + //try to map to an author + db.get(mapperkey + ":" + mapper, function (err, author) + { + //error? + if(err) + { + callback(err); + return; + } + + //there is no author with this mapper, so create one + if(author == null) + { + exports.createAuthor(null, function(err, author) + { + //error? + if(err) + { + callback(err); + return; + } + + //create the token2author relation + db.set(mapperkey + ":" + mapper, author.authorID); + + //return the author + callback(null, author); + }); + } + //there is a author with this mapper + else + { + //update the timestamp of this author + db.setSub("globalAuthor:" + author, ["timestamp"], new Date().getTime()); + + //return the author + callback(null, {authorID: author}); + } }); } /** * Internal function that creates the database entry for an author - * @param {String} token The token + * @param {String} name The name of the author */ -function createAuthor (token, callback) +exports.createAuthor = function(name, callback) { //create the new author name - var author = "g." + _randomString(16); + var author = "a." + randomString(16); //create the globalAuthors db entry - var authorObj = {colorId : Math.floor(Math.random()*32), name: null, timestamp: new Date().getTime()}; + var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()}; - //we do this in series to ensure this db entries are written in the correct order - async.series([ - //set the global author db entry - function(callback) - { - db.set("globalAuthor:" + author, authorObj, callback); - }, - //set the token2author db entry - function(callback) - { - db.set("token2author:" + token, author, callback); - } - ], function(err) - { - callback(err, author); - }); + //set the global author db entry + db.set("globalAuthor:" + author, authorObj); + + callback(null, {authorID: author}); } /** @@ -165,11 +193,14 @@ exports.setAuthorName = function (author, name, callback) /** * Generates a random String with the given length. Is needed to generate the Author Ids */ -function _randomString(len) { - // use only numbers and lowercase letters - var pieces = []; - for(var i=0;i 100000) + { + callback({stop: "text must be less than 100k chars"}); + return; + } + } + var pad = globalPads[id]; //return pad if its already loaded @@ -45,7 +78,7 @@ exports.getPad = function(id, callback) pad = new Pad(id); //initalize the pad - pad.init(function(err) + pad.init(text, function(err) { if(err) { @@ -58,6 +91,19 @@ exports.getPad = function(id, callback) } }); } - - //globalPads[id].timestamp = new Date().getTime(); } + +//checks if a pad exists +exports.doesPadExists = function(padId, callback) +{ + db.get("pad:"+padId, function(err, value) + { + callback(err, value != null); + }); +} + +exports.isValidPadId = function(padId) +{ + return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); +} + diff --git a/node/db/ReadOnlyManager.js b/node/db/ReadOnlyManager.js index 30d726b6..cd18a188 100644 --- a/node/db/ReadOnlyManager.js +++ b/node/db/ReadOnlyManager.js @@ -3,7 +3,7 @@ */ /* - * 2011 Peter 'Pita' Martischka + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ exports.getReadOnlyId = function (padId, callback) //there is no readOnly Entry in the database, let's create one if(dbReadOnlyId == null) { - readOnlyId = randomString(10); + readOnlyId = "r." + randomString(16); db.set("pad2readonly:" + padId, readOnlyId); db.set("readonly2pad:" + readOnlyId, padId); @@ -74,10 +74,12 @@ exports.getPadId = function(readOnlyId, callback) */ function randomString(len) { - // use only numbers and lowercase letters - var pieces = []; - for(var i=0;i validSession + if(sessionInfo.groupID == groupID && sessionInfo.validUntil > now) + { + validSession = true; + } + + sessionAuthor = sessionInfo.authorID; + + callback(); + }); + }, + //get author for token + function(callback) + { + //get author for this token + authorManager.getAuthor4Token(token, function(err, author) + { + tokenAuthor = author; + callback(err); + }); + } + ], callback); + }, + //get more informations of this pad, if avaiable + function(callback) + { + //skip this if the pad doesn't exists + if(padExists == false) + { + callback(); + return; + } + + padManager.getPad(padID, function(err, pad) + { + if(err) {callback(err); return} + + //is it a public pad? + isPublic = pad.getPublicStatus(); + + //is it password protected? + isPasswordProtected = pad.isPasswordProtected(); + + //is password correct? + if(isPasswordProtected && password && pad.isCorrectPassword(password)) + { + passwordStatus = "correct"; + } + + callback(); + }); + }, + function(callback) + { + //- a valid session for this group is avaible AND pad exists + if(validSession && padExists) + { + //- the pad is not password protected + if(!isPasswordProtected) + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + } + //- the pad is password protected and password is correct + else if(isPasswordProtected && passwordStatus == "correct") + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + } + //- the pad is password protected but wrong password given + else if(isPasswordProtected && passwordStatus == "wrong") + { + //--> deny access, ask for new password and tell them that the password is wrong + statusObject = {accessStatus: "wrongPassword"}; + } + //- the pad is password protected but no password given + else if(isPasswordProtected && passwordStatus == "notGiven") + { + //--> ask for password + statusObject = {accessStatus: "needPassword"}; + } + else + { + throw new Error("Ops, something wrong happend"); + } + } + //- a valid session for this group avaible but pad doesn't exists + else if(validSession && !padExists) + { + //--> grant access + statusObject = {accessStatus: "grant", authorID: sessionAuthor}; + } + // there is no valid session avaiable AND pad exists + else if(!validSession && padExists) + { + //-- its public and not password protected + if(isPublic && !isPasswordProtected) + { + //--> grant access, with author of token + statusObject = {accessStatus: "grant", authorID: tokenAuthor}; + } + //- its public and password protected and password is correct + else if(isPublic && isPasswordProtected && passwordStatus == "correct") + { + //--> grant access, with author of token + statusObject = {accessStatus: "grant", authorID: tokenAuthor}; + } + //- its public and the pad is password protected but wrong password given + else if(isPublic && isPasswordProtected && passwordStatus == "wrong") + { + //--> deny access, ask for new password and tell them that the password is wrong + statusObject = {accessStatus: "wrongPassword"}; + } + //- its public and the pad is password protected but no password given + else if(isPublic && isPasswordProtected && passwordStatus == "notGiven") + { + //--> ask for password + statusObject = {accessStatus: "needPassword"}; + } + //- its not public + else if(!isPublic) + { + //--> deny access + statusObject = {accessStatus: "deny"}; + } + else + { + throw new Error("Ops, something wrong happend"); + } + } + // there is no valid session avaiable AND pad doesn't exists + else + { + //--> deny access + statusObject = {accessStatus: "deny"}; + } + + callback(); + } + ], function(err) + { + callback(err, statusObject); + }); +} diff --git a/node/db/SessionManager.js b/node/db/SessionManager.js new file mode 100644 index 00000000..fc6fe306 --- /dev/null +++ b/node/db/SessionManager.js @@ -0,0 +1,397 @@ +/** + * The Session Manager provides functions to manage session in the database + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var db = require("./DB").db; +var async = require("async"); +var groupMangager = require("./GroupManager"); +var authorMangager = require("./AuthorManager"); + +exports.doesSessionExist = function(sessionID, callback) +{ + //check if the database entry of this session exists + db.get("session:" + sessionID, function (err, session) + { + callback(err, session != null); + }); +} + +/** + * Creates a new session between an author and a group + */ +exports.createSession = function(groupID, authorID, validUntil, callback) +{ + var sessionID; + + async.series([ + //check if group exists + function(callback) + { + groupMangager.doesGroupExist(groupID, function(err, exists) + { + //error + if(err) + { + callback(err); + } + //group does not exist + else if(exists == false) + { + callback({stop: "groupID does not exist"}); + } + //everything is fine, continue + else + { + callback(); + } + }); + }, + //check if author exists + function(callback) + { + authorMangager.doesAuthorExists(authorID, function(err, exists) + { + //error + if(err) + { + callback(err); + } + //author does not exist + else if(exists == false) + { + callback({stop: "authorID does not exist"}); + } + //everything is fine, continue + else + { + callback(); + } + }); + }, + //check validUntil and create the session db entry + function(callback) + { + //check if rev is a number + if(typeof validUntil != "number") + { + //try to parse the number + if(!isNaN(parseInt(validUntil))) + { + validUntil = parseInt(validUntil); + } + else + { + callback({stop: "validUntil is not a number"}); + return; + } + } + + //ensure this is not a negativ number + if(validUntil < 0) + { + callback({stop: "validUntil is a negativ number"}); + return; + } + + //ensure this is not a float value + if(!is_int(validUntil)) + { + callback({stop: "validUntil is a float value"}); + return; + } + + //check if validUntil is in the future + if(Math.floor(new Date().getTime()/1000) > validUntil) + { + callback({stop: "validUntil is in the past"}); + return; + } + + //generate sessionID + sessionID = "s." + randomString(16); + + //set the session into the database + db.set("session:" + sessionID, {"groupID": groupID, "authorID": authorID, "validUntil": validUntil}); + + callback(); + }, + //set the group2sessions entry + function(callback) + { + //get the entry + db.get("group2sessions:" + groupID, function(err, group2sessions) + { + //did a error happen? + if(err) + { + callback(err); + return; + } + + //the entry doesn't exist so far, let's create it + if(group2sessions == null) + { + group2sessions = {sessionIDs : {}}; + } + + //add the entry for this session + group2sessions.sessionIDs[sessionID] = 1; + + //save the new element back + db.set("group2sessions:" + groupID, group2sessions); + + callback(); + }); + }, + //set the author2sessions entry + function(callback) + { + //get the entry + db.get("author2sessions:" + authorID, function(err, author2sessions) + { + //did a error happen? + if(err) + { + callback(err); + return; + } + + //the entry doesn't exist so far, let's create it + if(author2sessions == null) + { + author2sessions = {sessionIDs : {}}; + } + + //add the entry for this session + author2sessions.sessionIDs[sessionID] = 1; + + //save the new element back + db.set("author2sessions:" + authorID, author2sessions); + + callback(); + }); + } + ], function(err) + { + //return error and sessionID + callback(err, {sessionID: sessionID}); + }) +} + +exports.getSessionInfo = function(sessionID, callback) +{ + //check if the database entry of this session exists + db.get("session:" + sessionID, function (err, session) + { + //error + if(err) + { + callback(err); + } + //session does not exists + else if(session == null) + { + callback({stop: "sessionID does not exist"}) + } + //everything is fine, return the sessioninfos + else + { + callback(null, session); + } + }); +} + +/** + * Deletes a session + */ +exports.deleteSession = function(sessionID, callback) +{ + var authorID, groupID; + var group2sessions, author2sessions; + + async.series([ + function(callback) + { + //get the session entry + db.get("session:" + sessionID, function (err, session) + { + //error + if(err) + { + callback(err); + } + //session does not exists + else if(session == null) + { + callback({stop: "sessionID does not exist"}) + } + //everything is fine, return the sessioninfos + else + { + authorID = session.authorID; + groupID = session.groupID; + + callback(); + } + }); + }, + //get the group2sessions entry + function(callback) + { + db.get("group2sessions:" + groupID, function (err, _group2sessions) + { + group2sessions = _group2sessions; + callback(err); + }); + }, + //get the author2sessions entry + function(callback) + { + db.get("author2sessions:" + authorID, function (err, _author2sessions) + { + author2sessions = _author2sessions; + callback(err); + }); + }, + //remove the values from the database + function(callback) + { + //remove the session + db.remove("session:" + sessionID); + + //remove session from group2sessions + delete group2sessions.sessionIDs[sessionID]; + db.set("group2sessions:" + groupID, group2sessions); + + //remove session from author2sessions + delete author2sessions.sessionIDs[sessionID]; + db.set("author2sessions:" + authorID, author2sessions); + + callback(); + } + ], function(err) + { + callback(err); + }) +} + +exports.listSessionsOfGroup = function(groupID, callback) +{ + groupMangager.doesGroupExist(groupID, function(err, exists) + { + //error + if(err) + { + callback(err); + } + //group does not exist + else if(exists == false) + { + callback({stop: "groupID does not exist"}); + } + //everything is fine, continue + else + { + listSessionsWithDBKey("group2sessions:" + groupID, callback); + } + }); +} + +exports.listSessionsOfAuthor = function(authorID, callback) +{ + authorMangager.doesAuthorExists(authorID, function(err, exists) + { + //error + if(err) + { + callback(err); + } + //group does not exist + else if(exists == false) + { + callback({stop: "authorID does not exist"}); + } + //everything is fine, continue + else + { + listSessionsWithDBKey("author2sessions:" + authorID, callback); + } + }); +} + +//this function is basicly the code listSessionsOfAuthor and listSessionsOfGroup has in common +function listSessionsWithDBKey (dbkey, callback) +{ + var sessions; + + async.series([ + function(callback) + { + //get the group2sessions entry + db.get(dbkey, function(err, sessionObject) + { + sessions = sessionObject ? sessionObject.sessionIDs : null; + callback(err); + }); + }, + function(callback) + { + //collect all sessionIDs in an arrary + var sessionIDs = []; + for (var i in sessions) + { + sessionIDs.push(i); + } + + //foreach trough the sessions and get the sessioninfos + async.forEach(sessionIDs, function(sessionID, callback) + { + exports.getSessionInfo(sessionID, function(err, sessionInfo) + { + sessions[sessionID] = sessionInfo; + callback(err); + }); + }, callback); + } + ], function(err) + { + callback(err, sessions); + }); +} + +/** + * Generates a random String with the given length. Is needed to generate the SessionIDs + */ +function randomString(len) +{ + var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var randomstring = ''; + for (var i = 0; i < len; i++) + { + var rnum = Math.floor(Math.random() * chars.length); + randomstring += chars.substring(rnum, rnum + 1); + } + return randomstring; +} + +//checks if a number is an int +function is_int(value) +{ + return (parseFloat(value) == parseInt(value)) && !isNaN(value) +} diff --git a/node/easysync_tests.js b/node/easysync_tests.js index 9eb68ec1..e0d82c33 100644 --- a/node/easysync_tests.js +++ b/node/easysync_tests.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka + * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/node/handler/APIHandler.js b/node/handler/APIHandler.js new file mode 100644 index 00000000..5aa29a8a --- /dev/null +++ b/node/handler/APIHandler.js @@ -0,0 +1,144 @@ +/** + * The API Handler handles all API http requests + */ + +/* + * 2011 Peter 'Pita' Martischka (Primary Technology Ltd) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var fs = require("fs"); +var api = require("../db/API"); + +//ensure we have an apikey +var apikey = null; +try +{ + apikey = fs.readFileSync("../APIKEY.txt","utf8"); +} +catch(e) +{ + apikey = randomString(32); + fs.writeFileSync("../APIKEY.txt",apikey,"utf8"); +} + +//a list of all functions +var functions = { + "createGroup" : [], + "createGroupIfNotExistsFor" : ["groupMapper"], +// "deleteGroup" : ["groupID"], + "listPads" : ["groupID"], + "createPad" : ["padID", "text"], + "createGroupPad" : ["groupID", "padName", "text"], + "createAuthor" : ["name"], + "createAuthorIfNotExistsFor" : ["authorMapper" , "name"], + "createSession" : ["groupID", "authorID", "validUntil"], + "deleteSession" : ["sessionID"], + "getSessionInfo" : ["sessionID"], + "listSessionsOfGroup" : ["groupID"], + "listSessionsOfAuthor" : ["authorID"], + "getText" : ["padID", "rev"], + "setText" : ["padID", "text"], + "getRevisionsCount" : ["padID"], +// "deletePad" : ["padID"], + "getReadOnlyID" : ["padID"], + "setPublicStatus" : ["padID", "publicStatus"], + "getPublicStatus" : ["padID"], + "setPassword" : ["padID", "password"], + "isPasswordProtected" : ["padID"] +}; + +/** + * Handles a HTTP API call + * @param functionName the name of the called function + * @param fields the params of the called function + * @req express request object + * @res express response object + */ +exports.handle = function(functionName, fields, req, res) +{ + //check the api key! + if(fields["apikey"] != apikey) + { + res.send({code: 4, message: "no or wrong API Key", data: null}); + return; + } + + //check if this is a valid function name + var isKnownFunctionname = false; + for(var knownFunctionname in functions) + { + if(knownFunctionname == functionName) + { + isKnownFunctionname = true; + break; + } + } + + //say goodbye if this is a unkown function + if(!isKnownFunctionname) + { + res.send({code: 3, message: "no such function", data: null}); + return; + } + + //put the function parameters in an array + var functionParams = []; + for(var i=0;iYou have no permission to access this pad"); + } + else if(obj.accessStatus == "needPassword") + { + $("#editorloadingbox").html("You need a password to access this pad
" + + ""+ + ""); + } + else if(obj.accessStatus == "wrongPassword") + { + $("#editorloadingbox").html("You're password was wrong
" + + ""+ + ""); + } + } + + //if we haven't recieved the clientVars yet, then this message should it be + else if (!receivedClientVars) + { + //log the message if (window.console) console.log(obj); receivedClientVars = true; + //set some client vars clientVars = obj; clientVars.userAgent = "Anonymous"; clientVars.collab_client_vars.clientAgent = "Anonymous"; + //initalize the pad pad.init(); initalized = true; @@ -195,20 +236,17 @@ function handshake() { pad.changeViewOption('showLineNumbers', false); } - // If the Monospacefont value is set to true then change it to monospace. if (useMonospaceFontGlobal == true) { pad.changeViewOption('useMonospaceFont', true); } - // if the globalUserName value is set we need to tell the server and the client about the new authorname if (globalUserName !== false) { pad.notifyChangeName(globalUserName); // Notifies the server $('#myusernameedit').attr({"value":globalUserName}); // Updates the current users UI } - } //This handles every Message after the clientVars else diff --git a/static/tests.html b/static/tests.html new file mode 100644 index 00000000..f15f4567 --- /dev/null +++ b/static/tests.html @@ -0,0 +1,164 @@ + + + + + API Test and Examples Page + + + + + +
APIKEY:
+
+

createGroup()

+ + + + +
+
+
+
createGroup()
+
createGroupIfNotExistsFor(groupMapper)
+
listPads(groupID)
+
createPad(padID,text)
+
createGroupPad(groupID,padName,text)
+
createAuthor(name)
+
createAuthorIfNotExistsFor(authorMapper,name)
+
createSession(groupID,authorID,validUntil)
+
deleteSession(sessionID)
+
getSessionInfo(sessionID)
+
listSessionsOfGroup(groupID)
+
listSessionsOfAuthor(authorID)
+
getText(padID,rev)
+
setText(padID,text)
+
getRevisionsCount(padID)
+
deletePad(padID)
+
getReadOnlyID(padID)
+
setPublicStatus(padID,publicStatus)
+
getPublicStatus(padID)
+
setPassword(padID,password)
+
isPasswordProtected(padID)
+ + diff --git a/static/timeslider.html b/static/timeslider.html index 110b6d12..5282972d 100644 --- a/static/timeslider.html +++ b/static/timeslider.html @@ -105,17 +105,26 @@ { changesetLoader.handleSocketResponse(message); } + else if(message.accessStatus) + { + $("body").html("

You have no permission to access this pad

") + } }); }); //sends a message over the socket function sendSocketMsg(type, data) { + var sessionID = readCookie("sessionID"); + var password = readCookie("password"); + var msg = { "component" : "timeslider", "type": type, "data": data, "padId": padId, "token": token, + "sessionID": sessionID, + "password": password, "protocolVersion": 2}; socket.json.send(msg);