mirror of
https://github.com/jsbin/jsbin.git
synced 2026-01-18 15:18:04 +00:00
- Added allowReadOnly in config - defaults to true - read only has CORS support - Add handler for :rev route, and autoload the latest (needs more cleaning up) - Allow xhr requests to both /api/x and /abcd/123 (how it originally worked)
1028 lines
31 KiB
JavaScript
1028 lines
31 KiB
JavaScript
var async = require('asyncjs'),
|
|
path = require('path'),
|
|
crypto = require('crypto'),
|
|
utils = require('../utils'),
|
|
errors = require('../errors'),
|
|
custom = require('../custom'),
|
|
blacklist = require('../blacklist'),
|
|
apiConfig = require('../config').api || {};
|
|
scripts = require('../../scripts.json'),
|
|
processors = require('../processors'),
|
|
_ = require('underscore'),
|
|
Observable = utils.Observable;
|
|
|
|
module.exports = Observable.extend({
|
|
constructor: function BinHandler(sandbox) {
|
|
Observable.apply(this, arguments);
|
|
|
|
this.models = sandbox.models;
|
|
this.helpers = sandbox.helpers;
|
|
this.mailer = sandbox.mailer;
|
|
|
|
// For now we bind all methods to the class scope. In reality only those
|
|
// used as route callbacks need to be bound.
|
|
var methods = Object.getOwnPropertyNames(BinHandler.prototype).filter(function (prop) {
|
|
return typeof this[prop] === 'function';
|
|
}, this);
|
|
|
|
utils.bindAll(this, methods);
|
|
},
|
|
getDefault: function (req, res, next) {
|
|
if (req.subdomain && custom[req.subdomain]) {
|
|
return this.getCustom(req, res, next);
|
|
}
|
|
this.renderFiles(req, res);
|
|
},
|
|
getFromPost: function (req, res, next) {
|
|
var data = utils.extract(req.body, 'html', 'css', 'javascript');
|
|
|
|
'html css javascript'.split(' ').forEach(function (panel) {
|
|
if (data[panel]) {
|
|
data[panel] = decodeURIComponent(data[panel]);
|
|
}
|
|
});
|
|
|
|
// for simplicity's sake
|
|
if (req.body.js && !data.javascript) {
|
|
data.javascript = req.body.js;
|
|
}
|
|
|
|
data.settings = {};
|
|
|
|
this.render(req, res, data);
|
|
},
|
|
getCustom: function (req, res, next) {
|
|
var config = custom[req.subdomain],
|
|
overrides = config.defaults,
|
|
_this = this;
|
|
|
|
this.loadFiles(this.defaultFiles(), function (err, defaults) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
for (var key in defaults) {
|
|
if (overrides[key]) {
|
|
defaults[key] = overrides[key];
|
|
}
|
|
}
|
|
|
|
_this.render(req, res, defaults, config);
|
|
});
|
|
},
|
|
getBin: function (req, res, next) {
|
|
this.render(req, res, req.bin);
|
|
},
|
|
// creates a object that's API request friendly - ie. gets rendered
|
|
// output if there's a processor and hides internal fields
|
|
apiPrepareBin: function (bin) {
|
|
var out = _.pick(bin,
|
|
'html',
|
|
'javascript',
|
|
'css',
|
|
'original_html',
|
|
'original_javascript',
|
|
'original_css',
|
|
'created',
|
|
'last_updated'
|
|
);
|
|
|
|
out.permalink = this.helpers.urlForBin(bin, true);
|
|
|
|
if (bin.settings && bin.settings.processors) {
|
|
out.processors = bin.settings.processors;
|
|
}
|
|
|
|
// note: if there's no processor, then the "original_*" fields
|
|
|
|
return out;
|
|
},
|
|
loadBinRevision: function (req, res, next) {
|
|
if (req.param('rev')) {
|
|
next();
|
|
} else {
|
|
this.models.bin.latest({ id: req.bin.url }, function(err, bin) {
|
|
req.bin = bin;
|
|
next();
|
|
});
|
|
}
|
|
},
|
|
apiGetBin: function (req, res, next) {
|
|
this.applyProcessors(req.bin);
|
|
res.json(this.apiPrepareBin(req.bin));
|
|
},
|
|
live: function (req, res, next) {
|
|
req.live = true;
|
|
next();
|
|
},
|
|
embed: function (req, res, next) {
|
|
req.embed = true;
|
|
next();
|
|
},
|
|
getBinPreview: function (req, res, next) {
|
|
var options = {
|
|
edit: !req.param('quiet'),
|
|
silent: !!req.param('quiet'),
|
|
csrf: req.session._csrf
|
|
};
|
|
|
|
this.formatPreview(req.bin, options, function (err, formatted) {
|
|
if (err) {
|
|
next(err);
|
|
}
|
|
|
|
if (req.xhr) {
|
|
res.json(this.apiPrepareBin(req.bin));
|
|
} else if (formatted) {
|
|
res.send(formatted);
|
|
} else {
|
|
res.contentType('js');
|
|
res.send(req.bin.javascript);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
// TODO decide whether this is used anymore
|
|
getBinSource: function (req, res) {
|
|
res.contentType('json');
|
|
var output = JSON.stringify(this.templateFromBin(req.bin));
|
|
if (!req.ajax) {
|
|
res.contentType('js');
|
|
output = 'var template = ' + output;
|
|
}
|
|
res.send(output);
|
|
},
|
|
getBinSourceFile: function (req, res) {
|
|
var format = req.params.format,
|
|
settings = req.bin.settings || {},
|
|
reverseProcessorLookup = {},
|
|
key;
|
|
|
|
if (format === 'js' || format === 'json') {
|
|
format = 'javascript';
|
|
}
|
|
|
|
if (format === 'md') {
|
|
format = 'markdown';
|
|
}
|
|
|
|
if (settings.processors) {
|
|
// first shuffle the bin around so they can request .less and get the .css panel...yeah, funky
|
|
// { html: 'markdown' }
|
|
for (key in settings.processors) {
|
|
if (settings.processors.hasOwnProperty(key)) {
|
|
reverseProcessorLookup[settings.processors[key]] = key;
|
|
}
|
|
}
|
|
|
|
// if we want the raw preprocessed content, just map
|
|
if (reverseProcessorLookup[format]) {
|
|
req.bin[format] = req.bin[reverseProcessorLookup[format]];
|
|
} else if (settings.processors[format]) {
|
|
// else we need to convert and process the source
|
|
if (processors[settings.processors[format]] !== undefined) {
|
|
req.bin[format] = processors[settings.processors[format]](req.bin[format]);
|
|
// this delete ensures it doesn't happen again (in case we're looking at .html)
|
|
delete req.bin.settings.processors[format];
|
|
}
|
|
}
|
|
}
|
|
|
|
res.contentType(format);
|
|
if (format !== 'html') {
|
|
res.send(req.bin[format]);
|
|
} else {
|
|
this.getBinPreview(req, res);
|
|
}
|
|
},
|
|
getUserBins: function (req, res, next) {
|
|
var _this = this;
|
|
if (!req.session.user && !req.params.user) {
|
|
return res.send('');
|
|
}
|
|
|
|
this.models.user.getBins(req.params.user || req.session.user.name, function (err, bins) {
|
|
if (err) {
|
|
// TODO handle unknown user
|
|
return next(err);
|
|
}
|
|
|
|
// TODO handle private users
|
|
|
|
_this.renderHistory(req, res, bins);
|
|
});
|
|
},
|
|
getLatestForUser: function (req, res, next) {
|
|
var _this = this;
|
|
|
|
if (!req.user) {
|
|
return next(new errors.NotFound('User not found'));
|
|
}
|
|
|
|
this.models.user.getLatestBin(req.user.name, function (err, bin) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (bin.active) {
|
|
req.bin = bin;
|
|
next();
|
|
} else {
|
|
next(new errors.NotFound('Bin has been banned'));
|
|
}
|
|
});
|
|
},
|
|
redirectToLatest: function (req, res) {
|
|
var path = req.originalUrl.replace('latest', req.bin.revision);
|
|
res.redirect(303, path);
|
|
},
|
|
createBin: function (req, res, next) {
|
|
var params = utils.extract(req.body, 'html', 'css', 'javascript', 'settings'),
|
|
_this = this;
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
_this.models.bin.create(params, function (err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
_this.completeCreateBin(result, req, res, next);
|
|
});
|
|
});
|
|
},
|
|
apiCreateBin: function (req, res, next) {
|
|
var params = utils.extract(req.body, 'html', 'css', 'javascript', 'settings'),
|
|
_this = this;
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
params.settings = params.settings || '{ processors: {} }'; // set default processors
|
|
_this.models.bin.create(params, function (err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
res.json(result);
|
|
});
|
|
});
|
|
},
|
|
claimBin: function (req, res, next) {
|
|
// Handler for ambiguous endpoint that can be used to create a new bin with
|
|
// a provided bin url, clone the bin and create a new endpoint or update an
|
|
// existing one with no revision. We use :url instead of :bin in the route
|
|
// to prevent 404ing on a bin to be created.
|
|
if (req.param('method') === 'save') {
|
|
req.bin = {url: req.param('url'), revision: 0};
|
|
return this.createRevision(req, res, next);
|
|
}
|
|
|
|
// If we're not claiming a new url then it's either a simple revision
|
|
// or a clone. We pass this on to the next route which should use
|
|
// .createRevisionOrClone() and :bin in the route segment to load the bin.
|
|
next();
|
|
},
|
|
createRevisionOrClone: function (req, res, next) {
|
|
// Another endpoint that does two things based on the content of the
|
|
// POST body. Need to check for "new" in the method which is "clone".
|
|
if (req.param('method', '').indexOf('new') > -1) {
|
|
this.createBin(req, res, next);
|
|
} else {
|
|
this.createRevision(req, res, next);
|
|
}
|
|
},
|
|
apiCreateRevision: function (req, res, next) {
|
|
var that = this,
|
|
params = utils.extract(req.body, 'html', 'css', 'javascript', 'settings');
|
|
|
|
params.url = req.params.bin;
|
|
params.revision = parseInt(req.params.rev, 10) || req.bin.revision;
|
|
params.settings = params.settings || '{ processors: {} }'; // set default processors
|
|
params.summary = utils.titleForBin(params);
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
var username = req.session.user ? req.session.user.name : undefined;
|
|
|
|
that.models.user.isOwnerOf(username, params, function (err, result) {
|
|
var method = 'create';
|
|
|
|
if (result.isowner || result.found === false) { // if anonymous or user is owner
|
|
params.revision += 1; // bump the revision from the *latest*
|
|
that.models.bin.createRevision(params, function (err, result) {
|
|
var query = {id: req.params.bin, revision: result.revision};
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
that.models.bin.load(query, function (err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
res.json(result);
|
|
});
|
|
});
|
|
} else {
|
|
res.status(403); // forbidden
|
|
res.json({ error: 'You are not the owner of this bin so you cannot create a revision' });
|
|
}
|
|
});
|
|
});
|
|
},
|
|
createRevision: function (req, res, next) {
|
|
var panel = req.param('panel'),
|
|
params = {},
|
|
_this = this;
|
|
|
|
if (req.param('method') === 'save') {
|
|
params = utils.extract(req.body, 'html', 'css', 'javascript', 'settings');
|
|
params.url = req.bin.url;
|
|
params.revision = parseInt(req.params.rev, 10) || 1, //req.bin.revision;
|
|
params.summary = utils.titleForBin(params);
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
var username = req.session.user ? req.session.user.name : undefined;
|
|
|
|
_this.models.user.isOwnerOf(username, params, function (err, result) {
|
|
var method = 'create';
|
|
|
|
if (result.isowner || result.found === false) {
|
|
method = 'createRevision';
|
|
params.revision = req.bin.revision + 1; // bump the revision from the *latest*
|
|
} else {
|
|
delete params.revision;
|
|
}
|
|
|
|
_this.models.bin[method](params, function (err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
_this.completeCreateBin(result, req, res, next);
|
|
});
|
|
});
|
|
});
|
|
} else if (req.param('method') === 'update') {
|
|
params[panel] = req.param('content');
|
|
params.streamingKey = req.param('checksum');
|
|
params.revision = req.param('revision');
|
|
params.url = req.param('code');
|
|
params.settings = req.param('settings');
|
|
params.summary = utils.titleForBin(params);
|
|
params.panel = panel;
|
|
params.panel_open = !!params[panel];
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
_this.models.bin.updatePanel(panel, params, function (err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (req.session.user) {
|
|
_this.models.user.updateOwners(req.session.user.name, params, function () {});
|
|
_this.emit('latest-for-user', req.session.user.name, req.bin);
|
|
}
|
|
|
|
_this.emit('updated', req.bin, {
|
|
panelId: panel,
|
|
content: params[panel]
|
|
});
|
|
|
|
res.json({ok: true, error: false});
|
|
});
|
|
});
|
|
} else {
|
|
next();
|
|
}
|
|
},
|
|
reload: function (req, res, next) {
|
|
var params = {},
|
|
_this = this;
|
|
|
|
params.streamingKey = req.param('checksum');
|
|
params.revision = req.param('revision');
|
|
params.url = req.param('code');
|
|
|
|
this.validateBin(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (req.session.user) {
|
|
_this.models.user.touchOwners(req.session.user.name, params, function () {});
|
|
_this.emit('latest-for-user', req.session.user.name, req.bin);
|
|
}
|
|
|
|
_this.emit('reload', req.bin);
|
|
|
|
res.json({ok: true, error: false});
|
|
});
|
|
},
|
|
completeCreateBin: function (bin, req, res, next) {
|
|
var _this = this;
|
|
|
|
if (!bin.summary) bin.summary = utils.titleForBin(bin);
|
|
|
|
function render() {
|
|
_this.emit('created', req.bin);
|
|
_this.renderCreated(req, res, bin);
|
|
}
|
|
|
|
// If we have a logged in user then assign the bin to them.
|
|
if (req.session.user && req.session.user.name) {
|
|
_this.models.user.setBinOwner(req.session.user.name, bin, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
render();
|
|
});
|
|
} else {
|
|
render();
|
|
}
|
|
},
|
|
downloadBin: function (req, res, next) {
|
|
var bin = req.bin,
|
|
filename = ['jsbin', bin.url, bin.revision, 'html'].join('.'),
|
|
options = {analytics: false, edit: false, silent: true};
|
|
|
|
this.formatPreview(bin, options, function (err, formatted) {
|
|
if (err) {
|
|
next(err);
|
|
}
|
|
|
|
res.header('Content-Disposition', 'attachment; filename=' + filename);
|
|
|
|
if (formatted) {
|
|
res.send(formatted);
|
|
} else {
|
|
res.contentType('js');
|
|
res.send(bin.javascript);
|
|
}
|
|
});
|
|
},
|
|
notFound: function (req, res, next) {
|
|
var files = this.defaultFiles(),
|
|
_this = this,
|
|
errorMessage;
|
|
|
|
files[0] = 'not_found.html';
|
|
files[2] = 'not_found.js';
|
|
|
|
if (req.isApi) {
|
|
res.status(404);
|
|
errorMessage = 'Could not find bin with ID "' + req.param('bin') + '"';
|
|
if (req.param('rev')) { errorMessage += ' and revision ' + req.param('rev'); }
|
|
res.json({ error: errorMessage });
|
|
} else {
|
|
this.loadFiles(files, function (err, results) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
results.url = req.param('bin');
|
|
|
|
// Need to get the most recent revision from the database.
|
|
_this.models.bin.latest({id: results.url}, function (err, bin) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
// We have a missing bin, we check the latest returned bin, if this
|
|
// is active then we simply render it assuming the user mistyped the
|
|
// url.
|
|
if (bin && bin.active) {
|
|
results = bin;
|
|
} else {
|
|
// If we have a bin then take the latest revision plus one.
|
|
results.revision = bin && (bin.revision + 1);
|
|
}
|
|
|
|
if (req.url.indexOf('edit') > -1) {
|
|
_this.render(req, res, results);
|
|
} else {
|
|
var options = {edit: true, silent: true, csrf: req.session._csrf};
|
|
_this.formatPreview(results, options, function (err, formatted) {
|
|
if (err) {
|
|
next(err);
|
|
}
|
|
|
|
if (formatted) {
|
|
res.send(formatted);
|
|
} else {
|
|
res.contentType('js');
|
|
res.send(req.bin.javascript);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
loadBin: function (req, res, next) {
|
|
var rev = parseInt(req.params.rev, 10) || 1,
|
|
query = {id: req.params.bin, revision: rev},
|
|
helpers = this.helpers;
|
|
|
|
function complete(err, result) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (!result) {
|
|
return next(new errors.BinNotFound('Could not find bin: ' + req.params.bin));
|
|
} else if (!result.active) {
|
|
return next(new errors.NotFound('Bin has been reported: ' + req.params.bin));
|
|
} else {
|
|
req.bin = result;
|
|
// manually add the full url to the bin to allow templates access
|
|
req.bin.permalink = helpers.urlForBin(req.bin, true);
|
|
next();
|
|
}
|
|
}
|
|
|
|
// TODO: Re-factor this logic.
|
|
if ((req.params.rev || req.path.indexOf('latest') === -1) && req.path.indexOf('save') === -1) {
|
|
this.models.bin.load(query, complete);
|
|
} else {
|
|
this.models.bin.latest(query, complete);
|
|
}
|
|
},
|
|
validateBin: function (bin, fn) {
|
|
var reserved = this.helpers.set('reserved') || [];
|
|
|
|
if (!blacklist.validate(bin)) {
|
|
fn(new errors.BadRequest('Unable to save: Post contains blacklisted content'));
|
|
} else if (bin.url && reserved.length && reserved.indexOf(bin.url) > -1) {
|
|
fn(new errors.BadRequest('Unable to save: This is a reserved url'));
|
|
} else {
|
|
fn();
|
|
}
|
|
},
|
|
render: function (req, res, bin, config) {
|
|
var template = this.templateFromBin(bin),
|
|
helpers = this.helpers,
|
|
version = helpers.production ? helpers.set('version') : 'debug',
|
|
created = req.flash('checksum') || {},
|
|
root = helpers.url('', true),
|
|
_this = this,
|
|
jsbin;
|
|
|
|
// Insert the subdomain if the request has one. Ideally this should be
|
|
// done by the helper.url() function but it's not currently aware of the
|
|
// request object.
|
|
if (req.subdomain && custom[req.subdomain]) {
|
|
root = root.replace('://', '://' + req.subdomain + '.');
|
|
}
|
|
|
|
jsbin = this.jsbin(bin, {
|
|
version: version,
|
|
token: req.session._csrf,
|
|
root: root,
|
|
static: helpers.urlForStatic(),
|
|
settings: config && config.settings,
|
|
// If we've pulled a just created bin out of the flash messages object
|
|
// then we check to see if the previously created bin is the one we're
|
|
// about to load. If so we add the checksum to the page which allows
|
|
// the spike logic to work in IE8.
|
|
checksum: created.url === bin.url && created.revision === bin.revision && created.checksum
|
|
});
|
|
|
|
// TODO I guess this isn't the clean way of doing this? -- RS
|
|
if (req.live || req.embed) {
|
|
jsbin.saveDisabled = true;
|
|
}
|
|
|
|
if (req.embed) {
|
|
jsbin.embed = true;
|
|
}
|
|
|
|
helpers.analytics(function (err, analytics) {
|
|
var url = helpers.urlForBin(bin),
|
|
user = req.session.user || {};
|
|
|
|
res.render('index', {
|
|
tips: '{}',
|
|
revision: bin.revision || 1,
|
|
home: user.name || null,
|
|
email: user.email || null,
|
|
flash_info: req.flash(req.flash.INFO),
|
|
gravatar: user.avatar,
|
|
jsbin: JSON.stringify(jsbin),
|
|
json_template: JSON.stringify(template).replace(/<\/script>/gi, '<\\/script>').replace(/<!--/g, '<\\!--'),
|
|
version: jsbin.version,
|
|
analytics: analytics,
|
|
token: req.session._csrf,
|
|
custom_css: config && config.css,
|
|
scripts: helpers.production ? false : scripts,
|
|
is_production: helpers.production,
|
|
root: root,
|
|
static: helpers.urlForStatic(),
|
|
url: url,
|
|
live: req.live,
|
|
embed: req.embed,
|
|
code_id: bin.url,
|
|
code_id_path: url,
|
|
code_id_domain: helpers.urlForBin(bin, true).replace(/^https?:\/\//, '')
|
|
});
|
|
});
|
|
},
|
|
renderFiles: function (req, res, files, url) {
|
|
var _this = this;
|
|
files = files || this.defaultFiles();
|
|
this.loadFiles(files, function (err, results) {
|
|
if (!err) {
|
|
results.url = url;
|
|
_this.render(req, res, results);
|
|
} else {
|
|
res.send(500, 'Unable to read file');
|
|
}
|
|
});
|
|
},
|
|
renderCreated: function (req, res, bin) {
|
|
var permalink = this.helpers.urlForBin(bin, true),
|
|
editPermalink = this.helpers.editUrlForBin(bin);
|
|
|
|
if (req.ajax) {
|
|
if (req.param('format', '').toLowerCase() === 'plain') {
|
|
return res.contentType('txt').send(this.helpers.editUrlForBin(bin, true));
|
|
}
|
|
|
|
var root = this.helpers.set('url full');
|
|
// Insert the subdomain if the request has one. Ideally this should be
|
|
// done by the helper.url() function but it's not currently aware of the
|
|
// request object.
|
|
if (req.subdomain && custom[req.subdomain]) {
|
|
root = root.replace('://', '://' + req.subdomain + '.');
|
|
permalink = permalink.replace('://', '://' + req.subdomain + '.');
|
|
}
|
|
|
|
|
|
res.json({
|
|
code: bin.url,
|
|
root: root,
|
|
created: (new Date()).toISOString(), // Should be part of bin.
|
|
revision: bin.revision,
|
|
url: permalink,
|
|
edit: editPermalink,
|
|
html: editPermalink,
|
|
js: editPermalink,
|
|
summary: utils.titleForBin(bin),
|
|
allowUpdate: !!bin.streamingKey,
|
|
checksum: bin.streamingKey
|
|
});
|
|
} else {
|
|
// Pass the bin id through in a flash message so that IE can get
|
|
// the checksum after the redirect.
|
|
res.flash('checksum', {url: bin.url, revision: bin.revision, checksum: bin.streamingKey});
|
|
res.redirect(303, editPermalink);
|
|
}
|
|
},
|
|
renderHistory: function (req, res, bins) {
|
|
var acceptsJSON = req.header('Accept', '').indexOf('application/json') > -1,
|
|
format = acceptsJSON ? 'json' : 'html',
|
|
helpers = this.helpers,
|
|
jsbin = JSON.stringify({
|
|
version: helpers.production ? helpers.set('version') : 'debug',
|
|
root: helpers.url('', true),
|
|
static: helpers.urlForStatic()
|
|
});
|
|
|
|
|
|
this.formatHistory(bins, format, function (err, history) {
|
|
if (acceptsJSON) {
|
|
res.send(history);
|
|
} else {
|
|
helpers.render('history', {
|
|
bins: history,
|
|
by_user: req.params.user ? ' by ' + req.params.user : ''
|
|
},
|
|
function (err, html) {
|
|
if (req.ajax) {
|
|
res.send(html);
|
|
} else {
|
|
res.render('list', {
|
|
list_history: html,
|
|
jsbin: jsbin,
|
|
scripts: [
|
|
"/js/vendor/jquery.js",
|
|
"/js/vendor/pretty-date.js",
|
|
"/js/render/saved-history-preview.js"
|
|
],
|
|
is_production: helpers.production
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
jsbin: function (bin, options) {
|
|
return {
|
|
root: options.root,
|
|
static: options.static || options.root,
|
|
version: options.version,
|
|
state: {
|
|
token: options.token,
|
|
stream: false,
|
|
code: bin.url || null,
|
|
revision: bin.url ? (bin.revision || 1) : null,
|
|
processors: bin.settings.processors || {},
|
|
checksum: options.checksum || null
|
|
},
|
|
settings: options.settings || {panels: []}
|
|
};
|
|
},
|
|
templateFromBin: function (bin) {
|
|
var template = utils.extract(bin, 'html', 'css', 'javascript');
|
|
|
|
'html css javascript'.split(' ').forEach(function (panel) {
|
|
template[panel] = utils.cleanForRender(template[panel] || '');
|
|
});
|
|
|
|
template.url = this.helpers.jsbinURL(bin); //.permalink;
|
|
return template;
|
|
},
|
|
defaultFiles: function () {
|
|
return ['html', 'css', 'js'].map(function (ext) {
|
|
return 'default.' + ext;
|
|
});
|
|
},
|
|
loadFiles: function (files, fn) {
|
|
files = files || this.defaultFiles();
|
|
|
|
async.files(files, this.helpers.set('views')).readFile("utf8").toArray(function (err, results) {
|
|
if (!err) {
|
|
fn(null, {
|
|
html: results[0].data,
|
|
css: results[1].data,
|
|
javascript: results[2].data,
|
|
settings: {}
|
|
});
|
|
} else {
|
|
fn(err);
|
|
}
|
|
});
|
|
},
|
|
// applies the processors to the bin and generates the html, js, etc
|
|
// based on the appropriate processor. Used in the previews and the API
|
|
// requests.
|
|
applyProcessors: function (bin) {
|
|
if (bin.settings && bin.settings.processors) {
|
|
for (var panel in bin.settings.processors) {
|
|
var processorName = bin.settings.processors[panel],
|
|
processor = processors[processorName],
|
|
code = bin[panel];
|
|
if (processor) {
|
|
bin['original_' + panel] = code;
|
|
bin[panel] = processor(code);
|
|
}
|
|
}
|
|
}
|
|
|
|
// nothing returned as it updates the bin object
|
|
},
|
|
formatPreview: function (bin, options, fn) {
|
|
this.applyProcessors(bin);
|
|
|
|
var formatted = bin.html || '',
|
|
helpers = this.helpers,
|
|
insert = [],
|
|
scripts = [],
|
|
_this = this,
|
|
parts, last, context;
|
|
|
|
options = options || {};
|
|
|
|
function onAnalyticsComplete(err, analytics) {
|
|
if (err) {
|
|
return fn(err);
|
|
}
|
|
|
|
// Insert JS at %code% or as the first script
|
|
if (formatted.indexOf('%code%') > -1) {
|
|
var jsparts = formatted.split('%code%');
|
|
formatted = jsparts.join(bin.javascript);
|
|
} else {
|
|
insert.push('<script>', bin.javascript.trim(), '</script>');
|
|
}
|
|
|
|
// Include 'Edit in JS Bin' button
|
|
if (options.edit) {
|
|
var data = {root: helpers.urlForStatic(''), csrf: options.csrf};
|
|
insert.push('<script src="' + helpers.urlForStatic('js/render/edit.js') + '"></script>');
|
|
insert.push('<script>jsbinShowEdit(' + JSON.stringify(data) + ');</script>');
|
|
}
|
|
|
|
// Trigger an event to allow listeners to apply scripts to the page.
|
|
// Scripts will be passed to helpers.urlForStatic() if no protocol is present.
|
|
if (!options.silent) {
|
|
_this.emit('render-scripts', scripts);
|
|
insert = insert.concat(scripts.map(function (script) {
|
|
script = script.indexOf('http') === 0 ? script : helpers.urlForStatic(script);
|
|
return '<script src="' + script + '"></script>';
|
|
}));
|
|
}
|
|
|
|
// Analytics
|
|
if (options.silent !== true && helpers.production && analytics) {
|
|
insert.push(analytics);
|
|
}
|
|
|
|
// Add CSS at %css% or find a place for it
|
|
if (formatted.indexOf('%css%') > -1) {
|
|
formatted = formatted.replace(/%css%/g, bin.css || '');
|
|
} else {
|
|
css = '\n<style id="jsbin-css">\n' + (bin.css || '') + '\n</style>';
|
|
parts = formatted.split('</head>');
|
|
last = parts.pop();
|
|
if (parts.length > 0) {
|
|
// Add it just after before the end head tag if we can
|
|
formatted = parts.join('</head>') + css + '\n</head>' + last;
|
|
} else {
|
|
// No <head>, now try just after </title>
|
|
parts = formatted.split('</title>');
|
|
last = parts.pop();
|
|
if (parts.length > 0) {
|
|
formatted = parts.join('</title>') + '</title>' + css + last;
|
|
} else {
|
|
// Otherwise add it as the first script
|
|
insert.unshift(css);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append scripts to the bottom of the page.
|
|
if (insert.length) {
|
|
parts = formatted.split('</body>');
|
|
last = parts.pop();
|
|
if (parts.length > 0) {
|
|
// Add the scripts just before the end body tag if there is one
|
|
formatted = parts.join('</body>') + insert.join('\n') + '\n</body>' + last;
|
|
} else {
|
|
// Otherwise just shove 'em at the end
|
|
formatted = last + '\n\n' + insert.join('\n');
|
|
}
|
|
}
|
|
|
|
context = {
|
|
domain: helpers.set('url host'),
|
|
permalink: helpers.editUrlForBin(bin, true)
|
|
};
|
|
|
|
// Append attribution comment to header.
|
|
helpers.render('comment', context, function (err, comment) {
|
|
formatted = formatted.replace(/<html[^>]*>/, function ($0) {
|
|
return $0 + '\n' + (comment || '').trim();
|
|
});
|
|
return fn(err || null, err ? undefined : formatted);
|
|
});
|
|
}
|
|
|
|
if (formatted) {
|
|
if (options.analytics !== false) {
|
|
helpers.analytics(true, onAnalyticsComplete);
|
|
} else {
|
|
onAnalyticsComplete();
|
|
}
|
|
} else {
|
|
fn(null, formatted);
|
|
}
|
|
},
|
|
formatHistory: function (bins, format, fn) {
|
|
// reorder the bins based latest edited, and group by bin.url
|
|
var helpers = this.helpers,
|
|
order = {},
|
|
urls = {},
|
|
orderedBins, loopOrder, i, length;
|
|
|
|
if (typeof format === 'function') {
|
|
fn = format;
|
|
format = 'html';
|
|
}
|
|
|
|
bins.forEach(function (bin) {
|
|
var time = new Date(bin.last_updated).getTime();
|
|
|
|
if (!urls[bin.url]) {
|
|
urls[bin.url] = [];
|
|
}
|
|
|
|
urls[bin.url].push(bin);
|
|
|
|
if (order[bin.url]) {
|
|
if (order[bin.url] < time) {
|
|
order[bin.url] = time;
|
|
}
|
|
} else {
|
|
order[bin.url] = time;
|
|
}
|
|
});
|
|
|
|
// Sort the revisions within the group
|
|
Object.keys(urls).forEach(function (group) {
|
|
urls[group].sort(function (a, b) {
|
|
var a_time = new Date(a.last_updated).getTime(),
|
|
b_time = new Date(b.last_updated).getTime();
|
|
return a_time == b_time ? 0 : a_time < b_time ? -1 : 1;
|
|
});
|
|
});
|
|
|
|
orderedBins = [];
|
|
loopOrder = Object.keys(order).sort(function (a, b) {
|
|
return order[a] < order[b] ? -1 : 1;
|
|
});
|
|
|
|
for (i = 0, length = loopOrder.length; i < length; i += 1) {
|
|
orderedBins.push.apply(orderedBins, urls[loopOrder[i]]);
|
|
}
|
|
|
|
bins = orderedBins.reverse();
|
|
|
|
this.loadFiles(null, function (err, defaults) {
|
|
var map = {}, data = [], key;
|
|
|
|
bins.forEach(function (bin) {
|
|
var query = utils.queryStringForBin(bin, defaults),
|
|
revisions = map[bin.url];
|
|
|
|
if (!revisions) {
|
|
revisions = map[bin.url] = [];
|
|
data.push(revisions);
|
|
}
|
|
|
|
revisions.push({
|
|
code: bin.url,
|
|
revision: bin.revision,
|
|
summary: bin.summary || utils.titleForBin(bin),
|
|
archive: bin.archive,
|
|
url: helpers.urlForBin(bin),
|
|
edit_url: helpers.editUrlForBin(bin) + '?' + query,
|
|
last_updated: bin.last_updated.toISOString(),
|
|
pretty_last_updated: utils.since(bin.last_updated),
|
|
is_first: !map[bin.url].length
|
|
});
|
|
});
|
|
|
|
fn(null, data);
|
|
});
|
|
},
|
|
report: function (req, res, next) {
|
|
var bin = req.bin,
|
|
_this = this;
|
|
|
|
this.models.bin.report(bin, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
var to = _this.helpers.set('notify report'),
|
|
context;
|
|
|
|
context = {
|
|
url: req.body.url,
|
|
reportee: req.param('email', 'Anonymous'),
|
|
from: req.body.email || null
|
|
};
|
|
|
|
if (_this.helpers.production && to && to.length) {
|
|
_this.mailer.reportBin(to, context);
|
|
}
|
|
|
|
res.render('report', {
|
|
root: _this.helpers.url(),
|
|
dave: _this.helpers.urlForStatic('/images/logo.png')
|
|
});
|
|
|
|
});
|
|
},
|
|
archiveBin: function (archive, req, res, next) {
|
|
if (!req.session.user) return res.send(403, {error: 'Not authorised.'});
|
|
var bin = {
|
|
url: req.param('bin'),
|
|
revision: req.param('rev'),
|
|
name: req.session.user.name,
|
|
archive: archive
|
|
};
|
|
|
|
this.models.bin.archive(bin, function (err) {
|
|
if (err) res.send(err);
|
|
res.send(200, bin);
|
|
});
|
|
}
|
|
});
|