Merge pull request #289 from jhollinger/no_spaces

Sanitize pad names
This commit is contained in:
John McLear 2012-01-08 07:30:07 -08:00
commit 642b716553
5 changed files with 145 additions and 70 deletions

View File

@ -38,6 +38,14 @@ var globalPads = {
remove: function (name) { delete this[':'+name]; } remove: function (name) { delete this[':'+name]; }
}; };
/**
* An array of padId transformations. These represent changes in pad name policy over
* time, and allow us to "play back" these changes so legacy padIds can be found.
*/
var padIdTransforms = [
[/\s+/g, '_']
];
/** /**
* Returns a Pad Object with the callback * Returns a Pad Object with the callback
* @param id A String with the id of the pad * @param id A String with the id of the pad
@ -110,6 +118,39 @@ exports.doesPadExists = function(padId, callback)
}); });
} }
//returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = function(padId, callback) {
var transform_index = arguments[2] || 0;
//we're out of possible transformations, so just return it
if(transform_index >= padIdTransforms.length)
{
callback(padId);
}
//check if padId exists
else
{
exports.doesPadExists(padId, function(junk, exists)
{
if(exists)
{
callback(padId);
}
else
{
//get the next transformation *that's different*
var transformedPadId = padId;
while(transformedPadId == padId && transform_index < padIdTransforms.length)
{
transformedPadId = padId.replace(padIdTransforms[transform_index][0], padIdTransforms[transform_index][1]);
transform_index += 1;
}
//check the next transform
exports.sanitizePadId(transformedPadId, callback, transform_index);
}
});
}
}
exports.isValidPadId = function(padId) exports.isValidPadId = function(padId)
{ {
return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); return /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);

View File

@ -21,6 +21,7 @@
var ERR = require("async-stacktrace"); var ERR = require("async-stacktrace");
var fs = require("fs"); var fs = require("fs");
var api = require("../db/API"); var api = require("../db/API");
var padManager = require("../db/PadManager");
//ensure we have an apikey //ensure we have an apikey
var apikey = null; var apikey = null;
@ -95,7 +96,33 @@ exports.handle = function(functionName, fields, req, res)
res.send({code: 3, message: "no such function", data: null}); res.send({code: 3, message: "no such function", data: null});
return; return;
} }
//sanitize any pad id's before continuing
if(fields["padID"])
{
padManager.sanitizePadId(fields["padID"], function(padId)
{
fields["padID"] = padId;
callAPI(functionName, fields, req, res);
});
}
else if(fields["padName"])
{
padManager.sanitizePadId(fields["padName"], function(padId)
{
fields["padName"] = padId;
callAPI(functionName, fields, req, res);
});
}
else
{
callAPI(functionName, fields, req, res);
}
}
//calls the api function
function callAPI(functionName, fields, req, res)
{
//put the function parameters in an array //put the function parameters in an array
var functionParams = []; var functionParams = [];
for(var i=0;i<functions[functionName].length;i++) for(var i=0;i<functions[functionName].length;i++)

View File

@ -232,101 +232,108 @@ async.waterfall([
res.send(html); res.send(html);
}); });
}); });
//serve pad.html under /p //redirects browser to the pad's sanitized url if needed. otherwise, renders the html
app.get('/p/:pad', function(req, res, next) function goToPad(req, res, render) {
{
//ensure the padname is valid and the url doesn't end with a / //ensure the padname is valid and the url doesn't end with a /
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url))
{ {
res.send('Such a padname is forbidden', 404); res.send('Such a padname is forbidden', 404);
return;
} }
else
res.header("Server", serverName); {
var filePath = path.normalize(__dirname + "/../static/pad.html"); padManager.sanitizePadId(req.params.pad, function(padId) {
res.sendfile(filePath, { maxAge: exports.maxAge }); //the pad id was sanitized, so we redirect to the sanitized version
if(padId != req.params.pad)
{
var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + padId);
res.header('Location', real_path);
res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302);
}
//the pad id was fine, so just render it
else
{
render();
}
});
}
}
//serve pad.html under /p
app.get('/p/:pad', function(req, res, next)
{
goToPad(req, res, function() {
res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/pad.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
}); });
//serve timeslider.html under /p/$padname/timeslider //serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/timeslider', function(req, res, next) app.get('/p/:pad/timeslider', function(req, res, next)
{ {
//ensure the padname is valid and the url doesn't end with a / goToPad(req, res, function() {
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) res.header("Server", serverName);
{ var filePath = path.normalize(__dirname + "/../static/timeslider.html");
res.send('Such a padname is forbidden', 404); res.sendfile(filePath, { maxAge: exports.maxAge });
return; });
}
res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/timeslider.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
}); });
//serve timeslider.html under /p/$padname/timeslider //serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/:rev?/export/:type', function(req, res, next) app.get('/p/:pad/:rev?/export/:type', function(req, res, next)
{ {
//ensure the padname is valid and the url doesn't end with a / goToPad(req, res, function() {
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"];
{ //send a 404 if we don't support this filetype
res.send('Such a padname is forbidden', 404); if(types.indexOf(req.params.type) == -1)
return; {
} next();
return;
var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; }
//send a 404 if we don't support this filetype
if(types.indexOf(req.params.type) == -1) //if abiword is disabled, and this is a format we only support with abiword, output a message
{ if(settings.abiword == null &&
next(); ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1)
return; {
} res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
return;
//if abiword is disabled, and this is a format we only support with abiword, output a message }
if(settings.abiword == null &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) res.header("Access-Control-Allow-Origin", "*");
{ res.header("Server", serverName);
res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
return; hasPadAccess(req, res, function()
} {
exportHandler.doExport(req, res, req.params.pad, req.params.type);
res.header("Access-Control-Allow-Origin", "*"); });
res.header("Server", serverName);
hasPadAccess(req, res, function()
{
exportHandler.doExport(req, res, req.params.pad, req.params.type);
}); });
}); });
//handle import requests //handle import requests
app.post('/p/:pad/import', function(req, res, next) app.post('/p/:pad/import', function(req, res, next)
{ {
//ensure the padname is valid and the url doesn't end with a / goToPad(req, res, function() {
if(!padManager.isValidPadId(req.params.pad) || /\/$/.test(req.url)) //if abiword is disabled, skip handling this request
{ if(settings.abiword == null)
res.send('Such a padname is forbidden', 404); {
return; next();
} return;
}
//if abiword is disabled, skip handling this request
if(settings.abiword == null)
{
next();
return;
}
res.header("Server", serverName); res.header("Server", serverName);
hasPadAccess(req, res, function() hasPadAccess(req, res, function()
{ {
importHandler.doImport(req, res, req.params.pad); importHandler.doImport(req, res, req.params.pad);
});
}); });
}); });
var apiLogger = log4js.getLogger("API"); var apiLogger = log4js.getLogger("API");
//This is for making an api call, collecting all post information and passing it to the apiHandler //This is for making an api call, collecting all post information and passing it to the apiHandler
var apiCaller = function(req, res, fields) { var apiCaller = function(req, res, fields)
{
res.header("Server", serverName); res.header("Server", serverName);
res.header("Content-Type", "application/json; charset=utf-8"); res.header("Content-Type", "application/json; charset=utf-8");

View File

@ -194,7 +194,7 @@ function handshake()
padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces
if(!isReconnect) if(!isReconnect)
document.title = document.title + " | " + padId; document.title = document.title + " | " + padId.replace(/_+/g, ' ');
var token = readCookie("token"); var token = readCookie("token");
if (token == null) if (token == null)

View File

@ -65,7 +65,7 @@
padId = decodeURIComponent(urlParts[urlParts.length-2]); padId = decodeURIComponent(urlParts[urlParts.length-2]);
//set the title //set the title
document.title = document.title + " | " + padId; document.title = document.title + " | " + padId.replace(/_+/g, ' ');
//ensure we have a token //ensure we have a token
token = readCookie("token"); token = readCookie("token");