Skip to content
This repository has been archived by the owner on Oct 21, 2019. It is now read-only.

Commit

Permalink
Permissions implemented and tested, documentation completed, getArea …
Browse files Browse the repository at this point in the history
…method added, road map migrated into the README.md file
  • Loading branch information
Tom Boutell committed Dec 31, 2012
1 parent 34acc06 commit 7df4644
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 125 deletions.
2 changes: 0 additions & 2 deletions .gitignore
@@ -1,4 +1,2 @@
*.DS_Store
node_modules
public/uploads
temp/uploadfs
245 changes: 235 additions & 10 deletions README.md

Large diffs are not rendered by default.

161 changes: 110 additions & 51 deletions jot.js
Expand Up @@ -17,13 +17,28 @@ module.exports = function() {

function jot() {
var self = this;
var app, files, areas, uploadfs, nunjucksEnv;
var app, files, areas, uploadfs, nunjucksEnv, permissions;

self.init = function(options, callback) {
app = options.app;
files = options.files;
areas = options.areas;
uploadfs = options.uploadfs;
permissions = options.permissions;

// Default is to allow anyone to do anything.
// You will probably want to at least check for req.user.
// Possible actions are edit-area and edit-media.
// edit-area calls will include a slug as the third parameter.
// edit-media calls for existing files may include a file, with an
// "owner" property set to the id or username property of req.user
// at the time the file was last edited. edit-media calls with
// no existing file parameter also occur, for new file uploads.
if (!permissions) {
permissions = function(req, action, fileOrSlug, callback) {
return callback(null);
};
}

nunjucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader(__dirname + '/views'));
nunjucksEnv.addFilter('json', function(data) {
Expand Down Expand Up @@ -58,7 +73,7 @@ function jot() {

app.get('/jot/file-iframe/:id', validId, function(req, res) {
var id = req.params.id;
render(res, 'fileIframe.html', { id: id, error: false, uploaded: false });
return render(res, 'fileIframe.html', { id: id, error: false, uploaded: false });
});

// Deliver details about a previously uploaded file as a JSON response
Expand All @@ -71,8 +86,13 @@ function jot() {
res.send("Not Found");
return;
}
file.url = uploadfs.getUrl() + '/images/' + id;
res.send(file);
permissions(req, 'edit-media', file, function(err) {
if (err) {
return forbid(res);
}
file.url = uploadfs.getUrl() + '/images/' + id;
return res.send(file);
});
}
});

Expand All @@ -92,10 +112,13 @@ function jot() {
var info;

function gotExisting(err, existing) {
// This is a good place to add permissions checks

// Let uploadfs do the heavy lifting of scaling and storage to fs or s3
uploadfs.copyImageIn(src, '/images/' + id, update);
permissions(req, 'edit-media', existing, function(err) {
if (err) {
return forbid(res);
}
// Let uploadfs do the heavy lifting of scaling and storage to fs or s3
return uploadfs.copyImageIn(src, '/images/' + id, update);
});
}

function update(err, infoArg) {
Expand All @@ -107,6 +130,13 @@ function jot() {
info.name = slugify(file.name);
info.createdAt = new Date();

// Do our best to record who owns this file to allow permissions
// checks later. If req.user exists and has an _id, id or username property,
// record that
if (req.user) {
info.owner = req.user._id || req.user.id || req.user.username;
}

files.update({ _id: info._id }, info, { upsert: true, safe: true }, inserted);
}

Expand All @@ -126,50 +156,60 @@ function jot() {

app.get('/jot/edit-area', function(req, res) {
var slug = req.query.slug;
var isNew = false;
if (!slug) {
return notfound(req, res);
} else {
areas.findOne({ slug: slug }, function(err, area) {
if (!area) {
var area = {
slug: slug,
_id: generateId(),
content: null,
isNew: true
};
area.wid = 'w-' + area._id;
return render(res, 'editArea.html', area);
}
else
{
area.wid = 'w-' + area._id;
area.isNew = false;
return render(res, 'editArea.html', area);
}
});
}
permissions(req, 'edit-area', slug, function(err) {
if (err) {
return forbid(res);
}
var isNew = false;
if (!slug) {
return notfound(req, res);
} else {
areas.findOne({ slug: slug }, function(err, area) {
if (!area) {
var area = {
slug: slug,
_id: generateId(),
content: null,
isNew: true
};
area.wid = 'w-' + area._id;
return render(res, 'editArea.html', area);
}
else
{
area.wid = 'w-' + area._id;
area.isNew = false;
return render(res, 'editArea.html', area);
}
});
}
});
});

app.post('/jot/edit-area', function(req, res) {
var slug = req.body.slug;
var area = {
slug: req.body.slug,
content: validateContent(req.body.content)
};
permissions(req, 'edit-area', slug, function(err) {
if (err) {
return forbid(res);
}
var area = {
slug: req.body.slug,
content: validateContent(req.body.content)
};

// TODO: validate content. XSS, tag balancing, allowed tags and attributes,
// sensible use of widgets. All that stuff A1.5 does well
// TODO: validate content. XSS, tag balancing, allowed tags and attributes,
// sensible use of widgets. All that stuff A1.5 does well

areas.update({ slug: area.slug }, area, { upsert: true, safe: true }, updated);
areas.update({ slug: area.slug }, area, { upsert: true, safe: true }, updated);

function updated(err) {
if (err) {
console.log(err);
return notfound(req, res);
function updated(err) {
if (err) {
console.log(err);
return notfound(req, res);
}
res.send(area.content);
}
res.send(area.content);
}
});
});

// A simple oembed proxy to avoid cross-site scripting restrictions.
Expand Down Expand Up @@ -214,7 +254,23 @@ function jot() {
return callback(null);
};

// Returns an object with a property for each named area
// Invokes the callback with an error if any, and if no error,
// the area object requested if it exists. The area object is
// guaranteed to have `slug` and `content` properties. The
// `content` property contains rich content markup ready to
// display in the browser.

self.getArea = function(slug, callback) {
areas.findOne({ slug: slug }, function(err, area) {
if (err) {
return callback(err);
}
return callback(null, area);
});
};

// Invokes the callback with an error if any, and if no error,
// an object with a property for each named area
// matching the given page slug, plus a slug property.
// Very handy for rendering pages and page-like collections
// of areas. A simple convention is used to group areas into
Expand All @@ -225,10 +281,10 @@ function jot() {
// string at the beginning can use indexes).

self.getAreasForPage = function(slug, callback) {
var pattern = new RegExp('^' + RegExp.quote(slug) + ':', 'i');
var pattern = new RegExp('^' + RegExp.quote(slug) + ':');
areas.find({ slug: pattern }).toArray(function(err, areaDocs) {
if (err) {
return callback('Not found');
return callback(err);
}
var data = {};
// Organize the areas by name
Expand All @@ -238,7 +294,6 @@ function jot() {
data[results[1]] = area;
}
});
data.slug = slug;
return callback(null, data);
});
};
Expand All @@ -256,6 +311,11 @@ function jot() {
res.send('500 error, URL was ' + req.url);
}

function forbid(res) {
res.statusCode = 403;
res.send('Forbidden');
}

function notfound(req, res) {
res.statusCode = 404;
res.send('404 not found error, URL was ' + req.url);
Expand Down Expand Up @@ -334,10 +394,9 @@ function jot() {
// what we can do with jQuery on the server side. Note that
// browser side validation is not enough because browsers are
// inherently not trusted
var $content = jQuery(content);
$content.find('.jot-edit-widget').remove();
var wrapper = jQuery('<div></div>');
wrapper.append($content);
wrapper.html(content);
wrapper.find('.jot-edit-widget').remove();
return wrapper.html();
}
}
Expand Down
7 changes: 0 additions & 7 deletions todo.txt

This file was deleted.

10 changes: 6 additions & 4 deletions views/area.html
@@ -1,9 +1,11 @@
<div class="jot-area" data-jot-slug="{{ slug }}">
<div class="jot-normal-view">
<div class="jot-area-controls">
<a class="jot-button jot-edit-area" href="#">Edit</a>
</div>
<div class="jot-clear"></div>
{% if edit %}
<div class="jot-area-controls">
<a class="jot-button jot-edit-area" href="#">Edit</a>
</div>
<div class="jot-clear"></div>
{% endif %}
<div class="jot-content">
{{ content }}
</div>
Expand Down
1 change: 1 addition & 0 deletions views/editArea.html
Expand Up @@ -29,6 +29,7 @@
<a class="jot-button jot-save" data-save-area>Save</a>
<a class="jot-button jot-cancel" data-cancel-area>Cancel</a>
</div>
<div class="jot-clear"></div>
</form>

<script>
Expand Down
1 change: 1 addition & 0 deletions wiki/.gitignore
@@ -1,2 +1,3 @@
public/uploads
temp/uploadfs
node_modules
13 changes: 13 additions & 0 deletions wiki/public/css/wiki.css
Expand Up @@ -30,6 +30,11 @@ h4
font-size: 125%;
}

p
{
margin-bottom: 1em;
}

em, i
{
font-style: italic;
Expand Down Expand Up @@ -66,3 +71,11 @@ strong, b
border-top: 1px dotted #ccc;
padding: 1%;
}

.login-status
{
float: right;
padding-top: 40px;
padding-right: 20px;
}

20 changes: 10 additions & 10 deletions wiki/views/base.html
Expand Up @@ -18,17 +18,17 @@
<body>
{% block beforeBody %}
{% endblock %}

{% block body %}
{% endblock %}
{% block afterBody %}
{# Append js calls to the domready block to call them #}
{# when all js files are loaded and the DOM is ready #}
<script type="text/javascript">
$(function() {
{% block domready %}
{% endblock %}
});
</script>
{% endblock %}

<script type="text/javascript">
$(function() {
jot.enableAreas();
jot.enablePlayers();
});
</script>
{# Must be present in the page in order to use jot's widget editors #}
{{ jotTemplates() }}
</body>
</html>
11 changes: 8 additions & 3 deletions wiki/views/layout.html
Expand Up @@ -3,6 +3,14 @@
{# Document with typical page structure #}

{% block body %}
<div class="login-status">
{% if user %}
<a class="jot-button" href="/logout">Log Out</a>
{% else %}
<a class="jot-button" href="/login">Log In to Edit</a>
<p>Username: admin Password: demo</p>
{% endif %}
</div>
<div class="header">
<h1>Jot Wiki</h1>
</div>
Expand All @@ -28,6 +36,3 @@ <h1>Jot Wiki</h1>
</div>
{% endblock %}

{% block domready %}
{{ super() }}
{% endblock %}

0 comments on commit 7df4644

Please sign in to comment.